@ran-sh/dsh-crew 0.3.5 → 0.3.7

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 (78) 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 +125 -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 +125 -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 +3446 -2765
  35. package/package.json +131 -131
  36. package/scripts/build-client.mjs +28 -28
  37. package/scripts/live-crew-smoke.mjs +39 -39
  38. package/scripts/live-policy-matrix.mjs +177 -177
  39. package/scripts/policy-probe.mjs +101 -101
  40. package/scripts/setup.mjs +284 -284
  41. package/scripts/smoke-real.mjs +110 -110
  42. package/scripts/smoke.mjs +78 -78
  43. package/scripts/verify-installer-fix.mjs +26 -26
  44. package/src/adaptive-routing.mjs +260 -260
  45. package/src/client/activation-summary.tsx +64 -64
  46. package/src/client/collapsible-sections.mjs +55 -0
  47. package/src/client/entry.tsx +236 -236
  48. package/src/client/index.tsx +1213 -1120
  49. package/src/config-readiness.mjs +59 -59
  50. package/src/delivery.mjs +205 -205
  51. package/src/dsh-cli-runtime.mjs +239 -239
  52. package/src/failure-classification.mjs +172 -172
  53. package/src/hub/entry.mjs +98 -98
  54. package/src/hub-client.mjs +132 -132
  55. package/src/hub-compatibility.mjs +49 -49
  56. package/src/i18n.mjs +19 -19
  57. package/src/install/cli.mjs +28 -28
  58. package/src/install/install-legacy.mjs +462 -462
  59. package/src/install/install.mjs +451 -451
  60. package/src/install/npx-lifecycle.mjs +1055 -1019
  61. package/src/mcp-runtime.mjs +257 -257
  62. package/src/model-catalog.mjs +173 -173
  63. package/src/model-routing.mjs +391 -391
  64. package/src/policy.mjs +197 -197
  65. package/src/readiness-matrix.mjs +169 -169
  66. package/src/runtime-controls.mjs +90 -90
  67. package/src/runtime-identity.mjs +108 -108
  68. package/src/server.mjs +477 -477
  69. package/src/status-shard.mjs +52 -52
  70. package/src/structured-error-code.mjs +38 -38
  71. package/src/vision-route.mjs +138 -138
  72. package/src/workflow-runtime.mjs +573 -573
  73. package/src/workflow.mjs +160 -160
  74. package/src/workspace-audit.mjs +231 -231
  75. package/src/workspace-isolation.mjs +365 -365
  76. package/statusline/statusline.sh +14 -14
  77. package/statusline/worker-segment.sh +35 -35
  78. package/worker.cordis.yml +77 -77
@@ -1,10 +1,10 @@
1
- // DSH CLI/runtime resolution for Crew-owned installs.
2
- //
3
- // The resolver is deliberately independent from the official ~/.dsh state.
4
- // A reusable CLI may live under the Crew DSH_HOME, while global dsh/npx remain
5
- // compatibility fallbacks. Status and uninstall callers can resolve without
6
- // allowing a download.
7
-
1
+ // DSH CLI/runtime resolution for Crew-owned installs.
2
+ //
3
+ // The resolver is deliberately independent from the official ~/.dsh state.
4
+ // A reusable CLI may live under the Crew DSH_HOME, while global dsh/npx remain
5
+ // compatibility fallbacks. Status and uninstall callers can resolve without
6
+ // allowing a download.
7
+
8
8
  import { spawnSync } from 'node:child_process';
9
9
  import { createRequire } from 'node:module';
