@ran-sh/dsh-crew 0.5.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -7
- package/README.zh.md +18 -7
- package/docs/installation.md +1 -1
- package/docs/ui-surfaces.md +65 -46
- package/lib/client.js +565 -28
- package/official-web-bridge/lib/client.js +565 -28
- package/package.json +1 -1
- package/src/client/collapsible-sections.mjs +3 -2
- package/src/client/host-readiness.mjs +16 -3
- package/src/client/index.tsx +331 -54
- package/src/config-readiness.mjs +66 -5
- package/src/credential-lifecycle.mjs +73 -0
- package/src/credential-purge-adapters.mjs +127 -0
- package/src/credential-purge-state.mjs +59 -0
- package/src/credential-reference-inventory.mjs +78 -0
- package/src/dsh-cli-runtime.mjs +36 -9
- package/src/extension-contract.mjs +16 -10
- package/src/hub/index.mjs +1004 -438
- package/src/hub-client.mjs +25 -5
- package/src/hub-compatibility.mjs +23 -7
- package/src/install/install.mjs +4 -4
- package/src/install/npx-lifecycle.mjs +495 -134
- package/src/job-contracts.mjs +27 -6
- 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 +248 -95
- package/src/policy.mjs +62 -31
- package/src/provider-config-scrub.mjs +71 -0
- package/src/provider-delete-adapters.mjs +451 -0
- package/src/provider-desired-state.mjs +37 -0
- package/src/provider-health.mjs +124 -0
- package/src/provider-inventory.mjs +132 -0
- package/src/provider-lifecycle-state.mjs +95 -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 +66 -21
- package/src/runtime-readiness-snapshot.mjs +87 -0
- package/src/server.mjs +64 -21
- 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
|
@@ -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';
|
|
@@ -48,24 +49,25 @@ import {
|
|
|
48
49
|
|
|
49
50
|
export const CREW_APP_DIRNAME = 'app';
|
|
50
51
|
export const RELEASES_DIRNAME = 'releases';
|
|
51
|
-
export const CURRENT_POINTER_FILENAME = 'current.json';
|
|
52
|
+
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
55
|
+
const CREW_ROUTE_BASE = '/_dsh/dsh-crew';
|
|
56
|
+
|
|
57
|
+
export function npmCliInvocation(args, {
|
|
58
|
+
platform = process.platform,
|
|
59
|
+
environment = process.env,
|
|
60
|
+
} = {}) {
|
|
61
|
+
if (platform !== 'win32') return { command: 'npm', args: [...args] };
|
|
62
|
+
const unsafe = args.find((arg) => /[\0\r\n"%!^&|<>]/.test(String(arg)));
|
|
63
|
+
if (unsafe !== undefined) throw new Error(`unsafe npm argument: ${unsafe}`);
|
|
64
|
+
const commandLine = ['npm.cmd', ...args.map((arg) => `"${String(arg)}"`)].join(' ');
|
|
65
|
+
return {
|
|
66
|
+
command: environment.ComSpec || environment.COMSPEC || 'cmd.exe',
|
|
67
|
+
args: ['/d', '/s', '/c', commandLine],
|
|
68
|
+
windowsVerbatimArguments: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
69
71
|
|
|
70
72
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
71
73
|
|
|
@@ -232,18 +234,18 @@ export function copyProductionDependencyTree({
|
|
|
232
234
|
return { copied, missing, missingOptional };
|
|
233
235
|
}
|
|
234
236
|
|
|
235
|
-
function defaultNpmInstaller(stageRoot, log) {
|
|
236
|
-
const npmArgs = [
|
|
237
|
-
'install', '--prefix', stageRoot,
|
|
238
|
-
'--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund',
|
|
239
|
-
'--no-package-lock', '--loglevel=error',
|
|
240
|
-
];
|
|
241
|
-
const invocation = npmCliInvocation(npmArgs);
|
|
242
|
-
const result = spawnSync(invocation.command, invocation.args, {
|
|
243
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 600_000,
|
|
244
|
-
windowsHide: true, windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
245
|
-
env: sanitizedPackageManagerEnv(),
|
|
246
|
-
});
|
|
237
|
+
function defaultNpmInstaller(stageRoot, log) {
|
|
238
|
+
const npmArgs = [
|
|
239
|
+
'install', '--prefix', stageRoot,
|
|
240
|
+
'--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund',
|
|
241
|
+
'--no-package-lock', '--loglevel=error',
|
|
242
|
+
];
|
|
243
|
+
const invocation = npmCliInvocation(npmArgs);
|
|
244
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
245
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 600_000,
|
|
246
|
+
windowsHide: true, windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
247
|
+
env: sanitizedPackageManagerEnv(),
|
|
248
|
+
});
|
|
247
249
|
if (result.status !== 0) {
|
|
248
250
|
log(`- npm fallback install failed (${result.status}): ${(result.stderr || result.stdout || '').trim().slice(-300)}`);
|
|
249
251
|
return false;
|
|
@@ -476,7 +478,7 @@ function commitStagedRelease({ stageDir, manifest, home }) {
|
|
|
476
478
|
|
|
477
479
|
const STALE_INCOMPLETE_MS = 24 * 60 * 60 * 1000;
|
|
478
480
|
|
|
479
|
-
function gcOldReleases({ home, keep = KEEP_RELEASES }) {
|
|
481
|
+
function gcOldReleases({ home, keep = KEEP_RELEASES }) {
|
|
480
482
|
const pointer = readCurrentPointer({ home });
|
|
481
483
|
const releasesDir = crewReleasesDir({ home });
|
|
482
484
|
if (!existsSync(releasesDir)) return;
|
|
@@ -505,9 +507,123 @@ function gcOldReleases({ home, keep = KEEP_RELEASES }) {
|
|
|
505
507
|
try { rmSync(victim, { recursive: true, force: true }); removed.push(victim); } catch { /* best effort */ }
|
|
506
508
|
}
|
|
507
509
|
return removed;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function listManagedReleases({ home = homedir() } = {}) {
|
|
513
|
+
const pointer = readCurrentPointer({ home });
|
|
514
|
+
const root = crewReleasesDir({ home });
|
|
515
|
+
if (!existsSync(root)) return [];
|
|
516
|
+
return readdirSync(root, { withFileTypes: true })
|
|
517
|
+
.filter((entry) => entry.isDirectory())
|
|
518
|
+
.map((entry) => {
|
|
519
|
+
const path = join(root, entry.name);
|
|
520
|
+
const manifest = readManifest(path);
|
|
521
|
+
if (!manifest?.name || !manifest?.version) return null;
|
|
522
|
+
const validation = validateInstalledPayload(path, { expectedName: manifest.name, expectedVersion: manifest.version });
|
|
523
|
+
return {
|
|
524
|
+
name: manifest.name,
|
|
525
|
+
version: manifest.version,
|
|
526
|
+
path,
|
|
527
|
+
current: pointer?.path === path,
|
|
528
|
+
healthy: validation.ok,
|
|
529
|
+
};
|
|
530
|
+
})
|
|
531
|
+
.filter(Boolean)
|
|
532
|
+
.sort((a, b) => compareVersions(b.version, a.version) || b.path.localeCompare(a.path));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function restartOwnedRuntime(fetchImpl = globalThis.fetch) {
|
|
536
|
+
const response = await fetchImpl('http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart', {
|
|
537
|
+
method: 'POST',
|
|
538
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
539
|
+
body: JSON.stringify({ confirm: true }),
|
|
540
|
+
});
|
|
541
|
+
const body = await response.json();
|
|
542
|
+
return response.ok && body?.ok === true ? body : { ok: false, code: body?.code ?? 'CREW_3210_RESTART_FAILED' };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function verifyRuntimeVersion(version, fetchImpl = globalThis.fetch) {
|
|
546
|
+
const response = await fetchImpl('http://127.0.0.1:3210/_dsh/dsh-crew/runtime', { headers: { accept: 'application/json' } });
|
|
547
|
+
const body = await response.json();
|
|
548
|
+
return response.ok && body?.ok === true && body.runtime_version === version
|
|
549
|
+
? { ok: true, runtime_version: body.runtime_version }
|
|
550
|
+
: { ok: false, code: 'RUNTIME_VERSION_MISMATCH' };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export async function npxReleases({ home = homedir(), log = console.log } = {}) {
|
|
554
|
+
const releases = listManagedReleases({ home });
|
|
555
|
+
log(JSON.stringify(releases, null, 2));
|
|
556
|
+
return { ok: true, releases };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function npxRollback({
|
|
560
|
+
home = homedir(),
|
|
561
|
+
version,
|
|
562
|
+
log = console.log,
|
|
563
|
+
installer = realInstaller,
|
|
564
|
+
ensureRuntime,
|
|
565
|
+
validatePayload = validateInstalledPayload,
|
|
566
|
+
activate,
|
|
567
|
+
restart = () => restartOwnedRuntime(),
|
|
568
|
+
verifyRuntime = (targetVersion) => verifyRuntimeVersion(targetVersion),
|
|
569
|
+
} = {}) {
|
|
570
|
+
const targetVersion = typeof version === 'string' ? version.trim() : '';
|
|
571
|
+
if (!targetVersion) return { ok: false, error: 'rollback requires a target version' };
|
|
572
|
+
const current = readCurrentPointer({ home });
|
|
573
|
+
if (!current?.path || !existsSync(current.path)) return { ok: false, error: 'no active Crew payload to roll back' };
|
|
574
|
+
const target = listManagedReleases({ home }).find((release) => release.version === targetVersion);
|
|
575
|
+
if (!target) return { ok: false, error: `retained release ${targetVersion} was not found` };
|
|
576
|
+
if (target.path === current.path) return { ok: true, idempotent: true, version: target.version, path: target.path };
|
|
577
|
+
const targetManifest = readManifest(target.path);
|
|
578
|
+
const validation = validatePayload(target.path, { expectedName: targetManifest?.name, expectedVersion: targetManifest?.version });
|
|
579
|
+
if (!validation.ok) return { ok: false, error: 'target release failed payload validation' };
|
|
580
|
+
const previousManifest = readManifest(current.path);
|
|
581
|
+
const activateReleaseFn = activate ?? (({ releaseDir, manifest }) => activateRelease({ home, releaseDir, manifest, log, installer, ensureRuntime }));
|
|
582
|
+
const switchPointer = (release) => writeCurrentPointer({ home, name: release.name, version: release.version, path: release.path });
|
|
583
|
+
try {
|
|
584
|
+
switchPointer(target);
|
|
585
|
+
if (!await activateReleaseFn({ releaseDir: target.path, manifest: targetManifest })) throw new Error('target release activation failed');
|
|
586
|
+
const restarted = await restart(target.version);
|
|
587
|
+
if (restarted?.ok === false) throw Object.assign(new Error('target runtime restart failed'), { code: restarted.code });
|
|
588
|
+
const runtime = await verifyRuntime(target.version);
|
|
589
|
+
if (runtime?.ok !== true) throw Object.assign(new Error('target runtime verification failed'), { code: runtime?.code ?? 'RUNTIME_VERSION_MISMATCH' });
|
|
590
|
+
log(`✓ rolled back Crew payload to ${target.version}`);
|
|
591
|
+
return { ok: true, rolled_back: true, version: target.version, path: target.path, restart: restarted, runtime };
|
|
592
|
+
} catch (error) {
|
|
593
|
+
const prior = { name: current.name, version: current.version, path: current.path };
|
|
594
|
+
let recovery;
|
|
595
|
+
try {
|
|
596
|
+
switchPointer(prior);
|
|
597
|
+
if (!previousManifest) throw Object.assign(new Error('previous release manifest unavailable'), { stage: 'activation' });
|
|
598
|
+
const activated = await activateReleaseFn({ releaseDir: prior.path, manifest: previousManifest });
|
|
599
|
+
if (activated !== true) throw Object.assign(new Error('previous release activation failed'), { stage: 'activation' });
|
|
600
|
+
const restarted = await restart(prior.version);
|
|
601
|
+
if (restarted?.ok !== true) throw Object.assign(new Error('previous runtime restart failed'), { stage: 'restart' });
|
|
602
|
+
const runtime = await verifyRuntime(prior.version);
|
|
603
|
+
if (runtime?.ok !== true) throw Object.assign(new Error('previous runtime verification failed'), { stage: 'verification' });
|
|
604
|
+
const restoredPointer = readCurrentPointer({ home });
|
|
605
|
+
if (restoredPointer?.path !== prior.path || restoredPointer.version !== prior.version) {
|
|
606
|
+
throw Object.assign(new Error('previous release pointer was not restored'), { stage: 'pointer' });
|
|
607
|
+
}
|
|
608
|
+
recovery = { ok: true, version: prior.version, path: prior.path };
|
|
609
|
+
} catch (recoveryError) {
|
|
610
|
+
recovery = {
|
|
611
|
+
ok: false,
|
|
612
|
+
code: 'RELEASE_ROLLBACK_RECOVERY_FAILED',
|
|
613
|
+
stage: ['activation', 'restart', 'verification', 'pointer'].includes(recoveryError?.stage) ? recoveryError.stage : 'unknown',
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
ok: false,
|
|
618
|
+
error: error?.message ?? 'release rollback failed',
|
|
619
|
+
code: error?.code ?? 'RELEASE_ROLLBACK_FAILED',
|
|
620
|
+
restored: recovery.ok === true,
|
|
621
|
+
recovery,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function registrationLinkPath({ home, name }) {
|
|
511
627
|
return join(crewProfileDir({ home }), 'node_modules', ...name.split('/'));
|
|
512
628
|
}
|
|
513
629
|
|
|
@@ -569,30 +685,30 @@ async function activateRelease({ home, releaseDir, manifest, log, installer }) {
|
|
|
569
685
|
}
|
|
570
686
|
log(`✓ Harness plugin registered (dedicated dsh-crew profile → ${releaseDir})`);
|
|
571
687
|
|
|
572
|
-
const codex = installer.installCodex({ home, root: releaseDir });
|
|
688
|
+
const codex = installer.installCodex({ home, root: releaseDir });
|
|
573
689
|
if (codex.ok === false) {
|
|
574
690
|
log(`✗ Codex Desktop integration failed: ${(codex.actions ?? []).join('; ')}`);
|
|
575
691
|
return false;
|
|
576
692
|
}
|
|
577
|
-
log('✓ Codex Desktop integration');
|
|
578
|
-
|
|
579
|
-
if (installer.installZCode) {
|
|
580
|
-
const zcode = installer.installZCode({ home, root: releaseDir });
|
|
581
|
-
if (zcode.ok === false) {
|
|
582
|
-
log(`✗ ZCode integration failed (${zcode.code ?? 'unknown'})`);
|
|
583
|
-
return false;
|
|
584
|
-
}
|
|
585
|
-
log('✓ ZCode integration');
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const startup = installer.installWindowsStartup?.({ home, root: releaseDir });
|
|
589
|
-
if (startup?.ok === false) {
|
|
590
|
-
log(`✗ Windows login startup failed (${startup.code ?? 'unknown'})`);
|
|
591
|
-
return false;
|
|
592
|
-
}
|
|
593
|
-
if (startup?.supported) log('✓ Windows login startup');
|
|
594
|
-
|
|
595
|
-
const claude = await installer.installClaudeCode({ home, root: releaseDir });
|
|
693
|
+
log('✓ Codex Desktop integration');
|
|
694
|
+
|
|
695
|
+
if (installer.installZCode) {
|
|
696
|
+
const zcode = installer.installZCode({ home, root: releaseDir });
|
|
697
|
+
if (zcode.ok === false) {
|
|
698
|
+
log(`✗ ZCode integration failed (${zcode.code ?? 'unknown'})`);
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
log('✓ ZCode integration');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const startup = installer.installWindowsStartup?.({ home, root: releaseDir });
|
|
705
|
+
if (startup?.ok === false) {
|
|
706
|
+
log(`✗ Windows login startup failed (${startup.code ?? 'unknown'})`);
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
if (startup?.supported) log('✓ Windows login startup');
|
|
710
|
+
|
|
711
|
+
const claude = await installer.installClaudeCode({ home, root: releaseDir });
|
|
596
712
|
if (claude.ok === false) {
|
|
597
713
|
log(`✗ Claude Code integration failed`);
|
|
598
714
|
return false;
|
|
@@ -700,8 +816,14 @@ export const UPDATE_DEFAULT_SPEC = `${UPDATE_PACKAGE_NAME}@latest`;
|
|
|
700
816
|
*/
|
|
701
817
|
export function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } = {}) {
|
|
702
818
|
mkdirSync(destDir, { recursive: true });
|
|
703
|
-
|
|
704
|
-
|
|
819
|
+
// GNU tar interprets a `C:\...` argument as a remote rsh host ("Cannot
|
|
820
|
+
// connect to C:"), so anchor the invocation inside destDir and pass the
|
|
821
|
+
// archive by basename: every argument stays colon-free for both bsdtar
|
|
822
|
+
// (Windows 10+) and GNU tar, whatever PATH resolves first.
|
|
823
|
+
const localArchive = join(destDir, 'candidate.tgz');
|
|
824
|
+
copyFileSync(tgzPath, localArchive);
|
|
825
|
+
const result = runner('tar', ['-xzf', 'candidate.tgz'], {
|
|
826
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, cwd: destDir,
|
|
705
827
|
});
|
|
706
828
|
const root = join(destDir, 'package');
|
|
707
829
|
if (result.status !== 0 || !existsSync(join(root, 'package.json'))) {
|
|
@@ -779,15 +901,15 @@ export function resolveUpdateCandidate({
|
|
|
779
901
|
mkdirSync(tmpDir, { recursive: true });
|
|
780
902
|
try {
|
|
781
903
|
const effectiveSpec = process.env.DSH_CREW_UPDATE_SPEC ?? spec;
|
|
782
|
-
const packArgs = ['pack', effectiveSpec, '--pack-destination', tmpDir, '--json', '--loglevel=error'];
|
|
783
|
-
log(`- resolving update candidate from the configured npm registry (${effectiveSpec})`);
|
|
784
|
-
const invocation = npmCliInvocation(packArgs);
|
|
785
|
-
const packed = runner(invocation.command, invocation.args, {
|
|
786
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
787
|
-
timeout: 600_000, windowsHide: true,
|
|
788
|
-
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
789
|
-
env: sanitizedPackageManagerEnv(),
|
|
790
|
-
});
|
|
904
|
+
const packArgs = ['pack', effectiveSpec, '--pack-destination', tmpDir, '--json', '--loglevel=error'];
|
|
905
|
+
log(`- resolving update candidate from the configured npm registry (${effectiveSpec})`);
|
|
906
|
+
const invocation = npmCliInvocation(packArgs);
|
|
907
|
+
const packed = runner(invocation.command, invocation.args, {
|
|
908
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
909
|
+
timeout: 600_000, windowsHide: true,
|
|
910
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
911
|
+
env: sanitizedPackageManagerEnv(),
|
|
912
|
+
});
|
|
791
913
|
if (packed.status !== 0) {
|
|
792
914
|
return { ok: false, code: 'REGISTRY_PACK_FAILED', detail: `${effectiveSpec}: ${(packed.stderr || packed.stdout || '').trim().slice(-300)}` };
|
|
793
915
|
}
|
|
@@ -982,17 +1104,17 @@ export function npxStatus({
|
|
|
982
1104
|
} catch { dshPlugin = 'unknown'; }
|
|
983
1105
|
}
|
|
984
1106
|
|
|
985
|
-
const st = installer.installStatus
|
|
986
|
-
? installer.installStatus({ home, root: pointer?.path ?? runningPackageRoot() })
|
|
987
|
-
: realInstaller.installStatus({ home, root: pointer?.path ?? runningPackageRoot() });
|
|
988
|
-
const codex = st?.codex?.installed ? 'installed' : 'not installed';
|
|
989
|
-
const zcode = st?.zcode?.installed ? 'installed' : 'not installed';
|
|
990
|
-
const claude = st?.claude?.installed ? 'installed' : 'not installed';
|
|
1107
|
+
const st = installer.installStatus
|
|
1108
|
+
? installer.installStatus({ home, root: pointer?.path ?? runningPackageRoot() })
|
|
1109
|
+
: realInstaller.installStatus({ home, root: pointer?.path ?? runningPackageRoot() });
|
|
1110
|
+
const codex = st?.codex?.installed ? 'installed' : 'not installed';
|
|
1111
|
+
const zcode = st?.zcode?.installed ? 'installed' : 'not installed';
|
|
1112
|
+
const claude = st?.claude?.installed ? 'installed' : 'not installed';
|
|
991
1113
|
const official = officialWebIntegrationStatus({ home, releaseDir: pointer?.path });
|
|
992
|
-
const officialWeb = !official.enabled ? 'disabled' : official.healthy ? 'installed' : 'needs repair';
|
|
993
|
-
const startupState = installer.windowsStartupStatus?.({ home });
|
|
994
|
-
const windowsStartup = !startupState?.supported ? 'not supported'
|
|
995
|
-
: startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
|
|
1114
|
+
const officialWeb = !official.enabled ? 'disabled' : official.healthy ? 'installed' : 'needs repair';
|
|
1115
|
+
const startupState = installer.windowsStartupStatus?.({ home });
|
|
1116
|
+
const windowsStartup = !startupState?.supported ? 'not supported'
|
|
1117
|
+
: startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
|
|
996
1118
|
|
|
997
1119
|
log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
|
|
998
1120
|
log(`Installed DSH Crew payload: ${installedLine}`);
|
|
@@ -1008,10 +1130,10 @@ export function npxStatus({
|
|
|
1008
1130
|
}
|
|
1009
1131
|
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
|
|
1010
1132
|
log(`Official 3080 UI bridge: ${officialWeb}`);
|
|
1011
|
-
log(`Codex Desktop integration: ${codex}`);
|
|
1012
|
-
log(`ZCode integration: ${zcode}`);
|
|
1013
|
-
log(`Claude Code integration: ${claude}`);
|
|
1014
|
-
log(`Windows login startup: ${windowsStartup}`);
|
|
1133
|
+
log(`Codex Desktop integration: ${codex}`);
|
|
1134
|
+
log(`ZCode integration: ${zcode}`);
|
|
1135
|
+
log(`Claude Code integration: ${claude}`);
|
|
1136
|
+
log(`Windows login startup: ${windowsStartup}`);
|
|
1015
1137
|
|
|
1016
1138
|
return {
|
|
1017
1139
|
ok: true,
|
|
@@ -1020,11 +1142,11 @@ export function npxStatus({
|
|
|
1020
1142
|
installedPath: pointer?.path ?? null,
|
|
1021
1143
|
dshPlugin,
|
|
1022
1144
|
officialWeb,
|
|
1023
|
-
codex,
|
|
1024
|
-
zcode,
|
|
1025
|
-
claude,
|
|
1026
|
-
windowsStartup,
|
|
1027
|
-
};
|
|
1145
|
+
codex,
|
|
1146
|
+
zcode,
|
|
1147
|
+
claude,
|
|
1148
|
+
windowsStartup,
|
|
1149
|
+
};
|
|
1028
1150
|
}
|
|
1029
1151
|
|
|
1030
1152
|
export async function npxUninstall({
|
|
@@ -1040,23 +1162,23 @@ export async function npxUninstall({
|
|
|
1040
1162
|
const pointer = readCurrentPointer({ home });
|
|
1041
1163
|
const name = pointer?.name ?? readManifest(runningPackageRoot())?.name;
|
|
1042
1164
|
|
|
1043
|
-
const cx = installer.uninstallCodex({ home });
|
|
1165
|
+
const cx = installer.uninstallCodex({ home });
|
|
1044
1166
|
if (cx.ok !== false) log('✓ Codex Desktop integration removed');
|
|
1045
|
-
else fail('Codex Desktop integration removal failed');
|
|
1046
|
-
|
|
1047
|
-
if (installer.uninstallZCode) {
|
|
1048
|
-
const zc = installer.uninstallZCode({ home });
|
|
1049
|
-
if (zc.ok !== false) log('✓ ZCode integration removed');
|
|
1050
|
-
else fail('ZCode integration removal failed');
|
|
1051
|
-
}
|
|
1167
|
+
else fail('Codex Desktop integration removal failed');
|
|
1052
1168
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1169
|
+
if (installer.uninstallZCode) {
|
|
1170
|
+
const zc = installer.uninstallZCode({ home });
|
|
1171
|
+
if (zc.ok !== false) log('✓ ZCode integration removed');
|
|
1172
|
+
else fail('ZCode integration removal failed');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const cl = installer.uninstallClaudeCode ? await installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
|
|
1176
|
+
if (cl.ok !== false) log('✓ Claude Code integration removed');
|
|
1177
|
+
else fail('Claude Code integration removal failed');
|
|
1178
|
+
|
|
1179
|
+
const startup = installer.uninstallWindowsStartup?.({ home });
|
|
1180
|
+
if (startup?.ok === false) fail('Windows login startup removal failed');
|
|
1181
|
+
else if (startup?.supported) log('✓ Windows login startup removed');
|
|
1060
1182
|
|
|
1061
1183
|
const official = removeOfficialWebIntegration({ home, preserveIntent: !purge, remember: !purge });
|
|
1062
1184
|
if (!official.ok) fail(`official 3080 bridge removal failed (${official.code ?? 'unknown'})`);
|
|
@@ -1103,8 +1225,12 @@ Commands:
|
|
|
1103
1225
|
integrate show Crew inside the official 3080 UI; backend stays isolated on 3210
|
|
1104
1226
|
detach remove only the official 3080 bridge; isolated 3210 mode remains available
|
|
1105
1227
|
status read-only report of launcher/installed versions and integrations
|
|
1106
|
-
inspect print the machine-readable extension capability/readiness contract
|
|
1107
|
-
jobs machine-first job API: list|get|watch|cancel|submit
|
|
1228
|
+
inspect print the machine-readable extension capability/readiness contract
|
|
1229
|
+
jobs machine-first job API: list|get|watch|cancel|submit
|
|
1230
|
+
providers 3210 provider lifecycle API: list|delete-plan|delete|rollback|probe
|
|
1231
|
+
credentials 3210 credential references: list|purge-plan|purge (separate confirmation)
|
|
1232
|
+
releases list retained, validated Crew payload releases
|
|
1233
|
+
rollback switch to a retained payload version and verify the 3210 runtime
|
|
1108
1234
|
update resolve the newest permitted package from the configured npm registry (or
|
|
1109
1235
|
--candidate), stage and validate it, then activate; idempotent when current
|
|
1110
1236
|
uninstall remove the Crew-managed payload, registration, and integrations (config kept)
|
|
@@ -1113,20 +1239,25 @@ Options:
|
|
|
1113
1239
|
--candidate <path> update from a local payload directory or packed .tgz instead of the registry
|
|
1114
1240
|
--after <sequence> with jobs watch/get: return canonical events after this cursor
|
|
1115
1241
|
--detail <mode> with jobs get/watch: compact (default) or full
|
|
1116
|
-
--request <path> with jobs submit: JSON Job Request document
|
|
1242
|
+
--request <path> with jobs submit: JSON Job Request document
|
|
1243
|
+
--plan <id> with providers/credentials destructive actions: approved plan id
|
|
1244
|
+
--expected-revision <sha256> with providers/credentials actions: revision from plan
|
|
1245
|
+
--replacement-default <id> with providers delete-plan: replacement Harness Default provider
|
|
1246
|
+
--confirm with providers delete: confirm the destructive mutation
|
|
1117
1247
|
--purge with uninstall: also remove ~/.config/dsh-crew config/backups (destructive)
|
|
1118
1248
|
--help show this help
|
|
1119
1249
|
|
|
1120
1250
|
Primary install: npm install -g @ran-sh/dsh-crew (then run: dsh-crew install)
|
|
1121
1251
|
Source checkouts use scripts/setup.mjs instead.`;
|
|
1122
1252
|
|
|
1123
|
-
export async function npxInspect({
|
|
1253
|
+
export async function npxInspect({
|
|
1124
1254
|
log = console.log,
|
|
1125
1255
|
fetchImpl = globalThis.fetch,
|
|
1126
1256
|
readConfig = realInstaller.readGlobalConfig,
|
|
1127
|
-
} = {}) {
|
|
1128
|
-
const hubUrl = readConfig()?.hub_url ??
|
|
1129
|
-
|
|
1257
|
+
} = {}) {
|
|
1258
|
+
const hubUrl = String(readConfig()?.hub_url ?? PRODUCTION_HUB_URL).replace(/\/$/, '');
|
|
1259
|
+
await assertProductionHub({ hubUrl, requiredCapabilities: INSPECT_CAPABILITIES, purpose: 'inspect', fetchImpl });
|
|
1260
|
+
const url = `${String(hubUrl).replace(/\/$/, '')}/_dsh/dsh-crew/extension`;
|
|
1130
1261
|
const response = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
1131
1262
|
const body = await response.json();
|
|
1132
1263
|
if (!response.ok || body?.ok !== true || !body.extension) {
|
|
@@ -1136,7 +1267,7 @@ export async function npxInspect({
|
|
|
1136
1267
|
return { ok: true, extension: body.extension };
|
|
1137
1268
|
}
|
|
1138
1269
|
|
|
1139
|
-
export async function npxJobs({
|
|
1270
|
+
export async function npxJobs({
|
|
1140
1271
|
args = [],
|
|
1141
1272
|
after = 0,
|
|
1142
1273
|
detail = 'compact',
|
|
@@ -1144,12 +1275,15 @@ export async function npxJobs({
|
|
|
1144
1275
|
log = console.log,
|
|
1145
1276
|
fetchImpl = globalThis.fetch,
|
|
1146
1277
|
readConfig = realInstaller.readGlobalConfig,
|
|
1147
|
-
} = {}) {
|
|
1148
|
-
const hubUrl = String(readConfig()?.hub_url ?? 'http://127.0.0.1:3210').replace(/\/$/, '');
|
|
1149
|
-
const base = `${hubUrl}/_dsh/dsh-crew/jobs`;
|
|
1150
|
-
const action = args[0] ?? 'list';
|
|
1151
|
-
const id = args[1];
|
|
1152
|
-
|
|
1278
|
+
} = {}) {
|
|
1279
|
+
const hubUrl = String(readConfig()?.hub_url ?? 'http://127.0.0.1:3210').replace(/\/$/, '');
|
|
1280
|
+
const base = `${hubUrl}/_dsh/dsh-crew/jobs`;
|
|
1281
|
+
const action = args[0] ?? 'list';
|
|
1282
|
+
const id = args[1];
|
|
1283
|
+
const requiredCapabilities = JOB_CAPABILITY_REQUIREMENTS[action];
|
|
1284
|
+
if (!requiredCapabilities) throw new Error(`unknown jobs action: ${action}`);
|
|
1285
|
+
await assertProductionHub({ hubUrl, requiredCapabilities, purpose: 'job', fetchImpl });
|
|
1286
|
+
let url = base;
|
|
1153
1287
|
let init = { headers: { accept: 'application/json' } };
|
|
1154
1288
|
if (action === 'get' || action === 'watch') {
|
|
1155
1289
|
if (!id) throw new Error(`jobs ${action} requires a job id`);
|
|
@@ -1162,22 +1296,224 @@ export async function npxJobs({
|
|
|
1162
1296
|
if (!request) throw new Error('jobs submit requires --request <json-file>');
|
|
1163
1297
|
const document = JSON.parse(readFileSync(resolve(request), 'utf8'));
|
|
1164
1298
|
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(document) };
|
|
1165
|
-
}
|
|
1166
|
-
throw new Error(`unknown jobs action: ${action}`);
|
|
1167
|
-
}
|
|
1299
|
+
}
|
|
1168
1300
|
const response = await fetchImpl(url, init);
|
|
1169
1301
|
const body = await response.json();
|
|
1170
1302
|
if (!response.ok || body?.ok === false) throw new Error(body?.error ?? 'Crew jobs API unavailable');
|
|
1171
1303
|
log(JSON.stringify(body, null, 2));
|
|
1172
|
-
return { ok: true, body };
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1304
|
+
return { ok: true, body };
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const PROVIDER_CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
1308
|
+
list: Object.freeze(['provider-inventory']),
|
|
1309
|
+
'delete-plan': Object.freeze(['provider-inventory', 'provider-lifecycle-v1']),
|
|
1310
|
+
delete: Object.freeze(['provider-inventory', 'provider-lifecycle-v1']),
|
|
1311
|
+
rollback: Object.freeze(['provider-inventory', 'provider-lifecycle-v1']),
|
|
1312
|
+
probe: Object.freeze(['provider-inventory', 'provider-health-v1', 'provider-probe-stream-v1']),
|
|
1313
|
+
});
|
|
1314
|
+
const JOB_CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
1315
|
+
list: Object.freeze(['jobs']),
|
|
1316
|
+
get: Object.freeze(['jobs', 'jobs-wait']),
|
|
1317
|
+
watch: Object.freeze(['jobs', 'jobs-wait']),
|
|
1318
|
+
cancel: Object.freeze(['jobs', 'jobs-cancel']),
|
|
1319
|
+
submit: Object.freeze(['jobs', 'roles', 'attempt-index', 'model-policy']),
|
|
1320
|
+
});
|
|
1321
|
+
const INSPECT_CAPABILITIES = Object.freeze(['extension-contract', 'evidence', 'runtime-provenance-v1']);
|
|
1322
|
+
const CREDENTIAL_CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
1323
|
+
list: Object.freeze(['credential-reference-inventory-v1']),
|
|
1324
|
+
'purge-plan': Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
|
|
1325
|
+
purge: Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
|
|
1326
|
+
});
|
|
1327
|
+
export const PRODUCTION_HUB_URL = 'http://127.0.0.1:3210';
|
|
1328
|
+
|
|
1329
|
+
export async function assertProductionHub({ hubUrl, requiredCapabilities = [], purpose = 'command', fetchImpl = globalThis.fetch } = {}) {
|
|
1330
|
+
if (hubUrl !== PRODUCTION_HUB_URL) throw new Error(`${purpose} requires the isolated 3210 Crew Hub`);
|
|
1331
|
+
const response = await fetchImpl(`${hubUrl}${CREW_ROUTE_BASE}/runtime`, { headers: { accept: 'application/json' } });
|
|
1332
|
+
let body;
|
|
1333
|
+
try { body = await response.json(); } catch { body = null; }
|
|
1334
|
+
const validIdentity = response.ok && body?.ok === true
|
|
1335
|
+
&& body?.service === 'dsh-crew-hub'
|
|
1336
|
+
&& body?.execution_plane === 'hub-3210'
|
|
1337
|
+
&& body?.profile === 'dsh-crew'
|
|
1338
|
+
&& Number(body?.listen_port) === 3210
|
|
1339
|
+
&& typeof body?.runtime_id === 'string' && body.runtime_id.trim().length > 0;
|
|
1340
|
+
if (!validIdentity) throw new Error(`${purpose} requires a compatible isolated 3210 Crew Hub`);
|
|
1341
|
+
const advertised = new Set(Array.isArray(body.capabilities) ? body.capabilities.filter((value) => typeof value === 'string') : []);
|
|
1342
|
+
const missing = requiredCapabilities.filter((capability) => !advertised.has(capability));
|
|
1343
|
+
if (missing.length > 0) throw new Error(`missing ${purpose} capability: ${missing.join(', ')}`);
|
|
1344
|
+
return { ok: true, runtime: body, capabilities: [...advertised] };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Confirm the target is the live isolated Hub and advertises the lifecycle
|
|
1349
|
+
* surface before sending a provider request. This prevents a same-port stale
|
|
1350
|
+
* Hub from turning a later 404 into an ambiguous destructive failure.
|
|
1351
|
+
*/
|
|
1352
|
+
export async function assertProviderHubCapabilities({ hubUrl, action, fetchImpl = globalThis.fetch } = {}) {
|
|
1353
|
+
return assertProductionHub({
|
|
1354
|
+
hubUrl,
|
|
1355
|
+
requiredCapabilities: PROVIDER_CAPABILITY_REQUIREMENTS[action] ?? PROVIDER_CAPABILITY_REQUIREMENTS.list,
|
|
1356
|
+
purpose: 'provider lifecycle',
|
|
1357
|
+
fetchImpl,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Call the isolated 3210 provider lifecycle API. The CLI never parses or
|
|
1363
|
+
* edits Harness YAML itself; the Hub remains the sole mutation authority.
|
|
1364
|
+
*/
|
|
1365
|
+
export async function npxProviders({
|
|
1366
|
+
args = [],
|
|
1367
|
+
planId,
|
|
1368
|
+
expectedRevision,
|
|
1369
|
+
replacementDefault,
|
|
1370
|
+
confirm = false,
|
|
1371
|
+
purgeOrphanCredentials = false,
|
|
1372
|
+
log = console.log,
|
|
1373
|
+
fetchImpl = globalThis.fetch,
|
|
1374
|
+
readConfig = realInstaller.readGlobalConfig,
|
|
1375
|
+
} = {}) {
|
|
1376
|
+
const configuredHubUrl = String(readConfig()?.hub_url ?? PRODUCTION_HUB_URL).replace(/\/$/, '');
|
|
1377
|
+
const hubUrl = configuredHubUrl;
|
|
1378
|
+
const base = `${hubUrl}/_dsh/dsh-crew/providers`;
|
|
1379
|
+
const action = args[0] ?? 'list';
|
|
1380
|
+
const id = args[1];
|
|
1381
|
+
const options = new Map();
|
|
1382
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
1383
|
+
const value = args[index];
|
|
1384
|
+
if (!value?.startsWith('--')) continue;
|
|
1385
|
+
const [name, inline] = value.split('=', 2);
|
|
1386
|
+
if (name === '--confirm' || name === '--purge-orphan-credentials') options.set(name === '--confirm' ? 'confirm' : name, true);
|
|
1387
|
+
else if (inline !== undefined) options.set(name, inline);
|
|
1388
|
+
else if (args[index + 1] !== undefined) options.set(name, args[++index]);
|
|
1389
|
+
}
|
|
1390
|
+
const resolvedPlan = planId ?? options.get('--plan');
|
|
1391
|
+
const resolvedRevision = expectedRevision ?? options.get('--expected-revision');
|
|
1392
|
+
const resolvedReplacement = replacementDefault ?? options.get('--replacement-default');
|
|
1393
|
+
const resolvedConfirm = confirm === true || options.get('confirm') === true;
|
|
1394
|
+
if (purgeOrphanCredentials === true || options.get('--purge-orphan-credentials') === true) {
|
|
1395
|
+
throw new Error('credential purge requires a separate explicit confirmation flow');
|
|
1396
|
+
}
|
|
1397
|
+
await assertProviderHubCapabilities({ hubUrl, action, fetchImpl });
|
|
1398
|
+
let url = base;
|
|
1399
|
+
let init = { headers: { accept: 'application/json' } };
|
|
1400
|
+
if (action === 'list') {
|
|
1401
|
+
// keep defaults
|
|
1402
|
+
} else if (action === 'delete-plan') {
|
|
1403
|
+
if (!id) throw new Error('providers delete-plan requires a provider id');
|
|
1404
|
+
url = `${base}/${encodeURIComponent(id)}/delete-plan`;
|
|
1405
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ ...(resolvedReplacement ? { replacement_default: resolvedReplacement } : {}) }) };
|
|
1406
|
+
} else if (action === 'delete') {
|
|
1407
|
+
if (!id) throw new Error('providers delete requires a provider id');
|
|
1408
|
+
if (!resolvedPlan || !resolvedRevision || !resolvedConfirm) throw new Error('providers delete requires --plan, --expected-revision and --confirm');
|
|
1409
|
+
url = `${base}/${encodeURIComponent(id)}`;
|
|
1410
|
+
init = {
|
|
1411
|
+
method: 'DELETE',
|
|
1412
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1413
|
+
body: JSON.stringify({ plan_id: resolvedPlan, expected_revision: resolvedRevision, confirm: true }),
|
|
1414
|
+
};
|
|
1415
|
+
} else if (action === 'probe') {
|
|
1416
|
+
if (!id) throw new Error('providers probe requires a provider id');
|
|
1417
|
+
url = `${base}/${encodeURIComponent(id)}/probe`;
|
|
1418
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: '{}' };
|
|
1419
|
+
} else if (action === 'rollback') {
|
|
1420
|
+
if (!id) throw new Error('providers rollback requires a provider id');
|
|
1421
|
+
if (!resolvedPlan || !resolvedConfirm) throw new Error('providers rollback requires --plan and --confirm');
|
|
1422
|
+
url = `${base}/${encodeURIComponent(id)}/rollback`;
|
|
1423
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }) };
|
|
1424
|
+
} else {
|
|
1425
|
+
throw new Error(`unknown providers action: ${action}`);
|
|
1426
|
+
}
|
|
1427
|
+
const response = await fetchImpl(url, init);
|
|
1428
|
+
let body = await response.json();
|
|
1429
|
+
if (!response.ok || body?.ok === false) throw new Error(body?.code ?? body?.error ?? 'Crew provider API unavailable');
|
|
1430
|
+
if (action === 'delete' && body?.restart_required === true && body?.result?.state === 'RESTART_PENDING') {
|
|
1431
|
+
const restartResponse = await fetchImpl('http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart', {
|
|
1432
|
+
method: 'POST',
|
|
1433
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1434
|
+
body: JSON.stringify({ confirm: true }),
|
|
1435
|
+
});
|
|
1436
|
+
const restartBody = await restartResponse.json();
|
|
1437
|
+
if (!restartResponse.ok || restartBody?.ok !== true) throw new Error(restartBody?.code ?? restartBody?.error ?? 'Crew 3210 restart failed');
|
|
1438
|
+
const verifyUrl = `${base}/${encodeURIComponent(id)}/verify-delete`;
|
|
1439
|
+
const verifyResponse = await fetchImpl(verifyUrl, {
|
|
1440
|
+
method: 'POST',
|
|
1441
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1442
|
+
body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }),
|
|
1443
|
+
});
|
|
1444
|
+
const verifyBody = await verifyResponse.json();
|
|
1445
|
+
if (!verifyResponse.ok || verifyBody?.ok !== true) throw new Error(verifyBody?.code ?? verifyBody?.error ?? 'Crew provider deletion verification failed');
|
|
1446
|
+
body = { ...body, restart: restartBody, verification: verifyBody };
|
|
1447
|
+
}
|
|
1448
|
+
if (action === 'rollback' && body?.restart_required === true && body?.state === 'ROLLBACK_PENDING') {
|
|
1449
|
+
const restartResponse = await fetchImpl('http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart', {
|
|
1450
|
+
method: 'POST',
|
|
1451
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1452
|
+
body: JSON.stringify({ confirm: true }),
|
|
1453
|
+
});
|
|
1454
|
+
const restartBody = await restartResponse.json();
|
|
1455
|
+
if (!restartResponse.ok || restartBody?.ok !== true) throw new Error(restartBody?.code ?? restartBody?.error ?? 'Crew 3210 restart failed');
|
|
1456
|
+
const verifyUrl = `${base}/${encodeURIComponent(id)}/verify-rollback`;
|
|
1457
|
+
const verifyResponse = await fetchImpl(verifyUrl, {
|
|
1458
|
+
method: 'POST',
|
|
1459
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1460
|
+
body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }),
|
|
1461
|
+
});
|
|
1462
|
+
const verifyBody = await verifyResponse.json();
|
|
1463
|
+
if (!verifyResponse.ok || verifyBody?.ok !== true) throw new Error(verifyBody?.code ?? verifyBody?.error ?? 'Crew provider rollback verification failed');
|
|
1464
|
+
body = { ...body, restart: restartBody, verification: verifyBody };
|
|
1465
|
+
}
|
|
1466
|
+
log(JSON.stringify(body, null, 2));
|
|
1467
|
+
return { ok: true, body };
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/** Independent credential-reference inventory and irreversible purge CLI. */
|
|
1471
|
+
export async function npxCredentials({
|
|
1472
|
+
args = [],
|
|
1473
|
+
planId,
|
|
1474
|
+
expectedRevision,
|
|
1475
|
+
confirm = false,
|
|
1476
|
+
log = console.log,
|
|
1477
|
+
fetchImpl = globalThis.fetch,
|
|
1478
|
+
readConfig = realInstaller.readGlobalConfig,
|
|
1479
|
+
} = {}) {
|
|
1480
|
+
const hubUrl = String(readConfig()?.hub_url ?? PRODUCTION_HUB_URL).replace(/\/$/, '');
|
|
1481
|
+
const action = args[0] ?? 'list';
|
|
1482
|
+
const id = args[1];
|
|
1483
|
+
const requiredCapabilities = CREDENTIAL_CAPABILITY_REQUIREMENTS[action];
|
|
1484
|
+
if (!requiredCapabilities) throw new Error(`unknown credentials action: ${action}`);
|
|
1485
|
+
await assertProductionHub({ hubUrl, requiredCapabilities, purpose: 'credential', fetchImpl });
|
|
1486
|
+
const base = `${hubUrl}${CREW_ROUTE_BASE}/credential-references`;
|
|
1487
|
+
let url = base;
|
|
1488
|
+
let init = { headers: { accept: 'application/json' } };
|
|
1489
|
+
if (action === 'purge-plan') {
|
|
1490
|
+
if (!id) throw new Error('credentials purge-plan requires a reference id');
|
|
1491
|
+
url = `${base}/${encodeURIComponent(id)}/purge-plan`;
|
|
1492
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ ...(expectedRevision ? { expected_revision: expectedRevision } : {}) }) };
|
|
1493
|
+
} else if (action === 'purge') {
|
|
1494
|
+
if (!id) throw new Error('credentials purge requires a reference id');
|
|
1495
|
+
if (!planId || !expectedRevision || confirm !== true) throw new Error('credentials purge requires --plan, --expected-revision and --confirm');
|
|
1496
|
+
url = `${base}/${encodeURIComponent(id)}`;
|
|
1497
|
+
init = { method: 'DELETE', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ plan_id: planId, expected_revision: expectedRevision, confirm: true }) };
|
|
1498
|
+
}
|
|
1499
|
+
const response = await fetchImpl(url, init);
|
|
1500
|
+
const body = await response.json();
|
|
1501
|
+
if (!response.ok || body?.ok === false) throw new Error(body?.code ?? body?.error ?? 'Crew credential API unavailable');
|
|
1502
|
+
log(JSON.stringify(body, null, 2));
|
|
1503
|
+
return { ok: true, body };
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function normalizeCommand(argv) {
|
|
1507
|
+
const flags = argv.slice(1);
|
|
1508
|
+
let candidate;
|
|
1509
|
+
let after = 0;
|
|
1510
|
+
let detail = 'compact';
|
|
1511
|
+
let request;
|
|
1512
|
+
let planId;
|
|
1513
|
+
let expectedRevision;
|
|
1514
|
+
let replacementDefault;
|
|
1515
|
+
let confirm = false;
|
|
1516
|
+
let purgeOrphanCredentials = false;
|
|
1181
1517
|
for (let index = 0; index < flags.length; index += 1) {
|
|
1182
1518
|
if (flags[index] === '--candidate') {
|
|
1183
1519
|
candidate = flags[index + 1];
|
|
@@ -1199,17 +1535,34 @@ function normalizeCommand(argv) {
|
|
|
1199
1535
|
after = Number(flags[index].slice('--after='.length)); flags.splice(index, 1); index -= 1;
|
|
1200
1536
|
} else if (flags[index]?.startsWith('--detail=')) {
|
|
1201
1537
|
detail = flags[index].slice('--detail='.length); flags.splice(index, 1); index -= 1;
|
|
1202
|
-
} else if (flags[index]?.startsWith('--request=')) {
|
|
1203
|
-
request = flags[index].slice('--request='.length); flags.splice(index, 1); index -= 1;
|
|
1204
|
-
}
|
|
1538
|
+
} else if (flags[index]?.startsWith('--request=')) {
|
|
1539
|
+
request = flags[index].slice('--request='.length); flags.splice(index, 1); index -= 1;
|
|
1540
|
+
} else if (flags[index] === '--plan' || flags[index] === '--expected-revision' || flags[index] === '--replacement-default') {
|
|
1541
|
+
const name = flags[index];
|
|
1542
|
+
const value = flags[index + 1];
|
|
1543
|
+
if (name === '--plan') planId = value;
|
|
1544
|
+
if (name === '--expected-revision') expectedRevision = value;
|
|
1545
|
+
if (name === '--replacement-default') replacementDefault = value;
|
|
1546
|
+
flags.splice(index, 2); index -= 1;
|
|
1547
|
+
} else if (flags[index]?.startsWith('--plan=')) {
|
|
1548
|
+
planId = flags[index].slice('--plan='.length); flags.splice(index, 1); index -= 1;
|
|
1549
|
+
} else if (flags[index]?.startsWith('--expected-revision=')) {
|
|
1550
|
+
expectedRevision = flags[index].slice('--expected-revision='.length); flags.splice(index, 1); index -= 1;
|
|
1551
|
+
} else if (flags[index]?.startsWith('--replacement-default=')) {
|
|
1552
|
+
replacementDefault = flags[index].slice('--replacement-default='.length); flags.splice(index, 1); index -= 1;
|
|
1553
|
+
} else if (flags[index] === '--confirm') {
|
|
1554
|
+
confirm = true; flags.splice(index, 1); index -= 1;
|
|
1555
|
+
} else if (flags[index] === '--purge-orphan-credentials') {
|
|
1556
|
+
purgeOrphanCredentials = true; flags.splice(index, 1); index -= 1;
|
|
1557
|
+
}
|
|
1205
1558
|
}
|
|
1206
1559
|
const knownFlags = new Set(['--purge']);
|
|
1207
1560
|
const unknown = flags.filter((f) => f.startsWith('--') && !knownFlags.has(f));
|
|
1208
1561
|
const args = flags.filter((f) => !f.startsWith('--'));
|
|
1209
1562
|
if (!Number.isInteger(after) || after < 0) unknown.push('--after');
|
|
1210
1563
|
if (!['compact', 'full'].includes(detail)) unknown.push('--detail');
|
|
1211
|
-
return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, args, unknown };
|
|
1212
|
-
}
|
|
1564
|
+
return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown };
|
|
1565
|
+
}
|
|
1213
1566
|
|
|
1214
1567
|
/**
|
|
1215
1568
|
* CLI dispatcher used by bin/dsh-crew.mjs. Returns a process exit code.
|
|
@@ -1220,7 +1573,7 @@ export async function runNpxCli({
|
|
|
1220
1573
|
error = console.error,
|
|
1221
1574
|
commands = {},
|
|
1222
1575
|
} = {}) {
|
|
1223
|
-
const { command, purge, candidate, after, detail, request, args, unknown } = normalizeCommand(argv);
|
|
1576
|
+
const { command, purge, candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown } = normalizeCommand(argv);
|
|
1224
1577
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
1225
1578
|
log(USAGE);
|
|
1226
1579
|
return 0;
|
|
@@ -1229,7 +1582,7 @@ export async function runNpxCli({
|
|
|
1229
1582
|
error(USAGE);
|
|
1230
1583
|
return 1;
|
|
1231
1584
|
}
|
|
1232
|
-
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'inspect', 'jobs', 'update', 'uninstall'].includes(command)) {
|
|
1585
|
+
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'inspect', 'jobs', 'providers', 'credentials', 'releases', 'rollback', 'update', 'uninstall'].includes(command)) {
|
|
1233
1586
|
error(`unknown command: ${command ?? '<none>'}\n\n${USAGE}`);
|
|
1234
1587
|
return 1;
|
|
1235
1588
|
}
|
|
@@ -1239,15 +1592,23 @@ export async function runNpxCli({
|
|
|
1239
1592
|
integrate: commands.integrate ?? npxIntegrate,
|
|
1240
1593
|
detach: commands.detach ?? npxDetach,
|
|
1241
1594
|
status: commands.status ?? npxStatus,
|
|
1242
|
-
inspect: commands.inspect ?? npxInspect,
|
|
1243
|
-
jobs: commands.jobs ?? npxJobs,
|
|
1595
|
+
inspect: commands.inspect ?? npxInspect,
|
|
1596
|
+
jobs: commands.jobs ?? npxJobs,
|
|
1597
|
+
providers: commands.providers ?? npxProviders,
|
|
1598
|
+
credentials: commands.credentials ?? npxCredentials,
|
|
1599
|
+
releases: commands.releases ?? npxReleases,
|
|
1600
|
+
rollback: commands.rollback ?? npxRollback,
|
|
1244
1601
|
update: commands.update ?? npxUpdate,
|
|
1245
1602
|
uninstall: commands.uninstall ?? npxUninstall,
|
|
1246
1603
|
};
|
|
1247
1604
|
let result;
|
|
1248
1605
|
if (command === 'uninstall') result = await actions.uninstall({ purge, log });
|
|
1249
|
-
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
1250
|
-
else if (command === 'jobs') result = await actions.jobs({ args, after, detail, request, log });
|
|
1606
|
+
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
1607
|
+
else if (command === 'jobs') result = await actions.jobs({ args, after, detail, request, log });
|
|
1608
|
+
else if (command === 'providers') result = await actions.providers({ args, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, log });
|
|
1609
|
+
else if (command === 'credentials') result = await actions.credentials({ args, planId, expectedRevision, confirm, log });
|
|
1610
|
+
else if (command === 'releases') result = await actions.releases({ args, log });
|
|
1611
|
+
else if (command === 'rollback') result = await actions.rollback({ version: args?.[0], args, log });
|
|
1251
1612
|
else result = await actions[command]({ log });
|
|
1252
1613
|
return result?.ok === false ? 1 : 0;
|
|
1253
1614
|
} catch (err) {
|