@tiphareth/dsh-hardssh 0.1.2 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +49 -25
  3. package/SKILLS.md +140 -0
  4. package/lib/base/index.js +95 -0
  5. package/lib/client.js +1171 -1321
  6. package/lib/fs.js +307 -416
  7. package/lib/index.js +5428 -2991
  8. package/lib/ledger-B-LXlftp.js +43 -0
  9. package/lib/ledger-D2ezq1iW.js +376 -0
  10. package/lib/model-Cp7f70Mb.js +5 -0
  11. package/lib/registry-CViuzKYI.js +421 -0
  12. package/lib/subprocess.js +92 -549
  13. package/lib/types/backend.d.ts +73 -78
  14. package/lib/types/base/capability.d.ts +4 -0
  15. package/lib/types/base/ledger-router.d.ts +42 -26
  16. package/lib/types/base/ledger.d.ts +30 -19
  17. package/lib/types/base/model.d.ts +17 -8
  18. package/lib/types/base/plugin.d.ts +11 -0
  19. package/lib/types/base/registry.d.ts +28 -31
  20. package/lib/types/base/router.d.ts +0 -7
  21. package/lib/types/client/api.d.ts +8 -8
  22. package/lib/types/client/connect-host.d.ts +13 -0
  23. package/lib/types/client/index.d.ts +6 -4
  24. package/lib/types/client/locales.d.ts +15 -1
  25. package/lib/types/client/session-connect-gate.d.ts +71 -0
  26. package/lib/types/client/ssh/api.d.ts +11 -9
  27. package/lib/types/client/ssh/apply.d.ts +14 -8
  28. package/lib/types/client/ssh/locales.d.ts +8 -1
  29. package/lib/types/client/ssh/ops-tab.d.ts +34 -0
  30. package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
  31. package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
  32. package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
  33. package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
  34. package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
  35. package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
  36. package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
  37. package/lib/types/client/ssh/session-target.d.ts +17 -0
  38. package/lib/types/client/state.d.ts +4 -1
  39. package/lib/types/client/workspace-panel-entry.d.ts +30 -0
  40. package/lib/types/client/workspace-panel.d.ts +24 -0
  41. package/lib/types/client-http.d.ts +77 -6
  42. package/lib/types/core.d.ts +5 -16
  43. package/lib/types/fs.d.ts +18 -25
  44. package/lib/types/index.d.ts +138 -9
  45. package/lib/types/ledger.d.ts +16 -111
  46. package/lib/types/protocol.d.ts +0 -5
  47. package/lib/types/providers/index.d.ts +6 -3
  48. package/lib/types/providers/local/provider.d.ts +83 -23
  49. package/lib/types/providers/ssh/provider.d.ts +24 -57
  50. package/lib/types/remote/environment.d.ts +2 -0
  51. package/lib/types/remote/remote-fs.d.ts +12 -1
  52. package/lib/types/remote/remote-process.d.ts +22 -1
  53. package/lib/types/remote/remote-subprocess.d.ts +13 -1
  54. package/lib/types/remote/remote-terminal.d.ts +4 -0
  55. package/lib/types/remote-search.d.ts +3 -3
  56. package/lib/types/routes.d.ts +36 -8
  57. package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
  58. package/lib/types/runtime/workspace-core.d.ts +73 -38
  59. package/lib/types/runtime/workspace-migration.d.ts +113 -0
  60. package/lib/types/ssh/connection/lease.d.ts +23 -0
  61. package/lib/types/ssh/connection/manager.d.ts +359 -0
  62. package/lib/types/ssh/connection/pool.d.ts +8 -1
  63. package/lib/types/ssh/engine.d.ts +97 -267
  64. package/lib/types/ssh/known-hosts.d.ts +22 -5
  65. package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
  66. package/lib/types/ssh/plugin.d.ts +10 -15
  67. package/lib/types/ssh/protocol.d.ts +18 -0
  68. package/lib/types/ssh/routes.d.ts +15 -7
  69. package/lib/types/ssh/sftp/service.d.ts +216 -0
  70. package/lib/types/ssh/store.d.ts +20 -8
  71. package/lib/types/ssh/terminal/service.d.ts +49 -0
  72. package/lib/types/ssh/tunnel/service.d.ts +26 -0
  73. package/lib/types/ssh/vault.d.ts +51 -4
  74. package/lib/types/subprocess.d.ts +13 -21
  75. package/lib/types/switch/switch-fs.d.ts +52 -2
  76. package/lib/types/switch/switch-subprocess.d.ts +32 -3
  77. package/lib/types/tools.d.ts +7 -6
  78. package/lib/types/workspace-tool-ops.d.ts +33 -0
  79. package/lib/types/workspace.d.ts +18 -0
  80. package/lib/vault-3gpWct2Q.js +559 -0
  81. package/lib/workspace.js +2 -0
  82. package/package.json +58 -25
  83. package/scripts/export-legacy-workspaces.mjs +136 -0
  84. package/src/backend.ts +155 -483
  85. package/src/base/capability.ts +4 -0
  86. package/src/base/ledger-router.ts +214 -65
  87. package/src/base/ledger.ts +175 -48
  88. package/src/base/model.ts +18 -8
  89. package/src/base/plugin.ts +15 -4
  90. package/src/base/registry.ts +49 -41
  91. package/src/base/router.ts +0 -8
  92. package/src/client/api.ts +8 -36
  93. package/src/client/connect-host.ts +199 -0
  94. package/src/client/directory-flow.tsx +91 -36
  95. package/src/client/index.ts +56 -53
  96. package/src/client/locales.ts +14 -0
  97. package/src/client/session-connect-gate.ts +163 -0
  98. package/src/client/ssh/api.ts +245 -108
  99. package/src/client/ssh/apply.ts +23 -24
  100. package/src/client/ssh/locales.ts +16 -2
  101. package/src/client/ssh/ops-tab.tsx +79 -0
  102. package/src/client/ssh/panel/ClusterTab.tsx +9 -20
  103. package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
  104. package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
  105. package/src/client/ssh/panel/SshPanel.tsx +81 -77
  106. package/src/client/ssh/panel/TerminalTab.tsx +18 -37
  107. package/src/client/ssh/panel/TransferTab.tsx +43 -33
  108. package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
  109. package/src/client/ssh/panel/panel.module.css +90 -127
  110. package/src/client/ssh/session-target.ts +69 -0
  111. package/src/client/state.ts +9 -1
  112. package/src/client/workspace-badges.ts +99 -96
  113. package/src/client/workspace-panel-entry.tsx +91 -0
  114. package/src/client/workspace-panel.tsx +227 -0
  115. package/src/client/workspace.module.css +76 -2
  116. package/src/client-http.ts +118 -18
  117. package/src/core.ts +37 -47
  118. package/src/fs.ts +137 -85
  119. package/src/index.ts +535 -244
  120. package/src/ledger.ts +62 -416
  121. package/src/protocol.ts +0 -6
  122. package/src/providers/index.ts +8 -4
  123. package/src/providers/local/provider.ts +259 -87
  124. package/src/providers/ssh/provider.ts +158 -155
  125. package/src/remote/environment.ts +22 -1
  126. package/src/remote/remote-fs.ts +123 -34
  127. package/src/remote/remote-process.ts +265 -203
  128. package/src/remote/remote-subprocess.ts +61 -18
  129. package/src/remote/remote-terminal.ts +106 -21
  130. package/src/remote-search.ts +122 -26
  131. package/src/routes.ts +416 -395
  132. package/src/runtime/dsh-capabilities.ts +19 -0
  133. package/src/runtime/workspace-core.ts +171 -88
  134. package/src/runtime/workspace-migration.ts +472 -0
  135. package/src/ssh/connection/lease.ts +35 -0
  136. package/src/ssh/connection/manager.ts +1083 -0
  137. package/src/ssh/connection/pool.ts +341 -275
  138. package/src/ssh/engine.ts +269 -1477
  139. package/src/ssh/known-hosts.ts +42 -18
  140. package/src/ssh/local-transfer-policy.ts +83 -0
  141. package/src/ssh/plugin.ts +20 -19
  142. package/src/ssh/protocol.ts +22 -1
  143. package/src/ssh/routes.ts +419 -187
  144. package/src/ssh/sftp/service.ts +967 -0
  145. package/src/ssh/store.ts +169 -72
  146. package/src/ssh/terminal/service.ts +177 -0
  147. package/src/ssh/tools.ts +42 -20
  148. package/src/ssh/tunnel/service.ts +217 -0
  149. package/src/ssh/vault.ts +245 -91
  150. package/src/subprocess.ts +88 -71
  151. package/src/switch/switch-fs.ts +141 -10
  152. package/src/switch/switch-subprocess.ts +80 -6
  153. package/src/tools.ts +217 -221
  154. package/src/workspace-tool-ops.ts +101 -0
  155. package/src/workspace.ts +52 -0
  156. package/lib/environment-BL1jddfB.js +0 -449
  157. package/lib/switch-fs-CAJpFY9C.js +0 -193
  158. package/lib/switch-fs-RrZtG2gv.js +0 -210
  159. package/lib/types/client/manager-button.d.ts +0 -32
  160. package/lib/types/client/ssh/mount.d.ts +0 -13
  161. package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
  162. package/lib/types/client/ssh/panel/controller.d.ts +0 -23
  163. package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
  164. package/lib/types/client/workspace-gate.d.ts +0 -15
  165. package/lib/types/remote-runner.d.ts +0 -83
  166. package/lib/types/seam-state.d.ts +0 -69
  167. package/src/client/manager-button.tsx +0 -269
  168. package/src/client/ssh/mount.tsx +0 -83
  169. package/src/client/ssh/panel/HostsTab.tsx +0 -236
  170. package/src/client/ssh/panel/controller.ts +0 -48
  171. package/src/client/ssh/sidebar-entry.ts +0 -123
  172. package/src/client/workspace-gate.ts +0 -225
  173. package/src/remote-runner.ts +0 -201
  174. package/src/seam-state.ts +0 -185
