@ran-sh/dsh-crew 1.0.3 → 1.1.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.
package/scripts/setup.mjs CHANGED
@@ -13,67 +13,68 @@ import { homedir } from 'node:os';
13
13
  import * as realInstaller from '../src/install/install.mjs';
14
14
  import { crewDshHome, crewProfileDir, CREW_PROFILE_NAME } from '../src/install/install.mjs';
15
15
  import {
16
- ensureCrewDshRuntime,
17
- ensureCrewPluginRegistration,
18
- resolveDshCli,
16
+ ensureCrewDshRuntime,
17
+ ensureCrewPluginRegistration,
18
+ resolveDshCli,
19
19
  runResolvedDsh,
20
20
  describeDshCli,
21
21
  removeCrewPluginRegistration,
22
+ TARGET_DSH_VERSION,
22
23
  } from '../src/dsh-cli-runtime.mjs';
23
24
 
24
25
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
25
26
 
26
- function commandExists(name) {
27
- const r = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', [name], {
27
+ function commandExists(name) {
28
+ const r = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', [name], {
28
29
  encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
29
30
  });
30
- return r.status === 0;
31
- }
32
-
33
- const PACKAGE_MANAGER_COMMAND = /^(?:npm|npx|pnpm|yarn|yarnpkg|bun|bunx)$/i;
34
-
35
- export function spawnCommand(command, platform = process.platform) {
36
- if (platform === 'win32' && PACKAGE_MANAGER_COMMAND.test(command)) {
37
- return `${command}.cmd`;
38
- }
39
- return command;
40
- }
41
-
42
- export function spawnNeedsShell(command, platform = process.platform) {
43
- return platform === 'win32' && PACKAGE_MANAGER_COMMAND.test(command);
44
- }
45
-
46
- export function spawnInvocation(command, args = [], platform = process.platform, environment = process.env) {
47
- const executable = spawnCommand(command, platform);
48
- if (!spawnNeedsShell(command, platform)) return { command: executable, args };
49
- const unsafe = args.find((arg) => !/^[A-Za-z0-9@._:+/=\-]+$/.test(String(arg)));
50
- if (unsafe !== undefined) throw new Error(`unsafe package-manager argument: ${unsafe}`);
51
- return {
52
- command: environment.ComSpec || 'cmd.exe',
53
- args: ['/d', '/s', '/c', [executable, ...args].join(' ')],
54
- };
55
- }
56
-
57
- function run(cmd, args, opts = {}) {
58
- let invocation;
59
- try {
60
- invocation = spawnInvocation(cmd, args);
61
- } catch (error) {
62
- return { ok: false, status: -1, stdout: '', stderr: String(error?.message ?? error), error };
63
- }
64
- const r = spawnSync(invocation.command, invocation.args, {
65
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: opts.shell === true, cwd: opts.cwd ?? ROOT,
66
- env: opts.env ?? process.env,
67
- });
68
- const processError = r.error ? `${r.error.name}: ${r.error.message}` : '';
69
- return {
70
- ok: r.status === 0,
71
- status: r.status ?? -1,
72
- stdout: r.stdout ?? '',
73
- stderr: r.stderr || processError,
74
- error: r.error ?? null,
75
- };
76
- }
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
+ }
77
78
 
