@tiphareth/dsh-hardssh 0.1.2 → 0.2.3

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 +77 -50
  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 +333 -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 +42 -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 +59 -26
  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 +171 -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
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "@tiphareth/dsh-hardssh",
3
- "description": "Generic workspace base for the dsh harness (local + remote workspace providers with per-workspace routing), with SSH as one concrete provider: enter SSH mode from the session header, browse/edit remote files in a new left panel, let the local harness operate the remote server through remote_* agent tools, and manage hosts through the ssh_* tools and the SSH panel (legacy dsh-ssh merged in).",
4
- "version": "0.1.2",
3
+ "description": "Generic workspace base for the dsh harness (local + remote workspace providers with per-workspace routing), with SSH as one concrete provider: bind a server directory as an SSH workspace so the session's file I/O and commands run on that host, manage hosts and workspaces in the GUI, and operate remote servers through the ssh_* / remote_* agent tools",
4
+ "version": "0.2.3",
5
5
  "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/tiphareth0/dsh-hardssh.git",
9
+ "directory": "packages/dsh-hardssh"
10
+ },
11
+ "homepage": "https://github.com/tiphareth0/dsh-hardssh",
12
+ "bugs": {
13
+ "url": "https://github.com/tiphareth0/dsh-hardssh/issues"
14
+ },
6
15
  "keywords": [
7
16
  "dsh",
8
17
  "deepseek-harness",
@@ -17,6 +26,9 @@
17
26
  },
18
27
  "main": "lib/index.js",
19
28
  "types": "lib/types/index.d.ts",
29
+ "bin": {
30
+ "dsh-hardssh-export-legacy": "./scripts/export-legacy-workspaces.mjs"
31
+ },
20
32
  "exports": {
21
33
  ".": {
22
34
  "types": "./lib/types/index.d.ts",
@@ -30,6 +42,14 @@
30
42
  "types": "./lib/types/subprocess.d.ts",
31
43
  "default": "./lib/subprocess.js"
32
44
  },
45
+ "./workspace": {
46
+ "types": "./lib/types/workspace.d.ts",
47
+ "default": "./lib/workspace.js"
48
+ },
49
+ "./base": {
50
+ "types": "./lib/types/base/index.d.ts",
51
+ "default": "./lib/base/index.js"
52
+ },
33
53
  "./client": {
34
54
  "types": "./lib/types/client/index.d.ts",
35
55
  "default": "./lib/client.js"
@@ -45,17 +65,22 @@
45
65
  "inject": [
46
66
  "@deepseek-ai/dsh-client-runtime",
47
67
  "@deepseek-ai/dsh-client-connection",
68
+ "@deepseek-ai/dsh-client-ui-sidebar-right",
48
69
  "@deepseek-ai/dsh-client-ui-settings"
49
70
  ],
50
71
  "platform": "web"
51
72
  }
52
73
  },
53
74
  "scripts": {
54
- "build": "tsc -b && tsdown",
75
+ "clean": "node ./scripts/clean-build.mjs",
76
+ "build": "pnpm run clean && tsc -b && tsdown",
55
77
  "bundle": "tsdown",
56
78
  "watch": "tsdown --watch",
57
79
  "typecheck": "tsc -b --pretty false",
58
- "test": "vitest run",
80
+ "test": "vitest run --exclude tests/ssh/vault.test.ts",
81
+ "test:all": "vitest run",
82
+ "test:vault": "vitest run tests/ssh/vault.test.ts",
83
+ "workspace:export-legacy": "node ./scripts/export-legacy-workspaces.mjs",
59
84
  "prepare": "tsdown"
60
85
  },
61
86
  "dependencies": {
@@ -66,22 +91,25 @@
66
91
  },
