@ran-sh/dsh-crew 2.2.5 → 2.2.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.
- package/.claude-plugin/plugin.json +1 -1
- package/docs/readiness-matrix.md +9 -2
- package/official-web-bridge/lib/client.js +27 -12
- package/package.json +1 -1
- package/scripts/setup.mjs +9 -1
- package/src/client/quick-panel.tsx +6 -1
- package/src/extension-contract.mjs +9 -0
- package/src/hub/index.mjs +1 -0
- package/src/install/install-legacy.mjs +96 -28
- package/src/install/npx-lifecycle.mjs +10 -0
- package/src/install/official-frontend-assets.mjs +7 -4
- package/src/install/windows-startup.mjs +5 -2
- package/src/log-prune.mjs +1 -1
- package/src/readiness-matrix.mjs +9 -6
- package/src/runtime-identity.mjs +1 -1
- package/windows/start-dsh-crew.ps1 +19 -0
package/docs/readiness-matrix.md
CHANGED
|
@@ -12,9 +12,16 @@ The matrix is emitted by `hubStatus()` and is therefore visible inside the exist
|
|
|
12
12
|
- `FAIL` — a check actually ran and produced an incompatible or failed result.
|
|
13
13
|
- `BLOCKED` — the check could not run because required infrastructure or authorization was unavailable.
|
|
14
14
|
- `SKIP` — the row is intentionally not applicable for the active policy/path.
|
|
15
|
+
- `NOT_APPLICABLE` — the row asks about a route this machine does not use, and the
|
|
16
|
+
matrix says so instead of omitting it. The two built-in DeepSeek rows are the live
|
|
17
|
+
case: they read `NOT_APPLICABLE` / `WORKER_PROVIDER_FOLLOWS_DSH` whenever a known,
|
|
18
|
+
other provider is selected, and keep `NOT_RUN` when the selection is unknown.
|
|
15
19
|
- `NOT_RUN` — no trusted evidence has been supplied for the row.
|
|
16
20
|
|
|
17
|
-
`BLOCKED` and `SKIP` are not failures. `NOT_RUN` is not success
|
|
21
|
+
`BLOCKED` and `SKIP` are not failures. `NOT_RUN` is not success, and `NOT_APPLICABLE`
|
|
22
|
+
is an answer: it is reported as itself on any component that consumes the row, and it
|
|
23
|
+
cannot lower the readiness aggregate — only a `FAIL`, an unproven row or an
|
|
24
|
+
`UNAVAILABLE` component does.
|
|
18
25
|
|
|
19
26
|
## Evidence classes
|
|
20
27
|
|
|
@@ -61,7 +68,7 @@ actually run" — `provider_health`, `reviewer_health`, `model_execution`,
|
|
|
61
68
|
`worker_primary_callable`, `reviewer_primary_callable`, `reviewer_pipeline` — is
|
|
62
69
|
filtered to whatever route is currently selected, so an operator on other
|
|
63
70
|
providers is covered by those and loses nothing by the two DeepSeek rows
|
|
64
|
-
reading `
|
|
71
|
+
reading `NOT_APPLICABLE` under `follow-dsh`. A static row per provider was tried and replaced by these
|
|
65
72
|
dynamic signals; `opencode_go_mimo_qwen` was that attempt's last trace in this
|
|
66
73
|
document.
|
|
67
74
|
|
|
@@ -80,6 +80,7 @@ const T = {
|
|
|
80
80
|
openDiag: "打开诊断",
|
|
81
81
|
crew: "Crew",
|
|
82
82
|
enabled: "启用子 Agent",
|
|
83
|
+
frontendAutostart: "登录时启动 3080 前端(不打开浏览器)",
|
|
83
84
|
flash: "Worker / Flash",
|
|
84
85
|
pro: "Reviewer / Pro",
|
|
85
86
|
addModel: "+ 添加模型",
|
|
@@ -107,6 +108,7 @@ const T = {
|
|
|
107
108
|
openDiag: "Open diagnostics",
|
|
108
109
|
crew: "Crew",
|
|
109
110
|
enabled: "Enable sub-agents",
|
|
111
|
+
frontendAutostart: "Start the 3080 frontend at login (no browser)",
|
|
110
112
|
flash: "Worker / Flash",
|
|
111
113
|
pro: "Reviewer / Pro",
|
|
112
114
|
addModel: "+ Add model",
|
|
@@ -439,19 +441,32 @@ function QuickPanel({ ctx }) {
|
|
|
439
441
|
}),
|
|
440
442
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
441
443
|
className: "crew-quick-row crew-quick-master",
|
|
442
|
-
children: [
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
447
|
-
type: "checkbox",
|
|
448
|
-
disabled: busy,
|
|
449
|
-
checked: config.subagents_enabled !== false,
|
|
450
|
-
onChange: (e) => toggle("subagents_enabled", e.target.checked)
|
|
444
|
+
children: [
|
|
445
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
446
|
+
className: "crew-quick-section",
|
|
447
|
+
children: t.crew
|
|
451
448
|
}),
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
449
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
|
|
450
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
451
|
+
type: "checkbox",
|
|
452
|
+
disabled: busy,
|
|
453
|
+
checked: config.subagents_enabled !== false,
|
|
454
|
+
onChange: (e) => toggle("subagents_enabled", e.target.checked)
|
|
455
|
+
}),
|
|
456
|
+
" ",
|
|
457
|
+
t.enabled
|
|
458
|
+
] }),
|
|
459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
|
|
460
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
461
|
+
type: "checkbox",
|
|
462
|
+
disabled: busy,
|
|
463
|
+
checked: config.frontend_autostart === true,
|
|
464
|
+
onChange: (e) => toggle("frontend_autostart", e.target.checked)
|
|
465
|
+
}),
|
|
466
|
+
" ",
|
|
467
|
+
t.frontendAutostart
|
|
468
|
+
] })
|
|
469
|
+
]
|
|
455
470
|
}),
|
|
456
471
|
modelList("flash_model_priority", t.flash),
|
|
457
472
|
modelList("pro_model_priority", t.pro),
|
package/package.json
CHANGED
package/scripts/setup.mjs
CHANGED
|
@@ -384,11 +384,19 @@ export async function setupStatus({ log = console.log, root = ROOT, home = homed
|
|
|
384
384
|
} catch { dshPlugin = 'unknown'; }
|
|
385
385
|
}
|
|
386
386
|
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
|
|
387
|
+
// The frontend is on-demand by design: 3210 is the service, and 3080 is started
|
|
388
|
+
// for a browser session. Rendering it as anything else reads as a missing piece
|
|
389
|
+
// of the installation, which it is not.
|
|
390
|
+
const frontendConfig = (installer.readGlobalConfig ?? realInstaller.readGlobalConfig)({ configFile: join(home, '.config', 'dsh-crew', 'config.json') });
|
|
391
|
+
const frontend = frontendConfig?.frontend_autostart === true
|
|
392
|
+
? 'auto-started at login (dsh-crew open for a session URL)'
|
|
393
|
+
: 'available on demand (dsh-crew open)';
|
|
394
|
+
log(`Frontend (3080): ${frontend}`);
|
|
387
395
|
log(`Codex Desktop integration: ${codex}`);
|
|
388
396
|
log(`ZCode integration: ${zcode}`);
|
|
389
397
|
log(`Claude Code integration: ${claude}`);
|
|
390
398
|
log(`Windows login startup: ${windowsStartup}`);
|
|
391
|
-
return { ok: true, dshPlugin, codex, zcode, claude, windowsStartup };
|
|
399
|
+
return { ok: true, dshPlugin, frontend, codex, zcode, claude, windowsStartup };
|
|
392
400
|
}
|
|
393
401
|
|
|
394
402
|
export async function runSetupCli({ argv = process.argv.slice(2), run: actions = {}, log = console.log } = {}) {
|
|
@@ -19,6 +19,7 @@ const FULL = 'http://127.0.0.1:3210/_dsh/dsh-crew/control-plane';
|
|
|
19
19
|
type ModelEntry = { provider: string; model: string };
|
|
20
20
|
type QuickConfig = {
|
|
21
21
|
subagents_enabled?: boolean;
|
|
22
|
+
frontend_autostart?: boolean;
|
|
22
23
|
flash_model_priority?: ModelEntry[];
|
|
23
24
|
pro_model_priority?: ModelEntry[];
|
|
24
25
|
};
|
|
@@ -41,6 +42,7 @@ const T = {
|
|
|
41
42
|
openDiag: '打开诊断',
|
|
42
43
|
crew: 'Crew',
|
|
43
44
|
enabled: '启用子 Agent',
|
|
45
|
+
frontendAutostart: '登录时启动 3080 前端(不打开浏览器)',
|
|
44
46
|
flash: 'Worker / Flash',
|
|
45
47
|
pro: 'Reviewer / Pro',
|
|
46
48
|
addModel: '+ 添加模型',
|
|
@@ -66,6 +68,7 @@ const T = {
|
|
|
66
68
|
openDiag: 'Open diagnostics',
|
|
67
69
|
crew: 'Crew',
|
|
68
70
|
enabled: 'Enable sub-agents',
|
|
71
|
+
frontendAutostart: 'Start the 3080 frontend at login (no browser)',
|
|
69
72
|
flash: 'Worker / Flash',
|
|
70
73
|
pro: 'Reviewer / Pro',
|
|
71
74
|
addModel: '+ Add model',
|
|
@@ -157,7 +160,7 @@ export function QuickPanel({ ctx }: { ctx: any }) {
|
|
|
157
160
|
} finally { setBusy(false); }
|
|
158
161
|
}, [t]);
|
|
159
162
|
|
|
160
|
-
const toggle = (key: 'subagents_enabled', value: boolean) => {
|
|
163
|
+
const toggle = (key: 'subagents_enabled' | 'frontend_autostart', value: boolean) => {
|
|
161
164
|
setConfig((c) => ({ ...(c ?? {}), [key]: value }));
|
|
162
165
|
void patch({ [key]: value } as QuickConfig);
|
|
163
166
|
};
|
|
@@ -251,6 +254,8 @@ export function QuickPanel({ ctx }: { ctx: any }) {
|
|
|
251
254
|
<span className="crew-quick-section">{t.crew}</span>
|
|
252
255
|
<label><input type="checkbox" disabled={busy} checked={config.subagents_enabled !== false}
|
|
253
256
|
onChange={(e) => toggle('subagents_enabled', e.target.checked)} /> {t.enabled}</label>
|
|
257
|
+
<label><input type="checkbox" disabled={busy} checked={config.frontend_autostart === true}
|
|
258
|
+
onChange={(e) => toggle('frontend_autostart', e.target.checked)} /> {t.frontendAutostart}</label>
|
|
254
259
|
</div>
|
|
255
260
|
{modelList('flash_model_priority', t.flash)}
|
|
256
261
|
{modelList('pro_model_priority', t.pro)}
|
|
@@ -32,6 +32,10 @@ function workspaceComponent(workspace) {
|
|
|
32
32
|
function readinessFromRow(entry, { pass = 'READY', notRun = 'DEGRADED' } = {}) {
|
|
33
33
|
if (!entry) return component('UNAVAILABLE', 'NO_EVIDENCE');
|
|
34
34
|
if (entry.status === 'PASS') return component(pass, entry.reason_code ?? 'CHECK_PASSED');
|
|
35
|
+
// A row that does not apply to this machine is reported as such, not as a check
|
|
36
|
+
// that has not run: NOT_APPLICABLE says the question was answered, and it cannot
|
|
37
|
+
// pull the aggregate down (see the readiness roll-up below).
|
|
38
|
+
if (entry.status === 'NOT_APPLICABLE') return component('NOT_APPLICABLE', entry.reason_code ?? 'CHECK_NOT_APPLICABLE');
|
|
35
39
|
if (entry.status === 'NOT_RUN' || entry.status === 'SKIP') return component(notRun, entry.reason_code ?? 'CHECK_NOT_RUN');
|
|
36
40
|
return component('UNAVAILABLE', entry.reason_code ?? 'CHECK_FAILED');
|
|
37
41
|
}
|
|
@@ -50,7 +54,12 @@ function modelReadinessFromSnapshot(readinessSnapshot, matrix, runtime, expected
|
|
|
50
54
|
});
|
|
51
55
|
if (validation.ok && validation.state === 'CALLABLE') return component('READY', 'CURRENT_MODEL_CALLABLE', { captured_at: callability.captured_at, expires_at: callability.expires_at, runtime_id: callability.current_runtime_id });
|
|
52
56
|
if (validation.ok && validation.state === 'NOT_CALLABLE') return component('UNAVAILABLE', callability.roles.worker?.state === 'NOT_CALLABLE' ? callability.roles.worker.reason_code : callability.roles.reviewer.reason_code ?? 'CURRENT_MODEL_NOT_CALLABLE');
|
|
57
|
+
// Aging evidence is reported as aging, not as a fault, and "nothing has proven
|
|
58
|
+
// this route yet" is reported as unknown. The validator's own success code is
|
|
59
|
+
// `MODEL_CALLABILITY_VALID`, and rendering that next to DEGRADED read as a
|
|
60
|
+
// contradiction: the projection was valid, the evidence was simply not current.
|
|
53
61
|
if (validation.ok && validation.state === 'STALE') return component('DEGRADED', 'MODEL_EVIDENCE_STALE');
|
|
62
|
+
if (validation.ok) return component('DEGRADED', 'MODEL_CALLABILITY_UNKNOWN');
|
|
54
63
|
return component('DEGRADED', validation.reason_code ?? 'MODEL_CALLABILITY_UNKNOWN');
|
|
55
64
|
}
|
|
56
65
|
if (providerHealthEvidence?.status === 'FAIL') return readinessFromRow(providerHealthEvidence);
|
package/src/hub/index.mjs
CHANGED
|
@@ -114,6 +114,7 @@ const ROUTE_BASE = '/_dsh/dsh-crew';
|
|
|
114
114
|
// canonical authority; quick-config is a projection, never a second store.
|
|
115
115
|
export const QUICK_CONFIG_KEYS = Object.freeze([
|
|
116
116
|
'subagents_enabled',
|
|
117
|
+
'frontend_autostart',
|
|
117
118
|
'flash_model_priority',
|
|
118
119
|
'pro_model_priority',
|
|
119
120
|
]);
|
|
@@ -17,6 +17,14 @@ import { integrationRoot } from './crew-paths.mjs';
|
|
|
17
17
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
18
18
|
const MARKETPLACE_NAME = 'dsh-crew';
|
|
19
19
|
const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
|
|
20
|
+
// Every name this installer has ever registered Crew under in a Claude host, and
|
|
21
|
+
// the only names its uninstall may remove. `dsh-workers` is the pre-rename
|
|
22
|
+
// identity: an operator who upgraded keeps that marketplace, plugin entry and its
|
|
23
|
+
// permission rules until the same uninstall cleans them, and nothing else in the
|
|
24
|
+
// file is Crew's to touch.
|
|
25
|
+
const CREW_CLAUDE_MARKETPLACES = [MARKETPLACE_NAME, 'dsh-workers'];
|
|
26
|
+
const CREW_CLAUDE_PLUGIN_KEYS = [PLUGIN_KEY, 'dsh-workers@dsh-workers'];
|
|
27
|
+
const CREW_CLAUDE_PERMISSION_PREFIXES = ['mcp__plugin_dsh-crew_', 'mcp__plugin_dsh-workers_'];
|
|
20
28
|
// A Claude Code plugin refresh is a real copy of the plugin tree, and its cost
|
|
21
29
|
// tracks machine load: 163s measured idle, ~6 minutes measured during an
|
|
22
30
|
// activation. The install is the step that copies, so it gets the ceiling that
|
|
@@ -645,7 +653,7 @@ export function uninstallCodex({ home = homedir(), env = process.env } = {}) {
|
|
|
645
653
|
export function claudeIntegrationLine(result) {
|
|
646
654
|
if (result?.ok === false) return '✗ Claude Code integration failed';
|
|
647
655
|
if (result?.detected === false) {
|
|
648
|
-
return '- Claude Code not
|
|
656
|
+
return '- Claude Code not installed (claude CLI not found); nothing written';
|
|
649
657
|
}
|
|
650
658
|
if (result?.degraded === true) {
|
|
651
659
|
return `- Claude Code integration registered, but not loaded: ${result.reason ?? 'plugin snapshot not refreshed'}`;
|
|
@@ -906,9 +914,22 @@ export async function runClaudeStep(args, { timeoutMs = CLAUDE_STEP_TIMEOUT_MS,
|
|
|
906
914
|
};
|
|
907
915
|
}
|
|
908
916
|
|
|
909
|
-
export async function installClaudeCode({ home = homedir(), statusline = false, root = ROOT } = {}) {
|
|
917
|
+
export async function installClaudeCode({ home = homedir(), statusline = false, root = ROOT, resolveClaude = resolveClaudeCommand } = {}) {
|
|
910
918
|
const actions = [];
|
|
911
919
|
|
|
920
|
+
// This registration exists only to make Crew callable FROM Claude Code, so a
|
|
921
|
+
// host without the `claude` CLI has nothing for it to serve. Writing it anyway
|
|
922
|
+
// left Crew's marketplace, plugin entry and permission rules in a file Crew
|
|
923
|
+
// could never exercise, next to a footprint that read back as an integration
|
|
924
|
+
// wanting a CLI the machine does not have. Detect once, up front, and write
|
|
925
|
+
// nothing when the host cannot use it: `host_detected` then reports the truth
|
|
926
|
+
// with no stale registration to explain away.
|
|
927
|
+
const claudeCommand = resolveClaude();
|
|
928
|
+
if (!claudeCommand) {
|
|
929
|
+
actions.push('claude CLI not found; no Claude configuration written');
|
|
930
|
+
return { ok: true, detected: false, actions };
|
|
931
|
+
}
|
|
932
|
+
|
|
912
933
|
// The repository carries its own marketplace manifest (.claude-plugin/
|
|
913
934
|
// marketplace.json with source "."), so the marketplace root IS the plugin
|
|
914
935
|
// checkout itself — no parent-directory layout assumption. This keeps the
|
|
@@ -999,7 +1020,8 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
999
1020
|
// Materialize the install through the claude CLI: registers the marketplace
|
|
1000
1021
|
// in the plugin cache (settings alone leave a stale/absent cache entry) and
|
|
1001
1022
|
// pulls the plugin so the next session loads it. Best-effort: settings are
|
|
1002
|
-
// already correct, so a
|
|
1023
|
+
// already correct, so a CLI that fails on its own terms leaves the operator one
|
|
1024
|
+
// manual `plugin install` short rather than an unregistered host.
|
|
1003
1025
|
const registered = readJson(join(home, '.claude', 'plugins', 'known_marketplaces.json'), {})?.[MARKETPLACE_NAME];
|
|
1004
1026
|
const installedRecord = readJson(join(home, '.claude', 'plugins', 'installed_plugins.json'), {})?.plugins?.[PLUGIN_KEY];
|
|
1005
1027
|
const installedEntries = Array.isArray(installedRecord) ? installedRecord : [installedRecord];
|
|
@@ -1019,14 +1041,9 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
1019
1041
|
actions.push('cli: skipped (non-default home; test mode)');
|
|
1020
1042
|
return { ok: true, actions };
|
|
1021
1043
|
}
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
const claudeCommand = resolveClaudeCommand();
|
|
1026
|
-
if (!claudeCommand) {
|
|
1027
|
-
actions.push('cli: skipped (claude not found)');
|
|
1028
|
-
return { ok: true, detected: false, actions };
|
|
1029
|
-
}
|
|
1044
|
+
// `claudeCommand` was resolved before anything was written: reaching this line
|
|
1045
|
+
// already proves the CLI is there, so the step is best-effort rather than
|
|
1046
|
+
// possibly-pointless.
|
|
1030
1047
|
return refreshClaudePlugin({ home, root, actions, claudeCommand });
|
|
1031
1048
|
}
|
|
1032
1049
|
|
|
@@ -1230,25 +1247,76 @@ export function installHudSegment({ home = homedir() } = {}) {
|
|
|
1230
1247
|
return { ok: true, actions: [...(bak ? [`backup: ${bak}`] : []), 'statusLine: claude-hud now runs worker-segment.sh via --extra-cmd'] };
|
|
1231
1248
|
}
|
|
1232
1249
|
|
|
1250
|
+
// Only the status line `--statusline` installs is Crew's to remove: it points at
|
|
1251
|
+
// Crew's own script. Any other status line in the file belongs to whoever
|
|
1252
|
+
// configured it, even when it happens to name this checkout.
|
|
1253
|
+
function crewOwnedStatusLine(value) {
|
|
1254
|
+
const command = typeof value?.command === 'string' ? value.command : '';
|
|
1255
|
+
return command.includes('statusline.sh') && command.includes('dsh-crew');
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// The claude CLI records, per marketplace and plugin, what it materialized, in
|
|
1259
|
+
// files of its own. Unregistering the settings and leaving those records behind
|
|
1260
|
+
// hands the next install a snapshot with no owner, so an explicit uninstall
|
|
1261
|
+
// clears them too. Crew-scoped and best-effort: a file that is absent, unreadable
|
|
1262
|
+
// or shaped differently than expected is left exactly as it is found.
|
|
1263
|
+
function removeCrewClaudeCacheRecords({ home, actions }) {
|
|
1264
|
+
const dir = join(home, '.claude', 'plugins');
|
|
1265
|
+
const marketsFile = join(dir, 'known_marketplaces.json');
|
|
1266
|
+
const markets = readJson(marketsFile, null);
|
|
1267
|
+
if (markets && typeof markets === 'object' && !Array.isArray(markets)) {
|
|
1268
|
+
const owned = CREW_CLAUDE_MARKETPLACES.filter((name) => Object.hasOwn(markets, name));
|
|
1269
|
+
if (owned.length) {
|
|
1270
|
+
backup(marketsFile);
|
|
1271
|
+
for (const name of owned) delete markets[name];
|
|
1272
|
+
writeFileSync(marketsFile, JSON.stringify(markets, null, 2) + '\n');
|
|
1273
|
+
actions.push(`marketplace cache: removed ${owned.join(', ')}`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
const pluginsFile = join(dir, 'installed_plugins.json');
|
|
1277
|
+
const installed = readJson(pluginsFile, null);
|
|
1278
|
+
if (installed && typeof installed === 'object' && !Array.isArray(installed)
|
|
1279
|
+
&& installed.plugins && typeof installed.plugins === 'object' && !Array.isArray(installed.plugins)) {
|
|
1280
|
+
const owned = CREW_CLAUDE_PLUGIN_KEYS.filter((key) => Object.hasOwn(installed.plugins, key));
|
|
1281
|
+
if (owned.length) {
|
|
1282
|
+
backup(pluginsFile);
|
|
1283
|
+
for (const key of owned) delete installed.plugins[key];
|
|
1284
|
+
writeFileSync(pluginsFile, JSON.stringify(installed, null, 2) + '\n');
|
|
1285
|
+
actions.push(`plugin cache: removed ${owned.join(', ')}`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1233
1290
|
export function uninstallClaudeCode({ home = homedir() } = {}) {
|
|
1291
|
+
const actions = [];
|
|
1234
1292
|
const settingsFile = join(home, '.claude', 'settings.json');
|
|
1235
1293
|
const settings = readJson(settingsFile, null);
|
|
1236
|
-
if (
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1294
|
+
if (settings) {
|
|
1295
|
+
backup(settingsFile);
|
|
1296
|
+
const mpDir = join(home, '.config', 'dsh-crew', 'marketplace');
|
|
1297
|
+
if (Array.isArray(settings.extraKnownMarketplaces)) {
|
|
1298
|
+
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces.filter((m) => m?.path !== mpDir);
|
|
1299
|
+
} else if (settings.extraKnownMarketplaces) {
|
|
1300
|
+
for (const name of CREW_CLAUDE_MARKETPLACES) delete settings.extraKnownMarketplaces[name];
|
|
1301
|
+
}
|
|
1302
|
+
if (Array.isArray(settings.enabledPlugins)) {
|
|
1303
|
+
settings.enabledPlugins = settings.enabledPlugins.filter((p) => !CREW_CLAUDE_PLUGIN_KEYS.includes(p));
|
|
1304
|
+
} else if (settings.enabledPlugins) {
|
|
1305
|
+
for (const key of CREW_CLAUDE_PLUGIN_KEYS) delete settings.enabledPlugins[key];
|
|
1306
|
+
}
|
|
1307
|
+
if (settings.permissions?.allow) {
|
|
1308
|
+
settings.permissions.allow = settings.permissions.allow.filter((rule) => typeof rule !== 'string'
|
|
1309
|
+
|| !CREW_CLAUDE_PERMISSION_PREFIXES.some((prefix) => rule.startsWith(prefix)));
|
|
1310
|
+
}
|
|
1311
|
+
if (crewOwnedStatusLine(settings.statusLine)) {
|
|
1312
|
+
delete settings.statusLine;
|
|
1313
|
+
actions.push('statusLine: removed (Crew-installed)');
|
|
1314
|
+
}
|
|
1315
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
1316
|
+
actions.push('unregistered from settings.json (backup kept)');
|
|
1317
|
+
} else {
|
|
1318
|
+
actions.push('settings.json not found');
|
|
1251
1319
|
}
|
|
1252
|
-
|
|
1253
|
-
return { ok: true, actions
|
|
1320
|
+
removeCrewClaudeCacheRecords({ home, actions });
|
|
1321
|
+
return { ok: true, actions };
|
|
1254
1322
|
}
|
|
@@ -3327,6 +3327,15 @@ export function npxStatus({
|
|
|
3327
3327
|
}
|
|
3328
3328
|
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
|
|
3329
3329
|
log(`Official 3080 UI bridge: ${officialWeb}`);
|
|
3330
|
+
// The frontend is not an install that can be missing: 3210 is the service, and
|
|
3331
|
+
// 3080 is started on demand. Saying so is the difference between "this machine is
|
|
3332
|
+
// incomplete" and "run `dsh-crew open` when you want the browser surface" — and
|
|
3333
|
+
// whether 3080 happens to be up right now does not change either answer.
|
|
3334
|
+
const frontendConfig = (installer.readGlobalConfig ?? realInstaller.readGlobalConfig)({ configFile: join(home, '.config', 'dsh-crew', 'config.json') });
|
|
3335
|
+
const frontend = frontendConfig?.frontend_autostart === true
|
|
3336
|
+
? 'auto-started at login (dsh-crew open for a session URL)'
|
|
3337
|
+
: 'available on demand (dsh-crew open)';
|
|
3338
|
+
log(`Frontend (3080): ${frontend}`);
|
|
3330
3339
|
// The desktop app carries its own bridge entry, which pins a revision: after a
|
|
3331
3340
|
// payload update that entry is stale until it is re-pointed, so say which of the
|
|
3332
3341
|
// three states the machine is in instead of leaving the panel silently old.
|
|
@@ -3349,6 +3358,7 @@ export function npxStatus({
|
|
|
3349
3358
|
installedPath: pointer?.path ?? null,
|
|
3350
3359
|
dshPlugin,
|
|
3351
3360
|
officialWeb,
|
|
3361
|
+
frontend,
|
|
3352
3362
|
codex,
|
|
3353
3363
|
zcode,
|
|
3354
3364
|
claude,
|
|
@@ -19,9 +19,10 @@ function plan(home, root) {
|
|
|
19
19
|
// `core.autocrlf=true` checkout hashes to a different revision than the npm
|
|
20
20
|
// payload it installs, which made readiness look for a snapshot that does not
|
|
21
21
|
// exist and reported "needs repair" on a correct install (and made every payload
|
|
22
|
-
// update look like a stale desktop revision).
|
|
22
|
+
// update look like a stale desktop revision). Lone CR collapses too: one line
|
|
23
|
+
// break is one line break in these sources.
|
|
23
24
|
const canonicalBytes = (bytes) => (bytes.includes(0) ? bytes
|
|
24
|
-
: Buffer.from(bytes.toString('utf8').replace(/\r\n
|
|
25
|
+
: Buffer.from(bytes.toString('utf8').replace(/\r\n?/g, '\n'), 'utf8'));
|
|
25
26
|
for (const [name, bytes] of files) files.set(name, canonicalBytes(bytes));
|
|
26
27
|
const hash = createHash('sha256').update(JSON.stringify(metadata));
|
|
27
28
|
for (const [name, bytes] of files) hash.update(name).update(bytes);
|
|
@@ -43,10 +44,12 @@ function plan(home, root) {
|
|
|
43
44
|
// snapshot was written from the npm payload (LF). The bridge sources are text that
|
|
44
45
|
// either line ending serves, so a CRLF checkout must not read as a drifted snapshot;
|
|
45
46
|
// bytes are still compared first, and the tolerant path only applies to text.
|
|
47
|
+
// Lone CR is normalized as well, so a file saved with classic-Mac endings reads as
|
|
48
|
+
// the same source rather than as content drift.
|
|
46
49
|
function sameTextContent(installed, expected) {
|
|
47
50
|
if (installed.equals(expected)) return true;
|
|
48
51
|
if (installed.includes(0) || expected.includes(0)) return false;
|
|
49
|
-
const normalize = (buffer) => buffer.toString('utf8').replace(/\r\n
|
|
52
|
+
const normalize = (buffer) => buffer.toString('utf8').replace(/\r\n?/g, '\n');
|
|
50
53
|
return normalize(installed) === normalize(expected);
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -61,7 +64,7 @@ export function officialFrontendAssetsReady({ home = homedir(), root } = {}) {
|
|
|
61
64
|
try {
|
|
62
65
|
const p = plan(home, root);
|
|
63
66
|
const overlay = readFileSync(p.overlayFile, 'utf8');
|
|
64
|
-
return matches(p) && (overlay === p.overlay || overlay.replace(/\r\n
|
|
67
|
+
return matches(p) && (overlay === p.overlay || overlay.replace(/\r\n?/g, '\n') === p.overlay.replace(/\r\n?/g, '\n'));
|
|
65
68
|
} catch { return false; }
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -129,9 +129,12 @@ function sha256File(file) {
|
|
|
129
129
|
// Git checks these scripts out as CRLF on a `core.autocrlf=true` machine and
|
|
130
130
|
// PowerShell accepts either, so a line-ending difference is not a difference this
|
|
131
131
|
// check may report: comparing raw bytes made every Windows dev checkout read as
|
|
132
|
-
// "needs repair" while the npm payload (LF) was installed correctly.
|
|
132
|
+
// "needs repair" while the npm payload (LF) was installed correctly. Lone CR is
|
|
133
|
+
// collapsed too — an editor that saved one of these files with classic-Mac
|
|
134
|
+
// endings is still the same script, and only a real character difference should
|
|
135
|
+
// send the operator to a repair step.
|
|
133
136
|
function normalizeEol(text) {
|
|
134
|
-
return String(text).replace(/\r\n
|
|
137
|
+
return String(text).replace(/\r\n?/g, '\n');
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
function samePath(left, right) {
|
package/src/log-prune.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { readdirSync, rmSync, statSync } from 'node:fs';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { tmpdir } from 'node:os';
|
|
12
12
|
|
|
13
|
-
export const DEFAULT_KEEP_RUNS =
|
|
13
|
+
export const DEFAULT_KEEP_RUNS = 10;
|
|
14
14
|
export const DEFAULT_MAX_AGE_DAYS = 14;
|
|
15
15
|
const RECENT_GUARD_MS = 5 * 60 * 1000;
|
|
16
16
|
|
package/src/readiness-matrix.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// (Hub handshake / catalog read) are intentionally separate from execution
|
|
6
6
|
// verification (real worker, reviewer, cancellation, timeout, etc.).
|
|
7
7
|
|
|
8
|
-
const READINESS_STATUSES = Object.freeze(['PASS', 'FAIL', 'BLOCKED', 'SKIP', 'NOT_RUN']);
|
|
8
|
+
const READINESS_STATUSES = Object.freeze(['PASS', 'FAIL', 'BLOCKED', 'SKIP', 'NOT_APPLICABLE', 'NOT_RUN']);
|
|
9
9
|
|
|
10
10
|
export const READINESS_REASON_CODES = Object.freeze({
|
|
11
11
|
LIVE_CHECK_PASSED: 'LIVE_CHECK_PASSED',
|
|
@@ -22,7 +22,7 @@ export const READINESS_REASON_CODES = Object.freeze({
|
|
|
22
22
|
NO_CI_EVIDENCE: 'NO_CI_EVIDENCE',
|
|
23
23
|
NO_EXECUTION_EVIDENCE: 'NO_EXECUTION_EVIDENCE',
|
|
24
24
|
CREDENTIAL_STATUS_NOT_PROBED: 'CREDENTIAL_STATUS_NOT_PROBED',
|
|
25
|
-
|
|
25
|
+
WORKER_PROVIDER_FOLLOWS_DSH: 'WORKER_PROVIDER_FOLLOWS_DSH',
|
|
26
26
|
EVIDENCE_REPORTED: 'EVIDENCE_REPORTED',
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -166,15 +166,18 @@ export function buildReadinessMatrix({
|
|
|
166
166
|
// The two built-in DeepSeek rows describe a route only when the operator runs
|
|
167
167
|
// it. Under follow-dsh with a KNOWN, other selected provider they can never
|
|
168
168
|
// gather evidence, and NOT_RUN reads like a check that should have run rather
|
|
169
|
-
// than a route that was never chosen.
|
|
170
|
-
//
|
|
171
|
-
//
|
|
169
|
+
// than a route that was never chosen. They are reported NOT_APPLICABLE: the rows
|
|
170
|
+
// stay in the matrix (dropping them would hide that the question was asked and
|
|
171
|
+
// answered), and neither status can move the aggregate — only a FAIL, an
|
|
172
|
+
// UNPROVEN row or an UNAVAILABLE component does. An unknown selection stays
|
|
173
|
+
// NOT_RUN: this matrix is conservative, and "we could not tell" must not read as
|
|
174
|
+
// "not applicable". Reported evidence still wins, because it is applied below.
|
|
172
175
|
const selectionKnown = typeof workerSelection?.provider === 'string' && workerSelection.provider.length > 0;
|
|
173
176
|
const deepseekRouteSelected = workerProviderMode === 'deepseek-official'
|
|
174
177
|
|| workerSelection?.provider === 'deepseek-official';
|
|
175
178
|
if (workerProviderMode !== null && selectionKnown && !deepseekRouteSelected) {
|
|
176
179
|
for (const id of ['deepseek_flash', 'deepseek_pro']) {
|
|
177
|
-
rows[id] = baseRow(id, 'real-execution', '
|
|
180
|
+
rows[id] = baseRow(id, 'real-execution', 'NOT_APPLICABLE', READINESS_REASON_CODES.WORKER_PROVIDER_FOLLOWS_DSH, 'runtime-policy');
|
|
178
181
|
}
|
|
179
182
|
}
|
|
180
183
|
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export {
|
|
|
36
36
|
// included in the identity contract.
|
|
37
37
|
const RUNTIME_ID = randomUUID();
|
|
38
38
|
|
|
39
|
-
export const RUNTIME_VERSION = '2.2.
|
|
39
|
+
export const RUNTIME_VERSION = '2.2.7';
|
|
40
40
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
41
41
|
|
|
42
42
|
export const HUB_CAPABILITIES = Object.freeze([
|
|
@@ -127,6 +127,24 @@ function Write-LaunchLog {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
# Every launch appends to one file for the life of the machine, so it grows without
|
|
131
|
+
# bound while only its tail is ever read. Rotation runs once per launch before the
|
|
132
|
+
# first line is written and keeps two generations: a post-mortem needs the current
|
|
133
|
+
# run and the one before it. Deliberately best-effort — a locked, unreadable or
|
|
134
|
+
# huge log is a reason to leave it alone, never a reason to fail the launch.
|
|
135
|
+
$launcherLogMaxBytes = 5MB
|
|
136
|
+
function Rotate-LaunchLog {
|
|
137
|
+
try {
|
|
138
|
+
if (-not (Test-Path -LiteralPath $launcherLog -PathType Leaf)) { return }
|
|
139
|
+
if ((Get-Item -LiteralPath $launcherLog -ErrorAction Stop).Length -lt $launcherLogMaxBytes) { return }
|
|
140
|
+
$oldest = "$launcherLog.2"
|
|
141
|
+
$previous = "$launcherLog.1"
|
|
142
|
+
if (Test-Path -LiteralPath $oldest) { Remove-Item -LiteralPath $oldest -Force -ErrorAction Stop }
|
|
143
|
+
if (Test-Path -LiteralPath $previous) { Move-Item -LiteralPath $previous -Destination $oldest -Force -ErrorAction Stop }
|
|
144
|
+
Move-Item -LiteralPath $launcherLog -Destination $previous -Force -ErrorAction Stop
|
|
145
|
+
} catch { }
|
|
146
|
+
}
|
|
147
|
+
|
|
130
148
|
function Resolve-OfficialHarnessCommand {
|
|
131
149
|
$shim = Get-Command dsh.cmd -CommandType Application -ErrorAction Stop | Select-Object -First 1
|
|
132
150
|
$root = Join-Path (Split-Path -Parent $shim.Source) 'node_modules\@deepseek-ai\dsh'
|
|
@@ -1528,6 +1546,7 @@ if ($env:DSH_CREW_LAUNCHER_TEST_IMPORT -eq '1') { return }
|
|
|
1528
1546
|
|
|
1529
1547
|
try {
|
|
1530
1548
|
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
|
|
1549
|
+
Rotate-LaunchLog
|
|
1531
1550
|
Write-LaunchLog ('Launcher started; mode={0}; user={1}' -f $Mode, $env:USERNAME)
|
|
1532
1551
|
if (-not (Test-Path -LiteralPath $dshCli -PathType Leaf)) {
|
|
1533
1552
|
throw "DSH CLI was not found at $dshCli. Configure DSH_CREW_DSH_CLI to a Crew-owned official CLI entry, or run: npm install -g @ran-sh/dsh-crew@latest; dsh-crew update"
|