@ran-sh/dsh-crew 0.5.1 → 0.5.3
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/agents/ds-worker.md +5 -4
- package/codex/AGENTS.md +92 -0
- package/codex/agents/ds-worker.toml +1 -1
- package/docs/installation.md +63 -0
- package/docs/job-contracts.md +12 -4
- package/docs/readiness-matrix.md +6 -2
- 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 +127 -42
- package/src/client/host-readiness.mjs +2 -1
- package/src/client/index.tsx +29 -15
- package/src/config-readiness.mjs +50 -10
- package/src/hub/index.mjs +42 -15
- 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 +115 -0
- package/src/install/zcode.mjs +316 -0
- package/src/orchestrator.mjs +53 -0
- package/src/readiness-matrix.mjs +4 -3
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +38 -34
- package/src/workflow-runtime.mjs +27 -27
- package/src/workflow.mjs +63 -5
- package/windows/start-dsh-crew.cmd +57 -0
- package/windows/start-dsh-crew.ps1 +326 -0
- package/windows/start-dsh-crew.vbs +9 -0
- package/zcode/AGENTS.md +26 -0
- package/zcode/agents/ds-reviewer.md +36 -0
- package/zcode/agents/ds-worker.md +39 -0
- package/zcode/commands/dsh-config.md +6 -0
- package/zcode/commands/dsh-status.md +4 -0
package/src/config-readiness.mjs
CHANGED
|
@@ -1,31 +1,71 @@
|
|
|
1
1
|
import { buildReadinessMatrix, READINESS_REASON_CODES } from './readiness-matrix.mjs';
|
|
2
2
|
|
|
3
|
-
function warningCodes(catalogBody) {
|
|
3
|
+
function warningCodes(catalogBody) {
|
|
4
4
|
const hints = Array.isArray(catalogBody?.health?.hints) ? catalogBody.health.hints : [];
|
|
5
5
|
return [...new Set(hints
|
|
6
6
|
.filter((hint) => hint?.level === 'warning' && typeof hint?.code === 'string' && hint.code.trim())
|
|
7
7
|
.map((hint) => hint.code.trim()))];
|
|
8
|
-
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function verifiedHubJob(job) {
|
|
11
|
+
return job?.status === 'done'
|
|
12
|
+
&& job?.task_status === 'success'
|
|
13
|
+
&& job?.delivery_complete === true
|
|
14
|
+
&& job?.workspace_evidence_ok === true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildHubExecutionRows(hubJobs = []) {
|
|
18
|
+
const jobs = Array.isArray(hubJobs) ? hubJobs : [];
|
|
19
|
+
const workerPassed = jobs.some((job) => job?.role === 'worker' && verifiedHubJob(job));
|
|
20
|
+
const reviewerPassed = jobs.some((job) => job?.role === 'reviewer'
|
|
21
|
+
&& verifiedHubJob(job)
|
|
22
|
+
&& job?.review_verdict === 'approve');
|
|
23
|
+
return [
|
|
24
|
+
workerPassed
|
|
25
|
+
? { id: 'model_execution', status: 'PASS', reason_code: 'REAL_EXECUTION_PASSED', evidence_source: 'hub-jobs' }
|
|
26
|
+
: { id: 'model_execution', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
|
|
27
|
+
reviewerPassed
|
|
28
|
+
? { id: 'reviewer_pipeline', status: 'PASS', reason_code: 'REAL_REVIEW_PASSED', evidence_source: 'hub-jobs' }
|
|
29
|
+
: { id: 'reviewer_pipeline', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
|
|
30
|
+
];
|
|
31
|
+
}
|
|
9
32
|
|
|
10
33
|
/**
|
|
11
34
|
* Enrich the conservative runtime matrix with evidence the config report has
|
|
12
35
|
* already collected. This function performs no I/O and never reads provider
|
|
13
36
|
* configuration, credentials, quotas, pricing, or hidden catalog expectations.
|
|
14
37
|
*/
|
|
15
|
-
export function buildConfigReadinessMatrix({
|
|
16
|
-
platform = process.platform,
|
|
17
|
-
hubCompatibility = null,
|
|
18
|
-
workerProviderMode = null,
|
|
19
|
-
providerCatalogChecked = false,
|
|
20
|
-
providerCatalogBody = null,
|
|
21
|
-
|
|
38
|
+
export function buildConfigReadinessMatrix({
|
|
39
|
+
platform = process.platform,
|
|
40
|
+
hubCompatibility = null,
|
|
41
|
+
workerProviderMode = null,
|
|
42
|
+
providerCatalogChecked = false,
|
|
43
|
+
providerCatalogBody = null,
|
|
44
|
+
hubJobsChecked = false,
|
|
45
|
+
hubJobsBody = null,
|
|
46
|
+
} = {}) {
|
|
22
47
|
const warnings = warningCodes(providerCatalogBody);
|
|
23
48
|
const catalogResponseOk = !!providerCatalogBody
|
|
24
49
|
&& typeof providerCatalogBody === 'object'
|
|
25
50
|
&& providerCatalogBody.ok !== false;
|
|
26
51
|
const catalogOk = providerCatalogChecked && catalogResponseOk && warnings.length === 0;
|
|
27
52
|
|
|
28
|
-
const evidence = {};
|
|
53
|
+
const evidence = {};
|
|
54
|
+
const hubJobs = hubCompatibility?.compatible === true
|
|
55
|
+
&& hubJobsChecked
|
|
56
|
+
&& hubJobsBody?.ok !== false
|
|
57
|
+
&& Array.isArray(hubJobsBody?.jobs)
|
|
58
|
+
? hubJobsBody.jobs
|
|
59
|
+
: [];
|
|
60
|
+
for (const row of buildHubExecutionRows(hubJobs)) {
|
|
61
|
+
if (row.status === 'PASS') {
|
|
62
|
+
evidence[row.id] = {
|
|
63
|
+
status: row.status,
|
|
64
|
+
reason_code: row.reason_code,
|
|
65
|
+
evidence_source: row.evidence_source,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
29
69
|
if (
|
|
30
70
|
workerProviderMode !== 'deepseek-official'
|
|
31
71
|
&& hubCompatibility?.compatible === true
|
package/src/hub/index.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { buildDirectSelectionTrace, resolveWorkerModel, resolveModel } from '../
|
|
|
20
20
|
import { readHarnessModelCatalog } from '../model-catalog.mjs';
|
|
21
21
|
import { appendDeliveryInstructions, parseDeliveryReport, formatDeliveryMetadata } from '../delivery.mjs';
|
|
22
22
|
import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY } from '../workspace-audit.mjs';
|
|
23
|
-
import { buildOutcome, JOB_PHASES } from '../workflow.mjs';
|
|
23
|
+
import { applyWorkspaceEvidence, buildOutcome, JOB_PHASES } from '../workflow.mjs';
|
|
24
24
|
import { boundedMachineCodeFromError } from '../structured-error-code.mjs';
|
|
25
25
|
import { createCanonicalJobEvent, projectWorkflowView } from '../job-contracts.mjs';
|
|
26
26
|
import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
|
|
@@ -29,6 +29,7 @@ import { addContextReferences, buildWorkspaceTask, isSafeBranchName, loadWorkspa
|
|
|
29
29
|
import { buildExtensionContract } from '../extension-contract.mjs';
|
|
30
30
|
import { cleanupIsolatedWorkspace, createIsolatedWorkspace } from '../workspace-isolation.mjs';
|
|
31
31
|
import { assessWorkspaceReadiness } from '../workspace-readiness.mjs';
|
|
32
|
+
import { buildHubExecutionRows } from '../config-readiness.mjs';
|
|
32
33
|
|
|
33
34
|
// policy.mjs is pure (no @deepseek-ai imports, no ctx access), so importing it
|
|
34
35
|
// here is safe for the profile-realm discipline: it never pulls in package
|
|
@@ -119,7 +120,20 @@ export function hubCanonicalEvents(job = {}) {
|
|
|
119
120
|
}));
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
// ---------- job registry ----------
|
|
123
|
+
// ---------- job registry ----------
|
|
124
|
+
|
|
125
|
+
export function applyHubWorkspaceEvidence({ outcome, workspaceDiff, allowNoChanges = false, isolation = 'shared', role = 'worker' } = {}) {
|
|
126
|
+
const changes = workspaceDiff?.changes ?? {};
|
|
127
|
+
const hasChanges = ['modified', 'deleted', 'renamed', 'untracked']
|
|
128
|
+
.some((key) => Array.isArray(changes[key]) && changes[key].length > 0);
|
|
129
|
+
const evidenceAvailable = workspaceDiff?.kind === 'git' && workspaceDiff.dirtyBaseline !== true;
|
|
130
|
+
return applyWorkspaceEvidence(outcome, {
|
|
131
|
+
evidenceAvailable,
|
|
132
|
+
hasChanges,
|
|
133
|
+
allowNoChanges: allowNoChanges === true && isolation === 'worktree',
|
|
134
|
+
requireNoChangeAuthorization: role === 'worker',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
123
137
|
|
|
124
138
|
// Exported for the unit tests (test/hub-windows.test.mjs); instantiation
|
|
125
139
|
// needs only a duck-typed ctx, so spawn()'s path guard is testable without a
|
|
@@ -145,6 +159,10 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
145
159
|
startedAt: job.startedAt, endedAt: job.endedAt,
|
|
146
160
|
isolation: job.isolation ?? 'shared', workspace_branch: job.workspace_branch ?? null,
|
|
147
161
|
delivery_complete: !!job.delivery_complete,
|
|
162
|
+
allow_no_changes: job.allow_no_changes === true,
|
|
163
|
+
task_status: job.outcome?.task_status ?? null,
|
|
164
|
+
workspace_evidence_ok: job.outcome?.workspace_evidence_ok ?? null,
|
|
165
|
+
review_verdict: job.review?.verdict ?? null,
|
|
148
166
|
workspace_diff_available: !!job.workspaceDiff && job.workspaceDiff.kind === 'git',
|
|
149
167
|
workspace_retained: job.workspace_retained === true,
|
|
150
168
|
cleanup_warning: job.cleanup_warning ?? null,
|
|
@@ -182,7 +200,7 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
182
200
|
* `role` (worker | reviewer) records who does the work; `tier` remains the
|
|
183
201
|
* legacy model-class slot. Reviewer-role jobs always use the pro slot.
|
|
184
202
|
*/
|
|
185
|
-
async spawn({ task, tier = 'flash', role, attempt = 0, effort = 'max', cwd, source = 'api', preset, delivery = 'coding', client_job_id, requested_isolation, workspace_branch, timeout_seconds, profile_id, workspace_context }) {
|
|
203
|
+
async spawn({ task, tier = 'flash', role, attempt = 0, effort = 'max', cwd, source = 'api', preset, delivery = 'coding', client_job_id, requested_isolation, workspace_branch, timeout_seconds, profile_id, workspace_context, allow_no_changes }) {
|
|
186
204
|
// role is only honored when the caller explicitly names it; a legacy
|
|
187
205
|
// tier-only spawn (role === undefined) keeps the exact v0.1 resolution.
|
|
188
206
|
const hasRole = role === 'worker' || role === 'reviewer';
|
|
@@ -291,6 +309,7 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
291
309
|
effort, reasoning_effort: selection.reasoningEffort,
|
|
292
310
|
task, source, cwd: executionCwd, requested_cwd: cwd,
|
|
293
311
|
isolation: isolatedWorkspace ? 'worktree' : 'shared',
|
|
312
|
+
allow_no_changes: allow_no_changes === true,
|
|
294
313
|
workspace_branch: workspace_branch ?? null, isolatedWorkspace,
|
|
295
314
|
profile_id: profile_id ?? null, workspace_context: workspace_context ?? null,
|
|
296
315
|
prompt: workerPrompt, delivery: delivery === 'review' ? 'review' : 'coding',
|
|
@@ -451,6 +470,13 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
451
470
|
job.workspaceDiff = job.baseline.kind === 'git'
|
|
452
471
|
? await captureWorkspaceDiff({ cwd: executionCwd, baseline: job.baseline }).catch(() => ({ kind: 'no-git', reason: NOT_A_GIT_REPOSITORY, error: 'workspace diff failed' }))
|
|
453
472
|
: job.baseline;
|
|
473
|
+
job.outcome = applyHubWorkspaceEvidence({
|
|
474
|
+
outcome: job.outcome,
|
|
475
|
+
workspaceDiff: job.workspaceDiff,
|
|
476
|
+
allowNoChanges: job.allow_no_changes,
|
|
477
|
+
isolation: job.isolation,
|
|
478
|
+
role: job.role,
|
|
479
|
+
});
|
|
454
480
|
if (job.role === 'reviewer' && job.review && job.workspaceDiff?.kind === 'git') {
|
|
455
481
|
const changes = job.workspaceDiff.changes ?? {};
|
|
456
482
|
const mutated = ['modified', 'deleted', 'renamed', 'untracked'].some((key) => Array.isArray(changes[key]) && changes[key].length > 0);
|
|
@@ -609,6 +635,9 @@ export function resolveHubSpawnPayload(payload, getConfig = () => ({}), dependen
|
|
|
609
635
|
if (raw.constraints?.allow_fallback !== undefined && typeof raw.constraints.allow_fallback !== 'boolean') {
|
|
610
636
|
return { ok: false, code: 'JOB_CONSTRAINTS_INVALID', error: 'allow_fallback must be boolean' };
|
|
611
637
|
}
|
|
638
|
+
if (raw.constraints?.allow_no_changes !== undefined && typeof raw.constraints.allow_no_changes !== 'boolean') {
|
|
639
|
+
return { ok: false, code: 'JOB_CONSTRAINTS_INVALID', error: 'allow_no_changes must be boolean' };
|
|
640
|
+
}
|
|
612
641
|
const profileRegistry = dependencies.profileRegistry ?? loadRoleProfiles();
|
|
613
642
|
const workspaceRegistry = dependencies.workspaceRegistry ?? loadWorkspaceContexts();
|
|
614
643
|
if (!profileRegistry.ok) return { ok: false, code: 'PROFILE_FILE_INVALID', error: 'role profile registry is invalid' };
|
|
@@ -647,6 +676,7 @@ export function resolveHubSpawnPayload(payload, getConfig = () => ({}), dependen
|
|
|
647
676
|
workspace_branch: raw.workspace?.branch ?? withRefs.context?.default_branch ?? null,
|
|
648
677
|
timeout_seconds: raw.constraints?.timeout_seconds ?? profile.timeout_seconds,
|
|
649
678
|
allow_fallback: raw.constraints?.allow_fallback ?? profile.fallback,
|
|
679
|
+
allow_no_changes: raw.constraints?.allow_no_changes === true,
|
|
650
680
|
routing: profile.routing,
|
|
651
681
|
review_strictness: profile.review_strictness,
|
|
652
682
|
profile_id: resolvedProfile.profile_id,
|
|
@@ -852,12 +882,7 @@ export async function apply(ctx) {
|
|
|
852
882
|
? { status: 'UNAVAILABLE', reason_code: 'WORKSPACE_CONTEXT_NOT_FOUND' }
|
|
853
883
|
: await assessWorkspaceReadiness({ cwd: requestedContext?.repo_root ?? null });
|
|
854
884
|
const liveJobs = typeof hub.list === 'function' ? hub.list() : [];
|
|
855
|
-
const modelExecution = liveJobs
|
|
856
|
-
? { id: 'model_execution', status: 'PASS', reason_code: 'REAL_EXECUTION_PASSED' }
|
|
857
|
-
: { id: 'model_execution', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE' };
|
|
858
|
-
const reviewerExecution = liveJobs.some((job) => job?.role === 'reviewer' && job?.status === 'done')
|
|
859
|
-
? { id: 'reviewer_pipeline', status: 'PASS', reason_code: 'REAL_REVIEW_PASSED' }
|
|
860
|
-
: { id: 'reviewer_pipeline', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE' };
|
|
885
|
+
const [modelExecution, reviewerExecution] = buildHubExecutionRows(liveJobs);
|
|
861
886
|
const contract = buildExtensionContract({
|
|
862
887
|
config,
|
|
863
888
|
readinessMatrix: { rows: [
|
|
@@ -1033,8 +1058,8 @@ export async function apply(ctx) {
|
|
|
1033
1058
|
// Cache-busted import: the installer must always run the code
|
|
1034
1059
|
// currently on disk, not whatever this process first loaded —
|
|
1035
1060
|
// a stale cached copy once re-broke user settings after a fix.
|
|
1036
|
-
const { installClaudeCode, installCodex, installHudSegment, uninstallClaudeCode, uninstallCodex } =
|
|
1037
|
-
await import(`../install/install.mjs?t=${Date.now()}`);
|
|
1061
|
+
const { installClaudeCode, installCodex, installHudSegment, uninstallClaudeCode, uninstallCodex, installZCode, uninstallZCode } =
|
|
1062
|
+
await import(`../install/install.mjs?t=${Date.now()}`);
|
|
1038
1063
|
if (target === 'claude') {
|
|
1039
1064
|
const base = await installClaudeCode({ statusline: !!statusline });
|
|
1040
1065
|
const hud = installHudSegment({});
|
|
@@ -1043,10 +1068,12 @@ export async function apply(ctx) {
|
|
|
1043
1068
|
actions: [...base.actions, ...hud.actions.map((a) => `hud: ${a}`)],
|
|
1044
1069
|
});
|
|
1045
1070
|
}
|
|
1046
|
-
if (target === 'codex') return sendJson(res, 200, installCodex({}));
|
|
1047
|
-
if (target === '
|
|
1048
|
-
if (target === '
|
|
1049
|
-
|
|
1071
|
+
if (target === 'codex') return sendJson(res, 200, installCodex({}));
|
|
1072
|
+
if (target === 'zcode') return sendJson(res, 200, installZCode({}));
|
|
1073
|
+
if (target === 'claude-uninstall') return sendJson(res, 200, uninstallClaudeCode({}));
|
|
1074
|
+
if (target === 'codex-uninstall') return sendJson(res, 200, uninstallCodex({}));
|
|
1075
|
+
if (target === 'zcode-uninstall') return sendJson(res, 200, uninstallZCode({}));
|
|
1076
|
+
return sendJson(res, 400, { ok: false, error: 'target must be claude | codex | zcode | claude-uninstall | codex-uninstall | zcode-uninstall' });
|
|
1050
1077
|
} catch (err) {
|
|
1051
1078
|
return sendJson(res, 500, { ok: false, error: err?.message ?? String(err) });
|
|
1052
1079
|
}
|
|
@@ -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,115 @@
|
|
|
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
|
+
export const WINDOWS_HELPER_FILENAME = 'start-dsh-crew.ps1';
|
|
15
|
+
|
|
16
|
+
function defaultStartupDir({ home, env }) {
|
|
17
|
+
if (home === homedir() && env.APPDATA) {
|
|
18
|
+
return join(env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
19
|
+
}
|
|
20
|
+
return join(home, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function paths({ home, startupDir, env }) {
|
|
24
|
+
const launcherFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_LAUNCHER_FILENAME);
|
|
25
|
+
const helperFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_HELPER_FILENAME);
|
|
26
|
+
const resolvedStartupDir = startupDir ?? defaultStartupDir({ home, env });
|
|
27
|
+
return {
|
|
28
|
+
launcherFile,
|
|
29
|
+
helperFile,
|
|
30
|
+
startupFile: join(resolvedStartupDir, WINDOWS_STARTUP_FILENAME),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderVbs(template, launcherFile) {
|
|
35
|
+
const escaped = launcherFile.replace(/"/g, '""');
|
|
36
|
+
return template.replace('__LAUNCHER__', escaped);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function windowsStartupStatus({
|
|
40
|
+
home = homedir(),
|
|
41
|
+
startupDir,
|
|
42
|
+
platform = process.platform,
|
|
43
|
+
env = process.env,
|
|
44
|
+
} = {}) {
|
|
45
|
+
if (platform !== 'win32') return { supported: false, installed: false, ready: false };
|
|
46
|
+
const resolved = paths({ home, startupDir, env });
|
|
47
|
+
const installed = existsSync(resolved.startupFile)
|
|
48
|
+
|| existsSync(resolved.launcherFile)
|
|
49
|
+
|| existsSync(resolved.helperFile);
|
|
50
|
+
let ready = existsSync(resolved.startupFile)
|
|
51
|
+
&& existsSync(resolved.launcherFile)
|
|
52
|
+
&& existsSync(resolved.helperFile);
|
|
53
|
+
if (ready) {
|
|
54
|
+
try {
|
|
55
|
+
const startup = readFileSync(resolved.startupFile, 'utf16le').replace(/^\uFEFF/, '');
|
|
56
|
+
const launcher = readFileSync(resolved.launcherFile, 'utf8');
|
|
57
|
+
const helper = readFileSync(resolved.helperFile, 'utf8');
|
|
58
|
+
ready = startup.includes(resolved.launcherFile)
|
|
59
|
+
&& startup.includes('--watch')
|
|
60
|
+
&& launcher.includes(WINDOWS_HELPER_FILENAME)
|
|
61
|
+
&& helper.includes('DSH Crew managed Windows launcher')
|
|
62
|
+
&& helper.includes('DSHCrewServiceSupervisor');
|
|
63
|
+
} catch { ready = false; }
|
|
64
|
+
}
|
|
65
|
+
return { supported: true, installed, ready, ...resolved };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function installWindowsStartup({
|
|
69
|
+
home = homedir(),
|
|
70
|
+
root,
|
|
71
|
+
startupDir,
|
|
72
|
+
platform = process.platform,
|
|
73
|
+
env = process.env,
|
|
74
|
+
} = {}) {
|
|
75
|
+
if (platform !== 'win32') return { ok: true, supported: false, changed: false };
|
|
76
|
+
if (!root) return { ok: false, supported: true, code: 'STARTUP_SOURCE_REQUIRED' };
|
|
77
|
+
const sourceLauncher = join(root, 'windows', WINDOWS_LAUNCHER_FILENAME);
|
|
78
|
+
const sourceHelper = join(root, 'windows', WINDOWS_HELPER_FILENAME);
|
|
79
|
+
const sourceVbs = join(root, 'windows', 'start-dsh-crew.vbs');
|
|
80
|
+
if (!existsSync(sourceLauncher) || !existsSync(sourceHelper) || !existsSync(sourceVbs)) {
|
|
81
|
+
return { ok: false, supported: true, code: 'STARTUP_ASSET_MISSING' };
|
|
82
|
+
}
|
|
83
|
+
const resolved = paths({ home, startupDir, env });
|
|
84
|
+
mkdirSync(dirname(resolved.launcherFile), { recursive: true });
|
|
85
|
+
mkdirSync(dirname(resolved.startupFile), { recursive: true });
|
|
86
|
+
const beforeLauncher = existsSync(resolved.launcherFile) ? readFileSync(resolved.launcherFile) : null;
|
|
87
|
+
const beforeHelper = existsSync(resolved.helperFile) ? readFileSync(resolved.helperFile) : null;
|
|
88
|
+
const beforeStartup = existsSync(resolved.startupFile) ? readFileSync(resolved.startupFile) : null;
|
|
89
|
+
copyFileSync(sourceLauncher, resolved.launcherFile);
|
|
90
|
+
copyFileSync(sourceHelper, resolved.helperFile);
|
|
91
|
+
const rendered = renderVbs(readFileSync(sourceVbs, 'utf8'), resolved.launcherFile);
|
|
92
|
+
writeFileSync(resolved.startupFile, `\uFEFF${rendered}`, 'utf16le');
|
|
93
|
+
const changed = !beforeLauncher?.equals(readFileSync(resolved.launcherFile))
|
|
94
|
+
|| !beforeHelper?.equals(readFileSync(resolved.helperFile))
|
|
95
|
+
|| !beforeStartup?.equals(readFileSync(resolved.startupFile));
|
|
96
|
+
return { ok: true, supported: true, changed, ...resolved };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function uninstallWindowsStartup({
|
|
100
|
+
home = homedir(),
|
|
101
|
+
startupDir,
|
|
102
|
+
platform = process.platform,
|
|
103
|
+
env = process.env,
|
|
104
|
+
} = {}) {
|
|
105
|
+
if (platform !== 'win32') return { ok: true, supported: false, removed: false };
|
|
106
|
+
const resolved = paths({ home, startupDir, env });
|
|
107
|
+
let removed = false;
|
|
108
|
+
for (const file of [resolved.startupFile, resolved.launcherFile, resolved.helperFile]) {
|
|
109
|
+
if (existsSync(file)) {
|
|
110
|
+
rmSync(file, { force: true });
|
|
111
|
+
removed = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, supported: true, removed, ...resolved };
|
|
115
|
+
}
|