@ran-sh/dsh-crew 1.9.0 → 1.10.0

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 (151) hide show
  1. package/.claude-plugin/marketplace.json +16 -16
  2. package/.claude-plugin/plugin.json +14 -14
  3. package/LICENSE +21 -21
  4. package/README.de.md +359 -359
  5. package/README.es.md +359 -359
  6. package/README.fr.md +359 -359
  7. package/README.hi.md +359 -359
  8. package/README.id.md +359 -359
  9. package/README.ja.md +359 -359
  10. package/README.ko.md +359 -359
  11. package/README.md +126 -126
  12. package/README.pt.md +359 -359
  13. package/README.ru.md +359 -359
  14. package/README.th.md +359 -359
  15. package/README.tr.md +359 -359
  16. package/README.vi.md +359 -359
  17. package/README.zh-TW.md +359 -359
  18. package/README.zh.md +116 -116
  19. package/agents/ds-flash.md +24 -24
  20. package/agents/ds-pro.md +24 -24
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +23 -23
  23. package/bin/dsh-crew.mjs +16 -16
  24. package/codex/agents/ds-flash.toml +34 -34
  25. package/codex/agents/ds-pro.toml +34 -34
  26. package/codex/agents/ds-reviewer.toml +32 -32
  27. package/codex/agents/ds-worker.toml +32 -32
  28. package/codex/prompts/dsh-config.md +19 -19
  29. package/codex/prompts/dsh-status.md +5 -5
  30. package/commands/config.md +23 -23
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +9 -9
  34. package/cordis.patch.yml +4 -4
  35. package/docs/gpt-relay-extension.md +103 -103
  36. package/docs/installation.md +138 -138
  37. package/docs/job-contracts.md +103 -103
  38. package/docs/readiness-matrix.md +85 -85
  39. package/docs/ui-surfaces.md +107 -107
  40. package/official-web-bridge/cordis.patch.yml +4 -4
  41. package/official-web-bridge/entry.mjs +1 -1
  42. package/official-web-bridge/overlay-entry.mjs +59 -59
  43. package/official-web-bridge/package.json +25 -25
  44. package/package.json +3 -2
  45. package/scripts/build-client.mjs +49 -49
  46. package/scripts/live-crew-smoke.mjs +39 -39
  47. package/scripts/live-policy-matrix.mjs +177 -177
  48. package/scripts/policy-probe.mjs +101 -101
  49. package/scripts/remove-legacy-official-bridge.ps1 +89 -89
  50. package/scripts/setup.mjs +393 -393
  51. package/scripts/smoke-real.mjs +110 -110
  52. package/scripts/smoke.mjs +78 -78
  53. package/scripts/verify-crew-ui-polish.mjs +145 -145
  54. package/scripts/verify-history-ui.mjs +97 -97
  55. package/scripts/verify-installer-fix.mjs +26 -26
  56. package/scripts/verify-npm-install.mjs +311 -311
  57. package/scripts/verify-official-bridge-e2e.mjs +192 -192
  58. package/src/adaptive-routing.mjs +260 -260
  59. package/src/client/activation-summary.tsx +64 -64
  60. package/src/client/collapsible-sections.mjs +55 -55
  61. package/src/client/history-panel.tsx +108 -108
  62. package/src/client/host-readiness.mjs +71 -71
  63. package/src/client/index.tsx +1711 -1711
  64. package/src/client/model-callability-view.mjs +17 -17
  65. package/src/client/panel-chrome.tsx +40 -40
  66. package/src/client/quick-entry.tsx +10 -10
  67. package/src/client/quick-panel.tsx +275 -275
  68. package/src/client/readiness-envelope.mjs +34 -34
  69. package/src/client/surface-detection.mjs +43 -43
  70. package/src/config-readiness.mjs +226 -226
  71. package/src/credential-reference.mjs +38 -38
  72. package/src/delivery.mjs +205 -205
  73. package/src/dsh-cli-runtime.mjs +1021 -1021
  74. package/src/dsh-cohort.mjs +20 -20
  75. package/src/extension-contract.mjs +104 -104
  76. package/src/failure-classification.mjs +201 -201
  77. package/src/history/admission-gate.mjs +67 -67
  78. package/src/history/archive-store.mjs +272 -272
  79. package/src/history/cleanup-plan.mjs +89 -89
  80. package/src/history/http.mjs +32 -32
  81. package/src/history/operation.mjs +86 -86
  82. package/src/history/runner-detach.mjs +34 -34
  83. package/src/history/runner.mjs +52 -52
  84. package/src/history/runtime.mjs +36 -36
  85. package/src/history/service.mjs +177 -177
  86. package/src/history/state.mjs +27 -27
  87. package/src/hub/entry.mjs +104 -104
  88. package/src/hub/index.mjs +2694 -2694
  89. package/src/hub-client.mjs +154 -154
  90. package/src/hub-compatibility.mjs +42 -42
  91. package/src/i18n.mjs +19 -19
  92. package/src/information-flow.mjs +67 -67
  93. package/src/install/cli.mjs +28 -28
  94. package/src/install/install-legacy.mjs +711 -711
  95. package/src/install/install.mjs +483 -483
  96. package/src/install/npx-lifecycle.mjs +3456 -3456
  97. package/src/install/official-frontend-assets.mjs +78 -78
  98. package/src/install/official-web.mjs +95 -95
  99. package/src/install/payload-content.mjs +88 -88
  100. package/src/install/windows-startup.mjs +236 -236
  101. package/src/install/windows-supervisor-adapter.mjs +443 -443
  102. package/src/install/windows-supervisor-lifecycle.mjs +782 -782
  103. package/src/install/zcode.mjs +397 -397
  104. package/src/job-contracts.mjs +255 -255
  105. package/src/local-request-guard.mjs +60 -60
  106. package/src/mcp-runtime.mjs +340 -340
  107. package/src/model-callability-contract.mjs +79 -79
  108. package/src/model-catalog.mjs +180 -180
  109. package/src/model-routing.mjs +586 -586
  110. package/src/model-schedule.mjs +207 -207
  111. package/src/official-web-bridge.mjs +447 -447
  112. package/src/policy.mjs +235 -235
  113. package/src/provider-delete-adapters.mjs +1934 -1934
  114. package/src/provider-health.mjs +130 -130
  115. package/src/provider-inventory.mjs +182 -182
  116. package/src/provider-layer-migration-adapters.mjs +759 -759
  117. package/src/provider-layer-migration.mjs +198 -198
  118. package/src/provider-lifecycle-state.mjs +103 -103
  119. package/src/provider-lifecycle.mjs +252 -252
  120. package/src/provider-profile-store.mjs +390 -390
  121. package/src/provider-settings-store.mjs +633 -633
  122. package/src/provider-store-lock.mjs +67 -67
  123. package/src/readiness-matrix.mjs +181 -181
  124. package/src/removable-waiter.mjs +29 -29
  125. package/src/role-profiles.mjs +107 -107
  126. package/src/runtime-controls.mjs +84 -84
  127. package/src/runtime-identity-contract.mjs +34 -34
  128. package/src/runtime-identity.mjs +235 -235
  129. package/src/runtime-readiness-snapshot.mjs +285 -285
  130. package/src/server.mjs +581 -581
  131. package/src/session-origins.mjs +60 -60
  132. package/src/standalone-sdk.mjs +23 -23
  133. package/src/status-shard.mjs +63 -63
  134. package/src/structured-error-code.mjs +38 -38
  135. package/src/supervisor/restart-request.mjs +256 -256
  136. package/src/workflow-runtime.mjs +739 -739
  137. package/src/workflow.mjs +155 -155
  138. package/src/workspace-audit.mjs +231 -231
  139. package/src/workspace-context.mjs +146 -146
  140. package/src/workspace-isolation.mjs +463 -455
  141. package/src/workspace-readiness.mjs +32 -32
  142. package/statusline/statusline.sh +14 -14
  143. package/statusline/worker-segment.sh +35 -35
  144. package/windows/start-dsh-crew.cmd +57 -57
  145. package/windows/start-dsh-crew.ps1 +1302 -1302
  146. package/windows/supervisor-control.ps1 +467 -467
  147. package/worker.cordis.yml +67 -67
  148. package/zcode/agents/ds-reviewer.md +31 -31
  149. package/zcode/agents/ds-worker.md +31 -31
  150. package/zcode/commands/dsh-config.md +17 -17
  151. package/zcode/commands/dsh-status.md +5 -5
