@tiphareth/dsh-hardssh 0.1.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 (165) hide show
  1. package/README.md +50 -0
  2. package/cordis.patch.yml +26 -0
  3. package/lib/client.js +17780 -0
  4. package/lib/environment-BL1jddfB.js +449 -0
  5. package/lib/fs.js +478 -0
  6. package/lib/index.js +6758 -0
  7. package/lib/subprocess.js +600 -0
  8. package/lib/switch-fs-CAJpFY9C.js +193 -0
  9. package/lib/switch-fs-RrZtG2gv.js +210 -0
  10. package/lib/types/backend.d.ts +108 -0
  11. package/lib/types/base/capability.d.ts +107 -0
  12. package/lib/types/base/index.d.ts +18 -0
  13. package/lib/types/base/ledger-router.d.ts +48 -0
  14. package/lib/types/base/ledger.d.ts +82 -0
  15. package/lib/types/base/model.d.ts +108 -0
  16. package/lib/types/base/namespace.d.ts +57 -0
  17. package/lib/types/base/plugin.d.ts +102 -0
  18. package/lib/types/base/registry.d.ts +60 -0
  19. package/lib/types/base/router.d.ts +40 -0
  20. package/lib/types/client/api.d.ts +85 -0
  21. package/lib/types/client/directory-flow.d.ts +82 -0
  22. package/lib/types/client/icons.d.ts +19 -0
  23. package/lib/types/client/index.d.ts +34 -0
  24. package/lib/types/client/locales.d.ts +104 -0
  25. package/lib/types/client/manager-button.d.ts +32 -0
  26. package/lib/types/client/migrate.d.ts +20 -0
  27. package/lib/types/client/ssh/api.d.ts +89 -0
  28. package/lib/types/client/ssh/apply.d.ts +23 -0
  29. package/lib/types/client/ssh/locales.d.ts +156 -0
  30. package/lib/types/client/ssh/mount.d.ts +13 -0
  31. package/lib/types/client/ssh/panel/ClusterTab.d.ts +8 -0
  32. package/lib/types/client/ssh/panel/HostFingerprintDialog.d.ts +16 -0
  33. package/lib/types/client/ssh/panel/HostFormDialog.d.ts +13 -0
  34. package/lib/types/client/ssh/panel/HostsTab.d.ts +10 -0
  35. package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +16 -0
  36. package/lib/types/client/ssh/panel/SshPanel.d.ts +14 -0
  37. package/lib/types/client/ssh/panel/TerminalTab.d.ts +12 -0
  38. package/lib/types/client/ssh/panel/TransferTab.d.ts +8 -0
  39. package/lib/types/client/ssh/panel/TunnelsTab.d.ts +8 -0
  40. package/lib/types/client/ssh/panel/controller.d.ts +23 -0
  41. package/lib/types/client/ssh/panel/helpers.d.ts +15 -0
  42. package/lib/types/client/ssh/panel/xterm.css.d.ts +3 -0
  43. package/lib/types/client/ssh/sidebar-entry.d.ts +25 -0
  44. package/lib/types/client/state.d.ts +32 -0
  45. package/lib/types/client/text.d.ts +11 -0
  46. package/lib/types/client/workspace-badges.d.ts +38 -0
  47. package/lib/types/client/workspace-gate.d.ts +15 -0
  48. package/lib/types/client-http.d.ts +15 -0
  49. package/lib/types/core.d.ts +43 -0
  50. package/lib/types/fs.d.ts +36 -0
  51. package/lib/types/host-http.d.ts +25 -0
  52. package/lib/types/index.d.ts +53 -0
  53. package/lib/types/ledger.d.ts +132 -0
  54. package/lib/types/protocol.d.ts +101 -0
  55. package/lib/types/providers/index.d.ts +19 -0
  56. package/lib/types/providers/local/provider.d.ts +58 -0
  57. package/lib/types/providers/ssh/provider.d.ts +112 -0
  58. package/lib/types/remote/environment.d.ts +29 -0
  59. package/lib/types/remote/output.d.ts +38 -0
  60. package/lib/types/remote/remote-fs.d.ts +69 -0
  61. package/lib/types/remote/remote-process.d.ts +41 -0
  62. package/lib/types/remote/remote-subprocess.d.ts +31 -0
  63. package/lib/types/remote/remote-terminal.d.ts +41 -0
  64. package/lib/types/remote-runner.d.ts +83 -0
  65. package/lib/types/remote-search.d.ts +50 -0
  66. package/lib/types/routes.d.ts +24 -0
  67. package/lib/types/runtime/workspace-core.d.ts +59 -0
  68. package/lib/types/seam-state.d.ts +69 -0
  69. package/lib/types/shell.d.ts +8 -0
  70. package/lib/types/ssh/connection/lease.d.ts +16 -0
  71. package/lib/types/ssh/connection/pool.d.ts +57 -0
  72. package/lib/types/ssh/engine.d.ts +434 -0
  73. package/lib/types/ssh/exec/output.d.ts +31 -0
  74. package/lib/types/ssh/known-hosts.d.ts +89 -0
  75. package/lib/types/ssh/plugin.d.ts +57 -0
  76. package/lib/types/ssh/protocol.d.ts +236 -0
  77. package/lib/types/ssh/routes.d.ts +44 -0
  78. package/lib/types/ssh/store.d.ts +107 -0
  79. package/lib/types/ssh/tools.d.ts +35 -0
  80. package/lib/types/ssh/transfer/progress.d.ts +14 -0
  81. package/lib/types/ssh/vault.d.ts +110 -0
  82. package/lib/types/subprocess.d.ts +32 -0
  83. package/lib/types/switch/switch-fs.d.ts +88 -0
  84. package/lib/types/switch/switch-subprocess.d.ts +34 -0
  85. package/lib/types/tools.d.ts +12 -0
  86. package/package.json +134 -0
  87. package/src/backend.ts +624 -0
  88. package/src/base/capability.ts +86 -0
  89. package/src/base/index.ts +18 -0
  90. package/src/base/ledger-router.ts +128 -0
  91. package/src/base/ledger.ts +299 -0
  92. package/src/base/model.ts +118 -0
  93. package/src/base/namespace.ts +102 -0
  94. package/src/base/plugin.ts +170 -0
  95. package/src/base/registry.ts +109 -0
  96. package/src/base/router.ts +43 -0
  97. package/src/client/api.ts +196 -0
  98. package/src/client/css-modules.d.ts +5 -0
  99. package/src/client/directory-flow.tsx +482 -0
  100. package/src/client/icons.tsx +50 -0
  101. package/src/client/index.ts +210 -0
  102. package/src/client/locales.ts +105 -0
  103. package/src/client/manager-button.tsx +269 -0
  104. package/src/client/migrate.ts +109 -0
  105. package/src/client/ssh/api.ts +411 -0
  106. package/src/client/ssh/apply.ts +50 -0
  107. package/src/client/ssh/locales.ts +322 -0
  108. package/src/client/ssh/mount.tsx +83 -0
  109. package/src/client/ssh/panel/ClusterTab.tsx +123 -0
  110. package/src/client/ssh/panel/HostFingerprintDialog.tsx +82 -0
  111. package/src/client/ssh/panel/HostFormDialog.tsx +228 -0
  112. package/src/client/ssh/panel/HostsTab.tsx +236 -0
  113. package/src/client/ssh/panel/SessionSecretDialog.tsx +80 -0
  114. package/src/client/ssh/panel/SshPanel.tsx +77 -0
  115. package/src/client/ssh/panel/TerminalTab.tsx +176 -0
  116. package/src/client/ssh/panel/TransferTab.tsx +232 -0
  117. package/src/client/ssh/panel/TunnelsTab.tsx +177 -0
  118. package/src/client/ssh/panel/controller.ts +48 -0
  119. package/src/client/ssh/panel/helpers.ts +26 -0
  120. package/src/client/ssh/panel/panel.module.css +1006 -0
  121. package/src/client/ssh/panel/xterm.css.ts +2 -0
  122. package/src/client/ssh/sidebar-entry.ts +123 -0
  123. package/src/client/state.ts +99 -0
  124. package/src/client/text.ts +22 -0
  125. package/src/client/workspace-badges.ts +96 -0
  126. package/src/client/workspace-gate.ts +225 -0
  127. package/src/client/workspace.module.css +283 -0
  128. package/src/client-http.ts +45 -0
  129. package/src/core.ts +47 -0
  130. package/src/fs.ts +85 -0
  131. package/src/host-http.ts +74 -0
  132. package/src/index.ts +244 -0
  133. package/src/ledger.ts +416 -0
  134. package/src/protocol.ts +114 -0
  135. package/src/providers/index.ts +34 -0
  136. package/src/providers/local/provider.ts +179 -0
  137. package/src/providers/ssh/provider.ts +274 -0
  138. package/src/remote/environment.ts +123 -0
  139. package/src/remote/output.ts +142 -0
  140. package/src/remote/remote-fs.ts +532 -0
  141. package/src/remote/remote-process.ts +203 -0
  142. package/src/remote/remote-subprocess.ts +159 -0
  143. package/src/remote/remote-terminal.ts +141 -0
  144. package/src/remote-runner.ts +201 -0
  145. package/src/remote-search.ts +163 -0
  146. package/src/routes.ts +395 -0
  147. package/src/runtime/workspace-core.ts +154 -0
  148. package/src/seam-state.ts +185 -0
  149. package/src/shell.ts +10 -0
  150. package/src/ssh/connection/lease.ts +17 -0
  151. package/src/ssh/connection/pool.ts +275 -0
  152. package/src/ssh/engine.ts +1761 -0
  153. package/src/ssh/exec/output.ts +70 -0
  154. package/src/ssh/known-hosts.ts +214 -0
  155. package/src/ssh/plugin.ts +184 -0
  156. package/src/ssh/protocol.ts +227 -0
  157. package/src/ssh/routes.ts +892 -0
  158. package/src/ssh/store.ts +544 -0
  159. package/src/ssh/tools.ts +402 -0
  160. package/src/ssh/transfer/progress.ts +75 -0
  161. package/src/ssh/vault.ts +477 -0
  162. package/src/subprocess.ts +71 -0
  163. package/src/switch/switch-fs.ts +253 -0
  164. package/src/switch/switch-subprocess.ts +59 -0
  165. package/src/tools.ts +221 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Plugin registration / distribution — the extension contract that lets ANY