10
10
  import {
@@ -18,228 +18,228 @@ import {
18
18
  writeFileSync,
19
19
  } from 'node:fs';
20
20
  import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
21
- import { homedir } from 'node:os';
22
- import { crewDshHome, crewProfileDir } from './install/install.mjs';
23
-
21
+ import { homedir } from 'node:os';
22
+ import { crewDshHome, crewProfileDir } from './install/install.mjs';
23
+
24
24
  export const DSH_CLI_PACKAGE = '@deepseek-ai/dsh';
25
25
  export const CREW_DSH_RUNTIME_DIRNAME = 'runtime';
26
26
  const CREW_PROFILE_DEFAULT_BUNDLES = ['@deepseek-ai/dsh-base'];
27
27
  const PROFILE_PATCH_TEMPLATE = '[]\n';
28
28
  const PROFILE_PNPM_WORKSPACE = 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n';
29
-
30
- function defaultFindCommand(name) {
31
- const probe = process.platform === 'win32' ? 'where.exe' : 'which';
32
- const result = spawnSync(probe, [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
33
- if (result.status !== 0) return null;
34
- const lines = String(result.stdout ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
35
- if (lines.length === 0) return null;
36
- if (process.platform !== 'win32') return lines[0];
37
- // where.exe may list extensionless npm/sh shims first; those are not
38
- // directly spawnable by Node on Windows, so prefer a real executable.
39
- const executable = lines.find((line) => /\.(?:exe|cmd|bat)$/iu.test(line));
40
- return executable ?? lines[0];
41
- }
42
-
43
- function defaultExists(path) {
44
- try { return existsSync(path); } catch { return false; }
45
- }
46
-
47
- function packageVersion(entry, read = readFileSync) {
48
- try {
49
- const packageFile = join(entry, '..', '..', 'package.json');
50
- const parsed = JSON.parse(read(packageFile, 'utf8'));
51
- return typeof parsed.version === 'string' ? parsed.version : null;
52
- } catch { return null; }
53
- }
54
-
55
- export function crewDshRuntimeRoot({ home = homedir() } = {}) {
56
- return join(crewDshHome({ home }), CREW_DSH_RUNTIME_DIRNAME);
57
- }
58
-
59
- export function crewDshRuntimeEntry({ home = homedir(), platform = process.platform } = {}) {
60
- const suffix = platform === 'win32' ? '.cmd' : '';
61
- return join(crewDshRuntimeRoot({ home }), 'node_modules', '.bin', `dsh${suffix}`);
62
- }
63
-
64
- export function crewDshRuntimeModule({ home = homedir() } = {}) {
65
- return join(crewDshRuntimeRoot({ home }), 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js');
66
- }
67
-
68
- function isPathLike(value) {
69
- return typeof value === 'string' && (value.includes('/') || value.includes('\\') || extname(value) === '.cmd' || extname(value) === '.exe');
70
- }
71
-
72
- function descriptor({ kind, command, args = [], source, version = null, reusable = false }) {
73
- return Object.freeze({ kind, command, args: [...args], source, version, reusable });
74
- }
75
-
76
- function explicitDescriptor(value, { exists = defaultExists, platform = process.platform } = {}) {
77
- if (typeof value !== 'string' || !value.trim()) return null;
78
- const trimmed = value.trim();
79
- if (isPathLike(trimmed) && !exists(trimmed)) return null;
80
- if (extname(trimmed).toLowerCase() === '.js') {
81
- return descriptor({ kind: 'explicit-node', command: process.execPath, args: [trimmed], source: 'explicit', reusable: true });
82
- }
83
- return descriptor({ kind: platform === 'win32' && extname(trimmed).toLowerCase() === '.cmd' ? 'explicit-cmd' : 'explicit', command: trimmed, source: 'explicit', reusable: true });
84
- }
85
-
86
- /**
87
- * Resolve a CLI without reading DSH credentials or profile state.
88
- * `allowDownload` only affects the final npx descriptor; resolution itself
89
- * never starts a network operation.
90
- */
91
- export function resolveDshCli({
92
- home = homedir(),
93
- env = process.env,
94
- platform = process.platform,
95
- exists = defaultExists,
96
- findCommand = defaultFindCommand,
97
- allowDownload = false,
98
- includeCompatibility = true,
99
- read = readFileSync,
100
- } = {}) {
101
- const explicit = explicitDescriptor(env.DSH_CREW_DSH_CLI ?? env.DSH_CLI, { exists, platform });
102
- if (explicit) return explicit;
103
-
104
- const moduleEntry = crewDshRuntimeModule({ home });
105
- if (exists(moduleEntry)) {
106
- return descriptor({
107
- kind: 'crew-runtime',
108
- command: process.execPath,
109
- args: [moduleEntry],
110
- source: 'crew-runtime',
111
- version: packageVersion(moduleEntry, read),
112
- reusable: true,
113
- });
114
- }
115
-
116
- if (!includeCompatibility) return null;
117
-
118
- const global = findCommand('dsh');
119
- if (global) return descriptor({ kind: 'global', command: global, source: 'global' });
120
-
121
- const npx = findCommand('npx');
122
- if (!npx) return null;
123
- return descriptor({
124
- kind: allowDownload ? 'npx-download' : 'npx-local',
125
- command: npx,
126
- args: allowDownload ? ['--yes', DSH_CLI_PACKAGE] : ['--no-install', DSH_CLI_PACKAGE],
127
- source: allowDownload ? 'npx-download' : 'npx-local',
128
- });
129
- }
130
-
131
- function quoteWindowsArg(value) {
132
- const input = String(value);
133
- if (input.length > 0 && !/[\s"]/u.test(input)) return input;
134
- let output = '"';
135
- let slashes = 0;
136
- for (const char of input) {
137
- if (char === '\\') { slashes += 1; continue; }
138
- if (char === '"') {
139
- output += '\\'.repeat(slashes * 2 + 1) + '"';
140
- } else {
141
- output += '\\'.repeat(slashes) + char;
142
- }
143
- slashes = 0;
144
- }
145
- output += '\\'.repeat(slashes * 2) + '"';
146
- return output;
147
- }
148
-
149
- export { quoteWindowsArg };
150
-
151
- /** Build a shell-free invocation, including deterministic Windows .cmd handling. */
152
- export function buildDshInvocation(cli, args = [], { platform = process.platform, comspec = process.env.ComSpec ?? 'cmd.exe' } = {}) {
153
- const allArgs = [...(cli?.args ?? []), ...args].map(String);
154
- const command = String(cli?.command ?? '');
155
- if (platform === 'win32' && /\.(?:cmd|bat)$/iu.test(command)) {
156
- return {
157
- command: comspec,
158
- args: ['/d', '/s', '/c', [quoteWindowsArg(command), ...allArgs.map(quoteWindowsArg)].join(' ')],
159
- shell: false,
160
- };
161
- }
162
- return { command, args: allArgs, shell: false };
163
- }
164
-
165
- export function runResolvedDsh(cli, args = [], {
166
- home = homedir(),
167
- env = process.env,
168
- runner = spawnSync,
169
- platform = process.platform,
170
- comspec = process.env.ComSpec ?? 'cmd.exe',
171
- } = {}) {
172
- if (!cli) return { ok: false, status: -1, stdout: '', stderr: 'DSH CLI unavailable' };
173
- const invocation = buildDshInvocation(cli, args, { platform, comspec });
174
- const result = runner(invocation.command, invocation.args, {
175
- encoding: 'utf8',
176
- stdio: ['ignore', 'pipe', 'pipe'],
177
- shell: invocation.shell,
178
- env: { ...env, DSH_HOME: crewDshHome({ home }) },
179
- });
180
- return {
181
- ok: result.status === 0,
182
- status: result.status ?? -1,
183
- stdout: result.stdout ?? '',
184
- stderr: result.stderr ?? '',
185
- invocation,
186
- };
187
- }
188
-
189
- /**
190
- * Install a reusable DSH CLI into Crew-owned state. This is the only helper
191
- * that may invoke a package manager, and callers must explicitly opt into it.
192
- */
193
- export function ensureCrewDshRuntime({
194
- home = homedir(),
195
- packageSpec = DSH_CLI_PACKAGE,
196
- npmCommand = null,
197
- pnpmCommand = null,
198
- findCommand = defaultFindCommand,
199
- exists = defaultExists,
200
- runner = spawnSync,
201
- platform = process.platform,
202
- comspec = process.env.ComSpec ?? 'cmd.exe',
203
- env = process.env,
204
- } = {}) {
205
- const existing = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false });
206
- if (existing?.kind === 'crew-runtime') return { ok: true, cli: existing, reused: true };
207
-
208
- const pnpm = pnpmCommand ?? findCommand('pnpm');
209
- const npm = npmCommand ?? findCommand('npm');
210
- if (!pnpm && !npm) return { ok: false, code: 'DSH_RUNTIME_INSTALLER_NOT_FOUND', error: 'pnpm/npm unavailable' };
211
- const runtimeRoot = crewDshRuntimeRoot({ home });
212
- mkdirSync(runtimeRoot, { recursive: true });
213
- const packageManager = pnpm
214
- ? descriptor({ kind: 'pnpm', command: pnpm, source: 'pnpm' })
215
- : descriptor({ kind: 'npm', command: npm, source: 'npm' });
216
- const packageArgs = pnpm
217
- ? ['add', '--dir', runtimeRoot, '--ignore-scripts', packageSpec]
218
- : ['install', '--prefix', runtimeRoot, '--no-package-lock', '--ignore-scripts', '--omit=dev', packageSpec];
219
- const invocation = buildDshInvocation(packageManager, packageArgs, { platform, comspec });
220
- const result = runner(invocation.command, invocation.args, {
221
- encoding: 'utf8',
222
- stdio: ['ignore', 'pipe', 'pipe'],
223
- shell: invocation.shell,
224
- env: { ...env },
225
- });
226
- const cli = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false });
227
- if (result.status !== 0 || !cli || cli.kind !== 'crew-runtime') {
228
- return {
229
- ok: false,
230
- code: 'DSH_RUNTIME_INSTALL_FAILED',
231
- error: 'Crew-owned DSH runtime install failed',
232
- status: result.status ?? -1,
233
- stderrTail: String(result.stderr || result.stdout || '').trim().split(/\r?\n/).slice(-3).join(' | ').slice(0, 300),
234
- };
235
- }
236
- return { ok: true, cli, reused: false, version: cli.version, runtimeRoot };
237
- }
238
-
29
+
30
+ function defaultFindCommand(name) {
31
+ const probe = process.platform === 'win32' ? 'where.exe' : 'which';
32
+ const result = spawnSync(probe, [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
33
+ if (result.status !== 0) return null;
34
+ const lines = String(result.stdout ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
35
+ if (lines.length === 0) return null;
36
+ if (process.platform !== 'win32') return lines[0];
37
+ // where.exe may list extensionless npm/sh shims first; those are not
38
+ // directly spawnable by Node on Windows, so prefer a real executable.
39
+ const executable = lines.find((line) => /\.(?:exe|cmd|bat)$/iu.test(line));
40
+ return executable ?? lines[0];
41
+ }
42
+
43
+ function defaultExists(path) {
44
+ try { return existsSync(path); } catch { return false; }
45
+ }
46
+
47
+ function packageVersion(entry, read = readFileSync) {
48
+ try {
49
+ const packageFile = join(entry, '..', '..', 'package.json');
50
+ const parsed = JSON.parse(read(packageFile, 'utf8'));
51
+ return typeof parsed.version === 'string' ? parsed.version : null;
52
+ } catch { return null; }
53
+ }
54
+
55
+ export function crewDshRuntimeRoot({ home = homedir() } = {}) {
56
+ return join(crewDshHome({ home }), CREW_DSH_RUNTIME_DIRNAME);
57
+ }
58
+
59
+ export function crewDshRuntimeEntry({ home = homedir(), platform = process.platform } = {}) {
60
+ const suffix = platform === 'win32' ? '.cmd' : '';
61
+ return join(crewDshRuntimeRoot({ home }), 'node_modules', '.bin', `dsh${suffix}`);
62
+ }
63
+
64
+ export function crewDshRuntimeModule({ home = homedir() } = {}) {
65
+ return join(crewDshRuntimeRoot({ home }), 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js');
66
+ }
67
+
68
+ function isPathLike(value) {
69
+ return typeof value === 'string' && (value.includes('/') || value.includes('\\') || extname(value) === '.cmd' || extname(value) === '.exe');
70
+ }
71
+
72
+ function descriptor({ kind, command, args = [], source, version = null, reusable = false }) {
73
+ return Object.freeze({ kind, command, args: [...args], source, version, reusable });
74
+ }
75
+
76
+ function explicitDescriptor(value, { exists = defaultExists, platform = process.platform } = {}) {
77
+ if (typeof value !== 'string' || !value.trim()) return null;
78
+ const trimmed = value.trim();
79
+ if (isPathLike(trimmed) && !exists(trimmed)) return null;
80
+ if (extname(trimmed).toLowerCase() === '.js') {
81
+ return descriptor({ kind: 'explicit-node', command: process.execPath, args: [trimmed], source: 'explicit', reusable: true });
82
+ }
83
+ return descriptor({ kind: platform === 'win32' && extname(trimmed).toLowerCase() === '.cmd' ? 'explicit-cmd' : 'explicit', command: trimmed, source: 'explicit', reusable: true });
84
+ }
85
+
86
+ /**
87
+ * Resolve a CLI without reading DSH credentials or profile state.
88
+ * `allowDownload` only affects the final npx descriptor; resolution itself
89
+ * never starts a network operation.
90
+ */
91
+ export function resolveDshCli({
92
+ home = homedir(),
93
+ env = process.env,
94
+ platform = process.platform,
95
+ exists = defaultExists,
96
+ findCommand = defaultFindCommand,
97
+ allowDownload = false,
98
+ includeCompatibility = true,
99
+ read = readFileSync,
100
+ } = {}) {
101
+ const explicit = explicitDescriptor(env.DSH_CREW_DSH_CLI ?? env.DSH_CLI, { exists, platform });
102
+ if (explicit) return explicit;
103
+
104
+ const moduleEntry = crewDshRuntimeModule({ home });
105
+ if (exists(moduleEntry)) {
106
+ return descriptor({
107
+ kind: 'crew-runtime',
108
+ command: process.execPath,
109
+ args: [moduleEntry],
110
+ source: 'crew-runtime',
111
+ version: packageVersion(moduleEntry, read),
112
+ reusable: true,
113
+ });
114
+ }
115
+
116
+ if (!includeCompatibility) return null;
117
+
118
+ const global = findCommand('dsh');
119
+ if (global) return descriptor({ kind: 'global', command: global, source: 'global' });
120
+
121
+ const npx = findCommand('npx');
122
+ if (!npx) return null;
123
+ return descriptor({
124
+ kind: allowDownload ? 'npx-download' : 'npx-local',
125
+ command: npx,
126
+ args: allowDownload ? ['--yes', DSH_CLI_PACKAGE] : ['--no-install', DSH_CLI_PACKAGE],
127
+ source: allowDownload ? 'npx-download' : 'npx-local',
128
+ });
129
+ }
130
+
131
+ function quoteWindowsArg(value) {
132
+ const input = String(value);
133
+ if (input.length > 0 && !/[\s"]/u.test(input)) return input;
134
+ let output = '"';
135
+ let slashes = 0;
136
+ for (const char of input) {
137
+ if (char === '\\') { slashes += 1; continue; }
138
+ if (char === '"') {
139
+ output += '\\'.repeat(slashes * 2 + 1) + '"';
140
+ } else {
141
+ output += '\\'.repeat(slashes) + char;
142
+ }
143
+ slashes = 0;
144
+ }
145
+ output += '\\'.repeat(slashes * 2) + '"';
146
+ return output;
147
+ }
148
+
149
+ export { quoteWindowsArg };
150
+
151
+ /** Build a shell-free invocation, including deterministic Windows .cmd handling. */
152
+ export function buildDshInvocation(cli, args = [], { platform = process.platform, comspec = process.env.ComSpec ?? 'cmd.exe' } = {}) {
153
+ const allArgs = [...(cli?.args ?? []), ...args].map(String);
154
+ const command = String(cli?.command ?? '');
155
+ if (platform === 'win32' && /\.(?:cmd|bat)$/iu.test(command)) {
156
+ return {
157
+ command: comspec,
158
+ args: ['/d', '/s', '/c', [quoteWindowsArg(command), ...allArgs.map(quoteWindowsArg)].join(' ')],
159
+ shell: false,
160
+ };
161
+ }
162
+ return { command, args: allArgs, shell: false };
163
+ }
164
+
165
+ export function runResolvedDsh(cli, args = [], {
166
+ home = homedir(),
167
+ env = process.env,
168
+ runner = spawnSync,
169
+ platform = process.platform,
170
+ comspec = process.env.ComSpec ?? 'cmd.exe',
171
+ } = {}) {
172
+ if (!cli) return { ok: false, status: -1, stdout: '', stderr: 'DSH CLI unavailable' };
173
+ const invocation = buildDshInvocation(cli, args, { platform, comspec });
174
+ const result = runner(invocation.command, invocation.args, {
175
+ encoding: 'utf8',
176
+ stdio: ['ignore', 'pipe', 'pipe'],
177
+ shell: invocation.shell,
178
+ env: { ...env, DSH_HOME: crewDshHome({ home }) },
179
+ });
180
+ return {
181
+ ok: result.status === 0,
182
+ status: result.status ?? -1,
183
+ stdout: result.stdout ?? '',
184
+ stderr: result.stderr ?? '',
185
+ invocation,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Install a reusable DSH CLI into Crew-owned state. This is the only helper
191
+ * that may invoke a package manager, and callers must explicitly opt into it.
192
+ */
193
+ export function ensureCrewDshRuntime({
194
+ home = homedir(),
195
+ packageSpec = DSH_CLI_PACKAGE,
196
+ npmCommand = null,
197
+ pnpmCommand = null,
198
+ findCommand = defaultFindCommand,
199
+ exists = defaultExists,
200
+ runner = spawnSync,
201
+ platform = process.platform,
202
+ comspec = process.env.ComSpec ?? 'cmd.exe',
203
+ env = process.env,
204
+ } = {}) {
205
+ const existing = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false });
206
+ if (existing?.kind === 'crew-runtime') return { ok: true, cli: existing, reused: true };
207
+
208
+ const pnpm = pnpmCommand ?? findCommand('pnpm');
209
+ const npm = npmCommand ?? findCommand('npm');
210
+ if (!pnpm && !npm) return { ok: false, code: 'DSH_RUNTIME_INSTALLER_NOT_FOUND', error: 'pnpm/npm unavailable' };
211
+ const runtimeRoot = crewDshRuntimeRoot({ home });
212
+ mkdirSync(runtimeRoot, { recursive: true });
213
+ const packageManager = pnpm
214
+ ? descriptor({ kind: 'pnpm', command: pnpm, source: 'pnpm' })
215
+ : descriptor({ kind: 'npm', command: npm, source: 'npm' });
216
+ const packageArgs = pnpm
217
+ ? ['add', '--dir', runtimeRoot, '--ignore-scripts', packageSpec]
218
+ : ['install', '--prefix', runtimeRoot, '--no-package-lock', '--ignore-scripts', '--omit=dev', packageSpec];
219
+ const invocation = buildDshInvocation(packageManager, packageArgs, { platform, comspec });
220
+ const result = runner(invocation.command, invocation.args, {
221
+ encoding: 'utf8',
222
+ stdio: ['ignore', 'pipe', 'pipe'],
223
+ shell: invocation.shell,
224
+ env: { ...env },
225
+ });
226
+ const cli = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false });
227
+ if (result.status !== 0 || !cli || cli.kind !== 'crew-runtime') {
228
+ return {
229
+ ok: false,
230
+ code: 'DSH_RUNTIME_INSTALL_FAILED',
231
+ error: 'Crew-owned DSH runtime install failed',
232
+ status: result.status ?? -1,
233
+ stderrTail: String(result.stderr || result.stdout || '').trim().split(/\r?\n/).slice(-3).join(' | ').slice(0, 300),
234
+ };
235
+ }
236
+ return { ok: true, cli, reused: false, version: cli.version, runtimeRoot };
237
+ }
238
+
239
239
  export function describeDshCli(cli) {
240
- if (!cli) return 'unavailable';
241
- const version = cli.version ? `@${cli.version}` : '';
242
- return `${cli.kind}${version}`;
240
+ if (!cli) return 'unavailable';
241
+ const version = cli.version ? `@${cli.version}` : '';
242
+ return `${cli.kind}${version}`;
243
243
  }
244
244
 
245
245
  function isObject(value) {
@@ -400,19 +400,19 @@ export function ensureCrewPluginRegistration({ home = homedir(), root, name } =
400
400
  };
401
401
  } catch { return { ok: false, code: 'CREW_PLUGIN_NOT_LOADABLE' }; }
402
402
  }
403
-
404
- /**
405
- * Remove one Crew plugin registration without invoking a package manager.
406
- * This is intentionally limited to the derived Crew profile directory so a
407
- * status/uninstall probe cannot touch the official web profile or download
408
- * anything merely to remove a stale registration.
409
- */
403
+
404
+ /**
405
+ * Remove one Crew plugin registration without invoking a package manager.
406
+ * This is intentionally limited to the derived Crew profile directory so a
407
+ * status/uninstall probe cannot touch the official web profile or download
408
+ * anything merely to remove a stale registration.
409
+ */
410
410
  export function removeCrewPluginRegistration({ home = homedir(), name, profileRoot = crewProfileDir({ home }) } = {}) {
411
411
  if (!isSafePackageName(name)) {
412
412
  return { ok: false, code: 'INVALID_CREW_PLUGIN_NAME' };
413
413
  }
414
- const packageFile = join(profileRoot, 'package.json');
415
- if (!existsSync(packageFile)) return { ok: true, removed: false };
414
+ const packageFile = join(profileRoot, 'package.json');
415
+ if (!existsSync(packageFile)) return { ok: true, removed: false };
416
416
  let pkg;
417
417
  try { pkg = JSON.parse(readFileSync(packageFile, 'utf8')); } catch { return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' }; }
418
418
  if (!isObject(pkg)
@@ -426,18 +426,18 @@ export function removeCrewPluginRegistration({ home = homedir(), name, profileRo
426
426
  }
427
427
  const hadDependency = Boolean(pkg.dependencies?.[name]);
428
428
  const bundles = Array.isArray(pkg.dsh?.profile?.bundles) ? pkg.dsh.profile.bundles : [];
429
- const hadBundle = bundles.includes(name);
430
- if (!hadDependency && !hadBundle) return { ok: true, removed: false };
429
+ const hadBundle = bundles.includes(name);
430
+ if (!hadDependency && !hadBundle) return { ok: true, removed: false };
431
431
  const linkPath = profileLinkPath(profileRoot, name);
432
432
  let linkStat;
433
433
  try { linkStat = lstatSync(linkPath); } catch { linkStat = null; }
434
434
  if (linkStat && !linkStat.isSymbolicLink()) return { ok: false, code: 'CREW_PLUGIN_LINK_CONFLICT' };
435
435
  const next = { ...pkg };
436
- if (hadDependency) {
437
- next.dependencies = { ...pkg.dependencies };
438
- delete next.dependencies[name];
439
- if (Object.keys(next.dependencies).length === 0) delete next.dependencies;
440
- }
436
+ if (hadDependency) {
437
+ next.dependencies = { ...pkg.dependencies };
438
+ delete next.dependencies[name];
439
+ if (Object.keys(next.dependencies).length === 0) delete next.dependencies;
440
+ }
441
441
  if (hadBundle) {
442
442
  next.dsh = { ...pkg.dsh, profile: { ...pkg.dsh.profile, bundles: bundles.filter((item) => item !== name) } };
443
443
  }