@ran-sh/dsh-crew 0.3.6 → 0.3.8

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 (85) hide show
  1. package/.claude-plugin/plugin.json +8 -8
  2. package/.mcp.json +8 -8
  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 +94 -180
  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 +94 -180
  19. package/agents/ds-flash.md +26 -26
  20. package/agents/ds-pro.md +32 -32
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +22 -22
  23. package/codex/agents/ds-flash.toml +30 -30
  24. package/codex/agents/ds-pro.toml +31 -31
  25. package/codex/agents/ds-reviewer.toml +28 -28
  26. package/codex/agents/ds-worker.toml +28 -28
  27. package/codex/prompts/dsh-config.md +3 -3
  28. package/codex/prompts/dsh-status.md +1 -1
  29. package/commands/config.md +11 -11
  30. package/commands/off.md +5 -5
  31. package/commands/on.md +5 -5
  32. package/commands/status.md +5 -5
  33. package/cordis.patch.yml +4 -4
  34. package/lib/client.js +1655 -974
  35. package/official-web-bridge/cordis.patch.yml +4 -0
  36. package/official-web-bridge/entry.mjs +1 -0
  37. package/official-web-bridge/lib/client.js +3446 -0
  38. package/official-web-bridge/package.json +26 -0
  39. package/package.json +133 -131
  40. package/scripts/build-client.mjs +32 -28
  41. package/scripts/live-crew-smoke.mjs +39 -39
  42. package/scripts/live-policy-matrix.mjs +177 -177
  43. package/scripts/policy-probe.mjs +101 -101
  44. package/scripts/setup.mjs +284 -284
  45. package/scripts/smoke-real.mjs +110 -110
  46. package/scripts/smoke.mjs +78 -78
  47. package/scripts/verify-installer-fix.mjs +26 -26
  48. package/scripts/verify-official-bridge-e2e.mjs +159 -0
  49. package/src/adaptive-routing.mjs +260 -260
  50. package/src/client/activation-summary.tsx +64 -64
  51. package/src/client/collapsible-sections.mjs +55 -0
  52. package/src/client/entry.tsx +236 -236
  53. package/src/client/index.tsx +1213 -1120
  54. package/src/config-readiness.mjs +59 -59
  55. package/src/delivery.mjs +205 -205
  56. package/src/dsh-cli-runtime.mjs +458 -447
  57. package/src/failure-classification.mjs +172 -172
  58. package/src/hub/entry.mjs +98 -98
  59. package/src/hub-client.mjs +132 -132
  60. package/src/hub-compatibility.mjs +49 -49
  61. package/src/i18n.mjs +19 -19
  62. package/src/install/cli.mjs +28 -28
  63. package/src/install/install-legacy.mjs +462 -462
  64. package/src/install/install.mjs +451 -451
  65. package/src/install/npx-lifecycle.mjs +1119 -1055
  66. package/src/install/official-web.mjs +132 -0
  67. package/src/mcp-runtime.mjs +257 -257
  68. package/src/model-catalog.mjs +173 -173
  69. package/src/model-routing.mjs +391 -391
  70. package/src/official-web-bridge.mjs +196 -0
  71. package/src/policy.mjs +197 -197
  72. package/src/readiness-matrix.mjs +169 -169
  73. package/src/runtime-controls.mjs +90 -90
  74. package/src/runtime-identity.mjs +108 -108
  75. package/src/server.mjs +477 -477
  76. package/src/status-shard.mjs +52 -52
  77. package/src/structured-error-code.mjs +38 -38
  78. package/src/vision-route.mjs +138 -138
  79. package/src/workflow-runtime.mjs +573 -573
  80. package/src/workflow.mjs +160 -160
  81. package/src/workspace-audit.mjs +231 -231
  82. package/src/workspace-isolation.mjs +365 -365
  83. package/statusline/statusline.sh +14 -14
  84. package/statusline/worker-segment.sh +35 -35
  85. package/worker.cordis.yml +77 -77