3
+ * third-party plugin (not just SSH) contribute workspace capability.
4
+ *
5
+ * Two plugin kinds share one manifest+activate shape:
6
+ * - **Provider plugins** register `WorkspaceProvider`s (local, ssh, docker,
7
+ * wsl, cloud devbox, …) through the plugin context.
8
+ * - **Feature plugins** attach provider-agnostic features (indexing, audit
9
+ * tools, diagnostics, UI) to workspaces that expose the capabilities they
10
+ * need — without importing any provider implementation.
11
+ *
12
+ * DSH-specific surfaces (agent tools, routes, prompt sections, client
13
+ * entries) are contributed through the runtime bridge, never through this
14
+ * core contract, so `base` stays free of Cordis/React.
15
+ *
16
+ * @module @tiphareth/dsh-hardssh/base/plugin
17
+ */
18
+
19
+ import type { WorkspaceProvider, WorkspaceRecord } from './model.ts'
20
+ import type { WorkspaceRegistry } from './registry.ts'
21
+
22
+ /** A disposable registration handle. */
23
+ export interface Disposable {
24
+ dispose(): void
25
+ }
26
+
27
+ /** Static plugin identity, readable before any plugin code executes. */
28
+ export interface WorkspacePluginManifest {
29
+ /** Stable plugin id (dotted, reverse-dns style recommended). */
30
+ id: string
31
+ version: string
32
+ /** Compatibility gate; aligned with the base API major. */
33
+ apiVersion: number
34
+ displayName: string
35
+ /** What this plugin contributes (for capability gating / UI). */
36
+ contributes?: {
37
+ providers?: string[]
38
+ features?: string[]
39
+ clientEntries?: string[]
40
+ }
41
+ }
42
+
43
+ /** Context handed to a plugin's `activate`. */
44
+ export interface WorkspacePluginContext {
45
+ /** Register one provider; returns a disposer. */
46
+ registerProvider(provider: WorkspaceProvider): Disposable
47
+ /** Register one provider-agnostic feature. */
48
+ registerFeature(feature: WorkspaceFeature): Disposable
49
+ /** The shared workspace registry (list / watch workspaces). */
50
+ workspaces: WorkspaceRegistry
51
+ /** Fired when a workspace record changes (for feature lifecycle). */
52
+ onWorkspaceChange(listener: (workspace: WorkspaceRecord) => void): Disposable
53
+ }
54
+
55
+ /** A provider-agnostic feature attached to compatible workspaces. */
56
+ export interface WorkspaceFeature {
57
+ id: string
58
+ /** True when this feature wants the given workspace (capability gating). */
59
+ supports(input: {
60
+ workspace: WorkspaceRecord
61
+ capabilities: ReadonlySet<string>
62
+ }): boolean
63
+ /** Called once per compatible workspace while the feature is active. */
64
+ activate(context: WorkspaceFeatureContext): void | Promise<void>
65
+ deactivate?(): void | Promise<void>
66
+ }
67
+
68
+ /** Feature context: per-workspace connection + lifecycle. */
69
+ export interface WorkspaceFeatureContext {
70
+ workspace: WorkspaceRecord
71
+ /** Open connection; undefined only if the provider failed to open. */
72
+ connection: import('./model.ts').WorkspaceConnection | undefined
73
+ /** Mark the feature unusable for this workspace (degrades gracefully). */
74
+ unavailable(): void
75
+ }
76
+
77
+ /** The full plugin shape: static manifest + activate. */
78
+ export interface WorkspacePlugin {
79
+ readonly manifest: WorkspacePluginManifest
80
+ activate(context: WorkspacePluginContext): void | Promise<void>
81
+ deactivate?(): void | Promise<void>
82
+ }
83
+
84
+ /**
85
+ * Registry of loaded plugins (provider + feature plugins together). Keeps
86
+ * the workspace registry and plugin lifecycle in one place.
87
+ */
88
+ export class WorkspacePluginHost {
89
+ private readonly plugins = new Map<string, WorkspacePlugin>()
90
+ private readonly features = new Set<WorkspaceFeature>()
91
+ /** Per-plugin disposers collected from activate (provider/feature/workspace listeners). */
92
+ private readonly pluginDisposers = new Map<string, Array<() => void>>()
93
+
94
+ constructor(
95
+ private readonly registries: {
96
+ workspaces: WorkspaceRegistry
97
+ },
98
+ ) {}
99
+
100
+ /** Load a plugin: validate manifest, run activate. Throws on failure. */
101
+ async load(plugin: WorkspacePlugin): Promise<void> {
102
+ if (this.plugins.has(plugin.manifest.id)) {
103
+ throw new Error(`plugin '${plugin.manifest.id}' already loaded`)
104
+ }
105
+ if (plugin.manifest.apiVersion !== 1) {
106
+ throw new Error(`plugin '${plugin.manifest.id}' apiVersion ${plugin.manifest.apiVersion} is not supported (need 1)`)
107
+ }
108
+ const disposers: Array<() => void> = []
109
+ await plugin.activate({
110
+ registerProvider: (provider) => {
111
+ const disposer = this.registerProvider(provider)
112
+ disposers.push(disposer)
113
+ return { dispose: disposer }
114
+ },
115
+ registerFeature: (feature) => {
116
+ // Track contributions by plugin so unload tears them down.
117
+ const disposer = () => { this.features.delete(feature) }
118
+ disposers.push(disposer)
119
+ this.features.add(feature)
120
+ return { dispose: disposer }
121
+ },
122
+ workspaces: this.registries.workspaces,
123
+ onWorkspaceChange: (listener) => {
124
+ const disposer = this.registries.workspaces.subscribe(listener)
125
+ disposers.push(disposer)
126
+ return { dispose: disposer }
127
+ },
128
+ })
129
+ this.pluginDisposers.set(plugin.manifest.id, disposers)
130
+ this.plugins.set(plugin.manifest.id, plugin)
131
+ }
132
+
133
+ /** Unload a plugin, disposing every contribution (providers, features, listeners). */
134
+ async unload(id: string): Promise<void> {
135
+ const plugin = this.plugins.get(id)
136
+ if (plugin === undefined) return
137
+ await plugin.deactivate?.()
138
+ const disposers = this.pluginDisposers.get(id) ?? []
139
+ for (const dispose of disposers) {
140
+ try { dispose() } catch { /* disposer isolation */ }
141
+ }
142
+ this.pluginDisposers.delete(id)
143
+ this.plugins.delete(id)
144
+ }
145
+
146
+ private registerProvider(provider: WorkspaceProvider): () => void {
147
+ // The registry returns a disposer on register; a same-version duplicate
148
+ // no-ops, so the disposer may be a no-op too — unload still runs it.
149
+ let disposed = false
150
+ this.registries.workspaces.register(provider)
151
+ return () => {
152
+ if (disposed) return
153
+ disposed = true
154
+ this.registries.workspaces.unregister?.(provider.manifest.id)
155
+ }
156
+ }
157
+
158
+ /** Active features (for a runtime to wire up per-workspace surfaces). */
159
+ featuresFor(workspace: WorkspaceRecord, capabilities: ReadonlySet<string>): WorkspaceFeature[] {
160
+ return [...this.features].filter(feature => feature.supports({ workspace, capabilities }))
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Minimal capability set surface used by the DSH runtime to decide which
166
+ * features apply to a workspace. Computed from the provider manifest.
167
+ */
168
+ export function capabilitiesOf(provider: WorkspaceProvider): ReadonlySet<string> {
169
+ return new Set(provider.manifest.capabilities)
170
+ }
@@ -0,0 +1,109 @@
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
+ */
13
+
14
+ import type { WorkspaceProvider, WorkspaceProviderManifest } from './model.ts'
15
+
16
+ /** Fired when a provider registers (for surface mounts / cache invalidation). */
17
+ export type ProviderListener = (provider: WorkspaceProvider) => void
18
+
19
+ /** Duplicate / incompatible registration error. */
20
+ export class ProviderRegistrationError extends Error {
21
+ constructor(message: string) {
22
+ super(message)
23
+ this.name = 'ProviderRegistrationError'
24
+ }
25
+ }
26
+
27
+ /** In-memory provider registry (process-wide singleton by default). */
28
+ export class WorkspaceProviderRegistry {
29
+ private readonly providers = new Map<string, WorkspaceProvider>()
30
+ private readonly listeners = new Set<ProviderListener>()
31
+
32
+ /** Register a provider. Throws on id collision with an incompatible impl. */
33
+ register(provider: WorkspaceProvider): () => void {
34
+ const id = provider.manifest.id
35
+ const existing = this.providers.get(id)
36
+ if (existing !== undefined) {
37
+ const same = existing.manifest.version === provider.manifest.version
38
+ && existing.manifest.apiVersion === provider.manifest.apiVersion
39
+ if (!same) {
40
+ throw new ProviderRegistrationError(
41
+ `provider '${id}' already registered (${existing.manifest.version}, api ${existing.manifest.apiVersion}); refusing incompatible duplicate`,
42
+ )
43
+ }
44
+ // Same version: reuse. No-op.
45
+ return () => { /* already registered */ }
46
+ }
47
+ this.providers.set(id, provider)
48
+ const disposer = () => { this.providers.delete(id) }
49
+ for (const listener of [...this.listeners]) {
50
+ try { listener(provider) } catch { /* listener isolation */ }
51
+ }
52
+ return disposer
53
+ }
54
+
55
+ /** Look up a provider by id. */
56
+ get(id: string): WorkspaceProvider | undefined {
57
+ return this.providers.get(id)
58
+ }
59
+
60
+ /** Unregister a provider by id (no-op when absent or different version). */
61
+ unregister(id: string): void {
62
+ if (!this.providers.has(id)) return
63
+ this.providers.delete(id)
64
+ }
65
+
66
+ /** All registered providers. */
67
+ list(): WorkspaceProvider[] {
68
+ return [...this.providers.values()]
69
+ }
70
+
71
+ /** Subscribe to registrations. Returns a disposer. */
72
+ subscribe(listener: ProviderListener): () => void {
73
+ this.listeners.add(listener)
74
+ return () => { this.listeners.delete(listener) }
75
+ }
76
+
77
+ /** True when a provider id is registered. */
78
+ has(id: string): boolean {
79
+ return this.providers.has(id)
80
+ }
81
+
82
+ /** Static identity of a registered provider (for manifests / UI). */
83
+ manifestOf(id: string): WorkspaceProviderManifest | undefined {
84
+ return this.providers.get(id)?.manifest
85
+ }
86
+ }
87
+
88
+ /** The process-wide shared registry. */
89
+ export const globalWorkspaceRegistry = new WorkspaceProviderRegistry()
90
+
91
+ /**
92
+ * Workspace registry surface used by plugins: list / watch workspace records
93
+ * and register providers. Implemented by the runtime over the ledger + the
94
+ * provider registry.
95
+ */
96
+ export interface WorkspaceRegistry {
97
+ /** All known workspace records (provider-agnostic). */
98
+ listWorkspaces(): Promise<import('./model.ts').WorkspaceRecord[]>
99
+ /** Subscribe to workspace record changes. Returns a disposer. */
100
+ subscribe(listener: (workspace: import('./model.ts').WorkspaceRecord) => void): () => void
101
+ /** Register a provider (idempotent same-version; throws on conflict). */
102
+ register(provider: WorkspaceProvider): void
103
+ /** Unregister a provider by id (no-op when absent). */
104
+ unregister?(providerId: string): void
105
+ /** Look up a provider by id. */
106
+ provider(id: string): WorkspaceProvider | undefined
107
+ /** All registered providers. */
108
+ providers(): WorkspaceProvider[]
109
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Generic workspace routing — the switch-layer contracts. A `WorkspaceRouter`
3
+ * decides, for a session cwd or a namespaced target key, which workspace
4
+ * (and thus which provider connection) owns the operation. The switch
5
+ * facades (`switch-fs.ts` / `switch-subprocess.ts` in the runtime layer)
6
+ * consume this router and never branch on provider identity.
7
+ *
8
+ * @module @tiphareth/dsh-hardssh/base/router
9
+ */
10
+
11
+ import type { WorkspaceConnection } from './model.ts'
12
+ import type { WorkspaceNamespaceCodec, WorkspaceRoute } from './namespace.ts'
13
+
14
+ /** One resolved routing decision: the owning workspace connection + raw key. */
15
+ export interface WorkspaceResolution {
16
+ /** Open connection to the owning workspace (ready or degraded). */
17
+ connection: WorkspaceConnection
18
+ /** The raw (un-namespaced) target key for the connection's backend. */
19
+ rawKey: string
20
+ }
21
+
22
+ /**
23
+ * Route resolver contract. Implementations combine the ledger's anchor index
24
+ * (cwd → workspace) with the namespace codec (explicit key → workspace).
25
+ */
26
+ export interface WorkspaceRouter {
27
+ /** Resolve an explicit namespaced key; undefined = unrouteable (fail closed). */
28
+ fromNamespace(key: string): WorkspaceResolution | undefined
29
+ /** Resolve a session cwd to its workspace (undefined = local / unbound). */
30
+ fromAnchor(cwd: string | undefined): WorkspaceConnection | undefined
31
+ /** The namespace codec this router uses (for the switch facade to encode). */
32
+ codec: WorkspaceNamespaceCodec
33
+ /** Map a route back to a resolution (used by the switch to re-encode child keys). */
34
+ resolveRoute(route: WorkspaceRoute): WorkspaceResolution | undefined
35
+ }
36
+
37
+ /** Convenience base: default codec wiring plus helper for resolution lookup. */
38
+ export abstract class BaseWorkspaceRouter implements WorkspaceRouter {
39
+ abstract readonly codec: WorkspaceNamespaceCodec
40
+ abstract fromNamespace(key: string): WorkspaceResolution | undefined
41
+ abstract fromAnchor(cwd: string | undefined): WorkspaceConnection | undefined
42
+ abstract resolveRoute(route: WorkspaceRoute): WorkspaceResolution | undefined
43
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Browser-side API clients: the /api/dsh-hardssh route family (SSH workspace
3
+ * CRUD, remote dir browsing, workspace file ops) plus the two /api/dsh-ssh
4
+ * endpoints the config dialog needs (host create + test). Plain fetch, same
5
+ * origin 鈥?the only data path the panel components use.
6
+ */
7
+
8
+ import { HttpApiError, buildQuery, readJson } from '../client-http.ts'
9
+ import {
10
+ WORKSPACE_API,
11
+ type DirListing,
12
+ type FileRead,
13
+ type FileWriteResult,
14
+ type RemoteDirEntry,
15
+ type SearchView,
16
+ type SshWorkspaceRecord,
17
+ } from '../protocol.ts'
18
+
19
+ /** Error carrying the route's JSON error message and stable code/status. */
20
+ export class WorkspaceApiError extends HttpApiError {
21
+ constructor(
22
+ message: string,
23
+ code?: string,
24
+ status?: number,
25
+ ) {
26
+ super(message, code, status)
27
+ this.name = 'WorkspaceApiError'
28
+ }
29
+ }
30
+
31
+ /** The workspace route family client. */
32
+ export class WorkspaceApi {
33
+ /** List the SSH-bound workspaces. */
34
+ async listWorkspaces(): Promise<SshWorkspaceRecord[]> {
35
+ const response = await fetch(WORKSPACE_API.sshWorkspaces)
36
+ const body = await readJson<{ workspaces: SshWorkspaceRecord[] }>(response)
37
+ return body.workspaces
38
+ }
39
+
40
+ /** Create one SSH-bound workspace. */
41
+ async createWorkspace(input: { title: string; alias: string; remoteRoot: string }): Promise<SshWorkspaceRecord> {
42
+ const response = await fetch(WORKSPACE_API.sshWorkspaces, {
43
+ method: 'POST',
44
+ headers: { 'content-type': 'application/json' },
45
+ body: JSON.stringify(input),
46
+ })
47
+ const body = await readJson<{ workspace: SshWorkspaceRecord }>(response)
48
+ return body.workspace
49
+ }
50
+
51
+ /** Rename one SSH-bound workspace. */
52
+ async renameWorkspace(id: string, title: string): Promise<SshWorkspaceRecord> {
53
+ const response = await fetch(WORKSPACE_API.sshWorkspaces + '/item' + buildQuery({ id }), {
54
+ method: 'PATCH',
55
+ headers: { 'content-type': 'application/json' },
56
+ body: JSON.stringify({ title }),
57
+ })
58
+ const body = await readJson<{ workspace: SshWorkspaceRecord }>(response)
59
+ return body.workspace
60
+ }
61
+
62
+ /** Delete one SSH-bound workspace. */
63
+ async deleteWorkspace(id: string): Promise<void> {
64
+ const response = await fetch(WORKSPACE_API.sshWorkspaces + '/item' + buildQuery({ id }), {
65
+ method: 'DELETE',
66
+ })
67
+ await readJson<{ ok: true }>(response)
68
+ }
69
+
70
+ /** List the configured SSH hosts (from the shared dsh-ssh store). */
71
+ async listHosts(): Promise<Array<{ alias: string; host: string; port: number; user: string }>> {
72
+ const response = await fetch(WORKSPACE_API.sshWorkspaces + '/hosts')
73
+ const body = await readJson<{ hosts: Array<{ alias: string; host: string; port: number; user: string }> }>(response)
74
+ return body.hosts
75
+ }
76
+
77
+ /** Browse a remote directory (for the workspace picker). */
78
+ async listRemoteDir(alias: string, path?: string): Promise<{ path: string; entries: RemoteDirEntry[] }> {
79
+ const response = await fetch(WORKSPACE_API.sshWorkspaceDir + buildQuery({ alias, path }))
80
+ const body = await readJson<{ path: string; entries: RemoteDirEntry[] }>(response)
81
+ return body
82
+ }
83
+
84
+ async list(root: string, path: string): Promise<DirListing> {
85
+ const response = await fetch(WORKSPACE_API.tree + buildQuery({ root, path }))
86
+ const body = await readJson<{ listing: DirListing }>(response)
87
+ return body.listing
88
+ }
89
+
90
+ async read(root: string, path: string): Promise<FileRead> {
91
+ const response = await fetch(WORKSPACE_API.file + buildQuery({ root, path }))
92
+ const body = await readJson<{ file: FileRead }>(response)
93
+ return body.file
94
+ }
95
+
96
+ async write(root: string, path: string, content: string, expectedMtime?: number): Promise<FileWriteResult> {
97
+ const response = await fetch(WORKSPACE_API.file, {
98
+ method: 'PUT',
99
+ headers: { 'content-type': 'application/json' },
100
+ body: JSON.stringify({ root, path, content, expectedMtime }),
101
+ })
102
+ const body = await readJson<{ result: FileWriteResult }>(response)
103
+ return body.result
104
+ }
105
+
106
+ async search(root: string, queryText: string): Promise<SearchView> {
107
+ const response = await fetch(WORKSPACE_API.search + buildQuery({ root, query: queryText }))
108
+ const body = await readJson<{ search: SearchView }>(response)
109
+ return body.search
110
+ }
111
+ }
112
+
113
+ /** The two /api/dsh-ssh endpoints the config dialog needs (host create/test). */
114
+ export interface SshHostPayload {
115
+ alias?: string
116
+ host: string
117
+ port?: number
118
+ user: string
119
+ auth?: {
120
+ kind: 'key' | 'password'
121
+ keyPath?: string
122
+ passphrase?: string
123
+ password?: string
124
+ }
125
+ description?: string
126
+ }
127
+
128
+ export interface SshHostSummary {
129
+ alias: string
130
+ host: string
131
+ port: number
132
+ user: string
133
+ auth: 'key' | 'password'
134
+ keyReady: boolean
135
+ description?: string
136
+ createdAt: number
137
+ updatedAt: number
138
+ }
139
+
140
+ export interface TestResult {
141
+ ok: boolean
142
+ latencyMs?: number
143
+ error?: string
144
+ }
145
+
146
+ const HOSTS_API = '/api/dsh-ssh/hosts'
147
+ const TEST_API = '/api/dsh-ssh/test'
148
+
149
+ export class SshHostsApi {
150
+ async list(): Promise<SshHostSummary[]> {
151
+ const response = await fetch(HOSTS_API)
152
+ const body = await readJson<{ hosts: SshHostSummary[] }>(response)
153
+ return body.hosts
154
+ }
155
+
156
+ async create(payload: SshHostPayload): Promise<SshHostSummary> {
157
+ const response = await fetch(HOSTS_API, {
158
+ method: 'POST',
159
+ headers: { 'content-type': 'application/json' },
160
+ body: JSON.stringify(payload),
161
+ })
162
+ const body = await readJson<{ host: SshHostSummary }>(response)
163
+ return body.host
164
+ }
165
+
166
+ /** Update an existing host (PATCH /api/dsh-ssh/hosts?alias=鈥?. The payload
167
+ * may carry only the fields being changed; omitted auth keeps the stored
168
+ * secret. */
169
+ async update(alias: string, payload: SshHostPayload): Promise<SshHostSummary> {
170
+ const response = await fetch(`${HOSTS_API}?alias=${encodeURIComponent(alias)}`, {
171
+ method: 'PATCH',
172
+ headers: { 'content-type': 'application/json' },
173
+ body: JSON.stringify(payload),
174
+ })
175
+ const body = await readJson<{ host: SshHostSummary }>(response)
176
+ return body.host
177
+ }
178
+
179
+ /** Delete a host (DELETE /api/dsh-ssh/hosts?alias=鈥?. */
180
+ async remove(alias: string): Promise<void> {
181
+ const response = await fetch(`${HOSTS_API}?alias=${encodeURIComponent(alias)}`, {
182
+ method: 'DELETE',
183
+ })
184
+ await readJson<{ ok: true }>(response)
185
+ }
186
+
187
+ async test(alias: string): Promise<TestResult> {
188
+ const response = await fetch(TEST_API, {
189
+ method: 'POST',
190
+ headers: { 'content-type': 'application/json' },
191
+ body: JSON.stringify({ alias }),
192
+ })
193
+ const body = await readJson<{ result: TestResult }>(response)
194
+ return body.result
195
+ }
196
+ }
@@ -0,0 +1,5 @@
1
+ /* Minimal CSS Modules declaration (mirrors the other packages). */
2
+ declare module '*.module.css' {
3
+ const classes: Record<string, string>
4
+ export default classes
5
+ }