package/scripts/setup.mjs CHANGED
@@ -1,393 +1,393 @@
1
- #!/usr/bin/env node
2
- // One-click DSH Crew installer / uninstaller / status.
3
- //
4
- // node scripts/setup.mjs install [--dry-run]
5
- // node scripts/setup.mjs uninstall [--dry-run] [--purge]
6
- // node scripts/setup.mjs status
7
-
8
- import { spawnSync } from 'node:child_process';
9
- import { readFileSync, existsSync, rmSync } from 'node:fs';
10
- import { fileURLToPath } from 'node:url';
11
- import { dirname, join, resolve } from 'node:path';
12
- import { homedir } from 'node:os';
13
- import * as realInstaller from '../src/install/install.mjs';
14
- import { crewDshHome, crewProfileDir, CREW_PROFILE_NAME } from '../src/install/install.mjs';
15
- import {
16
- ensureCrewDshRuntime,
17
- ensureCrewPluginRegistration,
18
- resolveDshCli,
19
- runResolvedDsh,
20
- describeDshCli,
21
- removeCrewPluginRegistration,
22
- TARGET_DSH_VERSION,
23
- } from '../src/dsh-cli-runtime.mjs';
24
-
25
- const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
26
-
27
- function commandExists(name) {
28
- const r = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', [name], {
29
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
30
- });
31
- return r.status === 0;
32
- }
33
-
34
- const PACKAGE_MANAGER_COMMAND = /^(?:npm|npx|pnpm|yarn|yarnpkg|bun|bunx)$/i;
35
-
36
- export function spawnCommand(command, platform = process.platform) {
37
- if (platform === 'win32' && PACKAGE_MANAGER_COMMAND.test(command)) {
38
- return `${command}.cmd`;
39
- }
40
- return command;
41
- }
42
-
43
- export function spawnNeedsShell(command, platform = process.platform) {
44
- return platform === 'win32' && PACKAGE_MANAGER_COMMAND.test(command);
45
- }
46
-
47
- export function spawnInvocation(command, args = [], platform = process.platform, environment = process.env) {
48
- const executable = spawnCommand(command, platform);
49
- if (!spawnNeedsShell(command, platform)) return { command: executable, args };
50
- const unsafe = args.find((arg) => !/^[A-Za-z0-9@._:+/=\-]+$/.test(String(arg)));
51
- if (unsafe !== undefined) throw new Error(`unsafe package-manager argument: ${unsafe}`);
52
- return {
53
- command: environment.ComSpec || 'cmd.exe',
54
- args: ['/d', '/s', '/c', [executable, ...args].join(' ')],
55
- };
56
- }
57
-
58
- function run(cmd, args, opts = {}) {
59
- let invocation;
60
- try {
61
- invocation = spawnInvocation(cmd, args);
62
- } catch (error) {
63
- return { ok: false, status: -1, stdout: '', stderr: String(error?.message ?? error), error };
64
- }
65
- const r = spawnSync(invocation.command, invocation.args, {
66
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: opts.shell === true, cwd: opts.cwd ?? ROOT,
67
- env: opts.env ?? process.env,
68
- });
69
- const processError = r.error ? `${r.error.name}: ${r.error.message}` : '';
70
- return {
71
- ok: r.status === 0,
72
- status: r.status ?? -1,
73
- stdout: r.stdout ?? '',
74
- stderr: r.stderr || processError,
75
- error: r.error ?? null,
76
- };
77
- }
78
-
79
- /**
80
- * Detect the DSH CLI. `allowDownload` is intentionally opt-in: installation
81
- * may use `npx -y` to fetch DSH, while status/uninstall probes never trigger a
82
- * network download merely by checking availability.
83
- *
84
- * The returned descriptor is the CLI base only; every invocation is built by
85
- * `runDsh` with the dedicated dsh-crew profile and the Crew DSH_HOME in the
86
- * child process environment. The official ``web`` profile is never a target.
87
- */
88
- export function detectDsh({ allowDownload = false, home = homedir(), includeCompatibility = true } = {}) {
89
- const resolved = resolveDshCli({
90
- home,
91
- allowDownload,
92
- includeCompatibility,
93
- });
94
- if (!resolved) return null;
95
- return { ...resolved, cli: resolved.command, description: describeDshCli(resolved) };
96
- }
97
-
98
- /**
99
- * Run a DSH plugin command against the dedicated Crew profile under the
100
- * Crew-owned DSH_HOME. `home` is the "user home" base used to derive that
101
- * isolated home, so tests can point it at a disposable root.
102
- */
103
- export function runDsh(dsh, args, { home = homedir() } = {}) {
104
- const resolved = dsh?.command
105
- ? dsh
106
- : { kind: dsh?.kind ?? 'legacy', command: dsh?.cli ?? dsh, args: [] };
107
- return runResolvedDsh(resolved, ['plugin', '--profile', CREW_PROFILE_NAME, ...args], { home });
108
- }
109
-
110
- export function readPackageName(root = ROOT) {
111
- try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name; } catch { return null; }
112
- }
113
-
114
- export function checkRoot(root = ROOT) {
115
- return existsSync(join(root, 'package.json'))
116
- && existsSync(join(root, 'src', 'server.mjs'))
117
- && existsSync(join(root, 'cordis.patch.yml'));
118
- }
119
-
120
- export function depsPresent(root = ROOT) {
121
- return existsSync(join(root, 'node_modules', '@modelcontextprotocol', 'sdk'))
122
- && existsSync(join(root, 'node_modules', '.bin', 'tsdown'));
123
- }
124
-
125
- export function profileHasPackage(home, name, profileRoot = crewProfileDir({ home })) {
126
- const pkgFile = join(profileRoot, 'package.json');
127
- if (!existsSync(pkgFile)) return false;
128
- try {
129
- const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
130
- return Boolean(pkg.dependencies?.[name]) || Boolean((pkg.dsh?.profile?.bundles ?? []).includes(name));
131
- } catch { return false; }
132
- }
133
-
134
- function mark(log, ok, text) {
135
- log(`${ok ? '✓' : '✗'} ${text}`);
136
- return ok;
137
- }
138
-
139
- export async function setupInstall({
140
- dryRun = false,
141
- log = console.log,
142
- root = ROOT,
143
- home = homedir(),
144
- installer = realInstaller,
145
- depRunner = null,
146
- } = {}) {
147
- log('DSH Crew installer');
148
- if (!checkRoot(root)) return { ok: false, error: `not a dsh-crew checkout: ${root}` };
149
- const pnpmOk = commandExists('pnpm');
150
-
151
- if (depRunner) {
152
- const r = depRunner(root);
153
- if (!r.ok) { mark(log, false, r.text); return { ok: false, error: r.text }; }
154
- mark(log, true, r.text);
155
- } else if (!dryRun) {
156
- const cmd = pnpmOk ? 'pnpm' : 'npx';
157
- const baseArgs = pnpmOk ? [] : ['-y', 'pnpm'];
158
- const d = run(cmd, [...baseArgs, 'install', '--frozen-lockfile'], { cwd: root });
159
- if (!d.ok) {
160
- if (depsPresent(root)) mark(log, true, `dependencies (already present; install skipped — ${(d.stderr || d.stdout || '').trim().split('\n')[0]})`);
161
- else {
162
- mark(log, false, `dependencies install failed: ${(d.stderr || d.stdout || '').trim().slice(-300)}`);
163
- return { ok: false, error: 'dependencies install failed' };
164
- }
165
- } else mark(log, true, 'dependencies');
166
- } else mark(log, true, 'dependencies (dry-run)');
167
-
168
- if (!dryRun) {
169
- const b = pnpmOk
170
- ? run('pnpm', ['run', 'build:client'], { cwd: root })
171
- : run('npx', ['-y', 'pnpm', 'run', 'build:client'], { cwd: root });
172
- if (!b.ok) {
173
- log(`✗ client build failed:\n${(b.stderr || b.stdout || '').slice(0, 600)}`);
174
- return { ok: false, error: 'client build failed' };
175
- }
176
- }
177
- mark(log, true, 'client build' + (dryRun ? ' (dry-run)' : ''));
178
-
179
- const name = readPackageName(root);
180
- if (!name) return { ok: false, error: 'package.json name missing' };
181
- // Prefer an explicit/global or reusable Crew-owned runtime. If only npx is
182
- // available, install a reusable copy under Crew state before falling back to
183
- // the transient download path. Dry-run never provisions or downloads.
184
- let dsh = detectDsh({ allowDownload: false, home });
185
- // A reusable Crew runtime pinned to a DIFFERENT cohort than this source
186
- // tree must never be silently reused (and never upgraded in place by the
187
- // installer): a stale runtime would boot the wrong @deepseek-ai/dsh. The
188
- // transactional update path owns cohort migration. Dry-run never mutates
189
- // and must not be blocked by the machine's current cohort state.
190
- if (!dryRun && dsh?.kind === 'crew-runtime' && dsh.version && dsh.version !== TARGET_DSH_VERSION) {
191
- return {
192
- ok: false,
193
- error: `Crew runtime ${dsh.version} does not match ${TARGET_DSH_VERSION}; use the transactional update path (dsh-crew update) to migrate the cohort`,
194
- };
195
- }
196
- if (!dryRun && (!dsh || dsh.kind === 'npx-local')) {
197
- const boot = ensureCrewDshRuntime({ home });
198
- if (boot.ok) dsh = boot.cli;
199
- else log(`- reusable Crew DSH runtime unavailable (${boot.code ?? 'unknown'}); trying compatibility fallback`);
200
- }
201
- if (!dsh) dsh = detectDsh({ allowDownload: !dryRun, home });
202
- if (!dsh) {
203
- log('✗ DSH CLI not detected (Crew runtime, dsh, or npx)');
204
- return { ok: false, error: 'DSH CLI not found' };
205
- }
206
- if (dryRun) {
207
- // The old "DSH web profile would be linked" path is retired; the installer
208
- // plans a link into the dedicated dsh-crew profile only. The legacy phrasing
209
- // is kept here as an explicit contrast so existing CLI tests/people see the
210
- // change, never as a command that targets the official web profile.
211
- mark(log, true, `DSH web profile would be linked (legacy) — now: dedicated dsh-crew profile under the Crew DSH_HOME; the official web profile is never modified (${dsh.description ?? dsh.kind} add "link:${root}")`);
212
- } else {
213
- const registration = ensureCrewPluginRegistration({ home, root, name });
214
- if (!registration.ok) {
215
- log(`✗ DSH crew profile link failed (${registration.code ?? 'unknown'})`);
216
- return { ok: false, error: 'DSH profile link failed' };
217
- }
218
- mark(log, true, `DSH crew profile linked offline (dedicated Crew DSH_HOME, profile dsh-crew; ${registration.changed ? 'updated' : 'already current'})`);
219
- }
220
-
221
- if (dryRun) mark(log, true, 'Codex Desktop integration (dry-run)');
222
- else {
223
- const r = installer.installCodex ? installer.installCodex({ home, root }) : realInstaller.installCodex({ home, root });
224
- mark(log, r.ok !== false, r.ok === false ? `Codex Desktop integration failed: ${(r.actions ?? []).join('; ')}` : 'Codex Desktop integration');
225
- if (r.ok === false) return { ok: false, error: 'Codex integration failed' };
226
- }
227
-
228
- if (dryRun) mark(log, true, 'ZCode integration (dry-run)');
229
- else {
230
- const r = installer.installZCode
231
- ? installer.installZCode({ home, root })
232
- : realInstaller.installZCode({ home, root });
233
- if (r.ok === false) {
234
- mark(log, false, `ZCode integration failed (${r.code ?? 'unknown'})`);
235
- return { ok: false, error: 'ZCode integration failed' };
236
- }
237
- mark(log, true, 'ZCode integration');
238
- }
239
-
240
- if (dryRun) mark(log, true, 'Windows login startup (dry-run)');
241
- else {
242
- const r = installer.installWindowsStartup
243
- ? installer.installWindowsStartup({ home, root })
244
- : realInstaller.installWindowsStartup({ home, root });
245
- if (r.ok === false) {
246
- mark(log, false, `Windows login startup failed (${r.code ?? 'unknown'})`);
247
- return { ok: false, error: 'Windows login startup failed' };
248
- }
249
- if (r.supported) mark(log, true, 'Windows login startup');
250
- }
251
-
252
- if (commandExists('claude')) {
253
- if (dryRun) mark(log, true, 'Claude Code integration (dry-run)');
254
- else {
255
- const r = installer.installClaudeCode ? await installer.installClaudeCode({ home, root }) : await realInstaller.installClaudeCode({ home, root });
256
- const ok = r.ok !== false;
257
- mark(log, ok, ok ? 'Claude Code integration' : 'Claude Code integration failed');
258
- if (!ok) return { ok: false, error: 'Claude Code integration failed' };
259
- }
260
- } else log('- Claude Code not detected, skipped');
261
-
262
- log('');
263
- log('Done.');
264
- log('Restart DSH Web and Codex Desktop.');
265
- return { ok: true };
266
- }
267
-
268
- export async function setupUninstall({
269
- dryRun = false,
270
- purge = false,
271
- log = console.log,
272
- root = ROOT,
273
- home = homedir(),
274
- installer = realInstaller,
275
- } = {}) {
276
- log('DSH Crew uninstaller');
277
- const failures = [];
278
- const fail = (name, text) => { mark(log, false, text || `${name} failed`); failures.push(name); };
279
-
280
- if (dryRun) {
281
- mark(log, true, 'Codex Desktop integration would be removed');
282
- mark(log, true, 'ZCode integration would be removed');
283
- mark(log, true, 'Claude Code integration would be removed');
284
- mark(log, true, 'Windows login startup would be removed');
285
- mark(log, true, 'DSH crew profile would be removed');
286
- } else {
287
- const cx = installer.uninstallCodex ? installer.uninstallCodex({ home }) : realInstaller.uninstallCodex({ home });
288
- if (cx.ok !== false) mark(log, true, 'Codex Desktop integration removed');
289
- else fail('codex', 'Codex Desktop integration removal failed');
290
-
291
- const zc = installer.uninstallZCode
292
- ? installer.uninstallZCode({ home, root })
293
- : realInstaller.uninstallZCode({ home, root });
294
- if (zc.ok !== false) mark(log, true, 'ZCode integration removed');
295
- else fail('zcode', 'ZCode integration removal failed');
296
-
297
- const cl = installer.uninstallClaudeCode ? installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
298
- if (cl.ok !== false) mark(log, true, 'Claude Code integration removed');
299
- else fail('claude', 'Claude Code integration removal failed');
300
-
301
- const startup = installer.uninstallWindowsStartup
302
- ? installer.uninstallWindowsStartup({ home })
303
- : realInstaller.uninstallWindowsStartup({ home });
304
- if (startup.ok === false) fail('startup', 'Windows login startup removal failed');
305
- else if (startup.supported) mark(log, true, 'Windows login startup removed');
306
-
307
- const name = readPackageName(root);
308
- if (!name) fail('dsh', 'DSH plugin removal failed: package name missing');
309
- else {
310
- // Removing a registration is intentionally metadata-only and offline.
311
- // Calling `dsh plugin remove` delegates to pnpm, which may resolve the
312
- // whole profile and unexpectedly reach the network during uninstall.
313
- const removed = removeCrewPluginRegistration({ home, name });
314
- if (!removed.ok) fail('dsh', `DSH crew profile removal failed (${removed.code ?? 'unknown'})`);
315
- else mark(log, true, removed.removed ? 'DSH crew profile removed (offline, Crew-owned state)' : 'DSH crew profile already removed (dedicated Crew DSH_HOME, official web profile ignored)');
316
- }
317
- }
318
-
319
- if (!dryRun) {
320
- if (purge) {
321
- try { rmSync(join(home, '.config', 'dsh-crew'), { recursive: true, force: true }); mark(log, true, '~/.config/dsh-crew purged'); }
322
- catch { fail('purge', 'could not purge ~/.config/dsh-crew'); }
323
- } else log('\nConfig/backups kept.');
324
- }
325
-
326
- if (failures.length > 0) {
327
- log('\nFAILED: uninstall incomplete');
328
- return { ok: false, failures };
329
- }
330
- log('Done.');
331
- return { ok: true, failures: [] };
332
- }
333
-
334
- export async function setupStatus({ log = console.log, root = ROOT, home = homedir(), installer = realInstaller } = {}) {
335
- const st = installer.installStatus ? installer.installStatus({ home, root }) : realInstaller.installStatus({ home, root });
336
- const integrationLabel = (entry) => !entry?.installed ? 'not installed' : entry.ready === true ? 'installed' : 'needs repair';
337
- const claude = integrationLabel(st?.claude);
338
- const codex = integrationLabel(st?.codex);
339
- const zcode = integrationLabel(st?.zcode);
340
- const startupState = installer.windowsStartupStatus
341
- ? installer.windowsStartupStatus({ home, root })
342
- : realInstaller.windowsStartupStatus({ home, root });
343
- const windowsStartup = !startupState.supported ? 'not supported'
344
- : startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
345
- // Crew status reads ONLY the dedicated Crew profile under the Crew DSH_HOME;
346
- // the official web profile layout under the default DSH home is intentionally
347
- // never inspected.
348
- const crewProfDir = crewProfileDir({ home });
349
- const pkgFile = join(crewProfDir, 'package.json');
350
- let dshPlugin = 'not installed';
351
- if (existsSync(pkgFile)) {
352
- try {
353
- const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
354
- const name = readPackageName(root);
355
- dshPlugin = (name && (pkg.dependencies?.[name] ?? pkg.dsh?.profile?.bundles?.includes?.(name))) ? 'installed' : 'not installed';
356
- } catch { dshPlugin = 'unknown'; }
357
- }
358
- log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
359
- log(`Codex Desktop integration: ${codex}`);
360
- log(`ZCode integration: ${zcode}`);
361
- log(`Claude Code integration: ${claude}`);
362
- log(`Windows login startup: ${windowsStartup}`);
363
- return { ok: true, dshPlugin, codex, zcode, claude, windowsStartup };
364
- }
365
-
366
- export async function runSetupCli({ argv = process.argv.slice(2), run: actions = {}, log = console.log } = {}) {
367
- const action = argv[0];
368
- const flags = new Set(argv.slice(1));
369
- const dryRun = flags.has('--dry-run');
370
- const purge = flags.has('--purge');
371
- const install = actions.install ?? setupInstall;
372
- const uninstall = actions.uninstall ?? setupUninstall;
373
- const status = actions.status ?? setupStatus;
374
- try {
375
- let result;
376
- if (action === 'install') result = await install({ dryRun, log });
377
- else if (action === 'uninstall') result = await uninstall({ dryRun, purge, log });
378
- else if (action === 'status') result = await status({ log });
379
- else {
380
- console.error('usage: node scripts/setup.mjs <install|uninstall|status> [--dry-run] [--purge]');
381
- return 1;
382
- }
383
- return result?.ok === false ? 1 : 0;
384
- } catch (err) {
385
- console.error(`setup failed: ${err?.message ?? err}`);
386
- return 1;
387
- }
388
- }
389
-
390
- const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) || (
391
- process.argv[1] && resolve(join(process.cwd(), process.argv[1])) === fileURLToPath(import.meta.url)
392
- );
393
- if (isMain) process.exitCode = await runSetupCli({});
1
+ #!/usr/bin/env node
2
+ // One-click DSH Crew installer / uninstaller / status.
3
+ //
4
+ // node scripts/setup.mjs install [--dry-run]
5
+ // node scripts/setup.mjs uninstall [--dry-run] [--purge]
6
+ // node scripts/setup.mjs status
7
+
8
+ import { spawnSync } from 'node:child_process';
9
+ import { readFileSync, existsSync, rmSync } from 'node:fs';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+ import * as realInstaller from '../src/install/install.mjs';
14
+ import { crewDshHome, crewProfileDir, CREW_PROFILE_NAME } from '../src/install/install.mjs';
15
+ import {
16
+ ensureCrewDshRuntime,
17
+ ensureCrewPluginRegistration,
18
+ resolveDshCli,
19
+ runResolvedDsh,
20
+ describeDshCli,
21
+ removeCrewPluginRegistration,
22
+ TARGET_DSH_VERSION,
23
+ } from '../src/dsh-cli-runtime.mjs';
24
+
25
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
26
+
27
+ function commandExists(name) {
28
+ const r = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', [name], {
29
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
30
+ });
31
+ return r.status === 0;
32
+ }
33
+
34
+ const PACKAGE_MANAGER_COMMAND = /^(?:npm|npx|pnpm|yarn|yarnpkg|bun|bunx)$/i;
35
+
36
+ export function spawnCommand(command, platform = process.platform) {
37
+ if (platform === 'win32' && PACKAGE_MANAGER_COMMAND.test(command)) {
38
+ return `${command}.cmd`;
39
+ }
40
+ return command;
41
+ }
42
+
43
+ export function spawnNeedsShell(command, platform = process.platform) {
44
+ return platform === 'win32' && PACKAGE_MANAGER_COMMAND.test(command);
45
+ }
46
+
47
+ export function spawnInvocation(command, args = [], platform = process.platform, environment = process.env) {
48
+ const executable = spawnCommand(command, platform);
49
+ if (!spawnNeedsShell(command, platform)) return { command: executable, args };
50
+ const unsafe = args.find((arg) => !/^[A-Za-z0-9@._:+/=\-]+$/.test(String(arg)));
51
+ if (unsafe !== undefined) throw new Error(`unsafe package-manager argument: ${unsafe}`);
52
+ return {
53
+ command: environment.ComSpec || 'cmd.exe',
54
+ args: ['/d', '/s', '/c', [executable, ...args].join(' ')],
55
+ };
56
+ }
57
+
58
+ function run(cmd, args, opts = {}) {
59
+ let invocation;
60
+ try {
61
+ invocation = spawnInvocation(cmd, args);
62
+ } catch (error) {
63
+ return { ok: false, status: -1, stdout: '', stderr: String(error?.message ?? error), error };
64
+ }
65
+ const r = spawnSync(invocation.command, invocation.args, {
66
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: opts.shell === true, cwd: opts.cwd ?? ROOT,
67
+ env: opts.env ?? process.env,
68
+ });
69
+ const processError = r.error ? `${r.error.name}: ${r.error.message}` : '';
70
+ return {
71
+ ok: r.status === 0,
72
+ status: r.status ?? -1,
73
+ stdout: r.stdout ?? '',
74
+ stderr: r.stderr || processError,
75
+ error: r.error ?? null,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Detect the DSH CLI. `allowDownload` is intentionally opt-in: installation
81
+ * may use `npx -y` to fetch DSH, while status/uninstall probes never trigger a
82
+ * network download merely by checking availability.
83
+ *
84
+ * The returned descriptor is the CLI base only; every invocation is built by
85
+ * `runDsh` with the dedicated dsh-crew profile and the Crew DSH_HOME in the
86
+ * child process environment. The official ``web`` profile is never a target.
87
+ */
88
+ export function detectDsh({ allowDownload = false, home = homedir(), includeCompatibility = true } = {}) {
89
+ const resolved = resolveDshCli({
90
+ home,
91
+ allowDownload,
92
+ includeCompatibility,
93
+ });
94
+ if (!resolved) return null;
95
+ return { ...resolved, cli: resolved.command, description: describeDshCli(resolved) };
96
+ }
97
+
98
+ /**
99
+ * Run a DSH plugin command against the dedicated Crew profile under the
100
+ * Crew-owned DSH_HOME. `home` is the "user home" base used to derive that
101
+ * isolated home, so tests can point it at a disposable root.
102
+ */
103
+ export function runDsh(dsh, args, { home = homedir() } = {}) {
104
+ const resolved = dsh?.command
105
+ ? dsh
106
+ : { kind: dsh?.kind ?? 'legacy', command: dsh?.cli ?? dsh, args: [] };
107
+ return runResolvedDsh(resolved, ['plugin', '--profile', CREW_PROFILE_NAME, ...args], { home });
108
+ }
109
+
110
+ export function readPackageName(root = ROOT) {
111
+ try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name; } catch { return null; }
112
+ }
113
+
114
+ export function checkRoot(root = ROOT) {
115
+ return existsSync(join(root, 'package.json'))
116
+ && existsSync(join(root, 'src', 'server.mjs'))
117
+ && existsSync(join(root, 'cordis.patch.yml'));
118
+ }
119
+
120
+ export function depsPresent(root = ROOT) {
121
+ return existsSync(join(root, 'node_modules', '@modelcontextprotocol', 'sdk'))
122
+ && existsSync(join(root, 'node_modules', '.bin', 'tsdown'));
123
+ }
124
+
125
+ export function profileHasPackage(home, name, profileRoot = crewProfileDir({ home })) {
126
+ const pkgFile = join(profileRoot, 'package.json');
127
+ if (!existsSync(pkgFile)) return false;
128
+ try {
129
+ const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
130
+ return Boolean(pkg.dependencies?.[name]) || Boolean((pkg.dsh?.profile?.bundles ?? []).includes(name));
131
+ } catch { return false; }
132
+ }
133
+
134
+ function mark(log, ok, text) {
135
+ log(`${ok ? '✓' : '✗'} ${text}`);
136
+ return ok;
137
+ }
138
+
139
+ export async function setupInstall({
140
+ dryRun = false,
141
+ log = console.log,
142
+ root = ROOT,
143
+ home = homedir(),
144
+ installer = realInstaller,
145
+ depRunner = null,
146
+ } = {}) {
147
+ log('DSH Crew installer');
148
+ if (!checkRoot(root)) return { ok: false, error: `not a dsh-crew checkout: ${root}` };
149
+ const pnpmOk = commandExists('pnpm');
150
+
151
+ if (depRunner) {
152
+ const r = depRunner(root);
153
+ if (!r.ok) { mark(log, false, r.text); return { ok: false, error: r.text }; }
154
+ mark(log, true, r.text);
155
+ } else if (!dryRun) {
156
+ const cmd = pnpmOk ? 'pnpm' : 'npx';
157
+ const baseArgs = pnpmOk ? [] : ['-y', 'pnpm'];
158
+ const d = run(cmd, [...baseArgs, 'install', '--frozen-lockfile'], { cwd: root });
159
+ if (!d.ok) {
160
+ if (depsPresent(root)) mark(log, true, `dependencies (already present; install skipped — ${(d.stderr || d.stdout || '').trim().split('\n')[0]})`);
161
+ else {
162
+ mark(log, false, `dependencies install failed: ${(d.stderr || d.stdout || '').trim().slice(-300)}`);
163
+ return { ok: false, error: 'dependencies install failed' };
164
+ }
165
+ } else mark(log, true, 'dependencies');
166
+ } else mark(log, true, 'dependencies (dry-run)');
167
+
168
+ if (!dryRun) {
169
+ const b = pnpmOk
170
+ ? run('pnpm', ['run', 'build:client'], { cwd: root })
171
+ : run('npx', ['-y', 'pnpm', 'run', 'build:client'], { cwd: root });
172
+ if (!b.ok) {
173
+ log(`✗ client build failed:\n${(b.stderr || b.stdout || '').slice(0, 600)}`);
174
+ return { ok: false, error: 'client build failed' };
175
+ }
176
+ }
177
+ mark(log, true, 'client build' + (dryRun ? ' (dry-run)' : ''));
178
+
179
+ const name = readPackageName(root);
180
+ if (!name) return { ok: false, error: 'package.json name missing' };
181
+ // Prefer an explicit/global or reusable Crew-owned runtime. If only npx is
182
+ // available, install a reusable copy under Crew state before falling back to
183
+ // the transient download path. Dry-run never provisions or downloads.
184
+ let dsh = detectDsh({ allowDownload: false, home });
185
+ // A reusable Crew runtime pinned to a DIFFERENT cohort than this source
186
+ // tree must never be silently reused (and never upgraded in place by the
187
+ // installer): a stale runtime would boot the wrong @deepseek-ai/dsh. The
188
+ // transactional update path owns cohort migration. Dry-run never mutates
189
+ // and must not be blocked by the machine's current cohort state.
190
+ if (!dryRun && dsh?.kind === 'crew-runtime' && dsh.version && dsh.version !== TARGET_DSH_VERSION) {
191
+ return {
192
+ ok: false,
193
+ error: `Crew runtime ${dsh.version} does not match ${TARGET_DSH_VERSION}; use the transactional update path (dsh-crew update) to migrate the cohort`,
194
+ };
195
+ }
196
+ if (!dryRun && (!dsh || dsh.kind === 'npx-local')) {
197
+ const boot = ensureCrewDshRuntime({ home });
198
+ if (boot.ok) dsh = boot.cli;
199
+ else log(`- reusable Crew DSH runtime unavailable (${boot.code ?? 'unknown'}); trying compatibility fallback`);
200
+ }
201
+ if (!dsh) dsh = detectDsh({ allowDownload: !dryRun, home });
202
+ if (!dsh) {
203
+ log('✗ DSH CLI not detected (Crew runtime, dsh, or npx)');
204
+ return { ok: false, error: 'DSH CLI not found' };
205
+ }
206
+ if (dryRun) {
207
+ // The old "DSH web profile would be linked" path is retired; the installer
208
+ // plans a link into the dedicated dsh-crew profile only. The legacy phrasing
209
+ // is kept here as an explicit contrast so existing CLI tests/people see the
210
+ // change, never as a command that targets the official web profile.
211
+ mark(log, true, `DSH web profile would be linked (legacy) — now: dedicated dsh-crew profile under the Crew DSH_HOME; the official web profile is never modified (${dsh.description ?? dsh.kind} add "link:${root}")`);
212
+ } else {
213
+ const registration = ensureCrewPluginRegistration({ home, root, name });
214
+ if (!registration.ok) {
215
+ log(`✗ DSH crew profile link failed (${registration.code ?? 'unknown'})`);
216
+ return { ok: false, error: 'DSH profile link failed' };
217
+ }
218
+ mark(log, true, `DSH crew profile linked offline (dedicated Crew DSH_HOME, profile dsh-crew; ${registration.changed ? 'updated' : 'already current'})`);
219
+ }
220
+
221
+ if (dryRun) mark(log, true, 'Codex Desktop integration (dry-run)');
222
+ else {
223
+ const r = installer.installCodex ? installer.installCodex({ home, root }) : realInstaller.installCodex({ home, root });
224
+ mark(log, r.ok !== false, r.ok === false ? `Codex Desktop integration failed: ${(r.actions ?? []).join('; ')}` : 'Codex Desktop integration');
225
+ if (r.ok === false) return { ok: false, error: 'Codex integration failed' };
226
+ }
227
+
228
+ if (dryRun) mark(log, true, 'ZCode integration (dry-run)');
229
+ else {
230
+ const r = installer.installZCode
231
+ ? installer.installZCode({ home, root })
232
+ : realInstaller.installZCode({ home, root });
233
+ if (r.ok === false) {
234
+ mark(log, false, `ZCode integration failed (${r.code ?? 'unknown'})`);
235
+ return { ok: false, error: 'ZCode integration failed' };
236
+ }
237
+ mark(log, true, 'ZCode integration');
238
+ }
239
+
240
+ if (dryRun) mark(log, true, 'Windows login startup (dry-run)');
241
+ else {
242
+ const r = installer.installWindowsStartup
243
+ ? installer.installWindowsStartup({ home, root })
244
+ : realInstaller.installWindowsStartup({ home, root });
245
+ if (r.ok === false) {
246
+ mark(log, false, `Windows login startup failed (${r.code ?? 'unknown'})`);
247
+ return { ok: false, error: 'Windows login startup failed' };
248
+ }
249
+ if (r.supported) mark(log, true, 'Windows login startup');
250
+ }
251
+
252
+ if (commandExists('claude')) {
253
+ if (dryRun) mark(log, true, 'Claude Code integration (dry-run)');
254
+ else {
255
+ const r = installer.installClaudeCode ? await installer.installClaudeCode({ home, root }) : await realInstaller.installClaudeCode({ home, root });
256
+ const ok = r.ok !== false;
257
+ mark(log, ok, ok ? 'Claude Code integration' : 'Claude Code integration failed');
258
+ if (!ok) return { ok: false, error: 'Claude Code integration failed' };
259
+ }
260
+ } else log('- Claude Code not detected, skipped');
261
+
262
+ log('');
263
+ log('Done.');
264
+ log('Restart DSH Web and Codex Desktop.');
265
+ return { ok: true };
266
+ }
267
+
268
+ export async function setupUninstall({
269
+ dryRun = false,
270
+ purge = false,
271
+ log = console.log,
272
+ root = ROOT,
273
+ home = homedir(),
274
+ installer = realInstaller,
275
+ } = {}) {
276
+ log('DSH Crew uninstaller');
277
+ const failures = [];
278
+ const fail = (name, text) => { mark(log, false, text || `${name} failed`); failures.push(name); };
279
+
280
+ if (dryRun) {
281
+ mark(log, true, 'Codex Desktop integration would be removed');
282
+ mark(log, true, 'ZCode integration would be removed');
283
+ mark(log, true, 'Claude Code integration would be removed');
284
+ mark(log, true, 'Windows login startup would be removed');
285
+ mark(log, true, 'DSH crew profile would be removed');
286
+ } else {
287
+ const cx = installer.uninstallCodex ? installer.uninstallCodex({ home }) : realInstaller.uninstallCodex({ home });
288
+ if (cx.ok !== false) mark(log, true, 'Codex Desktop integration removed');
289
+ else fail('codex', 'Codex Desktop integration removal failed');
290
+
291
+ const zc = installer.uninstallZCode
292
+ ? installer.uninstallZCode({ home, root })
293
+ : realInstaller.uninstallZCode({ home, root });
294
+ if (zc.ok !== false) mark(log, true, 'ZCode integration removed');
295
+ else fail('zcode', 'ZCode integration removal failed');
296
+
297
+ const cl = installer.uninstallClaudeCode ? installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
298
+ if (cl.ok !== false) mark(log, true, 'Claude Code integration removed');
299
+ else fail('claude', 'Claude Code integration removal failed');
300
+
301
+ const startup = installer.uninstallWindowsStartup
302
+ ? installer.uninstallWindowsStartup({ home })
303
+ : realInstaller.uninstallWindowsStartup({ home });
304
+ if (startup.ok === false) fail('startup', 'Windows login startup removal failed');
305
+ else if (startup.supported) mark(log, true, 'Windows login startup removed');
306
+
307
+ const name = readPackageName(root);
308
+ if (!name) fail('dsh', 'DSH plugin removal failed: package name missing');
309
+ else {
310
+ // Removing a registration is intentionally metadata-only and offline.
311
+ // Calling `dsh plugin remove` delegates to pnpm, which may resolve the
312
+ // whole profile and unexpectedly reach the network during uninstall.
313
+ const removed = removeCrewPluginRegistration({ home, name });
314
+ if (!removed.ok) fail('dsh', `DSH crew profile removal failed (${removed.code ?? 'unknown'})`);
315
+ else mark(log, true, removed.removed ? 'DSH crew profile removed (offline, Crew-owned state)' : 'DSH crew profile already removed (dedicated Crew DSH_HOME, official web profile ignored)');
316
+ }
317
+ }
318
+
319
+ if (!dryRun) {
320
+ if (purge) {
321
+ try { rmSync(join(home, '.config', 'dsh-crew'), { recursive: true, force: true }); mark(log, true, '~/.config/dsh-crew purged'); }
322
+ catch { fail('purge', 'could not purge ~/.config/dsh-crew'); }
323
+ } else log('\nConfig/backups kept.');
324
+ }
325
+
326
+ if (failures.length > 0) {
327
+ log('\nFAILED: uninstall incomplete');
328
+ return { ok: false, failures };
329
+ }
330
+ log('Done.');
331
+ return { ok: true, failures: [] };
332
+ }
333
+
334
+ export async function setupStatus({ log = console.log, root = ROOT, home = homedir(), installer = realInstaller } = {}) {
335
+ const st = installer.installStatus ? installer.installStatus({ home, root }) : realInstaller.installStatus({ home, root });
336
+ const integrationLabel = (entry) => !entry?.installed ? 'not installed' : entry.ready === true ? 'installed' : 'needs repair';
337
+ const claude = integrationLabel(st?.claude);
338
+ const codex = integrationLabel(st?.codex);
339
+ const zcode = integrationLabel(st?.zcode);
340
+ const startupState = installer.windowsStartupStatus
341
+ ? installer.windowsStartupStatus({ home, root })
342
+ : realInstaller.windowsStartupStatus({ home, root });
343
+ const windowsStartup = !startupState.supported ? 'not supported'
344
+ : startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
345
+ // Crew status reads ONLY the dedicated Crew profile under the Crew DSH_HOME;
346
+ // the official web profile layout under the default DSH home is intentionally
347
+ // never inspected.
348
+ const crewProfDir = crewProfileDir({ home });
349
+ const pkgFile = join(crewProfDir, 'package.json');
350
+ let dshPlugin = 'not installed';
351
+ if (existsSync(pkgFile)) {
352
+ try {
353
+ const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
354
+ const name = readPackageName(root);
355
+ dshPlugin = (name && (pkg.dependencies?.[name] ?? pkg.dsh?.profile?.bundles?.includes?.(name))) ? 'installed' : 'not installed';
356
+ } catch { dshPlugin = 'unknown'; }
357
+ }
358
+ log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
359
+ log(`Codex Desktop integration: ${codex}`);
360
+ log(`ZCode integration: ${zcode}`);
361
+ log(`Claude Code integration: ${claude}`);
362
+ log(`Windows login startup: ${windowsStartup}`);
363
+ return { ok: true, dshPlugin, codex, zcode, claude, windowsStartup };
364
+ }
365
+
366
+ export async function runSetupCli({ argv = process.argv.slice(2), run: actions = {}, log = console.log } = {}) {
367
+ const action = argv[0];
368
+ const flags = new Set(argv.slice(1));
369
+ const dryRun = flags.has('--dry-run');
370
+ const purge = flags.has('--purge');
371
+ const install = actions.install ?? setupInstall;
372
+ const uninstall = actions.uninstall ?? setupUninstall;
373
+ const status = actions.status ?? setupStatus;
374
+ try {
375
+ let result;
376
+ if (action === 'install') result = await install({ dryRun, log });
377
+ else if (action === 'uninstall') result = await uninstall({ dryRun, purge, log });
378
+ else if (action === 'status') result = await status({ log });
379
+ else {
380
+ console.error('usage: node scripts/setup.mjs <install|uninstall|status> [--dry-run] [--purge]');
381
+ return 1;
382
+ }
383
+ return result?.ok === false ? 1 : 0;
384
+ } catch (err) {
385
+ console.error(`setup failed: ${err?.message ?? err}`);
386
+ return 1;
387
+ }
388
+ }
389
+
390
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) || (
391
+ process.argv[1] && resolve(join(process.cwd(), process.argv[1])) === fileURLToPath(import.meta.url)
392
+ );
393
+ if (isMain) process.exitCode = await runSetupCli({});