@ran-sh/dsh-crew 0.5.1 → 0.5.2
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/README.md +93 -112
- package/README.zh.md +93 -111
- package/codex/AGENTS.md +92 -0
- package/docs/installation.md +63 -0
- package/docs/ui-surfaces.md +1 -2
- package/lib/client.js +41 -4
- package/official-web-bridge/lib/client.js +41 -4
- package/package.json +6 -1
- package/scripts/setup.mjs +65 -19
- package/src/client/host-readiness.mjs +2 -1
- package/src/client/index.tsx +29 -15
- package/src/hub/index.mjs +8 -6
- package/src/install/install-legacy.mjs +65 -11
- package/src/install/install.mjs +3 -1
- package/src/install/npx-lifecycle.mjs +55 -19
- package/src/install/windows-startup.mjs +98 -0
- package/src/install/zcode.mjs +316 -0
- package/src/runtime-identity.mjs +1 -1
- package/windows/start-dsh-crew.cmd +55 -0
- package/windows/start-dsh-crew.vbs +9 -0
- package/zcode/AGENTS.md +21 -0
- package/zcode/agents/ds-reviewer.md +19 -0
- package/zcode/agents/ds-worker.md +20 -0
- package/zcode/commands/dsh-config.md +6 -0
- package/zcode/commands/dsh-status.md +4 -0
|
@@ -6,11 +6,14 @@ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, readd
|
|
|
6
6
|
import { dirname, join, resolve, relative } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
|
-
import { normalizeModelPriority } from '../model-routing.mjs';
|
|
9
|
+
import { normalizeModelPriority } from '../model-routing.mjs';
|
|
10
|
+
import { zcodeStatus } from './zcode.mjs';
|
|
10
11
|
|
|
11
12
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
12
13
|
const MARKETPLACE_NAME = 'dsh-crew';
|
|
13
|
-
const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
|
|
14
|
+
const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
|
|
15
|
+
const POLICY_START = '<!-- DSH CREW MANAGED POLICY:START -->';
|
|
16
|
+
const POLICY_END = '<!-- DSH CREW MANAGED POLICY:END -->';
|
|
14
17
|
// dsh_worker_config is included so the session commands (/dsh-crew:config,
|
|
15
18
|
// /dsh-config) and any orchestrator policy lookup run without an extra
|
|
16
19
|
// authorization prompt.
|
|
@@ -33,6 +36,50 @@ function readText(file) {
|
|
|
33
36
|
try { return readFileSync(file, 'utf8'); } catch { return null; }
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
function managedPolicyBlock(root) {
|
|
40
|
+
const policy = readText(join(root, 'codex', 'AGENTS.md'))?.trim();
|
|
41
|
+
if (!policy) return null;
|
|
42
|
+
return `${POLICY_START}\n${policy}\n${POLICY_END}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function installGlobalCodexPolicy({ home, root }) {
|
|
46
|
+
const file = join(home, '.codex', 'AGENTS.md');
|
|
47
|
+
const block = managedPolicyBlock(root);
|
|
48
|
+
if (!block) return { ok: false, action: 'global policy template missing' };
|
|
49
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
50
|
+
const current = readText(file) ?? '';
|
|
51
|
+
const managed = new RegExp(`${POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'm');
|
|
52
|
+
const template = readText(join(root, 'codex', 'AGENTS.md'))?.trim() ?? '';
|
|
53
|
+
let next;
|
|
54
|
+
if (managed.test(current)) next = current.replace(managed, block);
|
|
55
|
+
else if (current.trim() === template) next = `${block}\n`;
|
|
56
|
+
else next = `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}\n`;
|
|
57
|
+
if (next !== current) {
|
|
58
|
+
backup(file);
|
|
59
|
+
writeFileSync(file, next);
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, action: `global policy: ${file}` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function globalCodexPolicyReady({ home, root = ROOT }) {
|
|
65
|
+
const block = managedPolicyBlock(root);
|
|
66
|
+
const text = readText(join(home, '.codex', 'AGENTS.md'));
|
|
67
|
+
return !!block && typeof text === 'string' && text.includes(block);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function uninstallGlobalCodexPolicy({ home }) {
|
|
71
|
+
const file = join(home, '.codex', 'AGENTS.md');
|
|
72
|
+
const current = readText(file);
|
|
73
|
+
if (typeof current !== 'string') return null;
|
|
74
|
+
const managed = new RegExp(`(?:\\r?\\n){0,2}${POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\r?\\n)?`, 'm');
|
|
75
|
+
if (!managed.test(current)) return null;
|
|
76
|
+
const next = current.replace(managed, '').trimEnd();
|
|
77
|
+
backup(file);
|
|
78
|
+
if (next.trim()) writeFileSync(file, `${next}\n`);
|
|
79
|
+
else rmSync(file);
|
|
80
|
+
return `codex global policy: removed managed block`;
|
|
81
|
+
}
|
|
82
|
+
|
|
36
83
|
function tomlSection(text, name) {
|
|
37
84
|
if (typeof text !== 'string') return null;
|
|
38
85
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -256,7 +303,7 @@ export function writeGlobalConfig(patch) {
|
|
|
256
303
|
}
|
|
257
304
|
|
|
258
305
|
/** What is currently installed where — drives the settings-page buttons. */
|
|
259
|
-
export function installStatus({ home = homedir() } = {}) {
|
|
306
|
+
export function installStatus({ home = homedir(), root } = {}) {
|
|
260
307
|
const settings = readJson(join(home, '.claude', 'settings.json'), {});
|
|
261
308
|
const enabled = settings.enabledPlugins;
|
|
262
309
|
const claudeInstalled = !!(enabled && !Array.isArray(enabled) && enabled[PLUGIN_KEY]);
|
|
@@ -282,6 +329,7 @@ export function installStatus({ home = homedir() } = {}) {
|
|
|
282
329
|
status_prompt: !!readText(join(codexRoot, 'prompts', 'dsh-status.md'))?.trim(),
|
|
283
330
|
mcp: !!mcpTarget,
|
|
284
331
|
target_alignment: !!workerTarget && workerTarget === reviewerTarget && workerTarget === mcpTarget,
|
|
332
|
+
global_policy: globalCodexPolicyReady({ home }),
|
|
285
333
|
};
|
|
286
334
|
const codexInstalled = Object.values(components).some(Boolean)
|
|
287
335
|
|| existsSync(join(codexRoot, 'agents', 'ds-flash.toml'))
|
|
@@ -296,10 +344,11 @@ export function installStatus({ home = homedir() } = {}) {
|
|
|
296
344
|
missing: claudeMissing,
|
|
297
345
|
},
|
|
298
346
|
codex: { installed: codexInstalled, ready: missing.length === 0, components, missing },
|
|
347
|
+
zcode: zcodeStatus({ home, ...(root ? { root } : {}) }),
|
|
299
348
|
};
|
|
300
349
|
}
|
|
301
350
|
|
|
302
|
-
export function uninstallCodex({ home = homedir() } = {}) {
|
|
351
|
+
export function uninstallCodex({ home = homedir() } = {}) {
|
|
303
352
|
const actions = [];
|
|
304
353
|
// Both the v0.2 roles (ds-worker / ds-reviewer) and the deprecated v0.1
|
|
305
354
|
// aliases (ds-flash / ds-pro) are dsh-crew managed; uninstall removes only
|
|
@@ -308,10 +357,12 @@ export function uninstallCodex({ home = homedir() } = {}) {
|
|
|
308
357
|
const p = join(home, '.codex', 'agents', f);
|
|
309
358
|
if (existsSync(p)) { backup(p); rmSync(p); actions.push(`removed: ${p} (backup kept)`); }
|
|
310
359
|
}
|
|
311
|
-
for (const f of ['dsh-config.md', 'dsh-status.md']) {
|
|
360
|
+
for (const f of ['dsh-config.md', 'dsh-status.md']) {
|
|
312
361
|
const p = join(home, '.codex', 'prompts', f);
|
|
313
362
|
if (existsSync(p)) { rmSync(p); actions.push(`removed: ${p}`); }
|
|
314
|
-
}
|
|
363
|
+
}
|
|
364
|
+
const policyAction = uninstallGlobalCodexPolicy({ home });
|
|
365
|
+
if (policyAction) actions.push(policyAction);
|
|
315
366
|
// Remove only the dsh-crew entry from [mcp_servers], keeping any other
|
|
316
367
|
// MCP servers the user configured.
|
|
317
368
|
const configFile = join(home, '.codex', 'config.toml');
|
|
@@ -428,7 +479,7 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
428
479
|
return { ok: true, actions };
|
|
429
480
|
}
|
|
430
481
|
|
|
431
|
-
export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
|
|
482
|
+
export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
|
|
432
483
|
const actions = [];
|
|
433
484
|
const agentsDir = scope === 'project' ? join(process.cwd(), '.codex', 'agents') : join(home, '.codex', 'agents');
|
|
434
485
|
mkdirSync(agentsDir, { recursive: true });
|
|
@@ -455,10 +506,13 @@ export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
|
|
|
455
506
|
writeFileSync(join(promptsDir, f), readFileSync(join(promptsSrc, f), 'utf8'));
|
|
456
507
|
actions.push(`prompt: ${join(promptsDir, f)}`);
|
|
457
508
|
}
|
|
458
|
-
if (scope !== 'project') {
|
|
459
|
-
const act = writeGlobalCodexMcpServer(home, renderedPath);
|
|
460
|
-
actions.push(...act);
|
|
461
|
-
|
|
509
|
+
if (scope !== 'project') {
|
|
510
|
+
const act = writeGlobalCodexMcpServer(home, renderedPath);
|
|
511
|
+
actions.push(...act);
|
|
512
|
+
const policy = installGlobalCodexPolicy({ home, root });
|
|
513
|
+
if (!policy.ok) return { ok: false, actions: [...actions, policy.action] };
|
|
514
|
+
actions.push(policy.action);
|
|
515
|
+
}
|
|
462
516
|
return { ok: true, actions };
|
|
463
517
|
}
|
|
464
518
|
|
package/src/install/install.mjs
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// This module keeps all installer exports while replacing only global config
|
|
5
5
|
// read/write semantics with schema-v3 canonical authority.
|
|
6
6
|
|
|
7
|
-
export * from './install-legacy.mjs';
|
|
7
|
+
export * from './install-legacy.mjs';
|
|
8
|
+
export * from './windows-startup.mjs';
|
|
9
|
+
export * from './zcode.mjs';
|
|
8
10
|
|
|
9
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
12
|
import { dirname, join } from 'node:path';
|
|
@@ -549,14 +549,30 @@ async function activateRelease({ home, releaseDir, manifest, log, installer }) {
|
|
|
549
549
|
}
|
|
550
550
|
log(`✓ Harness plugin registered (dedicated dsh-crew profile → ${releaseDir})`);
|
|
551
551
|
|
|
552
|
-
const codex = installer.installCodex({ home, root: releaseDir });
|
|
552
|
+
const codex = installer.installCodex({ home, root: releaseDir });
|
|
553
553
|
if (codex.ok === false) {
|
|
554
554
|
log(`✗ Codex Desktop integration failed: ${(codex.actions ?? []).join('; ')}`);
|
|
555
555
|
return false;
|
|
556
556
|
}
|
|
557
|
-
log('✓ Codex Desktop integration');
|
|
558
|
-
|
|
559
|
-
|
|
557
|
+
log('✓ Codex Desktop integration');
|
|
558
|
+
|
|
559
|
+
if (installer.installZCode) {
|
|
560
|
+
const zcode = installer.installZCode({ home, root: releaseDir });
|
|
561
|
+
if (zcode.ok === false) {
|
|
562
|
+
log(`✗ ZCode integration failed (${zcode.code ?? 'unknown'})`);
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
log('✓ ZCode integration');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const startup = installer.installWindowsStartup?.({ home, root: releaseDir });
|
|
569
|
+
if (startup?.ok === false) {
|
|
570
|
+
log(`✗ Windows login startup failed (${startup.code ?? 'unknown'})`);
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
if (startup?.supported) log('✓ Windows login startup');
|
|
574
|
+
|
|
575
|
+
const claude = await installer.installClaudeCode({ home, root: releaseDir });
|
|
560
576
|
if (claude.ok === false) {
|
|
561
577
|
log(`✗ Claude Code integration failed`);
|
|
562
578
|
return false;
|
|
@@ -944,11 +960,17 @@ export function npxStatus({
|
|
|
944
960
|
} catch { dshPlugin = 'unknown'; }
|
|
945
961
|
}
|
|
946
962
|
|
|
947
|
-
const st = installer.installStatus
|
|
948
|
-
|
|
949
|
-
|
|
963
|
+
const st = installer.installStatus
|
|
964
|
+
? installer.installStatus({ home, root: pointer?.path ?? runningPackageRoot() })
|
|
965
|
+
: realInstaller.installStatus({ home, root: pointer?.path ?? runningPackageRoot() });
|
|
966
|
+
const codex = st?.codex?.installed ? 'installed' : 'not installed';
|
|
967
|
+
const zcode = st?.zcode?.installed ? 'installed' : 'not installed';
|
|
968
|
+
const claude = st?.claude?.installed ? 'installed' : 'not installed';
|
|
950
969
|
const official = officialWebIntegrationStatus({ home, releaseDir: pointer?.path });
|
|
951
|
-
const officialWeb = !official.enabled ? 'disabled' : official.healthy ? 'installed' : 'needs repair';
|
|
970
|
+
const officialWeb = !official.enabled ? 'disabled' : official.healthy ? 'installed' : 'needs repair';
|
|
971
|
+
const startupState = installer.windowsStartupStatus?.({ home });
|
|
972
|
+
const windowsStartup = !startupState?.supported ? 'not supported'
|
|
973
|
+
: startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
|
|
952
974
|
|
|
953
975
|
log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
|
|
954
976
|
log(`Installed DSH Crew payload: ${installedLine}`);
|
|
@@ -964,8 +986,10 @@ export function npxStatus({
|
|
|
964
986
|
}
|
|
965
987
|
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
|
|
966
988
|
log(`Official 3080 UI bridge: ${officialWeb}`);
|
|
967
|
-
log(`Codex Desktop integration: ${codex}`);
|
|
968
|
-
log(`
|
|
989
|
+
log(`Codex Desktop integration: ${codex}`);
|
|
990
|
+
log(`ZCode integration: ${zcode}`);
|
|
991
|
+
log(`Claude Code integration: ${claude}`);
|
|
992
|
+
log(`Windows login startup: ${windowsStartup}`);
|
|
969
993
|
|
|
970
994
|
return {
|
|
971
995
|
ok: true,
|
|
@@ -974,9 +998,11 @@ export function npxStatus({
|
|
|
974
998
|
installedPath: pointer?.path ?? null,
|
|
975
999
|
dshPlugin,
|
|
976
1000
|
officialWeb,
|
|
977
|
-
codex,
|
|
978
|
-
|
|
979
|
-
|
|
1001
|
+
codex,
|
|
1002
|
+
zcode,
|
|
1003
|
+
claude,
|
|
1004
|
+
windowsStartup,
|
|
1005
|
+
};
|
|
980
1006
|
}
|
|
981
1007
|
|
|
982
1008
|
export async function npxUninstall({
|
|
@@ -992,13 +1018,23 @@ export async function npxUninstall({
|
|
|
992
1018
|
const pointer = readCurrentPointer({ home });
|
|
993
1019
|
const name = pointer?.name ?? readManifest(runningPackageRoot())?.name;
|
|
994
1020
|
|
|
995
|
-
const cx = installer.uninstallCodex({ home });
|
|
1021
|
+
const cx = installer.uninstallCodex({ home });
|
|
996
1022
|
if (cx.ok !== false) log('✓ Codex Desktop integration removed');
|
|
997
|
-
else fail('Codex Desktop integration removal failed');
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1023
|
+
else fail('Codex Desktop integration removal failed');
|
|
1024
|
+
|
|
1025
|
+
if (installer.uninstallZCode) {
|
|
1026
|
+
const zc = installer.uninstallZCode({ home });
|
|
1027
|
+
if (zc.ok !== false) log('✓ ZCode integration removed');
|
|
1028
|
+
else fail('ZCode integration removal failed');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const cl = installer.uninstallClaudeCode ? await installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
|
|
1032
|
+
if (cl.ok !== false) log('✓ Claude Code integration removed');
|
|
1033
|
+
else fail('Claude Code integration removal failed');
|
|
1034
|
+
|
|
1035
|
+
const startup = installer.uninstallWindowsStartup?.({ home });
|
|
1036
|
+
if (startup?.ok === false) fail('Windows login startup removal failed');
|
|
1037
|
+
else if (startup?.supported) log('✓ Windows login startup removed');
|
|
1002
1038
|
|
|
1003
1039
|
const official = removeOfficialWebIntegration({ home, preserveIntent: !purge, remember: !purge });
|
|
1004
1040
|
if (!official.ok) fail(`official 3080 bridge removal failed (${official.code ?? 'unknown'})`);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
|
|
12
|
+
export const WINDOWS_STARTUP_FILENAME = 'DSH Crew.vbs';
|
|
13
|
+
export const WINDOWS_LAUNCHER_FILENAME = 'start-dsh-crew.cmd';
|
|
14
|
+
|
|
15
|
+
function defaultStartupDir({ home, env }) {
|
|
16
|
+
if (home === homedir() && env.APPDATA) {
|
|
17
|
+
return join(env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
18
|
+
}
|
|
19
|
+
return join(home, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function paths({ home, startupDir, env }) {
|
|
23
|
+
const launcherFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_LAUNCHER_FILENAME);
|
|
24
|
+
const resolvedStartupDir = startupDir ?? defaultStartupDir({ home, env });
|
|
25
|
+
return {
|
|
26
|
+
launcherFile,
|
|
27
|
+
startupFile: join(resolvedStartupDir, WINDOWS_STARTUP_FILENAME),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderVbs(template, launcherFile) {
|
|
32
|
+
const escaped = launcherFile.replace(/"/g, '""');
|
|
33
|
+
return template.replace('__LAUNCHER__', escaped);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function windowsStartupStatus({
|
|
37
|
+
home = homedir(),
|
|
38
|
+
startupDir,
|
|
39
|
+
platform = process.platform,
|
|
40
|
+
env = process.env,
|
|
41
|
+
} = {}) {
|
|
42
|
+
if (platform !== 'win32') return { supported: false, installed: false, ready: false };
|
|
43
|
+
const resolved = paths({ home, startupDir, env });
|
|
44
|
+
const installed = existsSync(resolved.startupFile) || existsSync(resolved.launcherFile);
|
|
45
|
+
let ready = existsSync(resolved.startupFile) && existsSync(resolved.launcherFile);
|
|
46
|
+
if (ready) {
|
|
47
|
+
try {
|
|
48
|
+
const text = readFileSync(resolved.startupFile, 'utf16le').replace(/^\uFEFF/, '');
|
|
49
|
+
ready = text.includes(resolved.launcherFile);
|
|
50
|
+
} catch { ready = false; }
|
|
51
|
+
}
|
|
52
|
+
return { supported: true, installed, ready, ...resolved };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function installWindowsStartup({
|
|
56
|
+
home = homedir(),
|
|
57
|
+
root,
|
|
58
|
+
startupDir,
|
|
59
|
+
platform = process.platform,
|
|
60
|
+
env = process.env,
|
|
61
|
+
} = {}) {
|
|
62
|
+
if (platform !== 'win32') return { ok: true, supported: false, changed: false };
|
|
63
|
+
if (!root) return { ok: false, supported: true, code: 'STARTUP_SOURCE_REQUIRED' };
|
|
64
|
+
const sourceLauncher = join(root, 'windows', WINDOWS_LAUNCHER_FILENAME);
|
|
65
|
+
const sourceVbs = join(root, 'windows', 'start-dsh-crew.vbs');
|
|
66
|
+
if (!existsSync(sourceLauncher) || !existsSync(sourceVbs)) {
|
|
67
|
+
return { ok: false, supported: true, code: 'STARTUP_ASSET_MISSING' };
|
|
68
|
+
}
|
|
69
|
+
const resolved = paths({ home, startupDir, env });
|
|
70
|
+
mkdirSync(dirname(resolved.launcherFile), { recursive: true });
|
|
71
|
+
mkdirSync(dirname(resolved.startupFile), { recursive: true });
|
|
72
|
+
const beforeLauncher = existsSync(resolved.launcherFile) ? readFileSync(resolved.launcherFile) : null;
|
|
73
|
+
const beforeStartup = existsSync(resolved.startupFile) ? readFileSync(resolved.startupFile) : null;
|
|
74
|
+
copyFileSync(sourceLauncher, resolved.launcherFile);
|
|
75
|
+
const rendered = renderVbs(readFileSync(sourceVbs, 'utf8'), resolved.launcherFile);
|
|
76
|
+
writeFileSync(resolved.startupFile, `\uFEFF${rendered}`, 'utf16le');
|
|
77
|
+
const changed = !beforeLauncher?.equals(readFileSync(resolved.launcherFile))
|
|
78
|
+
|| !beforeStartup?.equals(readFileSync(resolved.startupFile));
|
|
79
|
+
return { ok: true, supported: true, changed, ...resolved };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function uninstallWindowsStartup({
|
|
83
|
+
home = homedir(),
|
|
84
|
+
startupDir,
|
|
85
|
+
platform = process.platform,
|
|
86
|
+
env = process.env,
|
|
87
|
+
} = {}) {
|
|
88
|
+
if (platform !== 'win32') return { ok: true, supported: false, removed: false };
|
|
89
|
+
const resolved = paths({ home, startupDir, env });
|
|
90
|
+
let removed = false;
|
|
91
|
+
for (const file of [resolved.startupFile, resolved.launcherFile]) {
|
|
92
|
+
if (existsSync(file)) {
|
|
93
|
+
rmSync(file, { force: true });
|
|
94
|
+
removed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, supported: true, removed, ...resolved };
|
|
98
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// ZCode host integration. This module is deliberately file-only: it never
|
|
2
|
+
// launches ZCode, mutates credentials, or overwrites an unowned MCP server.
|
|
3
|
+
// The generated files make ZCode dispatch through the same dsh-crew MCP server
|
|
4
|
+
// used by Codex and Claude while keeping ZCode's native config precedence.
|
|
5
|
+
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
|
|
12
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
13
|
+
const HOST = 'zcode';
|
|
14
|
+
const SERVER = 'dsh-crew';
|
|
15
|
+
const POLICY_START = '<!-- DSH CREW MANAGED ZCODE POLICY:START -->';
|
|
16
|
+
const POLICY_END = '<!-- DSH CREW MANAGED ZCODE POLICY:END -->';
|
|
17
|
+
const OWNERSHIP_FILE = ({ home = homedir() } = {}) => join(home, '.config', 'dsh-crew', 'integrations', 'zcode.json');
|
|
18
|
+
|
|
19
|
+
// Keep the allowlist explicit. ZCode rejects wildcard tool permissions, and a
|
|
20
|
+
// future server tool is not silently exposed until a template intentionally
|
|
21
|
+
// opts into it.
|
|
22
|
+
export const ZCODE_MCP_TOOLS = Object.freeze([
|
|
23
|
+
'dsh_run_worker',
|
|
24
|
+
'dsh_spawn_worker',
|
|
25
|
+
'dsh_worker_status',
|
|
26
|
+
'dsh_worker_result',
|
|
27
|
+
'dsh_worker_cancel',
|
|
28
|
+
'dsh_worker_config',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function readText(file) {
|
|
32
|
+
try { return readFileSync(file, 'utf8'); } catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readJson(file, fallback = null) {
|
|
36
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return fallback; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function contentHash(text) {
|
|
40
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeJson(file, value) {
|
|
44
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
45
|
+
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function backup(file) {
|
|
49
|
+
if (!existsSync(file)) return null;
|
|
50
|
+
const path = `${file}.dsh-crew-backup-${Date.now()}`;
|
|
51
|
+
copyFileSync(file, path);
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizePath(value) {
|
|
56
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
57
|
+
try {
|
|
58
|
+
const path = resolve(value);
|
|
59
|
+
return process.platform === 'win32' ? path.toLowerCase() : path;
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function serverTarget(server) {
|
|
64
|
+
if (!server || typeof server !== 'object') return null;
|
|
65
|
+
if (server.command !== 'node' || !Array.isArray(server.args) || typeof server.args[0] !== 'string') return null;
|
|
66
|
+
return normalizePath(server.args[0]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function nativeServers(config) {
|
|
70
|
+
const servers = config?.mcp?.servers;
|
|
71
|
+
return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sharedServers(config) {
|
|
75
|
+
const servers = config?.mcpServers;
|
|
76
|
+
return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function configSource({ home = homedir() } = {}) {
|
|
80
|
+
const nativeFile = join(home, '.zcode', 'cli', 'config.json');
|
|
81
|
+
const sharedFile = join(home, '.agents', 'mcp.json');
|
|
82
|
+
const native = readJson(nativeFile, {});
|
|
83
|
+
const shared = readJson(sharedFile, {});
|
|
84
|
+
const nativeMap = nativeServers(native);
|
|
85
|
+
const sharedMap = sharedServers(shared);
|
|
86
|
+
// ZCode shadows .agents/mcp.json whenever its native config has at least
|
|
87
|
+
// one server. If native is empty, the shared file is the least-surprising
|
|
88
|
+
// compatibility path for users who already manage MCP centrally.
|
|
89
|
+
if (Object.keys(nativeMap).length > 0 || Object.keys(sharedMap).length === 0) {
|
|
90
|
+
return { file: nativeFile, kind: 'native', config: native, servers: nativeMap };
|
|
91
|
+
}
|
|
92
|
+
return { file: sharedFile, kind: 'shared', config: shared, servers: sharedMap };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveZCodeMcpTarget({ home = homedir() } = {}) {
|
|
96
|
+
const source = configSource({ home });
|
|
97
|
+
return serverTarget(source.servers[SERVER]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function expectedTarget({ root = ROOT } = {}) {
|
|
101
|
+
return resolve(join(root, 'src', 'server.mjs'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function managedPolicyBlock(root) {
|
|
105
|
+
const template = readText(join(root, 'zcode', 'AGENTS.md'))?.trim();
|
|
106
|
+
return template ? `${POLICY_START}\n${template}\n${POLICY_END}` : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function installPolicy({ home, root }) {
|
|
110
|
+
const file = join(home, '.zcode', 'AGENTS.md');
|
|
111
|
+
const block = managedPolicyBlock(root);
|
|
112
|
+
if (!block) return { ok: false, code: 'ZCODE_POLICY_TEMPLATE_MISSING' };
|
|
113
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
114
|
+
const current = readText(file) ?? '';
|
|
115
|
+
const escapedStart = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
116
|
+
const escapedEnd = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
117
|
+
const managed = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}`, 'm');
|
|
118
|
+
const next = managed.test(current)
|
|
119
|
+
? current.replace(managed, block)
|
|
120
|
+
: `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}\n`;
|
|
121
|
+
if (next !== current) { backup(file); writeFileSync(file, next); }
|
|
122
|
+
return { ok: true, file };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function removePolicy({ home }) {
|
|
126
|
+
const file = join(home, '.zcode', 'AGENTS.md');
|
|
127
|
+
const current = readText(file);
|
|
128
|
+
if (typeof current !== 'string') return false;
|
|
129
|
+
const start = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
130
|
+
const end = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
131
|
+
const managed = new RegExp(`(?:\\r?\\n){0,2}${start}[\\s\\S]*?${end}(?:\\r?\\n)?`, 'm');
|
|
132
|
+
if (!managed.test(current)) return false;
|
|
133
|
+
const next = current.replace(managed, '').trimEnd();
|
|
134
|
+
backup(file);
|
|
135
|
+
if (next.trim()) writeFileSync(file, `${next}\n`);
|
|
136
|
+
else rmSync(file, { force: true });
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ownership({ home = homedir() } = {}) {
|
|
141
|
+
const data = readJson(OWNERSHIP_FILE({ home }), null);
|
|
142
|
+
return data && typeof data === 'object' && data.host === HOST ? data : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function writeOwnership({ home, configFile, configKind, target, files = [] }) {
|
|
146
|
+
writeJson(OWNERSHIP_FILE({ home }), {
|
|
147
|
+
schema_version: 1,
|
|
148
|
+
host: HOST,
|
|
149
|
+
server: SERVER,
|
|
150
|
+
config_file: configFile,
|
|
151
|
+
config_kind: configKind,
|
|
152
|
+
target,
|
|
153
|
+
files,
|
|
154
|
+
managed_at: new Date().toISOString(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function templateFiles({ home, root }) {
|
|
159
|
+
return [
|
|
160
|
+
['agents', 'ds-worker.md'],
|
|
161
|
+
['agents', 'ds-reviewer.md'],
|
|
162
|
+
['commands', 'dsh-config.md'],
|
|
163
|
+
['commands', 'dsh-status.md'],
|
|
164
|
+
].map(([dir, file]) => ({ source: join(root, 'zcode', dir, file), dest: join(home, '.zcode', dir, file) }));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function installTemplates({ home, root, priorFiles = [] }) {
|
|
168
|
+
const actions = [];
|
|
169
|
+
const records = [];
|
|
170
|
+
for (const { source, dest } of templateFiles({ home, root })) {
|
|
171
|
+
if (!existsSync(source)) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source };
|
|
172
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
173
|
+
const before = readText(dest);
|
|
174
|
+
const rendered = readFileSync(source, 'utf8');
|
|
175
|
+
const previous = priorFiles.find((entry) => entry?.path === dest);
|
|
176
|
+
let backupFile = previous?.backup ?? null;
|
|
177
|
+
if (before !== rendered) {
|
|
178
|
+
if (before !== null && !backupFile) backupFile = backup(dest);
|
|
179
|
+
writeFileSync(dest, rendered);
|
|
180
|
+
}
|
|
181
|
+
actions.push(dest);
|
|
182
|
+
records.push({
|
|
183
|
+
path: dest,
|
|
184
|
+
preexisting: previous?.preexisting === true || (previous === undefined && before !== null),
|
|
185
|
+
...(backupFile ? { backup: backupFile } : {}),
|
|
186
|
+
managed_sha256: contentHash(rendered),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return { ok: true, actions, records };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function updateMcp({ home, root }) {
|
|
193
|
+
const source = configSource({ home });
|
|
194
|
+
const target = expectedTarget({ root });
|
|
195
|
+
const current = source.servers[SERVER];
|
|
196
|
+
const currentTarget = serverTarget(current);
|
|
197
|
+
const owned = ownership({ home });
|
|
198
|
+
if (current && currentTarget !== target) {
|
|
199
|
+
// Only an exact previously-owned entry may be repaired. Any foreign
|
|
200
|
+
// command, even if it is also named dsh-crew, is a hard collision.
|
|
201
|
+
if (!(owned && owned.config_file === source.file && normalizePath(owned.target) === currentTarget)) {
|
|
202
|
+
return { ok: false, code: 'ZCODE_MCP_COLLISION', config_file: source.file };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const next = { ...source.config };
|
|
207
|
+
if (source.kind === 'native') next.mcp = { ...(next.mcp ?? {}), servers: { ...source.servers, [SERVER]: { command: 'node', args: [target] } } };
|
|
208
|
+
else next.mcpServers = { ...source.servers, [SERVER]: { command: 'node', args: [target] } };
|
|
209
|
+
const changed = JSON.stringify(next) !== JSON.stringify(source.config);
|
|
210
|
+
if (changed) { backup(source.file); writeJson(source.file, next); }
|
|
211
|
+
return { ok: true, changed, config_file: source.file, config_kind: source.kind, target };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function installZCode({ home = homedir(), root = ROOT } = {}) {
|
|
215
|
+
const templates = templateFiles({ home, root });
|
|
216
|
+
const missing = templates.find(({ source }) => !existsSync(source));
|
|
217
|
+
if (missing) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source: missing.source };
|
|
218
|
+
const priorFiles = ownership({ home })?.files ?? [];
|
|
219
|
+
// Check for collisions before writing any other integration surface so a
|
|
220
|
+
// failed install is transaction-like and leaves user files untouched.
|
|
221
|
+
const mcp = updateMcp({ home, root });
|
|
222
|
+
if (!mcp.ok) return mcp;
|
|
223
|
+
const policy = installPolicy({ home, root });
|
|
224
|
+
if (!policy.ok) return policy;
|
|
225
|
+
const installed = installTemplates({ home, root, priorFiles });
|
|
226
|
+
if (!installed.ok) return installed;
|
|
227
|
+
writeOwnership({
|
|
228
|
+
home,
|
|
229
|
+
configFile: mcp.config_file,
|
|
230
|
+
configKind: mcp.config_kind,
|
|
231
|
+
target: mcp.target,
|
|
232
|
+
files: installed.records,
|
|
233
|
+
});
|
|
234
|
+
return { ok: true, ...mcp, policy_file: policy.file, files: installed.actions };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function zcodeComponents({ home = homedir(), root = ROOT } = {}) {
|
|
238
|
+
const expected = normalizePath(expectedTarget({ root }));
|
|
239
|
+
const source = configSource({ home });
|
|
240
|
+
const configured = serverTarget(source.servers[SERVER]);
|
|
241
|
+
const owned = ownership({ home });
|
|
242
|
+
const components = {
|
|
243
|
+
mcp: configured === expected,
|
|
244
|
+
policy: typeof readText(join(home, '.zcode', 'AGENTS.md')) === 'string' && readText(join(home, '.zcode', 'AGENTS.md')).includes(POLICY_START),
|
|
245
|
+
worker_agent: existsSync(join(home, '.zcode', 'agents', 'ds-worker.md')),
|
|
246
|
+
reviewer_agent: existsSync(join(home, '.zcode', 'agents', 'ds-reviewer.md')),
|
|
247
|
+
config_prompt: existsSync(join(home, '.zcode', 'commands', 'dsh-config.md')),
|
|
248
|
+
status_prompt: existsSync(join(home, '.zcode', 'commands', 'dsh-status.md')),
|
|
249
|
+
ownership: !!owned && owned.config_file === source.file && normalizePath(owned.target) === expected,
|
|
250
|
+
};
|
|
251
|
+
return { components, source, expected, configured, owned };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function zcodeStatus({ home = homedir(), root = ROOT } = {}) {
|
|
255
|
+
const { components, source, expected, configured } = zcodeComponents({ home, root });
|
|
256
|
+
const missing = Object.entries(components).filter(([, value]) => !value).map(([key]) => key);
|
|
257
|
+
const installed = Object.values(components).some(Boolean);
|
|
258
|
+
return {
|
|
259
|
+
installed,
|
|
260
|
+
ready: missing.length === 0,
|
|
261
|
+
components,
|
|
262
|
+
missing,
|
|
263
|
+
config_file: source.file,
|
|
264
|
+
config_kind: source.kind,
|
|
265
|
+
target: configured ?? expected,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function uninstallZCode({ home = homedir() } = {}) {
|
|
270
|
+
const actions = [];
|
|
271
|
+
const owned = ownership({ home });
|
|
272
|
+
if (owned) {
|
|
273
|
+
const file = owned.config_file;
|
|
274
|
+
const config = readJson(file, null);
|
|
275
|
+
if (config && typeof config === 'object') {
|
|
276
|
+
const kind = owned.config_kind === 'shared' ? 'shared' : 'native';
|
|
277
|
+
const servers = kind === 'shared' ? sharedServers(config) : nativeServers(config);
|
|
278
|
+
const current = servers[SERVER];
|
|
279
|
+
if (normalizePath(owned.target) === serverTarget(current)) {
|
|
280
|
+
const next = { ...config };
|
|
281
|
+
if (kind === 'shared') {
|
|
282
|
+
next.mcpServers = { ...servers }; delete next.mcpServers[SERVER];
|
|
283
|
+
if (Object.keys(next.mcpServers).length === 0) delete next.mcpServers;
|
|
284
|
+
} else {
|
|
285
|
+
next.mcp = { ...(next.mcp ?? {}), servers: { ...servers } }; delete next.mcp.servers[SERVER];
|
|
286
|
+
if (Object.keys(next.mcp.servers).length === 0) delete next.mcp.servers;
|
|
287
|
+
if (Object.keys(next.mcp).length === 0) delete next.mcp;
|
|
288
|
+
}
|
|
289
|
+
backup(file); writeJson(file, next); actions.push(`mcp: removed ${SERVER} from ${file}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
rmSync(OWNERSHIP_FILE({ home }), { force: true });
|
|
293
|
+
}
|
|
294
|
+
if (removePolicy({ home })) actions.push('policy: removed managed ZCode block');
|
|
295
|
+
const managedFiles = Array.isArray(owned?.files) && owned.files.length
|
|
296
|
+
? owned.files
|
|
297
|
+
: ['agents/ds-worker.md', 'agents/ds-reviewer.md', 'commands/dsh-config.md', 'commands/dsh-status.md']
|
|
298
|
+
.map((rel) => ({ path: join(home, '.zcode', rel) }));
|
|
299
|
+
for (const entry of managedFiles) {
|
|
300
|
+
const file = entry?.path;
|
|
301
|
+
if (!file || !existsSync(file)) continue;
|
|
302
|
+
const current = readText(file);
|
|
303
|
+
const unchanged = !entry.managed_sha256 || (current !== null && contentHash(current) === entry.managed_sha256);
|
|
304
|
+
if (!unchanged) continue;
|
|
305
|
+
if (entry.preexisting && entry.backup && existsSync(entry.backup)) {
|
|
306
|
+
copyFileSync(entry.backup, file);
|
|
307
|
+
actions.push(`restored: ${file}`);
|
|
308
|
+
} else {
|
|
309
|
+
rmSync(file, { force: true });
|
|
310
|
+
actions.push(`removed: ${file}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { ok: true, actions };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function zcodeOwnershipFile({ home = homedir() } = {}) { return OWNERSHIP_FILE({ home }); }
|