package/scripts/setup.mjs CHANGED
@@ -1,163 +1,163 @@
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 {
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
16
  ensureCrewDshRuntime,
17
17
  ensureCrewPluginRegistration,
18
18
  resolveDshCli,
19
- runResolvedDsh,
20
- describeDshCli,
21
- removeCrewPluginRegistration,
22
- } from '../src/dsh-cli-runtime.mjs';
23
-
24
- const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
25
-
26
- function commandExists(name) {
27
- const r = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', [name], {
28
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
29
- });
30
- return r.status === 0;
31
- }
32
-
33
- function run(cmd, args, opts = {}) {
34
- const r = spawnSync(cmd, args, {
35
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: opts.shell === true, cwd: opts.cwd ?? ROOT,
36
- env: opts.env ?? process.env,
37
- });
38
- return { ok: r.status === 0, status: r.status ?? -1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
39
- }
40
-
41
- /**
42
- * Detect the DSH CLI. `allowDownload` is intentionally opt-in: installation
43
- * may use `npx -y` to fetch DSH, while status/uninstall probes never trigger a
44
- * network download merely by checking availability.
45
- *
46
- * The returned descriptor is the CLI base only; every invocation is built by
47
- * `runDsh` with the dedicated dsh-crew profile and the Crew DSH_HOME in the
48
- * child process environment. The official ``web`` profile is never a target.
49
- */
50
- export function detectDsh({ allowDownload = false, home = homedir(), includeCompatibility = true } = {}) {
51
- const resolved = resolveDshCli({
52
- home,
53
- allowDownload,
54
- includeCompatibility,
55
- });
56
- if (!resolved) return null;
57
- return { ...resolved, cli: resolved.command, description: describeDshCli(resolved) };
58
- }
59
-
60
- /**
61
- * Run a DSH plugin command against the dedicated Crew profile under the
62
- * Crew-owned DSH_HOME. `home` is the "user home" base used to derive that
63
- * isolated home, so tests can point it at a disposable root.
64
- */
65
- export function runDsh(dsh, args, { home = homedir() } = {}) {
66
- const resolved = dsh?.command
67
- ? dsh
68
- : { kind: dsh?.kind ?? 'legacy', command: dsh?.cli ?? dsh, args: [] };
69
- return runResolvedDsh(resolved, ['plugin', '--profile', CREW_PROFILE_NAME, ...args], { home });
70
- }
71
-
72
- export function readPackageName(root = ROOT) {
73
- try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name; } catch { return null; }
74
- }
75
-
76
- export function checkRoot(root = ROOT) {
77
- return existsSync(join(root, 'package.json'))
78
- && existsSync(join(root, 'src', 'server.mjs'))
79
- && existsSync(join(root, 'cordis.patch.yml'));
80
- }
81
-
82
- export function depsPresent(root = ROOT) {
83
- return existsSync(join(root, 'node_modules', '@modelcontextprotocol', 'sdk'))
84
- && existsSync(join(root, 'node_modules', '.bin', 'tsdown'));
85
- }
86
-
87
- export function profileHasPackage(home, name, profileRoot = crewProfileDir({ home })) {
88
- const pkgFile = join(profileRoot, 'package.json');
89
- if (!existsSync(pkgFile)) return false;
90
- try {
91
- const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
92
- return Boolean(pkg.dependencies?.[name]) || Boolean((pkg.dsh?.profile?.bundles ?? []).includes(name));
93
- } catch { return false; }
94
- }
95
-
96
- function mark(log, ok, text) {
97
- log(`${ok ? '✓' : '✗'} ${text}`);
98
- return ok;
99
- }
100
-
101
- export async function setupInstall({
102
- dryRun = false,
103
- log = console.log,
104
- root = ROOT,
105
- home = homedir(),
106
- installer = realInstaller,
107
- depRunner = null,
108
- } = {}) {
109
- log('DSH Crew installer');
110
- if (!checkRoot(root)) return { ok: false, error: `not a dsh-crew checkout: ${root}` };
111
-
112
- if (depRunner) {
113
- const r = depRunner(root);
114
- if (!r.ok) { mark(log, false, r.text); return { ok: false, error: r.text }; }
115
- mark(log, true, r.text);
116
- } else if (!dryRun) {
117
- const pnpmOk = commandExists('pnpm');
118
- const cmd = pnpmOk ? 'pnpm' : 'npx';
119
- const baseArgs = pnpmOk ? [] : ['-y', 'pnpm'];
120
- const d = run(cmd, [...baseArgs, 'install', '--frozen-lockfile']);
121
- if (!d.ok) {
122
- if (depsPresent(root)) mark(log, true, `dependencies (already present; install skipped — ${(d.stderr || d.stdout || '').trim().split('\n')[0]})`);
123
- else {
124
- mark(log, false, `dependencies install failed: ${(d.stderr || d.stdout || '').trim().slice(-300)}`);
125
- return { ok: false, error: 'dependencies install failed' };
126
- }
127
- } else mark(log, true, 'dependencies');
128
- } else mark(log, true, 'dependencies (dry-run)');
129
-
130
- if (!dryRun) {
131
- const b = run('npx --no-install tsdown src/client/index.tsx --format cjs --platform browser --target es2022 --tsconfig tsconfig.client.json --deps.never-bundle react --deps.never-bundle react/jsx-runtime --deps.never-bundle react-dom --deps.never-bundle react-dom/client --out-dir .client-build --clean --logLevel warn && node scripts/build-client.mjs', [], { shell: true });
132
- if (!b.ok) {
133
- log(`✗ client build failed:\n${(b.stderr || b.stdout || '').slice(0, 600)}`);
134
- return { ok: false, error: 'client build failed' };
135
- }
136
- }
137
- mark(log, true, 'client build' + (dryRun ? ' (dry-run)' : ''));
138
-
139
- const name = readPackageName(root);
140
- if (!name) return { ok: false, error: 'package.json name missing' };
141
- // Prefer an explicit/global or reusable Crew-owned runtime. If only npx is
142
- // available, install a reusable copy under Crew state before falling back to
143
- // the transient download path. Dry-run never provisions or downloads.
144
- let dsh = detectDsh({ allowDownload: false, home });
145
- if (!dryRun && (!dsh || dsh.kind === 'npx-local')) {
146
- const boot = ensureCrewDshRuntime({ home });
147
- if (boot.ok) dsh = boot.cli;
148
- else log(`- reusable Crew DSH runtime unavailable (${boot.code ?? 'unknown'}); trying compatibility fallback`);
149
- }
150
- if (!dsh) dsh = detectDsh({ allowDownload: !dryRun, home });
151
- if (!dsh) {
152
- log('✗ DSH CLI not detected (Crew runtime, dsh, or npx)');
153
- return { ok: false, error: 'DSH CLI not found' };
154
- }
155
- if (dryRun) {
156
- // The old "DSH web profile would be linked" path is retired; the installer
157
- // plans a link into the dedicated dsh-crew profile only. The legacy phrasing
158
- // is kept here as an explicit contrast so existing CLI tests/people see the
159
- // change, never as a command that targets the official web profile.
160
- 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}")`);
19
+ runResolvedDsh,
20
+ describeDshCli,
21
+ removeCrewPluginRegistration,
22
+ } from '../src/dsh-cli-runtime.mjs';
23
+
24
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
25
+
26
+ function commandExists(name) {
27
+ const r = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', [name], {
28
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
29
+ });
30
+ return r.status === 0;
31
+ }
32
+
33
+ function run(cmd, args, opts = {}) {
34
+ const r = spawnSync(cmd, args, {
35
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: opts.shell === true, cwd: opts.cwd ?? ROOT,
36
+ env: opts.env ?? process.env,
37
+ });
38
+ return { ok: r.status === 0, status: r.status ?? -1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
39
+ }
40
+
41
+ /**
42
+ * Detect the DSH CLI. `allowDownload` is intentionally opt-in: installation
43
+ * may use `npx -y` to fetch DSH, while status/uninstall probes never trigger a
44
+ * network download merely by checking availability.
45
+ *
46
+ * The returned descriptor is the CLI base only; every invocation is built by
47
+ * `runDsh` with the dedicated dsh-crew profile and the Crew DSH_HOME in the
48
+ * child process environment. The official ``web`` profile is never a target.
49
+ */
50
+ export function detectDsh({ allowDownload = false, home = homedir(), includeCompatibility = true } = {}) {
51
+ const resolved = resolveDshCli({
52
+ home,
53
+ allowDownload,
54
+ includeCompatibility,
55
+ });
56
+ if (!resolved) return null;
57
+ return { ...resolved, cli: resolved.command, description: describeDshCli(resolved) };
58
+ }
59
+
60
+ /**
61
+ * Run a DSH plugin command against the dedicated Crew profile under the
62
+ * Crew-owned DSH_HOME. `home` is the "user home" base used to derive that
63
+ * isolated home, so tests can point it at a disposable root.
64
+ */
65
+ export function runDsh(dsh, args, { home = homedir() } = {}) {
66
+ const resolved = dsh?.command
67
+ ? dsh
68
+ : { kind: dsh?.kind ?? 'legacy', command: dsh?.cli ?? dsh, args: [] };
69
+ return runResolvedDsh(resolved, ['plugin', '--profile', CREW_PROFILE_NAME, ...args], { home });
70
+ }
71
+
72
+ export function readPackageName(root = ROOT) {
73
+ try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name; } catch { return null; }
74
+ }
75
+
76
+ export function checkRoot(root = ROOT) {
77
+ return existsSync(join(root, 'package.json'))
78
+ && existsSync(join(root, 'src', 'server.mjs'))
79
+ && existsSync(join(root, 'cordis.patch.yml'));
80
+ }
81
+
82
+ export function depsPresent(root = ROOT) {
83
+ return existsSync(join(root, 'node_modules', '@modelcontextprotocol', 'sdk'))
84
+ && existsSync(join(root, 'node_modules', '.bin', 'tsdown'));
85
+ }
86
+
87
+ export function profileHasPackage(home, name, profileRoot = crewProfileDir({ home })) {
88
+ const pkgFile = join(profileRoot, 'package.json');
89
+ if (!existsSync(pkgFile)) return false;
90
+ try {
91
+ const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
92
+ return Boolean(pkg.dependencies?.[name]) || Boolean((pkg.dsh?.profile?.bundles ?? []).includes(name));
93
+ } catch { return false; }
94
+ }
95
+
96
+ function mark(log, ok, text) {
97
+ log(`${ok ? '✓' : '✗'} ${text}`);
98
+ return ok;
99
+ }
100
+
101
+ export async function setupInstall({
102
+ dryRun = false,
103
+ log = console.log,
104
+ root = ROOT,
105
+ home = homedir(),
106
+ installer = realInstaller,
107
+ depRunner = null,
108
+ } = {}) {
109
+ log('DSH Crew installer');
110
+ if (!checkRoot(root)) return { ok: false, error: `not a dsh-crew checkout: ${root}` };
111
+
112
+ if (depRunner) {
113
+ const r = depRunner(root);
114
+ if (!r.ok) { mark(log, false, r.text); return { ok: false, error: r.text }; }
115
+ mark(log, true, r.text);
116
+ } else if (!dryRun) {
117
+ const pnpmOk = commandExists('pnpm');
118
+ const cmd = pnpmOk ? 'pnpm' : 'npx';
119
+ const baseArgs = pnpmOk ? [] : ['-y', 'pnpm'];
120
+ const d = run(cmd, [...baseArgs, 'install', '--frozen-lockfile']);
121
+ if (!d.ok) {
122
+ if (depsPresent(root)) mark(log, true, `dependencies (already present; install skipped — ${(d.stderr || d.stdout || '').trim().split('\n')[0]})`);
123
+ else {
124
+ mark(log, false, `dependencies install failed: ${(d.stderr || d.stdout || '').trim().slice(-300)}`);
125
+ return { ok: false, error: 'dependencies install failed' };
126
+ }
127
+ } else mark(log, true, 'dependencies');
128
+ } else mark(log, true, 'dependencies (dry-run)');
129
+
130
+ if (!dryRun) {
131
+ const b = run('npx --no-install tsdown src/client/index.tsx --format cjs --platform browser --target es2022 --tsconfig tsconfig.client.json --deps.never-bundle react --deps.never-bundle react/jsx-runtime --deps.never-bundle react-dom --deps.never-bundle react-dom/client --out-dir .client-build --clean --logLevel warn && node scripts/build-client.mjs', [], { shell: true });
132
+ if (!b.ok) {
133
+ log(`✗ client build failed:\n${(b.stderr || b.stdout || '').slice(0, 600)}`);
134
+ return { ok: false, error: 'client build failed' };
135
+ }
136
+ }
137
+ mark(log, true, 'client build' + (dryRun ? ' (dry-run)' : ''));
138
+
139
+ const name = readPackageName(root);
140
+ if (!name) return { ok: false, error: 'package.json name missing' };
141
+ // Prefer an explicit/global or reusable Crew-owned runtime. If only npx is
142
+ // available, install a reusable copy under Crew state before falling back to
143
+ // the transient download path. Dry-run never provisions or downloads.
144
+ let dsh = detectDsh({ allowDownload: false, home });
145
+ if (!dryRun && (!dsh || dsh.kind === 'npx-local')) {
146
+ const boot = ensureCrewDshRuntime({ home });
147
+ if (boot.ok) dsh = boot.cli;
148
+ else log(`- reusable Crew DSH runtime unavailable (${boot.code ?? 'unknown'}); trying compatibility fallback`);
149
+ }
150
+ if (!dsh) dsh = detectDsh({ allowDownload: !dryRun, home });
151
+ if (!dsh) {
152
+ log('✗ DSH CLI not detected (Crew runtime, dsh, or npx)');
153
+ return { ok: false, error: 'DSH CLI not found' };
154
+ }
155
+ if (dryRun) {
156
+ // The old "DSH web profile would be linked" path is retired; the installer
157
+ // plans a link into the dedicated dsh-crew profile only. The legacy phrasing
158
+ // is kept here as an explicit contrast so existing CLI tests/people see the
159
+ // change, never as a command that targets the official web profile.
160
+ 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}")`);
161
161
  } else {
162
162
  const registration = ensureCrewPluginRegistration({ home, root, name });
163
163
  if (!registration.ok) {
@@ -166,130 +166,130 @@ export async function setupInstall({
166
166
  }
167
167
  mark(log, true, `DSH crew profile linked offline (dedicated Crew DSH_HOME, profile dsh-crew; ${registration.changed ? 'updated' : 'already current'})`);
168
168
  }
169
-
170
- if (dryRun) mark(log, true, 'Codex Desktop integration (dry-run)');
171
- else {
172
- const r = installer.installCodex ? installer.installCodex({ home }) : realInstaller.installCodex({ home });
173
- mark(log, r.ok !== false, r.ok === false ? `Codex Desktop integration failed: ${(r.actions ?? []).join('; ')}` : 'Codex Desktop integration');
174
- if (r.ok === false) return { ok: false, error: 'Codex integration failed' };
175
- }
176
-
177
- if (commandExists('claude')) {
178
- if (dryRun) mark(log, true, 'Claude Code integration (dry-run)');
179
- else {
180
- const r = installer.installClaudeCode ? await installer.installClaudeCode({ home }) : await realInstaller.installClaudeCode({ home });
181
- const ok = r.ok !== false;
182
- mark(log, ok, ok ? 'Claude Code integration' : 'Claude Code integration failed');
183
- if (!ok) return { ok: false, error: 'Claude Code integration failed' };
184
- }
185
- } else log('- Claude Code not detected, skipped');
186
-
187
- log('');
188
- log('Done.');
189
- log('Restart DSH Web and Codex Desktop.');
190
- return { ok: true };
191
- }
192
-
193
- export async function setupUninstall({
194
- dryRun = false,
195
- purge = false,
196
- log = console.log,
197
- root = ROOT,
198
- home = homedir(),
199
- installer = realInstaller,
200
- } = {}) {
201
- log('DSH Crew uninstaller');
202
- const failures = [];
203
- const fail = (name, text) => { mark(log, false, text || `${name} failed`); failures.push(name); };
204
-
205
- if (dryRun) {
206
- mark(log, true, 'Codex Desktop integration would be removed');
207
- mark(log, true, 'Claude Code integration would be removed');
208
- mark(log, true, 'DSH crew profile would be removed');
209
- } else {
210
- const cx = installer.uninstallCodex ? installer.uninstallCodex({ home }) : realInstaller.uninstallCodex({ home });
211
- if (cx.ok !== false) mark(log, true, 'Codex Desktop integration removed');
212
- else fail('codex', 'Codex Desktop integration removal failed');
213
-
214
- const cl = installer.uninstallClaudeCode ? installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
215
- if (cl.ok !== false) mark(log, true, 'Claude Code integration removed');
216
- else fail('claude', 'Claude Code integration removal failed');
217
-
218
- const name = readPackageName(root);
219
- if (!name) fail('dsh', 'DSH plugin removal failed: package name missing');
220
- else {
221
- // Removing a registration is intentionally metadata-only and offline.
222
- // Calling `dsh plugin remove` delegates to pnpm, which may resolve the
223
- // whole profile and unexpectedly reach the network during uninstall.
224
- const removed = removeCrewPluginRegistration({ home, name });
225
- if (!removed.ok) fail('dsh', `DSH crew profile removal failed (${removed.code ?? 'unknown'})`);
226
- 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)');
227
- }
228
- }
229
-
230
- if (!dryRun) {
231
- if (purge) {
232
- try { rmSync(join(home, '.config', 'dsh-crew'), { recursive: true, force: true }); mark(log, true, '~/.config/dsh-crew purged'); }
233
- catch { fail('purge', 'could not purge ~/.config/dsh-crew'); }
234
- } else log('\nConfig/backups kept.');
235
- }
236
-
237
- if (failures.length > 0) {
238
- log('\nFAILED: uninstall incomplete');
239
- return { ok: false, failures };
240
- }
241
- log('Done.');
242
- return { ok: true, failures: [] };
243
- }
244
-
245
- export async function setupStatus({ log = console.log, root = ROOT, home = homedir(), installer = realInstaller } = {}) {
246
- const st = installer.installStatus ? installer.installStatus({ home }) : realInstaller.installStatus({ home });
247
- const claude = st?.claude?.installed ? 'installed' : 'not installed';
248
- const codex = st?.codex?.installed ? 'installed' : 'not installed';
249
- // Crew status reads ONLY the dedicated Crew profile under the Crew DSH_HOME;
250
- // the official web profile layout under the default DSH home is intentionally
251
- // never inspected.
252
- const crewProfDir = crewProfileDir({ home });
253
- const pkgFile = join(crewProfDir, 'package.json');
254
- let dshPlugin = 'not installed';
255
- if (existsSync(pkgFile)) {
256
- try {
257
- const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
258
- const name = readPackageName(root);
259
- dshPlugin = (name && (pkg.dependencies?.[name] ?? pkg.dsh?.profile?.bundles?.includes?.(name))) ? 'installed' : 'not installed';
260
- } catch { dshPlugin = 'unknown'; }
261
- }
262
- log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
263
- log(`Codex Desktop integration: ${codex}`);
264
- log(`Claude Code integration: ${claude}`);
265
- return { ok: true, dshPlugin, codex, claude };
266
- }
267
-
268
- export async function runSetupCli({ argv = process.argv.slice(2), run: actions = {}, log = console.log } = {}) {
269
- const action = argv[0];
270
- const flags = new Set(argv.slice(1));
271
- const dryRun = flags.has('--dry-run');
272
- const purge = flags.has('--purge');
273
- const install = actions.install ?? setupInstall;
274
- const uninstall = actions.uninstall ?? setupUninstall;
275
- const status = actions.status ?? setupStatus;
276
- try {
277
- let result;
278
- if (action === 'install') result = await install({ dryRun, log });
279
- else if (action === 'uninstall') result = await uninstall({ dryRun, purge, log });
280
- else if (action === 'status') result = await status({ log });
281
- else {
282
- console.error('usage: node scripts/setup.mjs <install|uninstall|status> [--dry-run] [--purge]');
283
- return 1;
284
- }
285
- return result?.ok === false ? 1 : 0;
286
- } catch (err) {
287
- console.error(`setup failed: ${err?.message ?? err}`);
288
- return 1;
289
- }
290
- }
291
-
292
- const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) || (
293
- process.argv[1] && resolve(join(process.cwd(), process.argv[1])) === fileURLToPath(import.meta.url)
294
- );
295
- if (isMain) process.exitCode = await runSetupCli({});
169
+
170
+ if (dryRun) mark(log, true, 'Codex Desktop integration (dry-run)');
171
+ else {
172
+ const r = installer.installCodex ? installer.installCodex({ home }) : realInstaller.installCodex({ home });
173
+ mark(log, r.ok !== false, r.ok === false ? `Codex Desktop integration failed: ${(r.actions ?? []).join('; ')}` : 'Codex Desktop integration');
174
+ if (r.ok === false) return { ok: false, error: 'Codex integration failed' };
175
+ }
176
+
177
+ if (commandExists('claude')) {
178
+ if (dryRun) mark(log, true, 'Claude Code integration (dry-run)');
179
+ else {
180
+ const r = installer.installClaudeCode ? await installer.installClaudeCode({ home }) : await realInstaller.installClaudeCode({ home });
181
+ const ok = r.ok !== false;
182
+ mark(log, ok, ok ? 'Claude Code integration' : 'Claude Code integration failed');
183
+ if (!ok) return { ok: false, error: 'Claude Code integration failed' };
184
+ }
185
+ } else log('- Claude Code not detected, skipped');
186
+
187
+ log('');
188
+ log('Done.');
189
+ log('Restart DSH Web and Codex Desktop.');
190
+ return { ok: true };
191
+ }
192
+
193
+ export async function setupUninstall({
194
+ dryRun = false,
195
+ purge = false,
196
+ log = console.log,
197
+ root = ROOT,
198
+ home = homedir(),
199
+ installer = realInstaller,
200
+ } = {}) {
201
+ log('DSH Crew uninstaller');
202
+ const failures = [];
203
+ const fail = (name, text) => { mark(log, false, text || `${name} failed`); failures.push(name); };
204
+
205
+ if (dryRun) {
206
+ mark(log, true, 'Codex Desktop integration would be removed');
207
+ mark(log, true, 'Claude Code integration would be removed');
208
+ mark(log, true, 'DSH crew profile would be removed');
209
+ } else {
210
+ const cx = installer.uninstallCodex ? installer.uninstallCodex({ home }) : realInstaller.uninstallCodex({ home });
211
+ if (cx.ok !== false) mark(log, true, 'Codex Desktop integration removed');
212
+ else fail('codex', 'Codex Desktop integration removal failed');
213
+
214
+ const cl = installer.uninstallClaudeCode ? installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
215
+ if (cl.ok !== false) mark(log, true, 'Claude Code integration removed');
216
+ else fail('claude', 'Claude Code integration removal failed');
217
+
218
+ const name = readPackageName(root);
219
+ if (!name) fail('dsh', 'DSH plugin removal failed: package name missing');
220
+ else {
221
+ // Removing a registration is intentionally metadata-only and offline.
222
+ // Calling `dsh plugin remove` delegates to pnpm, which may resolve the
223
+ // whole profile and unexpectedly reach the network during uninstall.
224
+ const removed = removeCrewPluginRegistration({ home, name });
225
+ if (!removed.ok) fail('dsh', `DSH crew profile removal failed (${removed.code ?? 'unknown'})`);
226
+ 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)');
227
+ }
228
+ }
229
+
230
+ if (!dryRun) {
231
+ if (purge) {
232
+ try { rmSync(join(home, '.config', 'dsh-crew'), { recursive: true, force: true }); mark(log, true, '~/.config/dsh-crew purged'); }
233
+ catch { fail('purge', 'could not purge ~/.config/dsh-crew'); }
234
+ } else log('\nConfig/backups kept.');
235
+ }
236
+
237
+ if (failures.length > 0) {
238
+ log('\nFAILED: uninstall incomplete');
239
+ return { ok: false, failures };
240
+ }
241
+ log('Done.');
242
+ return { ok: true, failures: [] };
243
+ }
244
+
245
+ export async function setupStatus({ log = console.log, root = ROOT, home = homedir(), installer = realInstaller } = {}) {
246
+ const st = installer.installStatus ? installer.installStatus({ home }) : realInstaller.installStatus({ home });
247
+ const claude = st?.claude?.installed ? 'installed' : 'not installed';
248
+ const codex = st?.codex?.installed ? 'installed' : 'not installed';
249
+ // Crew status reads ONLY the dedicated Crew profile under the Crew DSH_HOME;
250
+ // the official web profile layout under the default DSH home is intentionally
251
+ // never inspected.
252
+ const crewProfDir = crewProfileDir({ home });
253
+ const pkgFile = join(crewProfDir, 'package.json');
254
+ let dshPlugin = 'not installed';
255
+ if (existsSync(pkgFile)) {
256
+ try {
257
+ const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
258
+ const name = readPackageName(root);
259
+ dshPlugin = (name && (pkg.dependencies?.[name] ?? pkg.dsh?.profile?.bundles?.includes?.(name))) ? 'installed' : 'not installed';
260
+ } catch { dshPlugin = 'unknown'; }
261
+ }
262
+ log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
263
+ log(`Codex Desktop integration: ${codex}`);
264
+ log(`Claude Code integration: ${claude}`);
265
+ return { ok: true, dshPlugin, codex, claude };
266
+ }
267
+
268
+ export async function runSetupCli({ argv = process.argv.slice(2), run: actions = {}, log = console.log } = {}) {
269
+ const action = argv[0];
270
+ const flags = new Set(argv.slice(1));
271
+ const dryRun = flags.has('--dry-run');
272
+ const purge = flags.has('--purge');
273
+ const install = actions.install ?? setupInstall;
274
+ const uninstall = actions.uninstall ?? setupUninstall;
275
+ const status = actions.status ?? setupStatus;
276
+ try {
277
+ let result;
278
+ if (action === 'install') result = await install({ dryRun, log });
279
+ else if (action === 'uninstall') result = await uninstall({ dryRun, purge, log });
280
+ else if (action === 'status') result = await status({ log });
281
+ else {
282
+ console.error('usage: node scripts/setup.mjs <install|uninstall|status> [--dry-run] [--purge]');
283
+ return 1;
284
+ }
285
+ return result?.ok === false ? 1 : 0;
286
+ } catch (err) {
287
+ console.error(`setup failed: ${err?.message ?? err}`);
288
+ return 1;
289
+ }
290
+ }
291
+
292
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) || (
293
+ process.argv[1] && resolve(join(process.cwd(), process.argv[1])) === fileURLToPath(import.meta.url)
294
+ );
295
+ if (isMain) process.exitCode = await runSetupCli({});