78
79
  /**
79
80
  * Detect the DSH CLI. `allowDownload` is intentionally opt-in: installation
@@ -143,18 +144,18 @@ export async function setupInstall({
143
144
  installer = realInstaller,
144
145
  depRunner = null,
145
146
  } = {}) {
146
- log('DSH Crew installer');
147
- if (!checkRoot(root)) return { ok: false, error: `not a dsh-crew checkout: ${root}` };
148
- const pnpmOk = commandExists('pnpm');
149
-
150
- if (depRunner) {
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) {
151
152
  const r = depRunner(root);
152
153
  if (!r.ok) { mark(log, false, r.text); return { ok: false, error: r.text }; }
153
154
  mark(log, true, r.text);
154
- } else if (!dryRun) {
155
- const cmd = pnpmOk ? 'pnpm' : 'npx';
155
+ } else if (!dryRun) {
156
+ const cmd = pnpmOk ? 'pnpm' : 'npx';
156
157
  const baseArgs = pnpmOk ? [] : ['-y', 'pnpm'];
157
- const d = run(cmd, [...baseArgs, 'install', '--frozen-lockfile'], { cwd: root });
158
+ const d = run(cmd, [...baseArgs, 'install', '--frozen-lockfile'], { cwd: root });
158
159
  if (!d.ok) {
159
160
  if (depsPresent(root)) mark(log, true, `dependencies (already present; install skipped — ${(d.stderr || d.stdout || '').trim().split('\n')[0]})`);
160
161
  else {
@@ -163,11 +164,11 @@ export async function setupInstall({
163
164
  }
164
165
  } else mark(log, true, 'dependencies');
165
166
  } else mark(log, true, 'dependencies (dry-run)');
166
-
167
- if (!dryRun) {
168
- const b = pnpmOk
169
- ? run('pnpm', ['run', 'build:client'], { cwd: root })
170
- : run('npx', ['-y', 'pnpm', 'run', 'build:client'], { cwd: root });
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 });
171
172
  if (!b.ok) {
172
173
  log(`✗ client build failed:\n${(b.stderr || b.stdout || '').slice(0, 600)}`);
173
174
  return { ok: false, error: 'client build failed' };
@@ -181,6 +182,17 @@ export async function setupInstall({
181
182
  // available, install a reusable copy under Crew state before falling back to
182
183
  // the transient download path. Dry-run never provisions or downloads.
183
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
+ }
184
196
  if (!dryRun && (!dsh || dsh.kind === 'npx-local')) {
185
197
  const boot = ensureCrewDshRuntime({ home });
186
198
  if (boot.ok) dsh = boot.cli;
@@ -197,45 +209,45 @@ export async function setupInstall({
197
209
  // is kept here as an explicit contrast so existing CLI tests/people see the
198
210
  // change, never as a command that targets the official web profile.
199
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}")`);
200
- } else {
201
- const registration = ensureCrewPluginRegistration({ home, root, name });
202
- if (!registration.ok) {
203
- log(`✗ DSH crew profile link failed (${registration.code ?? 'unknown'})`);
204
- return { ok: false, error: 'DSH profile link failed' };
205
- }
206
- mark(log, true, `DSH crew profile linked offline (dedicated Crew DSH_HOME, profile dsh-crew; ${registration.changed ? 'updated' : 'already current'})`);
207
- }
208
-
209
- if (dryRun) mark(log, true, 'Codex Desktop integration (dry-run)');
210
- else {
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 {
211
223
  const r = installer.installCodex ? installer.installCodex({ home }) : realInstaller.installCodex({ home });
212
224
  mark(log, r.ok !== false, r.ok === false ? `Codex Desktop integration failed: ${(r.actions ?? []).join('; ')}` : 'Codex Desktop integration');
213
- if (r.ok === false) return { ok: false, error: 'Codex integration failed' };
214
- }
215
-
216
- if (dryRun) mark(log, true, 'ZCode integration (dry-run)');
217
- else {
218
- const r = installer.installZCode
219
- ? installer.installZCode({ home, root })
220
- : realInstaller.installZCode({ home, root });
221
- if (r.ok === false) {
222
- mark(log, false, `ZCode integration failed (${r.code ?? 'unknown'})`);
223
- return { ok: false, error: 'ZCode integration failed' };
224
- }
225
- mark(log, true, 'ZCode integration');
226
- }
227
-
228
- if (dryRun) mark(log, true, 'Windows login startup (dry-run)');
229
- else {
230
- const r = installer.installWindowsStartup
231
- ? installer.installWindowsStartup({ home, root })
232
- : realInstaller.installWindowsStartup({ home, root });
233
- if (r.ok === false) {
234
- mark(log, false, `Windows login startup failed (${r.code ?? 'unknown'})`);
235
- return { ok: false, error: 'Windows login startup failed' };
236
- }
237
- if (r.supported) mark(log, true, 'Windows login startup');
238
- }
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
+ }
239
251
 
240
252
  if (commandExists('claude')) {
241
253
  if (dryRun) mark(log, true, 'Claude Code integration (dry-run)');
@@ -266,31 +278,31 @@ export async function setupUninstall({
266
278
  const fail = (name, text) => { mark(log, false, text || `${name} failed`); failures.push(name); };
267
279
 
268
280
  if (dryRun) {
269
- mark(log, true, 'Codex Desktop integration would be removed');
270
- mark(log, true, 'ZCode integration would be removed');
271
- mark(log, true, 'Claude Code integration would be removed');
272
- mark(log, true, 'Windows login startup would be removed');
273
- mark(log, true, 'DSH crew profile would be removed');
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');
274
286
  } else {
275
287
  const cx = installer.uninstallCodex ? installer.uninstallCodex({ home }) : realInstaller.uninstallCodex({ home });
276
- if (cx.ok !== false) mark(log, true, 'Codex Desktop integration removed');
277
- else fail('codex', 'Codex Desktop integration removal failed');
278
-
279
- const zc = installer.uninstallZCode
280
- ? installer.uninstallZCode({ home, root })
281
- : realInstaller.uninstallZCode({ home, root });
282
- if (zc.ok !== false) mark(log, true, 'ZCode integration removed');
283
- else fail('zcode', 'ZCode integration removal failed');
284
-
285
- const cl = installer.uninstallClaudeCode ? installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
286
- if (cl.ok !== false) mark(log, true, 'Claude Code integration removed');
287
- else fail('claude', 'Claude Code integration removal failed');
288
-
289
- const startup = installer.uninstallWindowsStartup
290
- ? installer.uninstallWindowsStartup({ home })
291
- : realInstaller.uninstallWindowsStartup({ home });
292
- if (startup.ok === false) fail('startup', 'Windows login startup removal failed');
293
- else if (startup.supported) mark(log, true, 'Windows login startup removed');
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');
294
306
 
295
307
  const name = readPackageName(root);
296
308
  if (!name) fail('dsh', 'DSH plugin removal failed: package name missing');
@@ -320,15 +332,15 @@ export async function setupUninstall({
320
332
  }
321
333
 
322
334
  export async function setupStatus({ log = console.log, root = ROOT, home = homedir(), installer = realInstaller } = {}) {
323
- const st = installer.installStatus ? installer.installStatus({ home, root }) : realInstaller.installStatus({ home, root });
324
- const claude = st?.claude?.installed ? 'installed' : 'not installed';
325
- const codex = st?.codex?.installed ? 'installed' : 'not installed';
326
- const zcode = st?.zcode?.installed ? 'installed' : 'not installed';
327
- const startupState = installer.windowsStartupStatus
328
- ? installer.windowsStartupStatus({ home })
329
- : realInstaller.windowsStartupStatus({ home });
330
- const windowsStartup = !startupState.supported ? 'not supported'
331
- : startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
335
+ const st = installer.installStatus ? installer.installStatus({ home, root }) : realInstaller.installStatus({ home, root });
336
+ const claude = st?.claude?.installed ? 'installed' : 'not installed';
337
+ const codex = st?.codex?.installed ? 'installed' : 'not installed';
338
+ const zcode = st?.zcode?.installed ? 'installed' : 'not installed';
339
+ const startupState = installer.windowsStartupStatus
340
+ ? installer.windowsStartupStatus({ home })
341
+ : realInstaller.windowsStartupStatus({ home });
342
+ const windowsStartup = !startupState.supported ? 'not supported'
343
+ : startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
332
344
  // Crew status reads ONLY the dedicated Crew profile under the Crew DSH_HOME;
333
345
  // the official web profile layout under the default DSH home is intentionally
334
346
  // never inspected.
@@ -343,12 +355,12 @@ export async function setupStatus({ log = console.log, root = ROOT, home = homed
343
355
  } catch { dshPlugin = 'unknown'; }
344
356
  }
345
357
  log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
346
- log(`Codex Desktop integration: ${codex}`);
347
- log(`ZCode integration: ${zcode}`);
348
- log(`Claude Code integration: ${claude}`);
349
- log(`Windows login startup: ${windowsStartup}`);
350
- return { ok: true, dshPlugin, codex, zcode, claude, windowsStartup };
351
- }
358
+ log(`Codex Desktop integration: ${codex}`);
359
+ log(`ZCode integration: ${zcode}`);
360
+ log(`Claude Code integration: ${claude}`);
361
+ log(`Windows login startup: ${windowsStartup}`);
362
+ return { ok: true, dshPlugin, codex, zcode, claude, windowsStartup };
363
+ }
352
364
 
353
365
  export async function runSetupCli({ argv = process.argv.slice(2), run: actions = {}, log = console.log } = {}) {
354
366
  const action = argv[0];