@@ -1,19 +1,13 @@
1
1
  /**
2
- * Gated filesystem backends and the production workspace file service.
2
+ * The generic record source the SSH workspace API projects.
3
3
  *
4
- * UI workspace routes resolve an exact ledger anchor and then create a
5
- * request-scoped RemoteBackend. There is no implicit local fallback and no
6
- * dependency on the legacy global SSH mode state.
4
+ * `GenericWorkspaceStore` is the single record source: it projects the generic
5
+ * `WorkspaceLedger` (via `WorkspaceCore`) into the client SSH DTO and performs
6
+ * workspace CRUD through the generic core. There is no implicit local fallback
7
+ * and no dependency on any retired global SSH mode state.
7
8
  */
8
- import type { SshEngine } from './ssh/engine.ts';
9
- import { shellQuote } from './shell.ts';
10
- import { RemoteSearchService } from './remote-search.ts';
11
- import type { SshWorkspaceLedger } from './ledger.ts';
12
- import type { DirListing, FileRead, FileWriteResult, SearchView, SshWorkspaceRecord, WorkspaceEntry } from './protocol.ts';
13
- export declare const SEARCH_HIT_CAP = 200;
14
- export declare const SEARCH_SCAN_CAP = 20000;
15
- export declare const SEARCH_MAX_DEPTH = 4;
16
- export declare const REMOTE_SEARCH_TIMEOUT_MS = 20000;
9
+ import type { WorkspaceCore } from './runtime/workspace-core.ts';
10
+ import type { SshWorkspaceRecord, WorkspaceEntry } from './protocol.ts';
17
11
  export type BackendErrorCode = 'binary' | 'too-large' | 'conflict' | 'outside-root' | 'not-remote' | 'root-mismatch' | 'not-found' | 'forbidden' | 'invalid' | 'io';