67
92
  "peerDependencies": {
68
93
  "@deepseek-ai/cordis": "^4.0.2",
69
- "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.3",
70
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.3",
71
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
72
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.3",
73
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.2-alpha.3",
74
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.3",
75
- "@deepseek-ai/dsh-fs": "^0.1.2-alpha.3",
76
- "@deepseek-ai/dsh-fs-sandbox": "^0.1.2-alpha.3",
77
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.3",
94
+ "@deepseek-ai/dsh-client-connection": "*",
95
+ "@deepseek-ai/dsh-client-locale": "*",
96
+ "@deepseek-ai/dsh-client-runtime": "*",
97
+ "@deepseek-ai/dsh-client-ui-settings": "*",
98
+ "@deepseek-ai/dsh-client-ui-sidebar": "*",
99
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "*",
100
+ "@deepseek-ai/dsh-client-ui-slots": "*",
101
+ "@deepseek-ai/dsh-client-ui-layout": "*",
102
+ "@deepseek-ai/dsh-fs": "*",
103
+ "@deepseek-ai/dsh-fs-local": "*",
104
+ "@deepseek-ai/dsh-fs-sandbox": "*",
105
+ "@deepseek-ai/dsh-host-webserver": "*",
78
106
  "@deepseek-ai/schemastery": "^3.18.1",
79
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.3",
80
- "@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.3",
81
- "@deepseek-ai/dsh-subprocess-local": "^0.1.2-alpha.3",
82
- "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.3",
83
- "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.3",
84
- "@deepseek-ai/dsh-tools": "^0.1.2-alpha.3",
107
+ "@deepseek-ai/dsh-settings": "*",
108
+ "@deepseek-ai/dsh-subprocess": "*",
109
+ "@deepseek-ai/dsh-subprocess-local": "*",
110
+ "@deepseek-ai/dsh-system-prompt": "*",
111
+ "@deepseek-ai/dsh-timeout": "*",
112
+ "@deepseek-ai/dsh-tools": "*",
85
113
  "react": "^18.2.0",
86
114
  "react-dom": "^18.2.0"
87
115
  },
@@ -90,11 +118,13 @@
90
118
  "@deepseek-ai/dsh-client-connection": "0.1.2-alpha.3",
91
119
  "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.3",
92
120
  "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
93
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-alpha.3",
94
- "@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.3",
95
- "@deepseek-ai/dsh-client-ui-sidebar": "0.1.2-alpha.3",
96
- "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.3",
97
- "@deepseek-ai/dsh-client-ui-workspace": "0.1.2-alpha.3",
121
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.1",
122
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.5-rc.1",
123
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.1",
124
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.1.5-rc.1",
125
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.5-rc.1",
126
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
127
+ "@deepseek-ai/dsh-client-ui-workspace": "0.1.5-rc.1",
98
128
  "@deepseek-ai/dsh-fs": "0.1.2-alpha.3",
99
129
  "@deepseek-ai/dsh-fs-local": "0.1.2-alpha.3",
100
130
  "@deepseek-ai/dsh-fs-sandbox": "0.1.2-alpha.3",
@@ -125,10 +155,13 @@
125
155
  },
126
156
  "files": [
127
157
  "lib/*.js",
158
+ "lib/base/*.js",
128
159
  "lib/types/**/*.d.ts",
129
160
  "src",
161
+ "scripts/export-legacy-workspaces.mjs",
130
162
  "cordis.patch.yml",
131
- "README.md"
163
+ "README.md",
164
+ "SKILLS.md"
132
165
  ],
133
166
  "license": "BSD-3-Clause"
