@ran-sh/dsh-crew 0.3.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 (85) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +8 -0
  3. package/.mcp.json +8 -0
  4. package/LICENSE +21 -0
  5. package/README.de.md +359 -0
  6. package/README.es.md +359 -0
  7. package/README.fr.md +359 -0
  8. package/README.hi.md +359 -0
  9. package/README.id.md +359 -0
  10. package/README.ja.md +359 -0
  11. package/README.ko.md +359 -0
  12. package/README.md +360 -0
  13. package/README.pt.md +359 -0
  14. package/README.ru.md +359 -0
  15. package/README.th.md +359 -0
  16. package/README.tr.md +359 -0
  17. package/README.vi.md +359 -0
  18. package/README.zh-TW.md +359 -0
  19. package/README.zh.md +305 -0
  20. package/agents/ds-flash.md +26 -0
  21. package/agents/ds-pro.md +32 -0
  22. package/agents/ds-reviewer.md +23 -0
  23. package/agents/ds-worker.md +22 -0
  24. package/codex/agents/ds-flash.toml +30 -0
  25. package/codex/agents/ds-pro.toml +31 -0
  26. package/codex/agents/ds-reviewer.toml +28 -0
  27. package/codex/agents/ds-worker.toml +28 -0
  28. package/codex/prompts/dsh-config.md +3 -0
  29. package/codex/prompts/dsh-status.md +1 -0
  30. package/commands/config.md +11 -0
  31. package/commands/off.md +5 -0
  32. package/commands/on.md +5 -0
  33. package/commands/status.md +5 -0
  34. package/cordis.patch.yml +4 -0
  35. package/docs/images/dsh-crew-host.png +0 -0
  36. package/docs/images/dsh-crew-jobs.png +0 -0
  37. package/docs/images/dsh-crew-logo.png +0 -0
  38. package/docs/images/dsh-crew-overview.png +0 -0
  39. package/lib/client.js +2765 -0
  40. package/package.json +125 -0
  41. package/scripts/build-client.mjs +28 -0
  42. package/scripts/live-crew-smoke.mjs +39 -0
  43. package/scripts/live-policy-matrix.mjs +177 -0
  44. package/scripts/policy-probe.mjs +101 -0
  45. package/scripts/setup.mjs +294 -0
  46. package/scripts/smoke-real.mjs +110 -0
  47. package/scripts/smoke.mjs +78 -0
  48. package/scripts/verify-installer-fix.mjs +26 -0
  49. package/src/adaptive-routing.mjs +260 -0
  50. package/src/client/activation-summary.tsx +64 -0
  51. package/src/client/entry.tsx +236 -0
  52. package/src/client/index.tsx +1120 -0
  53. package/src/config-readiness.mjs +59 -0
  54. package/src/delivery.mjs +205 -0
  55. package/src/dsh-cli-runtime.mjs +251 -0
  56. package/src/failure-classification.mjs +172 -0
  57. package/src/hub/entry.mjs +98 -0
  58. package/src/hub/index.mjs +757 -0
  59. package/src/hub-client.mjs +132 -0
  60. package/src/hub-compatibility.mjs +49 -0
  61. package/src/i18n.mjs +19 -0
  62. package/src/install/cli.mjs +28 -0
  63. package/src/install/install-legacy.mjs +460 -0
  64. package/src/install/install.mjs +451 -0
  65. package/src/jobs.mjs +275 -0
  66. package/src/mcp-runtime.mjs +257 -0
  67. package/src/model-catalog.mjs +173 -0
  68. package/src/model-routing.mjs +391 -0
  69. package/src/multimodal.mjs +0 -0
  70. package/src/policy-legacy.mjs +830 -0
  71. package/src/policy.mjs +197 -0
  72. package/src/readiness-matrix.mjs +169 -0
  73. package/src/runtime-controls.mjs +90 -0
  74. package/src/runtime-identity.mjs +108 -0
  75. package/src/server.mjs +477 -0
  76. package/src/status-shard.mjs +52 -0
  77. package/src/structured-error-code.mjs +39 -0
  78. package/src/vision-route.mjs +138 -0
  79. package/src/workflow-runtime.mjs +567 -0
  80. package/src/workflow.mjs +160 -0
  81. package/src/workspace-audit.mjs +231 -0
  82. package/src/workspace-isolation.mjs +306 -0
  83. package/statusline/statusline.sh +14 -0
  84. package/statusline/worker-segment.sh +35 -0
  85. package/worker.cordis.yml +77 -0
