index.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. /**
  2. * Service Definition for the `ctx.directoryPicker` capability seam: how the web-GUI host lets an operator
  3. * select a workspace directory. Backends differ in interaction shape, not
  4. * just mechanism, so the service exposes a discriminated capability instead
  5. * of one method set: a `native` backend opens one OS chooser on the
  6. * host's display, while a `browse` backend serves listing/creation primitives
  7. * for an in-app browser (and thereby works for remote clients no OS dialog
  8. * can reach). Consumers switch on `capability().kind`; the union is
  9. * merge-extensible, and the documented default for an unknown kind is to
  10. * hide the picking affordance rather than fail.
  11. * @module @deepseek-ai/dsh-host-directory-picker
  12. */
  13. import { Context, Service } from '@deepseek-ai/cordis'
  14. import type { DirectoryListing } from './types.ts'
  15. export type { DirectoryEntry, DirectoryListing } from './types.ts'
  16. /** The native interaction: one OS directory chooser on the host display. */
  17. export interface DirectoryPickerNativeCapability {
  18. kind: 'native'
  19. /**
  20. * Open the chooser and wait for the operator.
  21. * @param signal - caller/connection lifetime; abort terminates the chooser.
  22. * @returns the chosen absolute path, or null when the operator cancels.
  23. */
  24. pick(signal: AbortSignal): Promise<string | null>
  25. }
  26. /**
  27. * The browse interaction: listing/creation primitives an in-app browser
  28. * drives one level at a time. Works for remote clients — nothing renders on
  29. * the host display.
  30. */
  31. export interface DirectoryPickerBrowseCapability {
  32. kind: 'browse'
  33. /**
  34. * List one directory level.
  35. * @param path - absolute directory to list; absent lists the home directory.
  36. * @param signal - caller lifetime; abort stops the scan (a stalled network
  37. * directory must not outlive a disconnected caller) and rejects with the
  38. * abort reason.
  39. * @returns the level's listing with ancestry; backends bound the complete
  40. * result, and a cut level reports `truncated`.
  41. * @throws {DirectoryPickerError} `directory-unreadable` when the target is not fully
  42. * qualified (a wire value must never resolve against the host cwd or, on
  43. * Windows, its current drive) or cannot be listed.
  44. */
  45. list(path?: string, signal?: AbortSignal): Promise<DirectoryListing>
  46. /**
  47. * Create one child directory under an existing parent.
  48. * @param path - absolute existing parent directory.
  49. * @param name - single non-blank path segment (no separators, not `.`/`..`).
  50. * @returns the created directory's absolute path.
  51. * @throws {DirectoryPickerError} `directory-exists` for an existing child,
  52. * `directory-create-failed` for a parent that is not fully qualified or any other failure.
  53. */
  54. createDirectory(path: string, name: string): Promise<string>
  55. }
  56. /**
  57. * Merge-extensible registry of interaction shapes keyed by capability kind: a
  58. * new backend declaration-merges its shape here (the entry's `kind` literal
  59. * must equal its key) instead of editing this package.
  60. */
  61. export interface DirectoryPickerCapabilities {
  62. native: DirectoryPickerNativeCapability
  63. browse: DirectoryPickerBrowseCapability
  64. }
  65. /** Union of interaction shapes a backend can provide, derived from the merge-extensible {@link DirectoryPickerCapabilities} map. */
  66. export type DirectoryPickerCapability = DirectoryPickerCapabilities[keyof DirectoryPickerCapabilities]
  67. /** Closed failure vocabulary of the browse primitives (mirrored onto the wire by consumers). */
  68. export type DirectoryPickerErrorCode = 'directory-unreadable' | 'directory-exists' | 'directory-create-failed'
  69. /** Typed failure thrown by browse primitives so consumers can map business codes without string matching. */
  70. export class DirectoryPickerError extends Error {
  71. /**
  72. * @param code - closed business code of the failure.
  73. * @param path - the absolute path the failure is about.
  74. * @param message - operator-facing description.
  75. */
  76. constructor(readonly code: DirectoryPickerErrorCode, readonly path: string, message: string) {
  77. super(message)
  78. this.name = 'DirectoryPickerError'
  79. }
  80. }
  81. declare module '@deepseek-ai/cordis' {
  82. interface Context {
  83. directoryPicker: DirectoryPicker
  84. }
  85. }
  86. /**
  87. * Abstract directory-picking service. Subclass, implement `capability()`, and
  88. * load the subclass as a plugin — it registers as `ctx.directoryPicker` (one
  89. * implementation per context; loading a second throws, cordis' standard
  90. * duplicate-service behavior). The capability object must be stable for the
  91. * service lifetime: consumers may capture it across calls.
  92. */
  93. export abstract class DirectoryPicker extends Service {
  94. constructor(ctx: Context) {
  95. super(ctx, 'directoryPicker')
  96. }
  97. /**
  98. * The backend's interaction capability.
  99. * @returns the discriminated capability consumers switch on.
  100. */
  101. abstract capability(): DirectoryPickerCapability
  102. }
  103. export default DirectoryPicker