134
- }
167
+ }
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Offline emergency export: convert the authoritative generic workspace ledger
5
+ * back to the frozen legacy SSH ledger shape. This command never changes
6
+ * anchors and defaults to preview-only; pass --apply to write the legacy file.
7
+ */
8
+ import { constants } from 'node:fs'
9
+ import { access, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
10
+ import { homedir } from 'node:os'
11
+ import { dirname, join } from 'node:path'
12
+ import { pathToFileURL } from 'node:url'
13
+ import { randomUUID } from 'node:crypto'
14
+
15
+ function parseArgs(argv) {
16
+ const result = {
17
+ genericPath: join(homedir(), '.dsh', 'workspaces', 'index.v1.json'),
18
+ legacyPath: join(homedir(), '.dsh', 'dsh-hardssh-workspaces.json'),
19
+ apply: false,
20
+ }
21
+ for (let index = 0; index < argv.length; index += 1) {
22
+ const arg = argv[index]
23
+ if (arg === '--apply') {
24
+ result.apply = true
25
+ } else if (arg === '--generic' || arg === '--legacy') {
26
+ const value = argv[index + 1]
27
+ if (value === undefined || value === '') throw new Error(`${arg} requires a path`)
28
+ if (arg === '--generic') result.genericPath = value
29
+ else result.legacyPath = value
30
+ index += 1
31
+ } else if (arg === '--help' || arg === '-h') {
32
+ process.stdout.write('Usage: export-legacy-workspaces [--generic PATH] [--legacy PATH] [--apply]\n')
33
+ process.exit(0)
34
+ } else {
35
+ throw new Error(`unknown argument '${arg}'`)
36
+ }
37
+ }
38
+ return result
39
+ }
40
+
41
+ function requiredString(value, label) {
42
+ if (typeof value !== 'string' || value === '') throw new Error(`invalid generic SSH workspace: ${label} is required`)
43
+ return value
44
+ }
45
+
46
+ function toLegacyRecord(value) {
47
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
48
+ throw new Error('generic workspace entries must be objects')
49
+ }
50
+ const provider = value.provider
51
+ const location = value.location
52
+ const anchor = value.anchor
53
+ if (typeof provider !== 'object' || provider === null || Array.isArray(provider)) throw new Error('generic workspace provider is invalid')
54
+ if (provider.id !== 'ssh') return undefined
55
+ if (typeof provider.connectionRef !== 'object' || provider.connectionRef === null || Array.isArray(provider.connectionRef)) {
56
+ throw new Error(`generic SSH workspace '${String(value.id)}' has no connectionRef`)
57
+ }
58
+ if (typeof location !== 'object' || location === null || Array.isArray(location)) throw new Error(`generic SSH workspace '${String(value.id)}' has no location`)
59
+ if (typeof anchor !== 'object' || anchor === null || Array.isArray(anchor)) throw new Error(`generic SSH workspace '${String(value.id)}' has no anchor`)
60
+ return {
61
+ id: requiredString(value.id, 'id'),
62
+ title: requiredString(value.title, 'title'),
63
+ alias: requiredString(provider.connectionRef.id, 'provider.connectionRef.id'),
64
+ remoteRoot: requiredString(location.root, 'location.root'),
65
+ anchorPath: requiredString(anchor.path, 'anchor.path'),
66
+ createdAt: requiredString(value.createdAt, 'createdAt'),
67
+ }
68
+ }
69
+
70
+ async function exists(path) {
71
+ try {
72
+ await access(path, constants.F_OK)
73
+ return true
74
+ } catch {
75
+ return false
76
+ }
77
+ }
78
+
79
+ async function atomicExport(path, bytes) {
80
+ await mkdir(dirname(path), { recursive: true })
81
+ const tempPath = `${path}.tmp-${process.pid}-${randomUUID()}`
82
+ const backupPath = await exists(path)
83
+ ? `${path}.backup-${new Date().toISOString().replace(/[:.]/g, '-')}-${randomUUID()}`
84
+ : undefined
85
+ await writeFile(tempPath, bytes, { encoding: 'utf8', mode: 0o600 })
86
+ try {
87
+ if (backupPath !== undefined) await rename(path, backupPath)
88
+ try {
89
+ await rename(tempPath, path)
90
+ } catch (error) {
91
+ if (backupPath !== undefined) await rename(backupPath, path).catch(() => undefined)
92
+ throw error
93
+ }
94
+ } finally {
95
+ await rm(tempPath, { force: true }).catch(() => undefined)
96
+ }
97
+ return backupPath
98
+ }
99
+
100
+ export async function exportLegacyWorkspaces(options) {
101
+ const raw = JSON.parse(await readFile(options.genericPath, 'utf8'))
102
+ if (!Array.isArray(raw)) throw new Error('generic workspace ledger must be a JSON array')
103
+ const records = []
104
+ const ids = new Set()
105
+ const anchors = new Set()
106
+ for (const value of raw) {
107
+ const mapped = toLegacyRecord(value)
108
+ if (mapped === undefined) continue
109
+ if (ids.has(mapped.id)) throw new Error(`duplicate SSH workspace id '${mapped.id}'`)
110
+ if (anchors.has(mapped.anchorPath)) throw new Error(`duplicate SSH workspace anchor '${mapped.anchorPath}'`)
111
+ ids.add(mapped.id)
112
+ anchors.add(mapped.anchorPath)
113
+ records.push(mapped)
114
+ }
115
+ const bytes = `${JSON.stringify(records, null, 2)}\n`
116
+ const backupPath = options.apply ? await atomicExport(options.legacyPath, bytes) : undefined
117
+ return {
118
+ mode: options.apply ? 'applied' : 'preview',
119
+ genericPath: options.genericPath,
120
+ legacyPath: options.legacyPath,
121
+ recordCount: records.length,
122
+ ids: records.map(record => record.id),
123
+ ...(backupPath === undefined ? {} : { backupPath }),
124
+ }
125
+ }
126
+
127
+ const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
128
+ if (isMain) {
129
+ exportLegacyWorkspaces(parseArgs(process.argv.slice(2))).then(
130
+ result => { process.stdout.write(`${JSON.stringify(result)}\n`) },
131
+ error => {
132
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
133
+ process.exitCode = 1
134
+ },
135
+ )
136
+ }