18
12
  export declare class BackendError extends Error {
19
13
  readonly code: BackendErrorCode;
@@ -21,26 +15,29 @@ export declare class BackendError extends Error {
21
15
  cause?: unknown;
22
16
  });
23
17
  }
24
- export interface WorkspaceFileContext {
25
- workspaceId: string;
26
- requestedRoot: string;
27
- anchorPath: string;
28
- alias: string;
29
- remoteRoot: string;
30
- }
31
- export interface WorkspaceFileService {
32
- resolveContext(root: string): Promise<WorkspaceFileContext>;
33
- list(root: string, rel: string): Promise<DirListing>;
34
- read(root: string, rel: string): Promise<FileRead>;
35
- write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
36
- search(root: string, query: string): Promise<SearchView>;
37
- }
38
- export interface WorkspaceBackend {
39
- assertRoot(root: string): void;
40
- list(root: string, rel: string): Promise<DirListing>;
41
- read(root: string, rel: string): Promise<FileRead>;
42
- write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
43
- search(root: string, query: string): Promise<SearchView>;
18
+ /**
19
+ * The SSH-workspace record source the workspace routes, agent tools, and the
20
+ * SSH host-delete reference guard satisfy. Every consumer depends on this
21
+ * narrow view, so none of them knows which ledger implementation is
22
+ * authoritative.
23
+ */
24
+ export interface WorkspaceStoreView {
25
+ /** Every SSH-bound workspace as the client DTO. */
26
+ list(): Promise<SshWorkspaceRecord[]>;
27
+ get(id: string): Promise<SshWorkspaceRecord | undefined>;
28
+ findByAnchor(path: string): Promise<SshWorkspaceRecord | undefined>;
29
+ /** Synchronous anchor lookup used by the session announcement. */
30
+ findByAnchorSync(path: string): SshWorkspaceRecord | undefined;
31
+ /** Create an SSH workspace (title/alias/remoteRoot); the anchor is managed by the store. */
32
+ create(input: {
33
+ title: string;
34
+ alias: string;
35
+ remoteRoot: string;
36
+ }): Promise<SshWorkspaceRecord>;
37
+ rename(id: string, title: string): Promise<SshWorkspaceRecord | undefined>;
38
+ remove(id: string): Promise<boolean>;
39
+ /** The shared anchor root (fail-closed gating before the runtime is ready). */
40
+ anchorsRoot(): string;
44
41
  }
45
42
  /**
46
43
  * Normalize a workspace-relative path.
@@ -54,55 +51,53 @@ export declare function normalizeRel(raw: string): string;
54
51
  export declare function isInside(root: string, abs: string): boolean;
55
52
  /** Resolve a normalized relative path below a POSIX root. */
56
53
  export declare function relToAbs(root: string, rel: string): string;
57
- export { shellQuote };
58
- /** Stable dir-first, case-insensitive ordering shared by every backend. */
54
+ /** Stable dir-first, case-insensitive ordering shared by every listing. */
59
55
  export declare function sortWorkspaceEntries(entries: WorkspaceEntry[]): WorkspaceEntry[];
60
- export type WorkspacePathFlavor = 'local' | 'remote';
61
- /** Return abs relative to root, rejecting prefix-collision/parent traversal. */
62
- export declare function relativeWorkspacePath(root: string, abs: string, flavor: WorkspacePathFlavor): string;
63
- /** Normalize filesystem/SFTP errors to stable route-visible codes. */
64
- export declare function toBackendIoError(error: unknown, path: string): BackendError;
65
56
  /** Map a BackendError to its stable HTTP status (routes use this). */
66
57
  export declare function backendErrorStatus(error: BackendError, ioStatus: 500 | 502): number;
67
58
  /**
68
- * Local implementation retained for explicit local-only consumers. It is
69
- * deliberately NOT selected by LedgerWorkspaceFileService.
59
+ * The single record source: projects the generic `WorkspaceLedger` (via
60
+ * `WorkspaceCore`) into the client SSH DTO and performs workspace CRUD through
61
+ * the generic core. Routes, agent tools, and the SSH host-delete guard stay
62
+ * store-agnostic behind `WorkspaceStoreView`. New SSH workspaces keep the
63
+ * established anchor layout (~/.dsh/ssh-workspaces/<id>) so existing
64
+ * sidebar/session bindings and the host workspace registry stay unchanged.
70
65
  */
71
- export declare class LocalBackend implements WorkspaceBackend {
72
- assertRoot(root: string): void;
73
- list(root: string, rel: string): Promise<DirListing>;
74
- read(root: string, rel: string): Promise<FileRead>;
75
- write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
76
- search(root: string, query: string): Promise<SearchView>;
77
- private resolvePath;
78
- /** Resolve the nearest existing ancestor; root and every existing target
79
- * ancestor must resolve below the real workspace root. */
80
- private assertRealpathWalk;
81
- }
82
- /** A request-scoped remote backend bound to one immutable ledger record. */
83
- export declare class RemoteBackend implements WorkspaceBackend {
84
- private readonly engine;
85
- private readonly record;
86
- private readonly searchService;
87
- private readonly remoteRoot;
88
- constructor(engine: SshEngine, record: SshWorkspaceRecord, searchService: RemoteSearchService);
89
- assertRoot(root: string): void;
90
- list(root: string, rel: string): Promise<DirListing>;
91
- read(root: string, rel: string): Promise<FileRead>;
92
- write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
93
- search(root: string, query: string): Promise<SearchView>;
94
- }
95
- /** Production UI service: exact ledger anchor -> request-scoped backend. */
96
- export declare class LedgerWorkspaceFileService implements WorkspaceFileService {
97
- private readonly ledger;
98
- private readonly engine;
99
- private readonly searchService;
100
- constructor(ledger: SshWorkspaceLedger, engine: SshEngine, searchService: RemoteSearchService);
101
- resolveContext(root: string): Promise<WorkspaceFileContext>;
102
- list(root: string, rel: string): Promise<DirListing>;
103
- read(root: string, rel: string): Promise<FileRead>;
104
- write(root: string, rel: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
105
- search(root: string, query: string): Promise<SearchView>;
106
- private resolveBackend;
66
+ export declare class GenericWorkspaceStore implements WorkspaceStoreView {
67
+ private readonly core;
68
+ /** Resolves when the generic runtime (migration + initialize) finished;
69
+ * rejects on any startup failure so every consumer fails closed instead
70
+ * of silently reading an un-migrated or corrupt ledger. */
71
+ private readonly ready;
72
+ /** Anchor root for newly created SSH workspaces (~/.dsh/ssh-workspaces). */
73
+ private readonly anchorBase;
74
+ constructor(core: WorkspaceCore,
75
+ /** Resolves when the generic runtime (migration + initialize) finished;
76
+ * rejects on any startup failure so every consumer fails closed instead
77
+ * of silently reading an un-migrated or corrupt ledger. */
78
+ ready: Promise<void>,
79
+ /** Anchor root for newly created SSH workspaces (~/.dsh/ssh-workspaces). */
80
+ anchorBase: string);
81
+ /** WorkspaceCore.list() exposes every provider, but the SSH DTO surface is SSH-only, so this filters before projecting. */
82
+ list(): Promise<SshWorkspaceRecord[]>;
83
+ /** WorkspaceCore.get() returns generic records, so this narrows to SSH and projects the DTO. */
84
+ get(id: string): Promise<SshWorkspaceRecord | undefined>;
85
+ /** WorkspaceCore.findByAnchor() also resolves non-SSH workspaces, so this projects only SSH owners. */
86
+ findByAnchor(path: string): Promise<SshWorkspaceRecord | undefined>;
87
+ /** WorkspaceCore has no synchronous anchor lookup on the public surface, so this resolves the detached ledger snapshot lexically. */
88
+ findByAnchorSync(path: string): SshWorkspaceRecord | undefined;
89
+ /** WorkspaceLedger.create() cannot generate SSH defaults or the SSH anchor layout, so this maps the SSH create input into a generic record first. */
90
+ create(input: {
91
+ title: string;
92
+ alias: string;
93
+ remoteRoot: string;
94
+ }): Promise<SshWorkspaceRecord>;
95
+ /** Title edits route through the generic update (the generic ledger has no SSH-specific rename). */
96
+ rename(id: string, title: string): Promise<SshWorkspaceRecord | undefined>;
97
+ /** WorkspaceCore.remove() is the generic delete; the anchor directory stays in place (the host workspace may still reference it). */
98
+ remove(id: string): Promise<boolean>;
99
+ /** The SSH anchor base this store creates its managed anchors under
100
+ * (~/.dsh/ssh-workspaces). */
101
+ anchorsRoot(): string;
107
102
  }
108
103
  //# sourceMappingURL=backend.d.ts.map
@@ -83,6 +83,10 @@ export interface WorkspaceSearchHit {
83
83
  /** Path relative to the search root. */
84
84
  rel: string;
85
85
  isDir: boolean;
86
+ /** Optional raw matched line (`path:line:content`, engine-style) for a
87
+ * content grep when the backend can report snippets; absent when the
88
+ * search only returns matching paths. */
89
+ match?: string;
86
90
  }
87
91
  /** Filename / content search capability. */
88
92
  export interface WorkspaceSearchService {
@@ -1,48 +1,64 @@
1
- /**
2
- * Ledger-backed workspace router the concrete `WorkspaceRouter` that ties
3
- * the ledger's anchor index, the namespace codec, and the provider registry
4
- * together. Any provider can be routed through this router with zero SSH
5
- * knowledge: a session cwd inside a workspace's anchor resolves to that
6
- * workspace's provider connection; an explicit `wfs://…` (or legacy `ssh:…`)
7
- * target key resolves to the owning connection.
8
- *
9
- * The router opens connections lazily and caches them per workspace id;
10
- * removed workspaces drop their connections so stale keys fail closed.
11
- *
12
- * @module @tiphareth/dsh-hardssh/base/ledger-router
13
- */
14
- import type { WorkspaceConnection, WorkspaceRecord } from './model.ts';
1
+ /** Ledger-backed routing, readiness, single-flight opening, and connection ownership. */
2
+ import type { WorkspaceLedger } from './ledger.ts';
3
+ import type { WorkspaceConnection, WorkspaceId, WorkspaceRecord } from './model.ts';
15
4
  import { type WorkspaceNamespaceCodec, type WorkspaceRoute } from './namespace.ts';
16
- import type { WorkspaceRegistry } from './registry.ts';
17
- import { type WorkspaceLedger } from './ledger.ts';
5
+ import type { WorkspaceProviderRegistry } from './registry.ts';
18
6
  import type { WorkspaceResolution, WorkspaceRouter } from './router.ts';
19
- /** Open-or-reuse strategy for workspace connections. */
7
+ /** Router configuration that remains independent of any concrete provider. */
20
8
  export interface LedgerRouterOptions {
21
9
  codec?: WorkspaceNamespaceCodec;
22
- /** Called with the record; resolves to a connection (undefined = cannot open). */
23
- open(record: WorkspaceRecord, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
24
- /** Called when a workspace is removed (drop cache / pool leases). */
25
- onRemoved?(workspaceId: string): void;
10
+ logger?: Pick<Console, 'warn' | 'error' | 'info' | 'debug'>;
26
11
  }
27
- /** Ledger + registry based router. */
12
+ /** Ledger + provider-registry based router. */
28
13
  export declare class LedgerWorkspaceRouter implements WorkspaceRouter {
29
14
  private readonly ledger;
30
- private readonly registry;
15
+ private readonly providers;
31
16
  private readonly options;
32
17
  readonly codec: WorkspaceNamespaceCodec;
33
18
  private readonly connections;
19
+ private readonly connectionBindings;
20
+ private readonly opening;
21
+ private readonly closing;
34
22
  private readonly anchors;
35
- constructor(ledger: WorkspaceLedger, registry: WorkspaceRegistry, options: LedgerRouterOptions);
23
+ private readonly lifecycleAbort;
24
+ private initializePromise;
25
+ private ready;
26
+ private closed;
27
+ private ledgerDisposer;
28
+ private providerDisposer;
29
+ constructor(ledger: WorkspaceLedger, providers: WorkspaceProviderRegistry, options?: LedgerRouterOptions);
30
+ /** ensureOpen() warms one supplied record only, so initialize loads, subscribes, and preopens the complete persisted ledger before readiness. */
31
+ initialize(): Promise<void>;
32
+ /** initialize() is asynchronous, so isReady provides the synchronous seam gate it cannot provide. */
33
+ isReady(): boolean;
34
+ /** isReady() only reports a snapshot, so whenReady lets asynchronous consumers await the initialization outcome. */
35
+ whenReady(): Promise<void>;
36
+ /** ensureOpen() requires a record, so openById performs fail-closed ledger identity lookup before single-flight opening. */
37
+ openById(id: WorkspaceId, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
38
+ /** openById() cannot translate a host anchor, so openByAnchor resolves ledger ownership before opening. */
39
+ openByAnchor(path: string, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
40
+ /** initialize() owns promise reuse, so initializeInternal performs the one actual load/subscribe/preopen sequence. */
41
+ private initializeInternal;
36
42
  private reindex;
37
43
  private openFor;
38
44
  fromNamespace(key: string): WorkspaceResolution | undefined;
39
45
  fromAnchor(cwd: string | undefined): WorkspaceConnection | undefined;
40
46
  resolveRoute(route: WorkspaceRoute): WorkspaceResolution | undefined;
41
- /** Async open used by the adapter at mount time (pre-warm active records). */
47
+ /** Async compatibility entry used by older adapters that already hold a detached record. */
42
48
  ensureOpen(record: WorkspaceRecord, signal?: AbortSignal): Promise<WorkspaceConnection | undefined>;
43
49
  /** All currently cached connections (for lifecycle/teardown). */
44
50
  connectionsSnapshot(): WorkspaceConnection[];
45
- /** Close every cached connection (plugin teardown). */
51
+ /** Close every cached and in-flight connection and release both subscriptions. */
46
52
  closeAll(): Promise<void>;
53
+ /** reindex() only updates synchronous ownership, so this handler also closes and reopens resources after ledger commits. */
54
+ private handleLedgerChange;
55
+ /** openFor() cannot observe provider lifecycle events itself, so this handler invalidates unregisters and preopens later registrations. */
56
+ private handleProviderChange;
57
+ /** openFor() reuses an in-flight promise, so reopen waits for stale work before retrying the current record. */
58
+ private reopen;
59
+ /** reindex() cannot await resource release, so detach removes routing synchronously and tracks close completion separately. */
60
+ private detach;
61
+ /** WorkspaceConnection.close() alone is not tracked, so closeConnection lets closeAll await invalidation work already in progress. */
62
+ private closeConnection;
47
63
  }
48
64
  //# sourceMappingURL=ledger-router.d.ts.map
@@ -1,17 +1,8 @@
1
1
  /**
2
- * Generic workspace ledger the provider-agnostic persistence and routing
3
- * index for workspace records. It generalizes the SSH-bound ledger's proven
4
- * mechanics (atomic write, sync anchor index, subscription) without any
5
- * SSH-typed fields: a record is a `WorkspaceRecord` whose `provider` ref
6
- * names the owning provider, and anchors are plain local directories.
7
- *
8
- * Concrete providers keep their own specialized stores (e.g. the SSH host
9
- * store) and reference them through `provider.connectionRef`; the ledger
10
- * itself never touches provider configs.
11
- *
12
- * @module @tiphareth/dsh-hardssh/base/ledger
2
+ * Provider-neutral workspace ledger with strict loading, atomic persistence,
3
+ * detached snapshots, and one serialized mutation queue.
13
4
  */
14
- import type { WorkspaceRecord } from './model.ts';
5
+ import type { WorkspaceCreateInput, WorkspaceRecord, WorkspaceUpdate } from './model.ts';
15
6
  /** One committed ledger snapshot. */
16
7
  export interface LedgerSnapshot {
17
8
  revision: number;
@@ -27,20 +18,36 @@ export type LedgerChange = {
27
18
  revision: number;
28
19
  before: WorkspaceRecord;
29
20
  record: WorkspaceRecord;
21
+ } | {
22
+ type: 'updated';
23
+ revision: number;
24
+ before: WorkspaceRecord;
25
+ record: WorkspaceRecord;
30
26
  } | {
31
27
  type: 'removed';
32
28
  revision: number;
33
29
  record: WorkspaceRecord;
30
+ } | {
31
+ type: 'replaced';
32
+ revision: number;
33
+ before: WorkspaceRecord[];
34
+ records: WorkspaceRecord[];
34
35
  };
35
36
  export type LedgerListener = (change: LedgerChange) => void;
36
37
  /** Normalize an anchor for comparison (Windows case-insensitive; POSIX not). */
37
38
  export declare function normalizeAnchorPath(path: string): string;
38
39
  /** True when `candidate` equals `anchor` or is one of its descendants (lexical). */
39
40
  export declare function isPathUnderAnchor(anchor: string, candidate: string): boolean;
41
+ /** Options for one atomic whole-ledger replacement. */
42
+ export interface LedgerReplaceOptions {
43
+ /** Copy the current persisted ledger here before overwriting it. */
44
+ backupPath?: string;
45
+ }
46
+ /** Generic ledger schema currently accepted by strict loading. */
47
+ export declare const WORKSPACE_LEDGER_SCHEMA_VERSION: 1;
40
48
  /**
41
- * The generic ledger. Persisted as a JSON file; mutations serialized through
42
- * a per-instance queue with atomic rename; a synchronous anchor index is
43
- * maintained so the switch layer can resolve a cwd without awaiting I/O.
49
+ * The generic ledger. The in-memory state changes only after an atomic save,
50
+ * and every public observation is detached from that state.
44
51
  */
45
52
  export declare class WorkspaceLedger {
46
53
  private readonly fileOverride?;
@@ -70,13 +77,17 @@ export declare class WorkspaceLedger {
70
77
  get(id: string): Promise<WorkspaceRecord | undefined>;
71
78
  findByAnchor(path: string): Promise<WorkspaceRecord | undefined>;
72
79
  findByAnchorSync(path: string): WorkspaceRecord | undefined;
73
- /** Create a record: materialize the anchor (managed mode), persist, commit. */
74
- create(input: Omit<WorkspaceRecord, 'id' | 'createdAt' | 'updatedAt'> & {
75
- id?: string;
76
- }): Promise<WorkspaceRecord>;
80
+ /** Create cannot preserve caller timestamps, so migration uses replaceAll instead. */
81
+ create(input: WorkspaceCreateInput): Promise<WorkspaceRecord>;
77
82
  rename(id: string, title: string): Promise<WorkspaceRecord | undefined>;
83
+ /** WorkspaceLedger.rename cannot change provider bindings or extension data, so update applies a generic patch. */
84
+ update(id: string, patch: WorkspaceUpdate): Promise<WorkspaceRecord | undefined>;
85
+ /** WorkspaceLedger.create persists one generated record, so it cannot atomically import a complete authoritative snapshot. */
86
+ replaceAll(records: readonly WorkspaceRecord[], options?: LedgerReplaceOptions): Promise<void>;
78
87
  /** Remove a record. The anchor directory is left in place (caller decides). */
79
88
  remove(id: string): Promise<boolean>;
80
89
  anchorsRoot(): string;
81
90
  }
91
+ /** isRecord validates one shape only, so this validator also rejects duplicate ids and ambiguous anchors across records. */
92
+ export declare function assertValidRecords(values: readonly unknown[]): asserts values is readonly WorkspaceRecord[];
82
93
  //# sourceMappingURL=ledger.d.ts.map
@@ -62,13 +62,22 @@ export interface WorkspaceRecord {
62
62
  /** Provider/feature extension bag. Never holds secrets. */
63
63
  extensions?: Record<string, unknown>;
64
64
  }
65
- /** One provider capability key (extensible via module augmentation). */
65
+ /** Provider API major implemented by the generic workspace foundations. */
66
+ export declare const WORKSPACE_PROVIDER_API_VERSION: 2;
67
+ /**
68
+ * Provider capability augmentation point. Runtime packages add their official
69
+ * capability types here; the platform-neutral base deliberately defines none.
70
+ */
66
71
  export interface WorkspaceCapabilityMap {
67
- 'workspace.fs': import('./capability.ts').WorkspaceFileSystem;
68
- 'workspace.process': import('./capability.ts').WorkspaceProcessRuntime;
69
- 'workspace.terminal': import('./capability.ts').WorkspaceTerminalService;
70
- 'workspace.search': import('./capability.ts').WorkspaceSearchService;
72
+ /** Unaugmented providers may carry private keys; runtime-known keys gain precise types through augmentation. */
73
+ [capability: string]: unknown;
71
74
  }
75
+ /** Input accepted when creating a record; identity and timestamps are ledger-owned. */
76
+ export type WorkspaceCreateInput = Omit<WorkspaceRecord, 'id' | 'createdAt' | 'updatedAt'> & {
77
+ id?: WorkspaceId;
78
+ };
79
+ /** Mutable record fields; identity, schema, and creation time remain immutable. */
80
+ export type WorkspaceUpdate = Partial<Pick<WorkspaceRecord, 'title' | 'provider' | 'location' | 'anchor' | 'labels' | 'extensions'>>;
72
81
  /** A resolved, open connection to one workspace's provider. */
73
82
  export interface WorkspaceConnection {
74
83
  readonly workspaceId: WorkspaceId;
@@ -91,8 +100,8 @@ export interface WorkspaceProvider {
91
100
  readonly manifest: WorkspaceProviderManifest;
92
101
  /** Validate a record before open (e.g. root shape, ref resolvable). */
93
102
  validate(record: WorkspaceRecord): void | Promise<void>;
94
- /** Open a connection for one record. */
95
- open(record: WorkspaceRecord, context?: WorkspaceOpenContext): Promise<WorkspaceConnection>;
103
+ /** Open a connection for one record under the router-owned lifecycle context. */
104
+ open(record: WorkspaceRecord, context: WorkspaceOpenContext): Promise<WorkspaceConnection>;
96
105
  }
97
106
  /** Static provider identity, checked before any plugin code runs. */
98
107
  export interface WorkspaceProviderManifest {
@@ -103,6 +112,6 @@ export interface WorkspaceProviderManifest {
103
112
  apiVersion: number;
104
113
  displayName: string;
105
114
  /** Capabilities this provider implements. */
106
- capabilities: Array<keyof WorkspaceCapabilityMap | string>;
115
+ capabilities: string[];
107
116
  }
108
117
  //# sourceMappingURL=model.d.ts.map
@@ -76,6 +76,17 @@ export interface WorkspacePlugin {
76
76
  /**
77
77
  * Registry of loaded plugins (provider + feature plugins together). Keeps
78
78
  * the workspace registry and plugin lifecycle in one place.
79
+ *
80
+ * NOT WIRED INTO PRODUCTION YET: the DSH runtime registers the builtin
81
+ * providers directly (`runtime/workspace-core.ts`) and no third-party plugin
82
+ * loads through this host — only tests do. It is kept because it is the
83
+ * documented distribution contract for out-of-tree providers (see SKILLS.md);
84
+ * provider teardown stays ownership-correct for when it is wired up: each
85
+ * registration releases the disposer that `register()` returned for THAT call,
86
+ * so a duplicate (same id+version) registration never unregisters a provider it
87
+ * does not own.
88
+ *
89
+ * @internal Not reachable from any production assembly today.
79
90
  */
80
91
  export declare class WorkspacePluginHost {
81
92
  private readonly registries;
@@ -1,18 +1,14 @@
1
- /**
2
- * Workspace provider registry — the single place providers register and are
3
- * looked up by id. The registry is provider-agnostic: SSH, local, and any
4
- * future provider all register here, and the switch layer resolves
5
- * workspaces through the registry without knowing which provider backs them.
6
- *
7
- * Registration is idempotent per (id, version, apiVersion): re-registering
8
- * the same provider is a no-op / reuse; registering a different or
9
- * incompatible implementation with the same id is an error.
10
- *
11
- * @module @tiphareth/dsh-hardssh/base/registry
12
- */
1
+ /** Provider-neutral registry with observable, ownership-safe registration lifecycle. */
13
2
  import type { WorkspaceProvider, WorkspaceProviderManifest } from './model.ts';
14
- /** Fired when a provider registers (for surface mounts / cache invalidation). */
15
- export type ProviderListener = (provider: WorkspaceProvider) => void;
3
+ /** Provider registration lifecycle event consumed by routers. */
4
+ export type ProviderChange = {
5
+ type: 'registered';
6
+ provider: WorkspaceProvider;
7
+ } | {
8
+ type: 'unregistered';
9
+ provider: WorkspaceProvider;
10
+ };
11
+ export type ProviderListener = (change: ProviderChange) => void;
16
12
  /** Duplicate / incompatible registration error. */
17
13
  export declare class ProviderRegistrationError extends Error {
18
14
  constructor(message: string);
@@ -21,40 +17,41 @@ export declare class ProviderRegistrationError extends Error {
21
17
  export declare class WorkspaceProviderRegistry {
22
18
  private readonly providers;
23
19
  private readonly listeners;
24
- /** Register a provider. Throws on id collision with an incompatible impl. */
20
+ /** Register a provider and return a disposer that only removes that exact registration. */
25
21
  register(provider: WorkspaceProvider): () => void;
26
22
  /** Look up a provider by id. */
27
23
  get(id: string): WorkspaceProvider | undefined;
28
- /** Unregister a provider by id (no-op when absent or different version). */
24
+ /**
25
+ * Remove a provider by id, whoever registered it.
26
+ *
27
+ * Ownership-unsafe by construction: callers that registered a provider must
28
+ * use the disposer returned by register() instead, otherwise teardown can
29
+ * delete a provider owned by another plugin. Kept for whole-registry
30
+ * operations (tests, process-level resets).
31
+ */
29
32
  unregister(id: string): void;
30
33
  /** All registered providers. */
31
34
  list(): WorkspaceProvider[];
32
- /** Subscribe to registrations. Returns a disposer. */
35
+ /** Subscribe must expose both registration and removal, which the old registration-only listener could not report. */
33
36
  subscribe(listener: ProviderListener): () => void;
34
37
  /** True when a provider id is registered. */
35
38
  has(id: string): boolean;
36
39
  /** Static identity of a registered provider (for manifests / UI). */
37
40
  manifestOf(id: string): WorkspaceProviderManifest | undefined;
41
+ /** register() cannot safely repeat listener isolation for both lifecycle directions, so emit centralizes it. */
42
+ private emit;
38
43
  }
39
- /** The process-wide shared registry. */
40
- export declare const globalWorkspaceRegistry: WorkspaceProviderRegistry;
41
- /**
42
- * Workspace registry surface used by plugins: list / watch workspace records
43
- * and register providers. Implemented by the runtime over the ledger + the
44
- * provider registry.
45
- */
44
+ /** Plugin-facing workspace registry surface. */
46
45
  export interface WorkspaceRegistry {
47
- /** All known workspace records (provider-agnostic). */
48
46
  listWorkspaces(): Promise<import('./model.ts').WorkspaceRecord[]>;
49
- /** Subscribe to workspace record changes. Returns a disposer. */
50
47
  subscribe(listener: (workspace: import('./model.ts').WorkspaceRecord) => void): () => void;
51
- /** Register a provider (idempotent same-version; throws on conflict). */
52
- register(provider: WorkspaceProvider): void;
53
- /** Unregister a provider by id (no-op when absent). */
48
+ register(provider: WorkspaceProvider): () => void;
49
+ /**
50
+ * Ownership-unsafe id-based removal; plugin teardown must release the
51
+ * disposer returned by `register()` instead of calling this.
52
+ */
54
53
  unregister?(providerId: string): void;
55
- /** Look up a provider by id. */
56
54
  provider(id: string): WorkspaceProvider | undefined;
57
- /** All registered providers. */
58
55
  providers(): WorkspaceProvider[];
59
56
  }
60
57
  //# sourceMappingURL=registry.d.ts.map
@@ -30,11 +30,4 @@ export interface WorkspaceRouter {
30
30
  /** Map a route back to a resolution (used by the switch to re-encode child keys). */
31
31
  resolveRoute(route: WorkspaceRoute): WorkspaceResolution | undefined;
32
32
  }
33
- /** Convenience base: default codec wiring plus helper for resolution lookup. */
34
- export declare abstract class BaseWorkspaceRouter implements WorkspaceRouter {
35
- abstract readonly codec: WorkspaceNamespaceCodec;
36
- abstract fromNamespace(key: string): WorkspaceResolution | undefined;
37
- abstract fromAnchor(cwd: string | undefined): WorkspaceConnection | undefined;
38
- abstract resolveRoute(route: WorkspaceRoute): WorkspaceResolution | undefined;
39
- }
40
33
  //# sourceMappingURL=router.d.ts.map
@@ -4,11 +4,15 @@
4
4
  * endpoints the config dialog needs (host create + test). Plain fetch, same
5
5
  * origin 鈥?the only data path the panel components use.
6
6
  */
7
- import { HttpApiError } from '../client-http.ts';
8
- import { type DirListing, type FileRead, type FileWriteResult, type RemoteDirEntry, type SearchView, type SshWorkspaceRecord } from '../protocol.ts';
9
- /** Error carrying the route's JSON error message and stable code/status. */
7
+ import { HttpApiError, type HttpErrorBody } from '../client-http.ts';
8
+ import { type RemoteDirEntry, type SshWorkspaceRecord } from '../protocol.ts';
9
+ /**
10
+ * Workspace-route error: the shared transport error under this family's name.
11
+ * It adds no parsing — `readJson` in ../client-http.ts owns status/body
12
+ * decoding for both route families.
13
+ */
10
14
  export declare class WorkspaceApiError extends HttpApiError {
11
- constructor(message: string, code?: string, status?: number);
15
+ constructor(message: string, status?: number, body?: HttpErrorBody);
12
16
  }
13
17
  /** The workspace route family client. */
14
18
  export declare class WorkspaceApi {
@@ -36,10 +40,6 @@ export declare class WorkspaceApi {
36
40
  path: string;
37
41
  entries: RemoteDirEntry[];
38
42
  }>;
39
- list(root: string, path: string): Promise<DirListing>;
40
- read(root: string, path: string): Promise<FileRead>;
41
- write(root: string, path: string, content: string, expectedMtime?: number): Promise<FileWriteResult>;
42
- search(root: string, queryText: string): Promise<SearchView>;
43
43
  }
44
44
  /** The two /api/dsh-ssh endpoints the config dialog needs (host create/test). */
45
45
  export interface SshHostPayload {
@@ -0,0 +1,13 @@
1
+ import type { SshApi } from './ssh/api.ts';
2
+ /**
3
+ * Probe + prompt until connected. Resolves true when the alias may proceed;
4
+ * false when the user cancelled or the failure is not interactive. When a
5
+ * credential was already supplied and the retry still fails (wrong password,
6
+ * unreachable host, network drop, …), the password dialog re-opens WITH the
7
+ * failure reason shown, VSCode style — no silent "stuck" states.
8
+ *
9
+ * @param api - the SSH API client.
10
+ * @param alias - the host alias to gate.
11
+ */
12
+ export declare function connectHost(api: SshApi, alias: string): Promise<boolean>;
13
+ //# sourceMappingURL=connect-host.d.ts.map
@@ -17,17 +17,19 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
17
17
  'dsh-hardssh': WorkspaceKey;
18
18
  }
19
19
  }
20
- export type { PanelControllerSnapshot } from './ssh/panel/controller.ts';
21
20
  export type { SshPanelProps } from './ssh/panel/SshPanel.tsx';
22
- export type { HostsTabProps } from './ssh/panel/HostsTab.tsx';
23
21
  export type { HostFormDialogProps } from './ssh/panel/HostFormDialog.tsx';
24
22
  export type { TerminalTabProps } from './ssh/panel/TerminalTab.tsx';
25
23
  export type { TransferTabProps } from './ssh/panel/TransferTab.tsx';
26
24
  export type { TunnelsTabProps } from './ssh/panel/TunnelsTab.tsx';
27
25
  export type { ClusterTabProps } from './ssh/panel/ClusterTab.tsx';
28
26
  export type { SshKey } from './ssh/locales.ts';
29
- /** Required services: slots for the header buttons + directory-flow holes,
30
- * locale for the copy, workspaces for the native local-directory chooser. */
27
+ /** Required services: slots for the extension seats, locale for copy,
28
+ * workspaces for the native local-directory chooser, sessions for the
29
+ * open/new-session connection gate, and sidebarRightTabs for the SSH
30
+ * operations console type. Keep the registry explicit: without it Cordis may
31
+ * activate this client before ui-sidebar-right provides the service; the
32
+ * guarded registration then degrades silently and no HardSSH tab exists. */
31
33
  export declare const inject: string[];
32
34
  /** Apply the browser half. */
33
35
  export declare function apply(ctx: ClientContext): void;