@ran-sh/dsh-crew 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -7
- package/README.zh.md +12 -7
- package/docs/installation.md +1 -1
- package/docs/ui-surfaces.md +65 -46
- package/lib/client.js +345 -22
- package/official-web-bridge/lib/client.js +345 -22
- package/package.json +1 -1
- package/src/client/collapsible-sections.mjs +3 -2
- package/src/client/index.tsx +227 -51
- package/src/config-readiness.mjs +56 -5
- package/src/extension-contract.mjs +9 -5
- package/src/hub/index.mjs +854 -445
- package/src/hub-client.mjs +24 -4
- package/src/hub-compatibility.mjs +23 -7
- package/src/install/install.mjs +4 -4
- package/src/install/npx-lifecycle.mjs +254 -87
- package/src/job-contracts.mjs +19 -3
- package/src/jobs.mjs +2 -4
- package/src/local-request-guard.mjs +60 -0
- package/src/mcp-runtime.mjs +7 -5
- package/src/model-routing.mjs +141 -49
- package/src/multimodal.mjs +0 -0
- package/src/official-web-bridge.mjs +242 -84
- package/src/policy.mjs +62 -31
- package/src/provider-config-scrub.mjs +71 -0
- package/src/provider-delete-adapters.mjs +424 -0
- package/src/provider-health.mjs +124 -0
- package/src/provider-inventory.mjs +132 -0
- package/src/provider-lifecycle-state.mjs +81 -0
- package/src/provider-lifecycle.mjs +214 -0
- package/src/provider-profile-store.mjs +171 -0
- package/src/readiness-matrix.mjs +11 -5
- package/src/removable-waiter.mjs +29 -0
- package/src/runtime-identity.mjs +43 -13
- package/src/server.mjs +34 -16
- package/src/status-shard.mjs +13 -2
- package/src/workflow-runtime.mjs +44 -10
- package/windows/start-dsh-crew.cmd +3 -3
- package/windows/start-dsh-crew.ps1 +49 -5
package/src/hub-client.mjs
CHANGED
|
@@ -113,8 +113,26 @@ export function hubRequestError(body, status) {
|
|
|
113
113
|
return err;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
// Transport deadlines: every Hub call is bounded so a wedged local HTTP
|
|
117
|
+
// exchange can never outlive the workflow timeout contract. Long polls get
|
|
118
|
+
// their requested wait plus grace; spawn gets a wider bound because worktree
|
|
119
|
+
// creation can clone sizeable repositories. The env override exists for
|
|
120
|
+
// deterministic deadline tests; production defaults stay at 10s.
|
|
121
|
+
const ENV_TIMEOUT_MS = Number(process.env.DSH_CREW_HUB_TIMEOUT_MS);
|
|
122
|
+
const DEFAULT_TIMEOUT_MS = Number.isInteger(ENV_TIMEOUT_MS) && ENV_TIMEOUT_MS > 0 ? ENV_TIMEOUT_MS : 10_000;
|
|
123
|
+
const SPAWN_TIMEOUT_MS = Math.max(DEFAULT_TIMEOUT_MS, 60_000);
|
|
124
|
+
const LONG_POLL_GRACE_MS = 5_000;
|
|
125
|
+
|
|
126
|
+
async function call(path, init, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
127
|
+
let res;
|
|
128
|
+
try {
|
|
129
|
+
res = await fetch(`${API}${path}`, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err?.name === 'TimeoutError' || err?.name === 'AbortError') {
|
|
132
|
+
throw hubRequestError({ error: `hub request timed out after ${timeoutMs}ms` }, 0);
|
|
133
|
+
}
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
118
136
|
const body = await res.json();
|
|
119
137
|
if (!res.ok || body.ok === false) throw hubRequestError(body, res.status);
|
|
120
138
|
return body;
|
|
@@ -125,8 +143,10 @@ export const hub = {
|
|
|
125
143
|
method: 'POST',
|
|
126
144
|
headers: { 'content-type': 'application/json' },
|
|
127
145
|
body: JSON.stringify(spec),
|
|
128
|
-
}).then((b) => b.job),
|
|
146
|
+
}, { timeoutMs: SPAWN_TIMEOUT_MS }).then((b) => b.job),
|
|
129
147
|
list: () => call('/jobs').then((b) => b.jobs),
|
|
130
|
-
get: (id, waitSeconds = 0) => call(`/jobs/${id}?wait=${waitSeconds}
|
|
148
|
+
get: (id, waitSeconds = 0) => call(`/jobs/${id}?wait=${waitSeconds}`, undefined, {
|
|
149
|
+
timeoutMs: waitSeconds > 0 ? (waitSeconds * 1000) + LONG_POLL_GRACE_MS : DEFAULT_TIMEOUT_MS,
|
|
150
|
+
}).then((b) => b.job),
|
|
131
151
|
cancel: (id) => call(`/jobs/${id}/cancel`, { method: 'POST' }).then((b) => b.job),
|
|
132
152
|
};
|
|
@@ -13,9 +13,16 @@ export function hubCompatibilityMessage(status = {}) {
|
|
|
13
13
|
return `DSH workers hub is reachable but incompatible (${code}${detail}). Update/restart the Hub plugin before using Hub execution.`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function resolveHubExecutionMode(requestedMode = 'auto', status = {}) {
|
|
17
|
-
if (requestedMode === 'standalone') {
|
|
18
|
-
|
|
16
|
+
export function resolveHubExecutionMode(requestedMode = 'auto', status = {}, { productionOnly = false } = {}) {
|
|
17
|
+
if (requestedMode === 'standalone') {
|
|
18
|
+
if (productionOnly) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
code: 'STANDALONE_EXECUTION_DISABLED',
|
|
22
|
+
error: 'Standalone execution is disabled for the production 3210 Crew transport.',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { ok: true, mode: 'standalone', reason: 'explicit-standalone' };
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
if (status.compatible === true) {
|
|
@@ -31,16 +38,25 @@ export function resolveHubExecutionMode(requestedMode = 'auto', status = {}) {
|
|
|
31
38
|
};
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
if (requestedMode === 'hub') {
|
|
41
|
+
if (requestedMode === 'hub') {
|
|
35
42
|
return {
|
|
36
43
|
ok: false,
|
|
37
44
|
code: status.code ?? HUB_COMPATIBILITY_CODES.UNREACHABLE,
|
|
38
45
|
error: 'Session mode is "hub" but the DSH workers hub is not reachable.',
|
|
39
46
|
hub: status,
|
|
40
47
|
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (productionOnly) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
code: status.code ?? HUB_COMPATIBILITY_CODES.UNREACHABLE,
|
|
54
|
+
error: `Production 3210 Crew transport is unavailable (${status.code ?? HUB_COMPATIBILITY_CODES.UNREACHABLE}).`,
|
|
55
|
+
hub: status,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
44
60
|
ok: true,
|
|
45
61
|
mode: 'standalone',
|
|
46
62
|
reason: 'hub-unreachable-fallback',
|
package/src/install/install.mjs
CHANGED
|
@@ -93,12 +93,12 @@ function legacySnapshotFrom(config = {}) {
|
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function attachReadMetadata(config, { version, canonical }) {
|
|
97
|
-
return {
|
|
96
|
+
function attachReadMetadata(config, { version, canonical }) {
|
|
97
|
+
return {
|
|
98
98
|
...config,
|
|
99
99
|
config_schema_version: version,
|
|
100
|
-
config_authority: canonical ? 'canonical' : 'legacy-import',
|
|
101
|
-
config_migration_required: !canonical,
|
|
100
|
+
config_authority: canonical ? 'canonical' : 'legacy-import',
|
|
101
|
+
config_migration_required: !canonical || version < CONFIG_SCHEMA_VERSION,
|
|
102
102
|
config_activation: runtimeActivationMetadata(),
|
|
103
103
|
};
|
|
104
104
|
}
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
readFileSync,
|
|
32
32
|
realpathSync,
|
|
33
33
|
rmSync,
|
|
34
|
+
copyFileSync,
|
|
34
35
|
writeFileSync,
|
|
35
36
|
} from 'node:fs';
|
|
36
37
|
import { createRequire } from 'node:module';
|
|
@@ -52,6 +53,21 @@ export const CURRENT_POINTER_FILENAME = 'current.json';
|
|
|
52
53
|
export const KEEP_RELEASES = 2;
|
|
53
54
|
export const INCOMPLETE_MARKER = '.dsh-crew-incomplete';
|
|
54
55
|
|
|
56
|
+
export function npmCliInvocation(args, {
|
|
57
|
+
platform = process.platform,
|
|
58
|
+
environment = process.env,
|
|
59
|
+
} = {}) {
|
|
60
|
+
if (platform !== 'win32') return { command: 'npm', args: [...args] };
|
|
61
|
+
const unsafe = args.find((arg) => /[\0\r\n"%!^&|<>]/.test(String(arg)));
|
|
62
|
+
if (unsafe !== undefined) throw new Error(`unsafe npm argument: ${unsafe}`);
|
|
63
|
+
const commandLine = ['npm.cmd', ...args.map((arg) => `"${String(arg)}"`)].join(' ');
|
|
64
|
+
return {
|
|
65
|
+
command: environment.ComSpec || environment.COMSPEC || 'cmd.exe',
|
|
66
|
+
args: ['/d', '/s', '/c', commandLine],
|
|
67
|
+
windowsVerbatimArguments: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
56
72
|
|
|
57
73
|
export function crewAppRoot({ home = homedir() } = {}) {
|
|
@@ -218,12 +234,17 @@ export function copyProductionDependencyTree({
|
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
function defaultNpmInstaller(stageRoot, log) {
|
|
221
|
-
const
|
|
222
|
-
const result = spawnSync(npm, [
|
|
237
|
+
const npmArgs = [
|
|
223
238
|
'install', '--prefix', stageRoot,
|
|
224
239
|
'--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund',
|
|
225
240
|
'--no-package-lock', '--loglevel=error',
|
|
226
|
-
]
|
|
241
|
+
];
|
|
242
|
+
const invocation = npmCliInvocation(npmArgs);
|
|
243
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
244
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 600_000,
|
|
245
|
+
windowsHide: true, windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
246
|
+
env: sanitizedPackageManagerEnv(),
|
|
247
|
+
});
|
|
227
248
|
if (result.status !== 0) {
|
|
228
249
|
log(`- npm fallback install failed (${result.status}): ${(result.stderr || result.stdout || '').trim().slice(-300)}`);
|
|
229
250
|
return false;
|
|
@@ -549,30 +570,30 @@ async function activateRelease({ home, releaseDir, manifest, log, installer }) {
|
|
|
549
570
|
}
|
|
550
571
|
log(`✓ Harness plugin registered (dedicated dsh-crew profile → ${releaseDir})`);
|
|
551
572
|
|
|
552
|
-
const codex = installer.installCodex({ home, root: releaseDir });
|
|
573
|
+
const codex = installer.installCodex({ home, root: releaseDir });
|
|
553
574
|
if (codex.ok === false) {
|
|
554
575
|
log(`✗ Codex Desktop integration failed: ${(codex.actions ?? []).join('; ')}`);
|
|
555
576
|
return false;
|
|
556
577
|
}
|
|
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 });
|
|
578
|
+
log('✓ Codex Desktop integration');
|
|
579
|
+
|
|
580
|
+
if (installer.installZCode) {
|
|
581
|
+
const zcode = installer.installZCode({ home, root: releaseDir });
|
|
582
|
+
if (zcode.ok === false) {
|
|
583
|
+
log(`✗ ZCode integration failed (${zcode.code ?? 'unknown'})`);
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
log('✓ ZCode integration');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const startup = installer.installWindowsStartup?.({ home, root: releaseDir });
|
|
590
|
+
if (startup?.ok === false) {
|
|
591
|
+
log(`✗ Windows login startup failed (${startup.code ?? 'unknown'})`);
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
if (startup?.supported) log('✓ Windows login startup');
|
|
595
|
+
|
|
596
|
+
const claude = await installer.installClaudeCode({ home, root: releaseDir });
|
|
576
597
|
if (claude.ok === false) {
|
|
577
598
|
log(`✗ Claude Code integration failed`);
|
|
578
599
|
return false;
|
|
@@ -680,8 +701,14 @@ export const UPDATE_DEFAULT_SPEC = `${UPDATE_PACKAGE_NAME}@latest`;
|
|
|
680
701
|
*/
|
|
681
702
|
export function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } = {}) {
|
|
682
703
|
mkdirSync(destDir, { recursive: true });
|
|
683
|
-
|
|
684
|
-
|
|
704
|
+
// GNU tar interprets a `C:\...` argument as a remote rsh host ("Cannot
|
|
705
|
+
// connect to C:"), so anchor the invocation inside destDir and pass the
|
|
706
|
+
// archive by basename: every argument stays colon-free for both bsdtar
|
|
707
|
+
// (Windows 10+) and GNU tar, whatever PATH resolves first.
|
|
708
|
+
const localArchive = join(destDir, 'candidate.tgz');
|
|
709
|
+
copyFileSync(tgzPath, localArchive);
|
|
710
|
+
const result = runner('tar', ['-xzf', 'candidate.tgz'], {
|
|
711
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, cwd: destDir,
|
|
685
712
|
});
|
|
686
713
|
const root = join(destDir, 'package');
|
|
687
714
|
if (result.status !== 0 || !existsSync(join(root, 'package.json'))) {
|
|
@@ -759,12 +786,14 @@ export function resolveUpdateCandidate({
|
|
|
759
786
|
mkdirSync(tmpDir, { recursive: true });
|
|
760
787
|
try {
|
|
761
788
|
const effectiveSpec = process.env.DSH_CREW_UPDATE_SPEC ?? spec;
|
|
762
|
-
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
763
789
|
const packArgs = ['pack', effectiveSpec, '--pack-destination', tmpDir, '--json', '--loglevel=error'];
|
|
764
790
|
log(`- resolving update candidate from the configured npm registry (${effectiveSpec})`);
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
791
|
+
const invocation = npmCliInvocation(packArgs);
|
|
792
|
+
const packed = runner(invocation.command, invocation.args, {
|
|
793
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
794
|
+
timeout: 600_000, windowsHide: true,
|
|
795
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
796
|
+
env: sanitizedPackageManagerEnv(),
|
|
768
797
|
});
|
|
769
798
|
if (packed.status !== 0) {
|
|
770
799
|
return { ok: false, code: 'REGISTRY_PACK_FAILED', detail: `${effectiveSpec}: ${(packed.stderr || packed.stdout || '').trim().slice(-300)}` };
|
|
@@ -960,17 +989,17 @@ export function npxStatus({
|
|
|
960
989
|
} catch { dshPlugin = 'unknown'; }
|
|
961
990
|
}
|
|
962
991
|
|
|
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';
|
|
992
|
+
const st = installer.installStatus
|
|
993
|
+
? installer.installStatus({ home, root: pointer?.path ?? runningPackageRoot() })
|
|
994
|
+
: realInstaller.installStatus({ home, root: pointer?.path ?? runningPackageRoot() });
|
|
995
|
+
const codex = st?.codex?.installed ? 'installed' : 'not installed';
|
|
996
|
+
const zcode = st?.zcode?.installed ? 'installed' : 'not installed';
|
|
997
|
+
const claude = st?.claude?.installed ? 'installed' : 'not installed';
|
|
969
998
|
const official = officialWebIntegrationStatus({ home, releaseDir: pointer?.path });
|
|
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';
|
|
999
|
+
const officialWeb = !official.enabled ? 'disabled' : official.healthy ? 'installed' : 'needs repair';
|
|
1000
|
+
const startupState = installer.windowsStartupStatus?.({ home });
|
|
1001
|
+
const windowsStartup = !startupState?.supported ? 'not supported'
|
|
1002
|
+
: startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
|
|
974
1003
|
|
|
975
1004
|
log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
|
|
976
1005
|
log(`Installed DSH Crew payload: ${installedLine}`);
|
|
@@ -986,10 +1015,10 @@ export function npxStatus({
|
|
|
986
1015
|
}
|
|
987
1016
|
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
|
|
988
1017
|
log(`Official 3080 UI bridge: ${officialWeb}`);
|
|
989
|
-
log(`Codex Desktop integration: ${codex}`);
|
|
990
|
-
log(`ZCode integration: ${zcode}`);
|
|
991
|
-
log(`Claude Code integration: ${claude}`);
|
|
992
|
-
log(`Windows login startup: ${windowsStartup}`);
|
|
1018
|
+
log(`Codex Desktop integration: ${codex}`);
|
|
1019
|
+
log(`ZCode integration: ${zcode}`);
|
|
1020
|
+
log(`Claude Code integration: ${claude}`);
|
|
1021
|
+
log(`Windows login startup: ${windowsStartup}`);
|
|
993
1022
|
|
|
994
1023
|
return {
|
|
995
1024
|
ok: true,
|
|
@@ -998,11 +1027,11 @@ export function npxStatus({
|
|
|
998
1027
|
installedPath: pointer?.path ?? null,
|
|
999
1028
|
dshPlugin,
|
|
1000
1029
|
officialWeb,
|
|
1001
|
-
codex,
|
|
1002
|
-
zcode,
|
|
1003
|
-
claude,
|
|
1004
|
-
windowsStartup,
|
|
1005
|
-
};
|
|
1030
|
+
codex,
|
|
1031
|
+
zcode,
|
|
1032
|
+
claude,
|
|
1033
|
+
windowsStartup,
|
|
1034
|
+
};
|
|
1006
1035
|
}
|
|
1007
1036
|
|
|
1008
1037
|
export async function npxUninstall({
|
|
@@ -1018,23 +1047,23 @@ export async function npxUninstall({
|
|
|
1018
1047
|
const pointer = readCurrentPointer({ home });
|
|
1019
1048
|
const name = pointer?.name ?? readManifest(runningPackageRoot())?.name;
|
|
1020
1049
|
|
|
1021
|
-
const cx = installer.uninstallCodex({ home });
|
|
1050
|
+
const cx = installer.uninstallCodex({ home });
|
|
1022
1051
|
if (cx.ok !== false) log('✓ Codex Desktop integration removed');
|
|
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
|
-
}
|
|
1052
|
+
else fail('Codex Desktop integration removal failed');
|
|
1030
1053
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1054
|
+
if (installer.uninstallZCode) {
|
|
1055
|
+
const zc = installer.uninstallZCode({ home });
|
|
1056
|
+
if (zc.ok !== false) log('✓ ZCode integration removed');
|
|
1057
|
+
else fail('ZCode integration removal failed');
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const cl = installer.uninstallClaudeCode ? await installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
|
|
1061
|
+
if (cl.ok !== false) log('✓ Claude Code integration removed');
|
|
1062
|
+
else fail('Claude Code integration removal failed');
|
|
1063
|
+
|
|
1064
|
+
const startup = installer.uninstallWindowsStartup?.({ home });
|
|
1065
|
+
if (startup?.ok === false) fail('Windows login startup removal failed');
|
|
1066
|
+
else if (startup?.supported) log('✓ Windows login startup removed');
|
|
1038
1067
|
|
|
1039
1068
|
const official = removeOfficialWebIntegration({ home, preserveIntent: !purge, remember: !purge });
|
|
1040
1069
|
if (!official.ok) fail(`official 3080 bridge removal failed (${official.code ?? 'unknown'})`);
|
|
@@ -1081,8 +1110,9 @@ Commands:
|
|
|
1081
1110
|
integrate show Crew inside the official 3080 UI; backend stays isolated on 3210
|
|
1082
1111
|
detach remove only the official 3080 bridge; isolated 3210 mode remains available
|
|
1083
1112
|
status read-only report of launcher/installed versions and integrations
|
|
1084
|
-
inspect print the machine-readable extension capability/readiness contract
|
|
1085
|
-
jobs machine-first job API: list|get|watch|cancel|submit
|
|
1113
|
+
inspect print the machine-readable extension capability/readiness contract
|
|
1114
|
+
jobs machine-first job API: list|get|watch|cancel|submit
|
|
1115
|
+
providers 3210 provider lifecycle API: list|delete-plan|delete|rollback|probe
|
|
1086
1116
|
update resolve the newest permitted package from the configured npm registry (or
|
|
1087
1117
|
--candidate), stage and validate it, then activate; idempotent when current
|
|
1088
1118
|
uninstall remove the Crew-managed payload, registration, and integrations (config kept)
|
|
@@ -1091,7 +1121,11 @@ Options:
|
|
|
1091
1121
|
--candidate <path> update from a local payload directory or packed .tgz instead of the registry
|
|
1092
1122
|
--after <sequence> with jobs watch/get: return canonical events after this cursor
|
|
1093
1123
|
--detail <mode> with jobs get/watch: compact (default) or full
|
|
1094
|
-
--request <path> with jobs submit: JSON Job Request document
|
|
1124
|
+
--request <path> with jobs submit: JSON Job Request document
|
|
1125
|
+
--plan <id> with providers delete: approved plan id
|
|
1126
|
+
--expected-revision <sha256> with providers delete: profile revision from delete-plan
|
|
1127
|
+
--replacement-default <id> with providers delete-plan: replacement Harness Default provider
|
|
1128
|
+
--confirm with providers delete: confirm the destructive mutation
|
|
1095
1129
|
--purge with uninstall: also remove ~/.config/dsh-crew config/backups (destructive)
|
|
1096
1130
|
--help show this help
|
|
1097
1131
|
|
|
@@ -1114,7 +1148,7 @@ export async function npxInspect({
|
|
|
1114
1148
|
return { ok: true, extension: body.extension };
|
|
1115
1149
|
}
|
|
1116
1150
|
|
|
1117
|
-
export async function npxJobs({
|
|
1151
|
+
export async function npxJobs({
|
|
1118
1152
|
args = [],
|
|
1119
1153
|
after = 0,
|
|
1120
1154
|
detail = 'compact',
|
|
@@ -1147,15 +1181,129 @@ export async function npxJobs({
|
|
|
1147
1181
|
const body = await response.json();
|
|
1148
1182
|
if (!response.ok || body?.ok === false) throw new Error(body?.error ?? 'Crew jobs API unavailable');
|
|
1149
1183
|
log(JSON.stringify(body, null, 2));
|
|
1150
|
-
return { ok: true, body };
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1184
|
+
return { ok: true, body };
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Call the isolated 3210 provider lifecycle API. The CLI never parses or
|
|
1189
|
+
* edits Harness YAML itself; the Hub remains the sole mutation authority.
|
|
1190
|
+
*/
|
|
1191
|
+
export async function npxProviders({
|
|
1192
|
+
args = [],
|
|
1193
|
+
planId,
|
|
1194
|
+
expectedRevision,
|
|
1195
|
+
replacementDefault,
|
|
1196
|
+
confirm = false,
|
|
1197
|
+
purgeOrphanCredentials = false,
|
|
1198
|
+
log = console.log,
|
|
1199
|
+
fetchImpl = globalThis.fetch,
|
|
1200
|
+
readConfig = realInstaller.readGlobalConfig,
|
|
1201
|
+
} = {}) {
|
|
1202
|
+
const configuredHubUrl = String(readConfig()?.hub_url ?? 'http://127.0.0.1:3210').replace(/\/$/, '');
|
|
1203
|
+
if (configuredHubUrl !== 'http://127.0.0.1:3210') throw new Error('providers CLI requires the isolated 3210 Crew Hub');
|
|
1204
|
+
const hubUrl = configuredHubUrl;
|
|
1205
|
+
const base = `${hubUrl}/_dsh/dsh-crew/providers`;
|
|
1206
|
+
const action = args[0] ?? 'list';
|
|
1207
|
+
const id = args[1];
|
|
1208
|
+
const options = new Map();
|
|
1209
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
1210
|
+
const value = args[index];
|
|
1211
|
+
if (!value?.startsWith('--')) continue;
|
|
1212
|
+
const [name, inline] = value.split('=', 2);
|
|
1213
|
+
if (name === '--confirm' || name === '--purge-orphan-credentials') options.set(name === '--confirm' ? 'confirm' : name, true);
|
|
1214
|
+
else if (inline !== undefined) options.set(name, inline);
|
|
1215
|
+
else if (args[index + 1] !== undefined) options.set(name, args[++index]);
|
|
1216
|
+
}
|
|
1217
|
+
const resolvedPlan = planId ?? options.get('--plan');
|
|
1218
|
+
const resolvedRevision = expectedRevision ?? options.get('--expected-revision');
|
|
1219
|
+
const resolvedReplacement = replacementDefault ?? options.get('--replacement-default');
|
|
1220
|
+
const resolvedConfirm = confirm === true || options.get('confirm') === true;
|
|
1221
|
+
if (purgeOrphanCredentials === true || options.get('--purge-orphan-credentials') === true) {
|
|
1222
|
+
throw new Error('credential purge requires a separate explicit confirmation flow');
|
|
1223
|
+
}
|
|
1224
|
+
let url = base;
|
|
1225
|
+
let init = { headers: { accept: 'application/json' } };
|
|
1226
|
+
if (action === 'list') {
|
|
1227
|
+
// keep defaults
|
|
1228
|
+
} else if (action === 'delete-plan') {
|
|
1229
|
+
if (!id) throw new Error('providers delete-plan requires a provider id');
|
|
1230
|
+
url = `${base}/${encodeURIComponent(id)}/delete-plan`;
|
|
1231
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ ...(resolvedReplacement ? { replacement_default: resolvedReplacement } : {}) }) };
|
|
1232
|
+
} else if (action === 'delete') {
|
|
1233
|
+
if (!id) throw new Error('providers delete requires a provider id');
|
|
1234
|
+
if (!resolvedPlan || !resolvedRevision || !resolvedConfirm) throw new Error('providers delete requires --plan, --expected-revision and --confirm');
|
|
1235
|
+
url = `${base}/${encodeURIComponent(id)}`;
|
|
1236
|
+
init = {
|
|
1237
|
+
method: 'DELETE',
|
|
1238
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1239
|
+
body: JSON.stringify({ plan_id: resolvedPlan, expected_revision: resolvedRevision, confirm: true }),
|
|
1240
|
+
};
|
|
1241
|
+
} else if (action === 'probe') {
|
|
1242
|
+
if (!id) throw new Error('providers probe requires a provider id');
|
|
1243
|
+
url = `${base}/${encodeURIComponent(id)}/probe`;
|
|
1244
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: '{}' };
|
|
1245
|
+
} else if (action === 'rollback') {
|
|
1246
|
+
if (!id) throw new Error('providers rollback requires a provider id');
|
|
1247
|
+
if (!resolvedPlan || !resolvedConfirm) throw new Error('providers rollback requires --plan and --confirm');
|
|
1248
|
+
url = `${base}/${encodeURIComponent(id)}/rollback`;
|
|
1249
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }) };
|
|
1250
|
+
} else {
|
|
1251
|
+
throw new Error(`unknown providers action: ${action}`);
|
|
1252
|
+
}
|
|
1253
|
+
const response = await fetchImpl(url, init);
|
|
1254
|
+
let body = await response.json();
|
|
1255
|
+
if (!response.ok || body?.ok === false) throw new Error(body?.code ?? body?.error ?? 'Crew provider API unavailable');
|
|
1256
|
+
if (action === 'delete' && body?.restart_required === true && body?.result?.state === 'RESTART_PENDING') {
|
|
1257
|
+
const restartResponse = await fetchImpl('http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart', {
|
|
1258
|
+
method: 'POST',
|
|
1259
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1260
|
+
body: JSON.stringify({ confirm: true }),
|
|
1261
|
+
});
|
|
1262
|
+
const restartBody = await restartResponse.json();
|
|
1263
|
+
if (!restartResponse.ok || restartBody?.ok !== true) throw new Error(restartBody?.code ?? restartBody?.error ?? 'Crew 3210 restart failed');
|
|
1264
|
+
const verifyUrl = `${base}/${encodeURIComponent(id)}/verify-delete`;
|
|
1265
|
+
const verifyResponse = await fetchImpl(verifyUrl, {
|
|
1266
|
+
method: 'POST',
|
|
1267
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1268
|
+
body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }),
|
|
1269
|
+
});
|
|
1270
|
+
const verifyBody = await verifyResponse.json();
|
|
1271
|
+
if (!verifyResponse.ok || verifyBody?.ok !== true) throw new Error(verifyBody?.code ?? verifyBody?.error ?? 'Crew provider deletion verification failed');
|
|
1272
|
+
body = { ...body, restart: restartBody, verification: verifyBody };
|
|
1273
|
+
}
|
|
1274
|
+
if (action === 'rollback' && body?.restart_required === true && body?.state === 'ROLLBACK_PENDING') {
|
|
1275
|
+
const restartResponse = await fetchImpl('http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart', {
|
|
1276
|
+
method: 'POST',
|
|
1277
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1278
|
+
body: JSON.stringify({ confirm: true }),
|
|
1279
|
+
});
|
|
1280
|
+
const restartBody = await restartResponse.json();
|
|
1281
|
+
if (!restartResponse.ok || restartBody?.ok !== true) throw new Error(restartBody?.code ?? restartBody?.error ?? 'Crew 3210 restart failed');
|
|
1282
|
+
const verifyUrl = `${base}/${encodeURIComponent(id)}/verify-rollback`;
|
|
1283
|
+
const verifyResponse = await fetchImpl(verifyUrl, {
|
|
1284
|
+
method: 'POST',
|
|
1285
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1286
|
+
body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }),
|
|
1287
|
+
});
|
|
1288
|
+
const verifyBody = await verifyResponse.json();
|
|
1289
|
+
if (!verifyResponse.ok || verifyBody?.ok !== true) throw new Error(verifyBody?.code ?? verifyBody?.error ?? 'Crew provider rollback verification failed');
|
|
1290
|
+
body = { ...body, restart: restartBody, verification: verifyBody };
|
|
1291
|
+
}
|
|
1292
|
+
log(JSON.stringify(body, null, 2));
|
|
1293
|
+
return { ok: true, body };
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function normalizeCommand(argv) {
|
|
1297
|
+
const flags = argv.slice(1);
|
|
1298
|
+
let candidate;
|
|
1299
|
+
let after = 0;
|
|
1300
|
+
let detail = 'compact';
|
|
1301
|
+
let request;
|
|
1302
|
+
let planId;
|
|
1303
|
+
let expectedRevision;
|
|
1304
|
+
let replacementDefault;
|
|
1305
|
+
let confirm = false;
|
|
1306
|
+
let purgeOrphanCredentials = false;
|
|
1159
1307
|
for (let index = 0; index < flags.length; index += 1) {
|
|
1160
1308
|
if (flags[index] === '--candidate') {
|
|
1161
1309
|
candidate = flags[index + 1];
|
|
@@ -1177,17 +1325,34 @@ function normalizeCommand(argv) {
|
|
|
1177
1325
|
after = Number(flags[index].slice('--after='.length)); flags.splice(index, 1); index -= 1;
|
|
1178
1326
|
} else if (flags[index]?.startsWith('--detail=')) {
|
|
1179
1327
|
detail = flags[index].slice('--detail='.length); flags.splice(index, 1); index -= 1;
|
|
1180
|
-
} else if (flags[index]?.startsWith('--request=')) {
|
|
1181
|
-
request = flags[index].slice('--request='.length); flags.splice(index, 1); index -= 1;
|
|
1182
|
-
}
|
|
1328
|
+
} else if (flags[index]?.startsWith('--request=')) {
|
|
1329
|
+
request = flags[index].slice('--request='.length); flags.splice(index, 1); index -= 1;
|
|
1330
|
+
} else if (flags[index] === '--plan' || flags[index] === '--expected-revision' || flags[index] === '--replacement-default') {
|
|
1331
|
+
const name = flags[index];
|
|
1332
|
+
const value = flags[index + 1];
|
|
1333
|
+
if (name === '--plan') planId = value;
|
|
1334
|
+
if (name === '--expected-revision') expectedRevision = value;
|
|
1335
|
+
if (name === '--replacement-default') replacementDefault = value;
|
|
1336
|
+
flags.splice(index, 2); index -= 1;
|
|
1337
|
+
} else if (flags[index]?.startsWith('--plan=')) {
|
|
1338
|
+
planId = flags[index].slice('--plan='.length); flags.splice(index, 1); index -= 1;
|
|
1339
|
+
} else if (flags[index]?.startsWith('--expected-revision=')) {
|
|
1340
|
+
expectedRevision = flags[index].slice('--expected-revision='.length); flags.splice(index, 1); index -= 1;
|
|
1341
|
+
} else if (flags[index]?.startsWith('--replacement-default=')) {
|
|
1342
|
+
replacementDefault = flags[index].slice('--replacement-default='.length); flags.splice(index, 1); index -= 1;
|
|
1343
|
+
} else if (flags[index] === '--confirm') {
|
|
1344
|
+
confirm = true; flags.splice(index, 1); index -= 1;
|
|
1345
|
+
} else if (flags[index] === '--purge-orphan-credentials') {
|
|
1346
|
+
purgeOrphanCredentials = true; flags.splice(index, 1); index -= 1;
|
|
1347
|
+
}
|
|
1183
1348
|
}
|
|
1184
1349
|
const knownFlags = new Set(['--purge']);
|
|
1185
1350
|
const unknown = flags.filter((f) => f.startsWith('--') && !knownFlags.has(f));
|
|
1186
1351
|
const args = flags.filter((f) => !f.startsWith('--'));
|
|
1187
1352
|
if (!Number.isInteger(after) || after < 0) unknown.push('--after');
|
|
1188
1353
|
if (!['compact', 'full'].includes(detail)) unknown.push('--detail');
|
|
1189
|
-
return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, args, unknown };
|
|
1190
|
-
}
|
|
1354
|
+
return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown };
|
|
1355
|
+
}
|
|
1191
1356
|
|
|
1192
1357
|
/**
|
|
1193
1358
|
* CLI dispatcher used by bin/dsh-crew.mjs. Returns a process exit code.
|
|
@@ -1198,7 +1363,7 @@ export async function runNpxCli({
|
|
|
1198
1363
|
error = console.error,
|
|
1199
1364
|
commands = {},
|
|
1200
1365
|
} = {}) {
|
|
1201
|
-
const { command, purge, candidate, after, detail, request, args, unknown } = normalizeCommand(argv);
|
|
1366
|
+
const { command, purge, candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown } = normalizeCommand(argv);
|
|
1202
1367
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
1203
1368
|
log(USAGE);
|
|
1204
1369
|
return 0;
|
|
@@ -1207,7 +1372,7 @@ export async function runNpxCli({
|
|
|
1207
1372
|
error(USAGE);
|
|
1208
1373
|
return 1;
|
|
1209
1374
|
}
|
|
1210
|
-
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'inspect', 'jobs', 'update', 'uninstall'].includes(command)) {
|
|
1375
|
+
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'inspect', 'jobs', 'providers', 'update', 'uninstall'].includes(command)) {
|
|
1211
1376
|
error(`unknown command: ${command ?? '<none>'}\n\n${USAGE}`);
|
|
1212
1377
|
return 1;
|
|
1213
1378
|
}
|
|
@@ -1217,15 +1382,17 @@ export async function runNpxCli({
|
|
|
1217
1382
|
integrate: commands.integrate ?? npxIntegrate,
|
|
1218
1383
|
detach: commands.detach ?? npxDetach,
|
|
1219
1384
|
status: commands.status ?? npxStatus,
|
|
1220
|
-
inspect: commands.inspect ?? npxInspect,
|
|
1221
|
-
jobs: commands.jobs ?? npxJobs,
|
|
1385
|
+
inspect: commands.inspect ?? npxInspect,
|
|
1386
|
+
jobs: commands.jobs ?? npxJobs,
|
|
1387
|
+
providers: commands.providers ?? npxProviders,
|
|
1222
1388
|
update: commands.update ?? npxUpdate,
|
|
1223
1389
|
uninstall: commands.uninstall ?? npxUninstall,
|
|
1224
1390
|
};
|
|
1225
1391
|
let result;
|
|
1226
1392
|
if (command === 'uninstall') result = await actions.uninstall({ purge, log });
|
|
1227
|
-
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
1228
|
-
else if (command === 'jobs') result = await actions.jobs({ args, after, detail, request, log });
|
|
1393
|
+
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
1394
|
+
else if (command === 'jobs') result = await actions.jobs({ args, after, detail, request, log });
|
|
1395
|
+
else if (command === 'providers') result = await actions.providers({ args, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, log });
|
|
1229
1396
|
else result = await actions[command]({ log });
|
|
1230
1397
|
return result?.ok === false ? 1 : 0;
|
|
1231
1398
|
} catch (err) {
|
package/src/job-contracts.mjs
CHANGED
|
@@ -119,7 +119,7 @@ function compactSelectionTrace(attempts) {
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function changedFilesFromView(view) {
|
|
122
|
+
function changedFilesFromView(view) {
|
|
123
123
|
if (Array.isArray(view?.candidate?.changed_files)) return view.candidate.changed_files;
|
|
124
124
|
const changes = view?.workspace_diff?.changes;
|
|
125
125
|
if (!changes || typeof changes !== 'object') return [];
|
|
@@ -129,7 +129,22 @@ function changedFilesFromView(view) {
|
|
|
129
129
|
...(Array.isArray(changes.renamed) ? changes.renamed : []),
|
|
130
130
|
...(Array.isArray(changes.untracked) ? changes.untracked : []),
|
|
131
131
|
])];
|
|
132
|
-
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function boundedExecutionContext(value) {
|
|
135
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
136
|
+
const executionPlane = boundedText(value.execution_plane, 64);
|
|
137
|
+
const profile = boundedText(value.profile, 64);
|
|
138
|
+
const runtimeId = boundedText(value.runtime_id, 128);
|
|
139
|
+
const port = Number.isInteger(value.listen_port) ? value.listen_port : null;
|
|
140
|
+
if (!executionPlane && !profile && !runtimeId && port === null) return null;
|
|
141
|
+
return {
|
|
142
|
+
execution_plane: executionPlane,
|
|
143
|
+
profile,
|
|
144
|
+
listen_port: port,
|
|
145
|
+
runtime_id: runtimeId,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
133
148
|
|
|
134
149
|
/**
|
|
135
150
|
* Build the machine-first Result Contract for a workflow.
|
|
@@ -162,7 +177,8 @@ export function buildEvidenceEnvelope(view = {}) {
|
|
|
162
177
|
delivery_complete: outcome.delivery?.complete === true,
|
|
163
178
|
review_verdict: review?.verdict ?? null,
|
|
164
179
|
},
|
|
165
|
-
selection_trace: compactSelectionTrace(selectionAttempts),
|
|
180
|
+
selection_trace: compactSelectionTrace(selectionAttempts),
|
|
181
|
+
execution_context: boundedExecutionContext(view.execution_context ?? selectionAttempts[0]?.execution_context),
|
|
166
182
|
changed_files: changedFiles,
|
|
167
183
|
changes: boundedStrings(outcome.changes),
|
|
168
184
|
tests: boundedTests(outcome.tests),
|
package/src/jobs.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { createRequire } from 'node:module';
|
|
|
12
12
|
import { appendDeliveryInstructions, parseDeliveryReport, formatDeliveryMetadata } from './delivery.mjs';
|
|
13
13
|
import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY } from './workspace-audit.mjs';
|
|
14
14
|
import { buildOutcome, JOB_PHASES } from './workflow.mjs';
|
|
15
|
+
import { raceWaiters } from './removable-waiter.mjs';
|
|
15
16
|
import { buildDirectSelectionTrace } from './model-routing.mjs';
|
|
16
17
|
|
|
17
18
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -255,10 +256,7 @@ export async function waitJob(id, timeoutMs) {
|
|
|
255
256
|
const job = jobs.get(id);
|
|
256
257
|
if (!job) throw new Error(`no such job: ${id}`);
|
|
257
258
|
if (job.status !== 'running') return job;
|
|
258
|
-
await
|
|
259
|
-
new Promise((res) => job.waiters.push(res)),
|
|
260
|
-
timeoutMs ? new Promise((res) => setTimeout(res, timeoutMs)) : new Promise(() => {}),
|
|
261
|
-
]);
|
|
259
|
+
await raceWaiters(job.waiters, { timeoutMs });
|
|
262
260
|
return job;
|
|
263
261
|
}
|
|
264
262
|
|