@@ -0,0 +1,294 @@
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
+ resolveDshCli,
18
+ runResolvedDsh,
19
+ describeDshCli,
20
+ removeCrewPluginRegistration,
21
+ } from '../src/dsh-cli-runtime.mjs';
22
+
23
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
24
+
25
+ function commandExists(name) {
26
+ const r = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', [name], {
27
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
28
+ });
29
+ return r.status === 0;
30
+ }
31
+
32
+ function run(cmd, args, opts = {}) {
33
+ const r = spawnSync(cmd, args, {
34
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: opts.shell === true, cwd: opts.cwd ?? ROOT,
35
+ env: opts.env ?? process.env,
36
+ });
37
+ return { ok: r.status === 0, status: r.status ?? -1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
38
+ }
39
+
40
+ /**
41
+ * Detect the DSH CLI. `allowDownload` is intentionally opt-in: installation
42
+ * may use `npx -y` to fetch DSH, while status/uninstall probes never trigger a
43
+ * network download merely by checking availability.
44
+ *
45
+ * The returned descriptor is the CLI base only; every invocation is built by
46
+ * `runDsh` with the dedicated dsh-crew profile and the Crew DSH_HOME in the
47
+ * child process environment. The official ``web`` profile is never a target.
48
+ */
49
+ export function detectDsh({ allowDownload = false, home = homedir(), includeCompatibility = true } = {}) {
50
+ const resolved = resolveDshCli({
51
+ home,
52
+ allowDownload,
53
+ includeCompatibility,
54
+ });
55
+ if (!resolved) return null;
56
+ return { ...resolved, cli: resolved.command, description: describeDshCli(resolved) };
57
+ }
58
+
59
+ /**
60
+ * Run a DSH plugin command against the dedicated Crew profile under the
61
+ * Crew-owned DSH_HOME. `home` is the "user home" base used to derive that
62
+ * isolated home, so tests can point it at a disposable root.
63
+ */
64
+ export function runDsh(dsh, args, { home = homedir() } = {}) {
65
+ const resolved = dsh?.command
66
+ ? dsh
67
+ : { kind: dsh?.kind ?? 'legacy', command: dsh?.cli ?? dsh, args: [] };
68
+ return runResolvedDsh(resolved, ['plugin', '--profile', CREW_PROFILE_NAME, ...args], { home });
69
+ }
70
+
71
+ export function readPackageName(root = ROOT) {
72
+ try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name; } catch { return null; }
73
+ }
74
+
75
+ export function checkRoot(root = ROOT) {
76
+ return existsSync(join(root, 'package.json'))
77
+ && existsSync(join(root, 'src', 'server.mjs'))
78
+ && existsSync(join(root, 'cordis.patch.yml'));
79
+ }
80
+
81
+ export function depsPresent(root = ROOT) {
82
+ return existsSync(join(root, 'node_modules', '@modelcontextprotocol', 'sdk'))
83
+ && existsSync(join(root, 'node_modules', '.bin', 'tsdown'));
84
+ }
85
+
86
+ export function profileHasPackage(home, name, profileRoot = crewProfileDir({ home })) {
87
+ const pkgFile = join(profileRoot, 'package.json');
88
+ if (!existsSync(pkgFile)) return false;
89
+ try {
90
+ const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
91
+ return Boolean(pkg.dependencies?.[name]) || Boolean((pkg.dsh?.profile?.bundles ?? []).includes(name));
92
+ } catch { return false; }
93
+ }
94
+
95
+ function mark(log, ok, text) {
96
+ log(`${ok ? '✓' : '✗'} ${text}`);
97
+ return ok;
98
+ }
99
+
100
+ export async function setupInstall({
101
+ dryRun = false,
102
+ log = console.log,
103
+ root = ROOT,
104
+ home = homedir(),
105
+ installer = realInstaller,
106
+ depRunner = null,
107
+ } = {}) {
108
+ log('DSH Crew installer');
109
+ if (!checkRoot(root)) return { ok: false, error: `not a dsh-crew checkout: ${root}` };
110
+
111
+ if (depRunner) {
112
+ const r = depRunner(root);
113
+ if (!r.ok) { mark(log, false, r.text); return { ok: false, error: r.text }; }
114
+ mark(log, true, r.text);
115
+ } else if (!dryRun) {
116
+ const pnpmOk = commandExists('pnpm');
117
+ const cmd = pnpmOk ? 'pnpm' : 'npx';
118
+ const baseArgs = pnpmOk ? [] : ['-y', 'pnpm'];
119
+ const d = run(cmd, [...baseArgs, 'install', '--frozen-lockfile']);
120
+ if (!d.ok) {
121
+ if (depsPresent(root)) mark(log, true, `dependencies (already present; install skipped — ${(d.stderr || d.stdout || '').trim().split('\n')[0]})`);
122
+ else {
123
+ mark(log, false, `dependencies install failed: ${(d.stderr || d.stdout || '').trim().slice(-300)}`);
124
+ return { ok: false, error: 'dependencies install failed' };
125
+ }
126
+ } else mark(log, true, 'dependencies');
127
+ } else mark(log, true, 'dependencies (dry-run)');
128
+
129
+ if (!dryRun) {
130
+ 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 });
131
+ if (!b.ok) {
132
+ log(`✗ client build failed:\n${(b.stderr || b.stdout || '').slice(0, 600)}`);
133
+ return { ok: false, error: 'client build failed' };
134
+ }
135
+ }
136
+ mark(log, true, 'client build' + (dryRun ? ' (dry-run)' : ''));
137
+
138
+ const name = readPackageName(root);
139
+ if (!name) return { ok: false, error: 'package.json name missing' };
140
+ // Prefer an explicit/global or reusable Crew-owned runtime. If only npx is
141
+ // available, install a reusable copy under Crew state before falling back to
142
+ // the transient download path. Dry-run never provisions or downloads.
143
+ let dsh = detectDsh({ allowDownload: false, home });
144
+ if (!dryRun && (!dsh || dsh.kind === 'npx-local')) {
145
+ const boot = ensureCrewDshRuntime({ home });
146
+ if (boot.ok) dsh = boot.cli;
147
+ else log(`- reusable Crew DSH runtime unavailable (${boot.code ?? 'unknown'}); trying compatibility fallback`);
148
+ }
149
+ if (!dsh) dsh = detectDsh({ allowDownload: !dryRun, home });
150
+ if (!dsh) {
151
+ log('✗ DSH CLI not detected (Crew runtime, dsh, or npx)');
152
+ return { ok: false, error: 'DSH CLI not found' };
153
+ }
154
+ if (dryRun) {
155
+ // The old "DSH web profile would be linked" path is retired; the installer
156
+ // plans a link into the dedicated dsh-crew profile only. The legacy phrasing
157
+ // is kept here as an explicit contrast so existing CLI tests/people see the
158
+ // change, never as a command that targets the official web profile.
159
+ 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}")`);
160
+ } else {
161
+ const add = runDsh(dsh, ['add', `link:${root}`], { home });
162
+ if (!add.ok) {
163
+ log(`✗ DSH crew profile link failed:\n${(add.stderr || add.stdout || '').slice(0, 400)}`);
164
+ return { ok: false, error: 'DSH profile link failed' };
165
+ }
166
+ mark(log, true, 'DSH crew profile linked (dedicated Crew DSH_HOME, profile dsh-crew)');
167
+ }
168
+
169
+ if (dryRun) mark(log, true, 'Codex Desktop integration (dry-run)');
170
+ else {
171
+ const r = installer.installCodex ? installer.installCodex({ home }) : realInstaller.installCodex({ home });
172
+ mark(log, r.ok !== false, r.ok === false ? `Codex Desktop integration failed: ${(r.actions ?? []).join('; ')}` : 'Codex Desktop integration');
173
+ if (r.ok === false) return { ok: false, error: 'Codex integration failed' };
174
+ }
175
+
176
+ if (commandExists('claude')) {
177
+ if (dryRun) mark(log, true, 'Claude Code integration (dry-run)');
178
+ else {
179
+ const r = installer.installClaudeCode ? await installer.installClaudeCode({ home }) : await realInstaller.installClaudeCode({ home });
180
+ const ok = r.ok !== false;
181
+ mark(log, ok, ok ? 'Claude Code integration' : 'Claude Code integration failed');
182
+ if (!ok) return { ok: false, error: 'Claude Code integration failed' };
183
+ }
184
+ } else log('- Claude Code not detected, skipped');
185
+
186
+ log('');
187
+ log('Done.');
188
+ log('Restart DSH Web and Codex Desktop.');
189
+ return { ok: true };
190
+ }
191
+
192
+ export async function setupUninstall({
193
+ dryRun = false,
194
+ purge = false,
195
+ log = console.log,
196
+ root = ROOT,
197
+ home = homedir(),
198
+ installer = realInstaller,
199
+ } = {}) {
200
+ log('DSH Crew uninstaller');
201
+ const failures = [];
202
+ const fail = (name, text) => { mark(log, false, text || `${name} failed`); failures.push(name); };
203
+
204
+ if (dryRun) {
205
+ mark(log, true, 'Codex Desktop integration would be removed');
206
+ mark(log, true, 'Claude Code integration would be removed');
207
+ mark(log, true, 'DSH crew profile would be removed');
208
+ } else {
209
+ const cx = installer.uninstallCodex ? installer.uninstallCodex({ home }) : realInstaller.uninstallCodex({ home });
210
+ if (cx.ok !== false) mark(log, true, 'Codex Desktop integration removed');
211
+ else fail('codex', 'Codex Desktop integration removal failed');
212
+
213
+ const cl = installer.uninstallClaudeCode ? installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
214
+ if (cl.ok !== false) mark(log, true, 'Claude Code integration removed');
215
+ else fail('claude', 'Claude Code integration removal failed');
216
+
217
+ const name = readPackageName(root);
218
+ if (!name) fail('dsh', 'DSH plugin removal failed: package name missing');
219
+ else {
220
+ // Removing a registration is intentionally metadata-only and offline.
221
+ // Calling `dsh plugin remove` delegates to pnpm, which may resolve the
222
+ // whole profile and unexpectedly reach the network during uninstall.
223
+ const removed = removeCrewPluginRegistration({ home, name });
224
+ if (!removed.ok) fail('dsh', `DSH crew profile removal failed (${removed.code ?? 'unknown'})`);
225
+ 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)');
226
+ }
227
+ }
228
+
229
+ if (!dryRun) {
230
+ if (purge) {
231
+ try { rmSync(join(home, '.config', 'dsh-crew'), { recursive: true, force: true }); mark(log, true, '~/.config/dsh-crew purged'); }
232
+ catch { fail('purge', 'could not purge ~/.config/dsh-crew'); }
233
+ } else log('\nConfig/backups kept.');
234
+ }
235
+
236
+ if (failures.length > 0) {
237
+ log('\nFAILED: uninstall incomplete');
238
+ return { ok: false, failures };
239
+ }
240
+ log('Done.');
241
+ return { ok: true, failures: [] };
242
+ }
243
+
244
+ export async function setupStatus({ log = console.log, root = ROOT, home = homedir(), installer = realInstaller } = {}) {
245
+ const st = installer.installStatus ? installer.installStatus({ home }) : realInstaller.installStatus({ home });
246
+ const claude = st?.claude?.installed ? 'installed' : 'not installed';
247
+ const codex = st?.codex?.installed ? 'installed' : 'not installed';
248
+ // Crew status reads ONLY the dedicated Crew profile under the Crew DSH_HOME;
249
+ // the official web profile layout under the default DSH home is intentionally
250
+ // never inspected.
251
+ const crewProfDir = crewProfileDir({ home });
252
+ const pkgFile = join(crewProfDir, 'package.json');
253
+ let dshPlugin = 'not installed';
254
+ if (existsSync(pkgFile)) {
255
+ try {
256
+ const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'));
257
+ const name = readPackageName(root);
258
+ dshPlugin = (name && (pkg.dependencies?.[name] ?? pkg.dsh?.profile?.bundles?.includes?.(name))) ? 'installed' : 'not installed';
259
+ } catch { dshPlugin = 'unknown'; }
260
+ }
261
+ log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
262
+ log(`Codex Desktop integration: ${codex}`);
263
+ log(`Claude Code integration: ${claude}`);
264
+ return { ok: true, dshPlugin, codex, claude };
265
+ }
266
+
267
+ export async function runSetupCli({ argv = process.argv.slice(2), run: actions = {}, log = console.log } = {}) {
268
+ const action = argv[0];
269
+ const flags = new Set(argv.slice(1));
270
+ const dryRun = flags.has('--dry-run');
271
+ const purge = flags.has('--purge');
272
+ const install = actions.install ?? setupInstall;
273
+ const uninstall = actions.uninstall ?? setupUninstall;
274
+ const status = actions.status ?? setupStatus;
275
+ try {
276
+ let result;
277
+ if (action === 'install') result = await install({ dryRun, log });
278
+ else if (action === 'uninstall') result = await uninstall({ dryRun, purge, log });
279
+ else if (action === 'status') result = await status({ log });
280
+ else {
281
+ console.error('usage: node scripts/setup.mjs <install|uninstall|status> [--dry-run] [--purge]');
282
+ return 1;
283
+ }
284
+ return result?.ok === false ? 1 : 0;
285
+ } catch (err) {
286
+ console.error(`setup failed: ${err?.message ?? err}`);
287
+ return 1;
288
+ }
289
+ }
290
+
291
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) || (
292
+ process.argv[1] && resolve(join(process.cwd(), process.argv[1])) === fileURLToPath(import.meta.url)
293
+ );
294
+ if (isMain) process.exitCode = await runSetupCli({});
@@ -0,0 +1,110 @@
1
+ // Real DSH smoke for the v0.3 runtime path. Spawns this branch's MCP server
2
+ // (src/server.mjs) over stdio and drives it with the MCP SDK.
3
+ // run|spawn : against a LIVE, Crew-owned DSH hub. The hub URL comes from the
4
+ // DSH_CREW_HUB environment variable and defaults to the dedicated
5
+ // Crew port 3210; the official ~/.dsh home / web profile is never
6
+ // read or touched.
7
+ // standalone: standalone worker path (jobs.mjs) using ONLY explicitly
8
+ // supplied process-environment credentials
9
+ // (DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL). This script never reads
10
+ // ~/.dsh, any official credential/settings store, or key files.
11
+ // If the credential environment is absent it exits BLOCKED-style.
12
+ // Uses a throwaway temp git repo; never touches the web profile or user repos.
13
+ // Usage: node scripts/smoke-real.mjs <tempGitRepo> [run|spawn|standalone]
14
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
16
+
17
+ /** Session descriptor for the MCP child. NEVER logs the key. */
18
+ function serverTransport(extraEnv = {}) {
19
+ return new StdioClientTransport({
20
+ command: 'node',
21
+ args: ['src/server.mjs'],
22
+ cwd: process.cwd(),
23
+ env: {
24
+ ...process.env,
25
+ // Crew-owned hub default; an explicit DSH_CREW_HUB overrides it.
26
+ DSH_CREW_HUB: process.env.DSH_CREW_HUB ?? 'http://127.0.0.1:3210',
27
+ ...extraEnv,
28
+ },
29
+ stderr: process.stderr,
30
+ });
31
+ }
32
+
33
+ const repo = process.argv[2];
34
+ const mode = process.argv[3] ?? 'run';
35
+ const effort = process.argv[4] ?? 'max';
36
+ if (!repo) { console.error('usage: node scripts/smoke-real.mjs <tempGitRepo> [run|spawn|standalone] [effort]'); process.exit(2); }
37
+
38
+ const TASK = [
39
+ 'In this git repository, create the file src/answer.mjs with exactly the content: `export const answer = 40 + 2;`',
40
+ 'Then run the single-line check: node -e \'const a = require("./src/answer.mjs"); if (a.answer !== 42) process.exit(1); console.log("answer ok");\' (ESM note: use import("./src/answer.mjs") or write .cjs — pick whatever works and run it).',
41
+ 'End your final message with the required Delivery Report:',
42
+ '## Diff',
43
+ '- src/answer.mjs — added answer',
44
+ '## Tests',
45
+ 'PASS — node check — answer is 42',
46
+ '## Risks',
47
+ 'none',
48
+ ].join('\n');
49
+
50
+ // For standalone mode, use ONLY explicitly supplied process-environment
51
+ // credentials (DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL). This script never reads
52
+ // ~/.dsh or any official credential/settings store. Absent credentials are a
53
+ // BLOCKED outcome, not an excuse to invent or read credentials.
54
+ const standaloneEnv = {};
55
+ if (mode === 'standalone') {
56
+ const apiKey = process.env.DEEPSEEK_API_KEY;
57
+ if (!apiKey) {
58
+ console.error('[smoke] BLOCKED: standalone requires DEEPSEEK_API_KEY in the process environment; credential: ABSENT (no official store will be read)');
59
+ process.exit(2);
60
+ }
61
+ standaloneEnv.DEEPSEEK_API_KEY = apiKey;
62
+ if (process.env.DEEPSEEK_BASE_URL) standaloneEnv.DEEPSEEK_BASE_URL = process.env.DEEPSEEK_BASE_URL;
63
+ standaloneEnv.DSH_SANDBOX_MODE = 'workspace-write';
64
+ console.log('[smoke] standalone: using process-environment credentials (not shown)');
65
+ }
66
+ const transport = serverTransport(standaloneEnv);
67
+ const client = new Client({ name: 'dsh-crew-smoke', version: '1.0.0', requestTimeout: 600 * 1000 });
68
+
69
+ async function call(name, args, timeoutMs) {
70
+ const res = await client.callTool({ name, arguments: args }, undefined, timeoutMs ? { timeout: timeoutMs } : undefined);
71
+ return JSON.parse(res.content.find((c) => c.type === 'text').text);
72
+ }
73
+
74
+ try {
75
+ await client.connect(transport);
76
+ console.log(`[smoke] connected; mode=${mode} cwd=${repo}`);
77
+ if (mode === 'standalone') {
78
+ // Force the standalone execution path (session mode) so jobs.mjs handles it.
79
+ await call('dsh_worker_config', { mode: 'standalone' });
80
+ }
81
+
82
+ if (mode === 'spawn') {
83
+ const spawnView = await call('dsh_spawn_worker', { task: TASK, role: 'worker', cwd: repo });
84
+ console.log('[smoke] spawn view:', JSON.stringify(spawnView, null, 2));
85
+ const wfId = spawnView.workflow_id ?? spawnView.id;
86
+ // poll a few times for the final result (workflow continues in background)
87
+ for (let i = 0; i < 24; i++) {
88
+ await new Promise((r) => setTimeout(r, 5000));
89
+ const v = await call('dsh_worker_result', { job_id: wfId, wait_seconds: 0 });
90
+ if (v.status !== 'running') {
91
+ console.log('[smoke] final (spawn):', JSON.stringify({ id: v.id, role: v.role, phase: v.phase, status: v.status, isolation: v.isolation, attempt: v.attempt, review: v.review?.verdict ?? null, candidate: v.candidate?.changed_files ?? null, error: v.error }, null, 2));
92
+ await client.close();
93
+ process.exit(v.status === 'done' ? 0 : 1);
94
+ }
95
+ if (i === 23) console.log('[smoke] still running after poll window: ', JSON.stringify(v));
96
+ }
97
+ await client.close();
98
+ process.exit(2);
99
+ }
100
+
101
+ const view = await call('dsh_run_worker', { task: TASK, role: 'worker', cwd: repo, effort, timeout_seconds: 540 }, 560 * 1000);
102
+ const k = (o) => ({ id: o.id, role: o.role, phase: o.phase, status: o.status, isolation: o.isolation, execution_cwd: o.execution_cwd, base_revision: o.base_revision, attempt: o.attempt, decision: o.decision, review: o.review?.verdict ?? null, candidate_available: o.candidate_available, candidate_changed: o.candidate?.changed_files ?? null, outcome_task: o.outcome?.task_status ?? null, tests: o.outcome?.tests_status ?? null, error: o.error, error_code: o.error_code, cleanup_warning: o.cleanup_warning, primary_dirty_hint: o.primary_workspace_dirty });
103
+ console.log('[smoke] final (run):', JSON.stringify(k(view), null, 2));
104
+ await client.close();
105
+ process.exit(view.status === 'done' ? 0 : 1);
106
+ } catch (err) {
107
+ console.error('[smoke] ERROR:', err?.message ?? err);
108
+ try { await client.close(); } catch {}
109
+ process.exit(3);
110
+ }
@@ -0,0 +1,78 @@
1
+ // Smoke test: dispatch one cheap flash job to verify the installation.
2
+ // Probes the hub first (if DSH web instance is running); falls back to
3
+ // standalone mode. Always exercises the path the user actually installed.
4
+
5
+ import { hubAvailable, hub } from '../src/hub-client.mjs';
6
+ import { readGlobalConfig } from '../src/install/install.mjs';
7
+
8
+ try {
9
+ const hubUrl = process.env.DSH_CREW_HUB ?? readGlobalConfig().hub_url;
10
+ const isHubUp = await hubAvailable();
11
+
12
+ if (isHubUp) {
13
+ // Dispatch through the hub
14
+ console.log(`hub at ${hubUrl} is reachable, using hub mode`);
15
+ const job = await hub.spawn({
16
+ task: 'Reply with exactly the word: ok',
17
+ tier: 'flash',
18
+ effort: 'off',
19
+ cwd: process.cwd(),
20
+ source: 'smoke',
21
+ });
22
+ console.log(`spawned ${job.id} ...`);
23
+ const result = await hub.get(job.id, 120);
24
+ if (result.status === 'done') {
25
+ console.log(`worker replied: ${result.result}`);
26
+ console.log('smoke test passed — configuration OK');
27
+ } else if (result.status === 'running') {
28
+ // The wait returns early once the job settles, so this is a real timeout.
29
+ console.error(`smoke test failed: job ${job.id} still running after 120s`);
30
+ console.error('Hint: it is still live in the DSH instance — check its session for what is stuck');
31
+ process.exit(1);
32
+ } else {
33
+ console.error(`smoke test failed: status=${result.status} error=${result.error}`);
34
+ console.error('Hint: check the DSH instance logs and verify DeepSeek provider credentials');
35
+ process.exit(1);
36
+ }
37
+ } else {
38
+ // Fall back to standalone mode
39
+ console.log(`no hub at ${hubUrl}, falling back to standalone mode`);
40
+
41
+ try {
42
+ const { startJob, waitJob, jobView } = await import('../src/jobs.mjs');
43
+ const job = startJob({
44
+ task: 'Reply with exactly the word: ok',
45
+ tier: 'flash',
46
+ effort: 'off',
47
+ cwd: process.cwd(),
48
+ });
49
+ console.log(`spawned ${job.id} (deepseek-v4-flash) ...`);
50
+ await waitJob(job.id, 120_000);
51
+ const v = jobView(job, { withResult: true });
52
+ if (v.status === 'done') {
53
+ console.log(`worker replied: ${v.result}`);
54
+ console.log('smoke test passed — configuration OK');
55
+ } else {
56
+ console.error(`smoke test failed: status=${v.status} error=${v.error}`);
57
+ console.error('Hint: DEEPSEEK_API_KEY missing or invalid in env or ~/.config/dsh-crew/.env, or runtime not installed');
58
+ process.exit(1);
59
+ }
60
+ } catch (err) {
61
+ // The standalone runtime lives in this package's own node_modules; the
62
+ // @deepseek-ai/* packages are peerDependencies, so an installed copy of
63
+ // the plugin has none of them and the dynamic import above is what tells
64
+ // us so. Anything else here is a dispatch failure, not a missing runtime.
65
+ if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
66
+ console.error('smoke test failed: the standalone runtime is not installed in this copy');
67
+ console.error('Hint: run "pnpm install" from a source checkout, or start a DSH instance to use hub mode');
68
+ } else {
69
+ console.error(`smoke test failed: ${err.message}`);
70
+ console.error('Hint: DEEPSEEK_API_KEY missing or invalid in env or ~/.config/dsh-crew/.env');
71
+ }
72
+ process.exit(1);
73
+ }
74
+ }
75
+ } catch (err) {
76
+ console.error(`smoke test failed: ${err.message}`);
77
+ process.exit(1);
78
+ }
@@ -0,0 +1,26 @@
1
+ // Live-verify the fixed installer against a fake home: the settings must
2
+ // point at the repository itself as the marketplace root (no parent-dir
3
+ // assumption), permission rules intact, CLI step skipped in test mode.
4
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import { installClaudeCode, uninstallClaudeCode } from '../src/install/install.mjs';
8
+
9
+ const home = mkdtempSync(join(tmpdir(), 'dsh-crew-fix-test-'));
10
+ try {
11
+ const r = await installClaudeCode({ home });
12
+ console.log('actions:', r.actions.slice(0, 3).join(' | '));
13
+ const s = JSON.parse(readFileSync(join(home, '.claude', 'settings.json'), 'utf8'));
14
+ const mp = s.extraKnownMarketplaces['dsh-crew'];
15
+ const cwd = process.cwd().replaceAll('\\', '/');
16
+ const mpPath = String(mp?.source?.path ?? '').replaceAll('\\', '/');
17
+ console.log('marketplace path is repo root:', mpPath === cwd, '->', mpPath);
18
+ console.log('permission rules:', (s.permissions?.allow ?? []).filter((x) => String(x).includes('dsh-crew')).length);
19
+ console.log('cli step skipped in test mode:', r.actions.some((a) => a.includes('test mode')));
20
+ if (mpPath !== cwd) throw new Error('marketplace not pointing at repo root');
21
+ const u = await uninstallClaudeCode({ home });
22
+ console.log('uninstall ok:', u.ok);
23
+ } finally {
24
+ rmSync(home, { recursive: true, force: true });
25
+ }
26
+ console.log('INSTALLER FIX VERIFIED');