@ran-sh/dsh-crew 1.5.0 → 1.6.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/.claude-plugin/plugin.json +1 -1
- package/lib/client.js +2 -905
- package/official-web-bridge/lib/client.js +1 -71
- package/package.json +1 -1
- package/src/client/collapsible-sections.mjs +5 -7
- package/src/client/index.tsx +3 -357
- package/src/client/quick-entry.tsx +1 -1
- package/src/client/quick-panel.tsx +3 -38
- package/src/client/surface-detection.mjs +1 -1
- package/src/hub/entry.mjs +6 -0
- package/src/hub/index.mjs +1 -76
- package/src/i18n.mjs +1 -1
- package/src/install/install-legacy.mjs +35 -43
- package/src/install/npx-lifecycle.mjs +16 -2
- package/src/policy-legacy.mjs +1 -51
- package/src/provider-inventory.mjs +1 -3
- package/src/release-in-use.mjs +110 -0
- package/src/runtime-controls.mjs +1 -7
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +1 -2
- package/src/image-output.mjs +0 -71
- package/src/multimodal.mjs +0 -0
- package/src/vision-route.mjs +0 -138
|
@@ -4,7 +4,7 @@ import { PanelHeader, PanelStyles } from './panel-chrome';
|
|
|
4
4
|
// DSH Crew QUICK CONTROLS panel for the official 3080 surface.
|
|
5
5
|
//
|
|
6
6
|
// Narrow by design: only the user-facing master switch, flash/pro model
|
|
7
|
-
// priority lists
|
|
7
|
+
// priority lists — all other operations belong
|
|
8
8
|
// to the native 3210 full control plane. Talks only to the quick endpoints
|
|
9
9
|
// (/_dsh/dsh-crew/quick-config, /quick-status). Runtime maintenance stays on 3210.
|
|
10
10
|
|
|
@@ -17,17 +17,9 @@ type QuickConfig = {
|
|
|
17
17
|
subagents_enabled?: boolean;
|
|
18
18
|
flash_model_priority?: ModelEntry[];
|
|
19
19
|
pro_model_priority?: ModelEntry[];
|
|
20
|
-
vision_enabled?: boolean;
|
|
21
|
-
imagegen_enabled?: boolean;
|
|
22
|
-
vision_provider?: string;
|
|
23
|
-
imagegen_provider?: string;
|
|
24
20
|
};
|
|
25
21
|
|
|
26
22
|
const RESTART_KEYS = new Set([
|
|
27
|
-
'vision_enabled',
|
|
28
|
-
'imagegen_enabled',
|
|
29
|
-
'vision_provider',
|
|
30
|
-
'imagegen_provider',
|
|
31
23
|
]);
|
|
32
24
|
|
|
33
25
|
const T = {
|
|
@@ -48,10 +40,7 @@ const T = {
|
|
|
48
40
|
flash: 'Worker / Flash',
|
|
49
41
|
pro: 'Reviewer / Pro',
|
|
50
42
|
addModel: '+ 添加模型',
|
|
51
|
-
|
|
52
|
-
vision: '视觉',
|
|
53
|
-
imagegen: '生图',
|
|
54
|
-
provider: 'Provider',
|
|
43
|
+
provider: 'Provider',
|
|
55
44
|
applyRestart: '应用并重启 Crew',
|
|
56
45
|
savedNeedsRestart: '配置已保存 · 需要重启 Crew 才会生效',
|
|
57
46
|
saved: '已保存',
|
|
@@ -76,9 +65,6 @@ const T = {
|
|
|
76
65
|
flash: 'Worker / Flash',
|
|
77
66
|
pro: 'Reviewer / Pro',
|
|
78
67
|
addModel: '+ Add model',
|
|
79
|
-
multimodal: 'Multimodal',
|
|
80
|
-
vision: 'Vision',
|
|
81
|
-
imagegen: 'Imagegen',
|
|
82
68
|
provider: 'Provider',
|
|
83
69
|
applyRestart: 'Apply & restart Crew',
|
|
84
70
|
savedNeedsRestart: 'Saved · restart Crew to take effect',
|
|
@@ -167,15 +153,11 @@ export function QuickPanel({ ctx }: { ctx: any }) {
|
|
|
167
153
|
} finally { setBusy(false); }
|
|
168
154
|
}, [t]);
|
|
169
155
|
|
|
170
|
-
const toggle = (key: 'subagents_enabled'
|
|
156
|
+
const toggle = (key: 'subagents_enabled', value: boolean) => {
|
|
171
157
|
setConfig((c) => ({ ...(c ?? {}), [key]: value }));
|
|
172
158
|
void patch({ [key]: value } as QuickConfig);
|
|
173
159
|
};
|
|
174
160
|
|
|
175
|
-
const setProvider = (key: 'vision_provider' | 'imagegen_provider', value: string) => {
|
|
176
|
-
setConfig((c) => ({ ...(c ?? {}), [key]: value }));
|
|
177
|
-
void patch({ [key]: value } as QuickConfig);
|
|
178
|
-
};
|
|
179
161
|
|
|
180
162
|
const moveModel = (listKey: 'flash_model_priority' | 'pro_model_priority', index: number, dir: -1 | 1) => {
|
|
181
163
|
const list = [...((config?.[listKey] as ModelEntry[] | undefined) ?? [])];
|
|
@@ -268,23 +250,6 @@ export function QuickPanel({ ctx }: { ctx: any }) {
|
|
|
268
250
|
</div>
|
|
269
251
|
{modelList('flash_model_priority', t.flash)}
|
|
270
252
|
{modelList('pro_model_priority', t.pro)}
|
|
271
|
-
<details className="crew-quick-group">
|
|
272
|
-
<summary><strong>{t.multimodal}</strong><span className="crew-quick-count">{t.vision} / {t.imagegen}</span></summary>
|
|
273
|
-
<div className="crew-quick-content">
|
|
274
|
-
<div className="crew-quick-media">
|
|
275
|
-
<label><input type="checkbox" disabled={busy} checked={config.vision_enabled === true}
|
|
276
|
-
onChange={(e) => toggle('vision_enabled', e.target.checked)} /> {t.vision}</label>
|
|
277
|
-
<input className="crew-quick-input" aria-label={`${t.vision} Provider`} placeholder={t.provider} disabled={busy} defaultValue={config.vision_provider ?? ''} key={`vision-${config.vision_provider ?? ''}`}
|
|
278
|
-
onBlur={(e) => setProvider('vision_provider', e.target.value)} />
|
|
279
|
-
</div>
|
|
280
|
-
<div className="crew-quick-media">
|
|
281
|
-
<label><input type="checkbox" disabled={busy} checked={config.imagegen_enabled === true}
|
|
282
|
-
onChange={(e) => toggle('imagegen_enabled', e.target.checked)} /> {t.imagegen}</label>
|
|
283
|
-
<input className="crew-quick-input" aria-label={`${t.imagegen} Provider`} placeholder={t.provider} disabled={busy} defaultValue={config.imagegen_provider ?? ''}
|
|
284
|
-
onBlur={(e) => setProvider('imagegen_provider', e.target.value)} />
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
</details>
|
|
288
253
|
{restartPending && (
|
|
289
254
|
<div className="crew-quick-row">
|
|
290
255
|
<a className="crew-quick-btn primary" href={FULL} target="_blank" rel="noopener noreferrer">{t.openFull}</a>
|
|
@@ -26,7 +26,7 @@ export function classifyCrewSurface({ bridgeStatus, runtime } = {}) {
|
|
|
26
26
|
/**
|
|
27
27
|
* Surface capability model. The NATIVE 3210 Crew harness is the single full
|
|
28
28
|
* control plane; the OFFICIAL 3080 surface is a narrow quick-controls panel
|
|
29
|
-
* (total switch
|
|
29
|
+
* (total switch and the flash/pro model priority lists) plus a
|
|
30
30
|
* deep link back to 3210. Unknown surfaces get diagnostics only — never write
|
|
31
31
|
* authority. This replaces the old binary full-vs-minimal assumption.
|
|
32
32
|
*/
|
package/src/hub/entry.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { apply as applyHub, inject, name, WorkerRegistry } from './index.mjs';
|
|
11
11
|
import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
|
|
12
12
|
import { getProcessAdaptiveHealthStore } from '../adaptive-routing.mjs';
|
|
13
|
+
import { claimReleaseInUse } from '../release-in-use.mjs';
|
|
13
14
|
|
|
14
15
|
const RUNTIME_PATH = '/_dsh/dsh-crew/runtime';
|
|
15
16
|
const ADAPTIVE_OBSERVER_INSTALLED = Symbol.for('@ran-sh/dsh-crew/adaptive-observer-installed');
|
|
@@ -92,6 +93,11 @@ export function installAdaptiveHealthObserver() {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
export async function apply(ctx) {
|
|
96
|
+
// Declare this release in use before anything else. The Hub loads several
|
|
97
|
+
// modules lazily with a cache-busting query, so deleting the release under a
|
|
98
|
+
// running process would break those routes with no way to recover but a
|
|
99
|
+
// restart. Retention reads these claims and leaves a live release alone.
|
|
100
|
+
claimReleaseInUse();
|
|
95
101
|
registerRuntimeEndpoint(ctx);
|
|
96
102
|
installAdaptiveHealthObserver();
|
|
97
103
|
return applyHub(ctx);
|
package/src/hub/index.mjs
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
normalizeGlobalConfig,
|
|
13
13
|
chooseDefaultTier,
|
|
14
14
|
normalizeWorkerProviderMode,
|
|
15
|
-
getMultimodalRegistrationPlan,
|
|
16
15
|
canDispatchRole,
|
|
17
16
|
resolveModelPolicy,
|
|
18
17
|
resolveRoleTierHint,
|
|
@@ -112,10 +111,6 @@ export const QUICK_CONFIG_KEYS = Object.freeze([
|
|
|
112
111
|
'subagents_enabled',
|
|
113
112
|
'flash_model_priority',
|
|
114
113
|
'pro_model_priority',
|
|
115
|
-
'vision_enabled',
|
|
116
|
-
'imagegen_enabled',
|
|
117
|
-
'vision_provider',
|
|
118
|
-
'imagegen_provider',
|
|
119
114
|
]);
|
|
120
115
|
const LEGACY_TIER_MODELS = { flash: 'deepseek-v4-flash', pro: 'deepseek-v4-pro' };
|
|
121
116
|
// Local copy (the hub must not import jobs.mjs, which pulls the DSH SDK into
|
|
@@ -1366,39 +1361,6 @@ export async function apply(ctx) {
|
|
|
1366
1361
|
while (credentialPurgePlans.size > 32) credentialPurgePlans.delete(credentialPurgePlans.keys().next().value);
|
|
1367
1362
|
};
|
|
1368
1363
|
|
|
1369
|
-
// Multimodal bridge: register describe_image / generate_image for the DS
|
|
1370
|
-
// model. Config is read per call so settings-page edits apply live; the
|
|
1371
|
-
// capability switches (vision_enabled / imagegen_enabled) decide which tools
|
|
1372
|
-
// are registered at plugin boot, so toggling them takes effect on restart.
|
|
1373
|
-
try {
|
|
1374
|
-
const { createMultimodalTools } = await import('../multimodal.mjs');
|
|
1375
|
-
const { readGlobalConfig } = await import('../install/install.mjs');
|
|
1376
|
-
const plan = getMultimodalRegistrationPlan(normalizeGlobalConfig(readGlobalConfig()));
|
|
1377
|
-
for (const tool of createMultimodalTools(() => readGlobalConfig())) {
|
|
1378
|
-
if (!plan.tools[tool.name]) {
|
|
1379
|
-
ctx.logger?.info?.(`dsh-crew: ${tool.name} not registered (disabled by capability switch)`);
|
|
1380
|
-
continue;
|
|
1381
|
-
}
|
|
1382
|
-
disposers.push(ctx.effect(() => ctx.tools.register(tool), `dsh-crew: ${tool.name} tool`));
|
|
1383
|
-
}
|
|
1384
|
-
} catch (err) {
|
|
1385
|
-
ctx.logger?.warn?.(`dsh-crew: multimodal tools unavailable: ${err?.message ?? err}`);
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
// Vision route: image paste on the text-only DS models (admission adapter +
|
|
1389
|
-
// pre-step transcription). Installed only while Crew Vision is enabled.
|
|
1390
|
-
try {
|
|
1391
|
-
const { installVisionRoute } = await import('../vision-route.mjs');
|
|
1392
|
-
const { readGlobalConfig } = await import('../install/install.mjs');
|
|
1393
|
-
if (getMultimodalRegistrationPlan(normalizeGlobalConfig(readGlobalConfig())).visionRoute) {
|
|
1394
|
-
disposers.push(installVisionRoute(ctx, () => readGlobalConfig()));
|
|
1395
|
-
} else {
|
|
1396
|
-
ctx.logger?.info?.('dsh-crew: vision route not installed (Crew Vision disabled by capability switch)');
|
|
1397
|
-
}
|
|
1398
|
-
} catch (err) {
|
|
1399
|
-
ctx.logger?.warn?.(`dsh-crew: vision route unavailable: ${err?.message ?? err}`);
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
1364
|
ctx.inject(['webServer'], (webCtx) => {
|
|
1403
1365
|
const webServer = webCtx.webServer;
|
|
1404
1366
|
const disposers = [];
|
|
@@ -2676,43 +2638,6 @@ export async function apply(ctx) {
|
|
|
2676
2638
|
},
|
|
2677
2639
|
}));
|
|
2678
2640
|
|
|
2679
|
-
disposers.push(webServer.register({
|
|
2680
|
-
kind: 'exact', path: `${ROUTE_BASE}/vision-models`,
|
|
2681
|
-
handler: async (req, res) => {
|
|
2682
|
-
if (!isLoopbackRequest(req)) return sendJson(res, 403, { ok: false, error: 'loopback only' });
|
|
2683
|
-
try {
|
|
2684
|
-
const url = new URL(req.url, 'http://localhost');
|
|
2685
|
-
const provider = url.searchParams.get('provider');
|
|
2686
|
-
const force = url.searchParams.get('refresh') === '1';
|
|
2687
|
-
const lang = url.searchParams.get('lang');
|
|
2688
|
-
adoptLang(lang);
|
|
2689
|
-
const { listVisionModels } = await import('../multimodal.mjs');
|
|
2690
|
-
const { readGlobalConfig } = await import('../install/install.mjs');
|
|
2691
|
-
return sendJson(res, 200, { ok: true, models: await listVisionModels(provider, force, () => readGlobalConfig(), lang) });
|
|
2692
|
-
} catch (err) {
|
|
2693
|
-
return sendJson(res, 500, { ok: false, error: err?.message ?? String(err) });
|
|
2694
|
-
}
|
|
2695
|
-
},
|
|
2696
|
-
}));
|
|
2697
|
-
|
|
2698
|
-
disposers.push(webServer.register({
|
|
2699
|
-
kind: 'exact', path: `${ROUTE_BASE}/provider-test`,
|
|
2700
|
-
handler: async (req, res) => {
|
|
2701
|
-
if (!isLoopbackRequest(req)) return sendJson(res, 403, { ok: false, error: 'loopback only' });
|
|
2702
|
-
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'POST only' });
|
|
2703
|
-
try {
|
|
2704
|
-
// The entry comes from the panel form so unsaved edits can be probed.
|
|
2705
|
-
const entry = await readBody(req);
|
|
2706
|
-
adoptLang(entry?.lang);
|
|
2707
|
-
const { testProvider } = await import('../multimodal.mjs');
|
|
2708
|
-
const result = await testProvider(entry, entry?.lang);
|
|
2709
|
-
return sendJson(res, 200, { ok: true, result });
|
|
2710
|
-
} catch (err) {
|
|
2711
|
-
return sendJson(res, 500, { ok: false, error: err?.message ?? String(err) });
|
|
2712
|
-
}
|
|
2713
|
-
},
|
|
2714
|
-
}));
|
|
2715
|
-
|
|
2716
2641
|
disposers.push(webServer.register({
|
|
2717
2642
|
kind: 'exact', path: `${ROUTE_BASE}/install/status`,
|
|
2718
2643
|
handler: async (req, res) => {
|
|
@@ -2761,7 +2686,7 @@ export async function apply(ctx) {
|
|
|
2761
2686
|
return () => { for (const d of disposers.reverse()) d(); };
|
|
2762
2687
|
});
|
|
2763
2688
|
|
|
2764
|
-
ctx.logger?.info?.('dsh-crew hub mounted (jobs API + installer endpoints
|
|
2689
|
+
ctx.logger?.info?.('dsh-crew hub mounted (jobs API + installer endpoints)');
|
|
2765
2690
|
return async () => {
|
|
2766
2691
|
for (const d of disposers.reverse()) { try { d(); } catch {} }
|
|
2767
2692
|
await hub.dispose();
|
package/src/i18n.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Server-side locale for strings that reach the user: connectivity-test
|
|
2
|
-
// reports, model-list labels
|
|
2
|
+
// reports, model-list labels and error messages.
|
|
3
3
|
//
|
|
4
4
|
// The panel owns the authoritative locale (DSH's setting may be absent, in
|
|
5
5
|
// which case the browser decides), so it passes `lang` with every request and
|
|
@@ -15,6 +15,20 @@ const MARKETPLACE_NAME = 'dsh-crew';
|
|
|
15
15
|
const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
|
|
16
16
|
const POLICY_START = '<!-- DSH CREW MANAGED POLICY:START -->';
|
|
17
17
|
const POLICY_END = '<!-- DSH CREW MANAGED POLICY:END -->';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The directory Codex actually reads.
|
|
21
|
+
*
|
|
22
|
+
* Codex honours `CODEX_HOME` and falls back to `~/.codex`; writing only to the
|
|
23
|
+
* latter silently does nothing for anyone who set it — the registration stays
|
|
24
|
+
* frozen on whatever release was current when they set it, while every install
|
|
25
|
+
* reports success because the file it wrote really did change. Resolve the same
|
|
26
|
+
* way Codex does, and treat an empty or whitespace value as unset.
|
|
27
|
+
*/
|
|
28
|
+
export function codexHomeDir(home = homedir(), env = process.env) {
|
|
29
|
+
const override = typeof env?.CODEX_HOME === 'string' ? env.CODEX_HOME.trim() : '';
|
|
30
|
+
return override ? resolve(override) : join(home, '.codex');
|
|
31
|
+
}
|
|
18
32
|
export const CODEX_LEGACY_POLICY_HASHES = Object.freeze([
|
|
19
33
|
'2d6f3839bb3df4bda90f481726281292b1a4b4585298b1cf9ec56215295b5c78',
|
|
20
34
|
]);
|
|
@@ -80,8 +94,8 @@ export function stripKnownLegacyCodexPolicy(text, { knownHashes = CODEX_LEGACY_P
|
|
|
80
94
|
return `${keptPrefix}${match[0]}${keptSuffix}`;
|
|
81
95
|
}
|
|
82
96
|
|
|
83
|
-
function installGlobalCodexPolicy({ home, root }) {
|
|
84
|
-
const file = join(home,
|
|
97
|
+
function installGlobalCodexPolicy({ home, root, env = process.env }) {
|
|
98
|
+
const file = join(codexHomeDir(home, env), 'AGENTS.md');
|
|
85
99
|
const block = managedPolicyBlock(root);
|
|
86
100
|
if (!block) return { ok: false, action: 'global policy template missing' };
|
|
87
101
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -100,14 +114,14 @@ function installGlobalCodexPolicy({ home, root }) {
|
|
|
100
114
|
return { ok: true, action: `global policy: ${file}` };
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
function globalCodexPolicyReady({ home, root = ROOT }) {
|
|
117
|
+
function globalCodexPolicyReady({ home, root = ROOT, env = process.env }) {
|
|
104
118
|
const block = managedPolicyBlock(root);
|
|
105
|
-
const text = readText(join(home,
|
|
119
|
+
const text = readText(join(codexHomeDir(home, env), 'AGENTS.md'));
|
|
106
120
|
return !!block && typeof text === 'string' && text.includes(block);
|
|
107
121
|
}
|
|
108
122
|
|
|
109
|
-
function uninstallGlobalCodexPolicy({ home }) {
|
|
110
|
-
const file = join(home,
|
|
123
|
+
function uninstallGlobalCodexPolicy({ home, env = process.env }) {
|
|
124
|
+
const file = join(codexHomeDir(home, env), 'AGENTS.md');
|
|
111
125
|
const current = readText(file);
|
|
112
126
|
if (typeof current !== 'string') return null;
|
|
113
127
|
const managed = new RegExp(`(?:\\r?\\n){0,2}${POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\r?\\n)?`, 'm');
|
|
@@ -282,26 +296,6 @@ export const GLOBAL_CONFIG_DEFAULTS = {
|
|
|
282
296
|
pro_model_priority: [],
|
|
283
297
|
pro_model_priority_configured: false,
|
|
284
298
|
pro_model_fallback: 'harness-default',
|
|
285
|
-
// ---- configurable-crew multimodal switches ----
|
|
286
|
-
// False = the Crew describe_image tool and vision route are not registered
|
|
287
|
-
// at DSH boot (takes effect after a DSH restart). Provider/model values are
|
|
288
|
-
// kept untouched so re-enabling restores the previous setup.
|
|
289
|
-
vision_enabled: false,
|
|
290
|
-
imagegen_enabled: false,
|
|
291
|
-
// Multimodal bridge: which subscription CLI lends the text-only DS model
|
|
292
|
-
// eyes (describe_image) and a brush (generate_image).
|
|
293
|
-
vision_provider: 'off', // claude-code | codex | grok | agy | <custom id> | off
|
|
294
|
-
vision_model: 'haiku', // model passed to the vision CLI; free-form
|
|
295
|
-
imagegen_provider: 'off', // codex | agy | grok | <custom id> | off
|
|
296
|
-
// User-defined providers, each either an OpenAI-compatible HTTP endpoint or a
|
|
297
|
-
// local command. Entry shape:
|
|
298
|
-
// { id, name, type: 'api' | 'cli', models: [],
|
|
299
|
-
// api: base_url, api_key, imagegen_model
|
|
300
|
-
// cli: vision_command, imagegen_command }
|
|
301
|
-
// CLI commands run via bash with shell-quoted placeholders: vision
|
|
302
|
-
// {image} {question} {model} (stdout is the answer), imagegen
|
|
303
|
-
// {prompt} {output} {size} (must write the file to {output}).
|
|
304
|
-
custom_providers: [],
|
|
305
299
|
// User-added model ids per provider, merged into the panel's model list.
|
|
306
300
|
extra_models: {},
|
|
307
301
|
// Hub-mode agent preset per tier: 'default' follows the DSH roster default.
|
|
@@ -356,8 +350,6 @@ export function mergeStoredGlobalConfig(stored) {
|
|
|
356
350
|
// workspace-targeted jobs on Windows; an explicit user-set `minimal` value
|
|
357
351
|
// is still preserved by the normal object merge above.
|
|
358
352
|
if (!has('preset_flash')) merged.preset_flash = 'default';
|
|
359
|
-
if (!has('vision_enabled')) merged.vision_enabled = stored.vision_provider !== 'off';
|
|
360
|
-
if (!has('imagegen_enabled')) merged.imagegen_enabled = stored.imagegen_provider !== 'off';
|
|
361
353
|
return merged;
|
|
362
354
|
}
|
|
363
355
|
|
|
@@ -393,7 +385,7 @@ export function writeGlobalConfig(patch) {
|
|
|
393
385
|
}
|
|
394
386
|
|
|
395
387
|
/** What is currently installed where — drives the settings-page buttons. */
|
|
396
|
-
export function installStatus({ home = homedir(), root = ROOT } = {}) {
|
|
388
|
+
export function installStatus({ home = homedir(), root = ROOT, env = process.env } = {}) {
|
|
397
389
|
const settings = readJson(join(home, '.claude', 'settings.json'), {});
|
|
398
390
|
const enabled = settings.enabledPlugins;
|
|
399
391
|
const claudeInstalled = !!(enabled && !Array.isArray(enabled) && enabled[PLUGIN_KEY]);
|
|
@@ -409,7 +401,7 @@ export function installStatus({ home = homedir(), root = ROOT } = {}) {
|
|
|
409
401
|
permissions: claudePermissionsReady(settings),
|
|
410
402
|
};
|
|
411
403
|
const claudeMissing = Object.entries(claudeComponents).filter(([, present]) => !present).map(([key]) => key);
|
|
412
|
-
const codexRoot =
|
|
404
|
+
const codexRoot = codexHomeDir(home, env);
|
|
413
405
|
const configFile = join(codexRoot, 'config.toml');
|
|
414
406
|
const configText = readText(configFile) ?? '';
|
|
415
407
|
const workerFile = join(codexRoot, 'agents', 'ds-worker.toml');
|
|
@@ -429,7 +421,7 @@ export function installStatus({ home = homedir(), root = ROOT } = {}) {
|
|
|
429
421
|
status_prompt: expectedStatusPrompt !== null && readText(join(codexRoot, 'prompts', 'dsh-status.md')) === expectedStatusPrompt,
|
|
430
422
|
mcp: !!expectedTarget && mcpTarget === expectedTarget,
|
|
431
423
|
target_alignment: !!expectedTarget && workerTarget === expectedTarget && reviewerTarget === expectedTarget && mcpTarget === expectedTarget,
|
|
432
|
-
global_policy: globalCodexPolicyReady({ home, root }),
|
|
424
|
+
global_policy: globalCodexPolicyReady({ home, root, env }),
|
|
433
425
|
};
|
|
434
426
|
const codexInstalled = Object.values(components).some(Boolean)
|
|
435
427
|
|| existsSync(join(codexRoot, 'agents', 'ds-flash.toml'))
|
|
@@ -448,24 +440,24 @@ export function installStatus({ home = homedir(), root = ROOT } = {}) {
|
|
|
448
440
|
};
|
|
449
441
|
}
|
|
450
442
|
|
|
451
|
-
export function uninstallCodex({ home = homedir() } = {}) {
|
|
443
|
+
export function uninstallCodex({ home = homedir(), env = process.env } = {}) {
|
|
452
444
|
const actions = [];
|
|
453
445
|
// Both the v0.2 roles (ds-worker / ds-reviewer) and the deprecated v0.1
|
|
454
446
|
// aliases (ds-flash / ds-pro) are dsh-crew managed; uninstall removes only
|
|
455
447
|
// these, never a user's own role files.
|
|
456
448
|
for (const f of ['ds-flash.toml', 'ds-pro.toml', 'ds-worker.toml', 'ds-reviewer.toml']) {
|
|
457
|
-
const p = join(home,
|
|
449
|
+
const p = join(codexHomeDir(home, env), 'agents', f);
|
|
458
450
|
if (existsSync(p)) { backup(p); rmSync(p); actions.push(`removed: ${p} (backup kept)`); }
|
|
459
451
|
}
|
|
460
452
|
for (const f of ['dsh-config.md', 'dsh-status.md']) {
|
|
461
|
-
const p = join(home,
|
|
453
|
+
const p = join(codexHomeDir(home, env), 'prompts', f);
|
|
462
454
|
if (existsSync(p)) { rmSync(p); actions.push(`removed: ${p}`); }
|
|
463
455
|
}
|
|
464
|
-
const policyAction = uninstallGlobalCodexPolicy({ home });
|
|
456
|
+
const policyAction = uninstallGlobalCodexPolicy({ home, env });
|
|
465
457
|
if (policyAction) actions.push(policyAction);
|
|
466
458
|
// Remove only the dsh-crew entry from [mcp_servers], keeping any other
|
|
467
459
|
// MCP servers the user configured.
|
|
468
|
-
const configFile = join(home,
|
|
460
|
+
const configFile = join(codexHomeDir(home, env), 'config.toml');
|
|
469
461
|
if (existsSync(configFile)) {
|
|
470
462
|
const before = readFileSync(configFile, 'utf8');
|
|
471
463
|
const lineRe = /(^|\n)[ \t]*dsh-crew\s*=\s*\{[^\n]*\}/;
|
|
@@ -590,9 +582,9 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
590
582
|
return { ok: true, actions };
|
|
591
583
|
}
|
|
592
584
|
|
|
593
|
-
export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
|
|
585
|
+
export function installCodex({ home = homedir(), scope, root = ROOT, env = process.env } = {}) {
|
|
594
586
|
const actions = [];
|
|
595
|
-
const agentsDir = scope === 'project' ? join(process.cwd(), '.codex', 'agents') : join(home,
|
|
587
|
+
const agentsDir = scope === 'project' ? join(process.cwd(), '.codex', 'agents') : join(codexHomeDir(home, env), 'agents');
|
|
596
588
|
mkdirSync(agentsDir, { recursive: true });
|
|
597
589
|
const srcDir = join(root, 'codex', 'agents');
|
|
598
590
|
// Windows drive paths must render with forward slashes: backslashes in a
|
|
@@ -609,7 +601,7 @@ export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
|
|
|
609
601
|
writeFileSync(dest, rendered);
|
|
610
602
|
actions.push(`role: ${dest}`);
|
|
611
603
|
}
|
|
612
|
-
const promptsDir = scope === 'project' ? join(process.cwd(), '.codex', 'prompts') : join(home,
|
|
604
|
+
const promptsDir = scope === 'project' ? join(process.cwd(), '.codex', 'prompts') : join(codexHomeDir(home, env), 'prompts');
|
|
613
605
|
mkdirSync(promptsDir, { recursive: true });
|
|
614
606
|
const promptsSrc = join(root, 'codex', 'prompts');
|
|
615
607
|
for (const f of readdirSync(promptsSrc).filter((f) => f.endsWith('.md'))) {
|
|
@@ -617,9 +609,9 @@ export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
|
|
|
617
609
|
actions.push(`prompt: ${join(promptsDir, f)}`);
|
|
618
610
|
}
|
|
619
611
|
if (scope !== 'project') {
|
|
620
|
-
const act = writeGlobalCodexMcpServer(home, renderedPath);
|
|
612
|
+
const act = writeGlobalCodexMcpServer(home, renderedPath, env);
|
|
621
613
|
actions.push(...act);
|
|
622
|
-
const policy = installGlobalCodexPolicy({ home, root });
|
|
614
|
+
const policy = installGlobalCodexPolicy({ home, root, env });
|
|
623
615
|
if (!policy.ok) return { ok: false, actions: [...actions, policy.action] };
|
|
624
616
|
actions.push(policy.action);
|
|
625
617
|
}
|
|
@@ -634,8 +626,8 @@ export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
|
|
|
634
626
|
* the user already configured. Idempotent (updates in place). Never requires
|
|
635
627
|
* the codex CLI.
|
|
636
628
|
*/
|
|
637
|
-
export function writeGlobalCodexMcpServer(home, renderedPath) {
|
|
638
|
-
const configFile = join(home,
|
|
629
|
+
export function writeGlobalCodexMcpServer(home, renderedPath, env = process.env) {
|
|
630
|
+
const configFile = join(codexHomeDir(home, env), 'config.toml');
|
|
639
631
|
const entry = `dsh-crew = { command = "node", args = ["${renderedPath}"] }`;
|
|
640
632
|
const backupFile = backup(configFile);
|
|
641
633
|
const existing = existsSync(configFile) ? readFileSync(configFile, 'utf8') : '';
|
|
@@ -42,6 +42,7 @@ import { homedir } from 'node:os';
|
|
|
42
42
|
import * as realInstaller from './install.mjs';
|
|
43
43
|
import { samePayloadContent, capturePayloadContent } from './payload-content.mjs';
|
|
44
44
|
import { crewDshHome, crewProfileDir } from './install.mjs';
|
|
45
|
+
import { liveReleaseClaims } from '../release-in-use.mjs';
|
|
45
46
|
import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
|
|
46
47
|
import {
|
|
47
48
|
ensureOfficialWebIntegration,
|
|
@@ -1085,9 +1086,15 @@ function gcOldReleases({ home, keep = KEEP_RELEASES, protect = null }) {
|
|
|
1085
1086
|
const releasesDir = crewReleasesDir({ home });
|
|
1086
1087
|
if (!existsSync(releasesDir)) return;
|
|
1087
1088
|
const removed = [];
|
|
1089
|
+
// A running Hub keeps executing the release it started from and re-reads
|
|
1090
|
+
// several modules from disk on every request, so removing that release breaks
|
|
1091
|
+
// those routes with no recovery but a restart. Protect what live processes
|
|
1092
|
+
// claim, not just what the pointer names.
|
|
1093
|
+
const claimed = liveReleaseClaims({ home }).map((dir) => resolve(dir));
|
|
1094
|
+
const live = new Set([...(protect ? [resolve(protect)] : []), ...claimed]);
|
|
1088
1095
|
const dirs = readdirSync(releasesDir)
|
|
1089
1096
|
.map((name) => join(releasesDir, name))
|
|
1090
|
-
.filter((dir) => (!pointer || dir !== pointer.path) && (
|
|
1097
|
+
.filter((dir) => (!pointer || dir !== pointer.path) && !live.has(resolve(dir)));
|
|
1091
1098
|
const incomplete = [];
|
|
1092
1099
|
const complete = [];
|
|
1093
1100
|
for (const dir of dirs) {
|
|
@@ -3434,7 +3441,14 @@ export async function runNpxCli({
|
|
|
3434
3441
|
else if (command === 'releases') result = await actions.releases({ args, log });
|
|
3435
3442
|
else if (command === 'rollback') result = await actions.rollback({ version: args?.[0], args, log });
|
|
3436
3443
|
else result = await actions[command]({ log });
|
|
3437
|
-
|
|
3444
|
+
// Surface why the command failed. Actions return a structured failure rather
|
|
3445
|
+
// than throwing, so without this the CLI exits 1 having printed nothing —
|
|
3446
|
+
// `dsh-crew rollback` with no version was a silent failure.
|
|
3447
|
+
if (result?.ok === false) {
|
|
3448
|
+
error(`dsh-crew ${command} failed: ${result.error ?? result.code ?? 'unknown error'}`);
|
|
3449
|
+
return 1;
|
|
3450
|
+
}
|
|
3451
|
+
return 0;
|
|
3438
3452
|
} catch (err) {
|
|
3439
3453
|
error(`dsh-crew ${command} failed: ${err?.message ?? err}`);
|
|
3440
3454
|
return 1;
|
package/src/policy-legacy.mjs
CHANGED
|
@@ -506,15 +506,6 @@ export function normalizeGlobalConfig(raw = {}) {
|
|
|
506
506
|
else { flashState = flashState ?? 'auto'; proState = proState ?? 'auto'; }
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
-
// Capability switches: an old config with provider=off must not flip the
|
|
510
|
-
// capability back on; anything else keeps the capability enabled.
|
|
511
|
-
const visionEnabled = has('vision_enabled')
|
|
512
|
-
? normalizeEnabled(raw.vision_enabled)
|
|
513
|
-
: raw.vision_provider !== 'off';
|
|
514
|
-
const imagegenEnabled = has('imagegen_enabled')
|
|
515
|
-
? normalizeEnabled(raw.imagegen_enabled)
|
|
516
|
-
: raw.imagegen_provider !== 'off';
|
|
517
|
-
|
|
518
509
|
const normalized = {
|
|
519
510
|
...raw,
|
|
520
511
|
subagents_enabled: normalizeEnabled(raw.subagents_enabled),
|
|
@@ -534,8 +525,6 @@ export function normalizeGlobalConfig(raw = {}) {
|
|
|
534
525
|
pro_model_priority: normalizeModelPriority(raw.pro_model_priority),
|
|
535
526
|
pro_model_priority_configured: raw.pro_model_priority_configured === true || normalizeModelPriority(raw.pro_model_priority).length > 0,
|
|
536
527
|
pro_model_fallback: raw.pro_model_fallback === 'harness-default' ? raw.pro_model_fallback : 'harness-default',
|
|
537
|
-
vision_enabled: visionEnabled,
|
|
538
|
-
imagegen_enabled: imagegenEnabled,
|
|
539
528
|
};
|
|
540
529
|
// This normalized legacy view is the single input to the v0.2 canonical
|
|
541
530
|
// migration, so every consumer sees the same worker/reviewer model policy
|
|
@@ -788,43 +777,4 @@ export function validateRoles(raw) {
|
|
|
788
777
|
if (!seen.has(r)) { seen.add(r); out.push(r); }
|
|
789
778
|
}
|
|
790
779
|
return { roles: out, dropped };
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// ---------- multimodal capability ----------
|
|
794
|
-
|
|
795
|
-
/** Capability availability after both the switch and the provider are checked. */
|
|
796
|
-
export function getCapabilities(config) {
|
|
797
|
-
const c = config;
|
|
798
|
-
const vision = {
|
|
799
|
-
enabled: normalizeBool(c.vision_enabled, true),
|
|
800
|
-
provider: c.vision_provider ?? 'off',
|
|
801
|
-
providerOff: c.vision_provider === 'off',
|
|
802
|
-
usable: normalizeBool(c.vision_enabled, true) && c.vision_provider !== 'off',
|
|
803
|
-
};
|
|
804
|
-
const imagegen = {
|
|
805
|
-
enabled: normalizeBool(c.imagegen_enabled, true),
|
|
806
|
-
provider: c.imagegen_provider ?? 'off',
|
|
807
|
-
providerOff: c.imagegen_provider === 'off',
|
|
808
|
-
usable: normalizeBool(c.imagegen_enabled, true) && c.imagegen_provider !== 'off',
|
|
809
|
-
};
|
|
810
|
-
return { vision, imagegen };
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* Registration plan for the hub's multimodal bridge: which tools to register
|
|
815
|
-
* and whether the vision route (deepseek-vision adapter + transcription
|
|
816
|
-
* waterfall) should be installed. Tool names are the decision points; the
|
|
817
|
-
* hub applies the plan at plugin boot, so capability switches take effect
|
|
818
|
-
* after a DSH restart.
|
|
819
|
-
*/
|
|
820
|
-
export function getMultimodalRegistrationPlan(config) {
|
|
821
|
-
const { vision, imagegen } = getCapabilities(config);
|
|
822
|
-
return {
|
|
823
|
-
tools: {
|
|
824
|
-
describe_image: vision.usable,
|
|
825
|
-
generate_image: imagegen.usable,
|
|
826
|
-
},
|
|
827
|
-
visionRoute: vision.usable,
|
|
828
|
-
requiresRestart: true,
|
|
829
|
-
};
|
|
830
|
-
}
|
|
780
|
+
}
|
|
@@ -92,7 +92,7 @@ function modelIds(provider) {
|
|
|
92
92
|
* Build a secret-free provider inventory from live Harness and Crew-owned
|
|
93
93
|
* observations. No credential value is accepted or copied into the result.
|
|
94
94
|
*/
|
|
95
|
-
export function buildProviderInventory({ catalog = {}, declarations = [], policy = {}, tombstones = {}, activeJobs = [],
|
|
95
|
+
export function buildProviderInventory({ catalog = {}, declarations = [], policy = {}, tombstones = {}, activeJobs = [], additionalProviderIds = [] } = {}) {
|
|
96
96
|
const catalogProviders = Array.isArray(catalog?.providers) ? catalog.providers : [];
|
|
97
97
|
const safeDeclarations = Array.isArray(declarations) ? declarations : [];
|
|
98
98
|
const ids = [];
|
|
@@ -121,7 +121,6 @@ export function buildProviderInventory({ catalog = {}, declarations = [], policy
|
|
|
121
121
|
? activeJobs.filter((job) => normalizeProviderId(job?.provider) === id
|
|
122
122
|
&& !['done', 'failed', 'cancelled'].includes(job?.status)).length
|
|
123
123
|
: Number.isInteger(activeJobs?.[id]) ? Math.max(0, activeJobs[id]) : 0;
|
|
124
|
-
const multimodal = Array.isArray(multimodalRefs?.[id]) ? multimodalRefs[id].length : Number.isInteger(multimodalRefs?.[id]) ? Math.max(0, multimodalRefs[id]) : 0;
|
|
125
124
|
const declarationFile = text(declaration?.file);
|
|
126
125
|
const locator = text(declaration?.locator);
|
|
127
126
|
|
|
@@ -173,7 +172,6 @@ export function buildProviderInventory({ catalog = {}, declarations = [], policy
|
|
|
173
172
|
reviewer_priority: findPriorityIndex(policy?.reviewer ?? policy?.review, 'priority', id),
|
|
174
173
|
reviewer_escalation: findPriorityIndex(policy?.reviewer ?? policy?.review, 'escalation_priority', id),
|
|
175
174
|
active_jobs: active,
|
|
176
|
-
multimodal_refs: multimodal,
|
|
177
175
|
},
|
|
178
176
|
desired_state: tombstoned ? 'absent' : 'present',
|
|
179
177
|
activation: declared ? 'restart-required' : 'live',
|