@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.
- package/README.md +50 -0
- package/cordis.patch.yml +26 -0
- package/lib/client.js +17780 -0
- package/lib/environment-BL1jddfB.js +449 -0
- package/lib/fs.js +478 -0
- package/lib/index.js +6758 -0
- package/lib/subprocess.js +600 -0
- package/lib/switch-fs-CAJpFY9C.js +193 -0
- package/lib/switch-fs-RrZtG2gv.js +210 -0
- package/lib/types/backend.d.ts +108 -0
- package/lib/types/base/capability.d.ts +107 -0
- package/lib/types/base/index.d.ts +18 -0
- package/lib/types/base/ledger-router.d.ts +48 -0
- package/lib/types/base/ledger.d.ts +82 -0
- package/lib/types/base/model.d.ts +108 -0
- package/lib/types/base/namespace.d.ts +57 -0
- package/lib/types/base/plugin.d.ts +102 -0
- package/lib/types/base/registry.d.ts +60 -0
- package/lib/types/base/router.d.ts +40 -0
- package/lib/types/client/api.d.ts +85 -0
- package/lib/types/client/directory-flow.d.ts +82 -0
- package/lib/types/client/icons.d.ts +19 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/locales.d.ts +104 -0
- package/lib/types/client/manager-button.d.ts +32 -0
- package/lib/types/client/migrate.d.ts +20 -0
- package/lib/types/client/ssh/api.d.ts +89 -0
- package/lib/types/client/ssh/apply.d.ts +23 -0
- package/lib/types/client/ssh/locales.d.ts +156 -0
- package/lib/types/client/ssh/mount.d.ts +13 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/HostFingerprintDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/HostFormDialog.d.ts +13 -0
- package/lib/types/client/ssh/panel/HostsTab.d.ts +10 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/SshPanel.d.ts +14 -0
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +12 -0
- package/lib/types/client/ssh/panel/TransferTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/controller.d.ts +23 -0
- package/lib/types/client/ssh/panel/helpers.d.ts +15 -0
- package/lib/types/client/ssh/panel/xterm.css.d.ts +3 -0
- package/lib/types/client/ssh/sidebar-entry.d.ts +25 -0
- package/lib/types/client/state.d.ts +32 -0
- package/lib/types/client/text.d.ts +11 -0
- package/lib/types/client/workspace-badges.d.ts +38 -0
- package/lib/types/client/workspace-gate.d.ts +15 -0
- package/lib/types/client-http.d.ts +15 -0
- package/lib/types/core.d.ts +43 -0
- package/lib/types/fs.d.ts +36 -0
- package/lib/types/host-http.d.ts +25 -0
- package/lib/types/index.d.ts +53 -0
- package/lib/types/ledger.d.ts +132 -0
- package/lib/types/protocol.d.ts +101 -0
- package/lib/types/providers/index.d.ts +19 -0
- package/lib/types/providers/local/provider.d.ts +58 -0
- package/lib/types/providers/ssh/provider.d.ts +112 -0
- package/lib/types/remote/environment.d.ts +29 -0
- package/lib/types/remote/output.d.ts +38 -0
- package/lib/types/remote/remote-fs.d.ts +69 -0
- package/lib/types/remote/remote-process.d.ts +41 -0
- package/lib/types/remote/remote-subprocess.d.ts +31 -0
- package/lib/types/remote/remote-terminal.d.ts +41 -0
- package/lib/types/remote-runner.d.ts +83 -0
- package/lib/types/remote-search.d.ts +50 -0
- package/lib/types/routes.d.ts +24 -0
- package/lib/types/runtime/workspace-core.d.ts +59 -0
- package/lib/types/seam-state.d.ts +69 -0
- package/lib/types/shell.d.ts +8 -0
- package/lib/types/ssh/connection/lease.d.ts +16 -0
- package/lib/types/ssh/connection/pool.d.ts +57 -0
- package/lib/types/ssh/engine.d.ts +434 -0
- package/lib/types/ssh/exec/output.d.ts +31 -0
- package/lib/types/ssh/known-hosts.d.ts +89 -0
- package/lib/types/ssh/plugin.d.ts +57 -0
- package/lib/types/ssh/protocol.d.ts +236 -0
- package/lib/types/ssh/routes.d.ts +44 -0
- package/lib/types/ssh/store.d.ts +107 -0
- package/lib/types/ssh/tools.d.ts +35 -0
- package/lib/types/ssh/transfer/progress.d.ts +14 -0
- package/lib/types/ssh/vault.d.ts +110 -0
- package/lib/types/subprocess.d.ts +32 -0
- package/lib/types/switch/switch-fs.d.ts +88 -0
- package/lib/types/switch/switch-subprocess.d.ts +34 -0
- package/lib/types/tools.d.ts +12 -0
- package/package.json +134 -0
- package/src/backend.ts +624 -0
- package/src/base/capability.ts +86 -0
- package/src/base/index.ts +18 -0
- package/src/base/ledger-router.ts +128 -0
- package/src/base/ledger.ts +299 -0
- package/src/base/model.ts +118 -0
- package/src/base/namespace.ts +102 -0
- package/src/base/plugin.ts +170 -0
- package/src/base/registry.ts +109 -0
- package/src/base/router.ts +43 -0
- package/src/client/api.ts +196 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/directory-flow.tsx +482 -0
- package/src/client/icons.tsx +50 -0
- package/src/client/index.ts +210 -0
- package/src/client/locales.ts +105 -0
- package/src/client/manager-button.tsx +269 -0
- package/src/client/migrate.ts +109 -0
- package/src/client/ssh/api.ts +411 -0
- package/src/client/ssh/apply.ts +50 -0
- package/src/client/ssh/locales.ts +322 -0
- package/src/client/ssh/mount.tsx +83 -0
- package/src/client/ssh/panel/ClusterTab.tsx +123 -0
- package/src/client/ssh/panel/HostFingerprintDialog.tsx +82 -0
- package/src/client/ssh/panel/HostFormDialog.tsx +228 -0
- package/src/client/ssh/panel/HostsTab.tsx +236 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +80 -0
- package/src/client/ssh/panel/SshPanel.tsx +77 -0
- package/src/client/ssh/panel/TerminalTab.tsx +176 -0
- package/src/client/ssh/panel/TransferTab.tsx +232 -0
- package/src/client/ssh/panel/TunnelsTab.tsx +177 -0
- package/src/client/ssh/panel/controller.ts +48 -0
- package/src/client/ssh/panel/helpers.ts +26 -0
- package/src/client/ssh/panel/panel.module.css +1006 -0
- package/src/client/ssh/panel/xterm.css.ts +2 -0
- package/src/client/ssh/sidebar-entry.ts +123 -0
- package/src/client/state.ts +99 -0
- package/src/client/text.ts +22 -0
- package/src/client/workspace-badges.ts +96 -0
- package/src/client/workspace-gate.ts +225 -0
- package/src/client/workspace.module.css +283 -0
- package/src/client-http.ts +45 -0
- package/src/core.ts +47 -0
- package/src/fs.ts +85 -0
- package/src/host-http.ts +74 -0
- package/src/index.ts +244 -0
- package/src/ledger.ts +416 -0
- package/src/protocol.ts +114 -0
- package/src/providers/index.ts +34 -0
- package/src/providers/local/provider.ts +179 -0
- package/src/providers/ssh/provider.ts +274 -0
- package/src/remote/environment.ts +123 -0
- package/src/remote/output.ts +142 -0
- package/src/remote/remote-fs.ts +532 -0
- package/src/remote/remote-process.ts +203 -0
- package/src/remote/remote-subprocess.ts +159 -0
- package/src/remote/remote-terminal.ts +141 -0
- package/src/remote-runner.ts +201 -0
- package/src/remote-search.ts +163 -0
- package/src/routes.ts +395 -0
- package/src/runtime/workspace-core.ts +154 -0
- package/src/seam-state.ts +185 -0
- package/src/shell.ts +10 -0
- package/src/ssh/connection/lease.ts +17 -0
- package/src/ssh/connection/pool.ts +275 -0
- package/src/ssh/engine.ts +1761 -0
- package/src/ssh/exec/output.ts +70 -0
- package/src/ssh/known-hosts.ts +214 -0
- package/src/ssh/plugin.ts +184 -0
- package/src/ssh/protocol.ts +227 -0
- package/src/ssh/routes.ts +892 -0
- package/src/ssh/store.ts +544 -0
- package/src/ssh/tools.ts +402 -0
- package/src/ssh/transfer/progress.ts +75 -0
- package/src/ssh/vault.ts +477 -0
- package/src/subprocess.ts +71 -0
- package/src/switch/switch-fs.ts +253 -0
- package/src/switch/switch-subprocess.ts +59 -0
- 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
|
+
}
|