@ran-sh/dsh-crew 1.0.2 → 1.0.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 +4 -0
- package/README.zh.md +4 -0
- package/lib/client.js +183 -6
- package/official-web-bridge/lib/client.js +183 -6
- package/package.json +1 -1
- package/src/client/index.tsx +92 -4
- package/src/credential-reference.mjs +38 -0
- package/src/hub/index.mjs +483 -14
- package/src/install/npx-lifecycle.mjs +456 -398
- package/src/model-catalog.mjs +8 -1
- package/src/provider-delete-adapters.mjs +165 -17
- package/src/provider-inventory.mjs +17 -3
- package/src/provider-layer-migration-adapters.mjs +759 -0
- package/src/provider-layer-migration.mjs +198 -0
- package/src/provider-lifecycle-state.mjs +4 -1
- package/src/provider-profile-store.mjs +193 -4
- package/src/provider-settings-store.mjs +82 -6
- package/src/provider-store-lock.mjs +67 -0
- package/src/runtime-identity.mjs +2 -1
|
@@ -49,10 +49,10 @@ import {
|
|
|
49
49
|
|
|
50
50
|
export const CREW_APP_DIRNAME = 'app';
|
|
51
51
|
export const RELEASES_DIRNAME = 'releases';
|
|
52
|
-
export const CURRENT_POINTER_FILENAME = 'current.json';
|
|
53
|
-
export const KEEP_RELEASES = 2;
|
|
54
|
-
export const INCOMPLETE_MARKER = '.dsh-crew-incomplete';
|
|
55
|
-
const CREW_ROUTE_BASE = '/_dsh/dsh-crew';
|
|
52
|
+
export const CURRENT_POINTER_FILENAME = 'current.json';
|
|
53
|
+
export const KEEP_RELEASES = 2;
|
|
54
|
+
export const INCOMPLETE_MARKER = '.dsh-crew-incomplete';
|
|
55
|
+
const CREW_ROUTE_BASE = '/_dsh/dsh-crew';
|
|
56
56
|
|
|
57
57
|
export function npmCliInvocation(args, {
|
|
58
58
|
platform = process.platform,
|
|
@@ -478,7 +478,7 @@ function commitStagedRelease({ stageDir, manifest, home }) {
|
|
|
478
478
|
|
|
479
479
|
const STALE_INCOMPLETE_MS = 24 * 60 * 60 * 1000;
|
|
480
480
|
|
|
481
|
-
function gcOldReleases({ home, keep = KEEP_RELEASES }) {
|
|
481
|
+
function gcOldReleases({ home, keep = KEEP_RELEASES }) {
|
|
482
482
|
const pointer = readCurrentPointer({ home });
|
|
483
483
|
const releasesDir = crewReleasesDir({ home });
|
|
484
484
|
if (!existsSync(releasesDir)) return;
|
|
@@ -507,123 +507,123 @@ function gcOldReleases({ home, keep = KEEP_RELEASES }) {
|
|
|
507
507
|
try { rmSync(victim, { recursive: true, force: true }); removed.push(victim); } catch { /* best effort */ }
|
|
508
508
|
}
|
|
509
509
|
return removed;
|
|
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 }) {
|
|
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 }) {
|
|
627
627
|
return join(crewProfileDir({ home }), 'node_modules', ...name.split('/'));
|
|
628
628
|
}
|
|
629
629
|
|
|
@@ -1225,12 +1225,12 @@ Commands:
|
|
|
1225
1225
|
integrate show Crew inside the official 3080 UI; backend stays isolated on 3210
|
|
1226
1226
|
detach remove only the official 3080 bridge; isolated 3210 mode remains available
|
|
1227
1227
|
status read-only report of launcher/installed versions and integrations
|
|
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
|
|
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|migration-status|migrate-plan|migrate|verify-migration|rollback-migration|verify-rollback-migration|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
|
|
1234
1234
|
update resolve the newest permitted package from the configured npm registry (or
|
|
1235
1235
|
--candidate), stage and validate it, then activate; idempotent when current
|
|
1236
1236
|
uninstall remove the Crew-managed payload, registration, and integrations (config kept)
|
|
@@ -1239,25 +1239,25 @@ Options:
|
|
|
1239
1239
|
--candidate <path> update from a local payload directory or packed .tgz instead of the registry
|
|
1240
1240
|
--after <sequence> with jobs watch/get: return canonical events after this cursor
|
|
1241
1241
|
--detail <mode> with jobs get/watch: compact (default) or full
|
|
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
|
|
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
|
|
1247
1247
|
--purge with uninstall: also remove ~/.config/dsh-crew config/backups (destructive)
|
|
1248
1248
|
--help show this help
|
|
1249
1249
|
|
|
1250
1250
|
Primary install: npm install -g @ran-sh/dsh-crew (then run: dsh-crew install)
|
|
1251
1251
|
Source checkouts use scripts/setup.mjs instead.`;
|
|
1252
1252
|
|
|
1253
|
-
export async function npxInspect({
|
|
1253
|
+
export async function npxInspect({
|
|
1254
1254
|
log = console.log,
|
|
1255
1255
|
fetchImpl = globalThis.fetch,
|
|
1256
1256
|
readConfig = realInstaller.readGlobalConfig,
|
|
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`;
|
|
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`;
|
|
1261
1261
|
const response = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
1262
1262
|
const body = await response.json();
|
|
1263
1263
|
if (!response.ok || body?.ok !== true || !body.extension) {
|
|
@@ -1267,7 +1267,7 @@ export async function npxInspect({
|
|
|
1267
1267
|
return { ok: true, extension: body.extension };
|
|
1268
1268
|
}
|
|
1269
1269
|
|
|
1270
|
-
export async function npxJobs({
|
|
1270
|
+
export async function npxJobs({
|
|
1271
1271
|
args = [],
|
|
1272
1272
|
after = 0,
|
|
1273
1273
|
detail = 'compact',
|
|
@@ -1275,15 +1275,15 @@ export async function npxJobs({
|
|
|
1275
1275
|
log = console.log,
|
|
1276
1276
|
fetchImpl = globalThis.fetch,
|
|
1277
1277
|
readConfig = realInstaller.readGlobalConfig,
|
|
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;
|
|
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;
|
|
1287
1287
|
let init = { headers: { accept: 'application/json' } };
|
|
1288
1288
|
if (action === 'get' || action === 'watch') {
|
|
1289
1289
|
if (!id) throw new Error(`jobs ${action} requires a job id`);
|
|
@@ -1296,224 +1296,282 @@ export async function npxJobs({
|
|
|
1296
1296
|
if (!request) throw new Error('jobs submit requires --request <json-file>');
|
|
1297
1297
|
const document = JSON.parse(readFileSync(resolve(request), 'utf8'));
|
|
1298
1298
|
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(document) };
|
|
1299
|
-
}
|
|
1299
|
+
}
|
|
1300
1300
|
const response = await fetchImpl(url, init);
|
|
1301
1301
|
const body = await response.json();
|
|
1302
1302
|
if (!response.ok || body?.ok === false) throw new Error(body?.error ?? 'Crew jobs API unavailable');
|
|
1303
1303
|
log(JSON.stringify(body, null, 2));
|
|
1304
|
-
return { ok: true, body };
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
const PROVIDER_CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
1308
|
-
list: Object.freeze(['provider-inventory']),
|
|
1309
|
-
'
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
url = `${base}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
if (!
|
|
1417
|
-
url = `${base}/${encodeURIComponent(id)}/
|
|
1418
|
-
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body:
|
|
1419
|
-
} else if (action === '
|
|
1420
|
-
if (!id) throw new Error('providers
|
|
1421
|
-
if (!resolvedPlan || !resolvedConfirm) throw new Error('providers
|
|
1422
|
-
url = `${base}/${encodeURIComponent(id)}/
|
|
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(
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
if (!
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1304
|
+
return { ok: true, body };
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const PROVIDER_CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
1308
|
+
list: Object.freeze(['provider-inventory']),
|
|
1309
|
+
'migration-status': Object.freeze(['provider-inventory']),
|
|
1310
|
+
'migrate-plan': Object.freeze(['provider-inventory', 'provider-layer-migration-v1']),
|
|
1311
|
+
migrate: Object.freeze(['provider-inventory', 'provider-layer-migration-v1']),
|
|
1312
|
+
'verify-migration': Object.freeze(['provider-inventory', 'provider-layer-migration-v1']),
|
|
1313
|
+
'rollback-migration': Object.freeze(['provider-inventory', 'provider-layer-migration-v1']),
|
|
1314
|
+
'verify-rollback-migration': Object.freeze(['provider-inventory', 'provider-layer-migration-v1']),
|
|
1315
|
+
'delete-plan': Object.freeze(['provider-inventory', 'provider-lifecycle-v1']),
|
|
1316
|
+
delete: Object.freeze(['provider-inventory', 'provider-lifecycle-v1']),
|
|
1317
|
+
rollback: Object.freeze(['provider-inventory', 'provider-lifecycle-v1']),
|
|
1318
|
+
probe: Object.freeze(['provider-inventory', 'provider-health-v1', 'provider-probe-stream-v1']),
|
|
1319
|
+
});
|
|
1320
|
+
const JOB_CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
1321
|
+
list: Object.freeze(['jobs']),
|
|
1322
|
+
get: Object.freeze(['jobs', 'jobs-wait']),
|
|
1323
|
+
watch: Object.freeze(['jobs', 'jobs-wait']),
|
|
1324
|
+
cancel: Object.freeze(['jobs', 'jobs-cancel']),
|
|
1325
|
+
submit: Object.freeze(['jobs', 'roles', 'attempt-index', 'model-policy']),
|
|
1326
|
+
});
|
|
1327
|
+
const INSPECT_CAPABILITIES = Object.freeze(['extension-contract', 'evidence', 'runtime-provenance-v1']);
|
|
1328
|
+
const CREDENTIAL_CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
1329
|
+
list: Object.freeze(['credential-reference-inventory-v1']),
|
|
1330
|
+
'purge-plan': Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
|
|
1331
|
+
purge: Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
|
|
1332
|
+
});
|
|
1333
|
+
export const PRODUCTION_HUB_URL = 'http://127.0.0.1:3210';
|
|
1334
|
+
|
|
1335
|
+
export async function assertProductionHub({ hubUrl, requiredCapabilities = [], purpose = 'command', fetchImpl = globalThis.fetch } = {}) {
|
|
1336
|
+
if (hubUrl !== PRODUCTION_HUB_URL) throw new Error(`${purpose} requires the isolated 3210 Crew Hub`);
|
|
1337
|
+
const response = await fetchImpl(`${hubUrl}${CREW_ROUTE_BASE}/runtime`, { headers: { accept: 'application/json' } });
|
|
1338
|
+
let body;
|
|
1339
|
+
try { body = await response.json(); } catch { body = null; }
|
|
1340
|
+
const validIdentity = response.ok && body?.ok === true
|
|
1341
|
+
&& body?.service === 'dsh-crew-hub'
|
|
1342
|
+
&& body?.execution_plane === 'hub-3210'
|
|
1343
|
+
&& body?.profile === 'dsh-crew'
|
|
1344
|
+
&& Number(body?.listen_port) === 3210
|
|
1345
|
+
&& typeof body?.runtime_id === 'string' && body.runtime_id.trim().length > 0;
|
|
1346
|
+
if (!validIdentity) throw new Error(`${purpose} requires a compatible isolated 3210 Crew Hub`);
|
|
1347
|
+
const advertised = new Set(Array.isArray(body.capabilities) ? body.capabilities.filter((value) => typeof value === 'string') : []);
|
|
1348
|
+
const missing = requiredCapabilities.filter((capability) => !advertised.has(capability));
|
|
1349
|
+
if (missing.length > 0) throw new Error(`missing ${purpose} capability: ${missing.join(', ')}`);
|
|
1350
|
+
return { ok: true, runtime: body, capabilities: [...advertised] };
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Confirm the target is the live isolated Hub and advertises the lifecycle
|
|
1355
|
+
* surface before sending a provider request. This prevents a same-port stale
|
|
1356
|
+
* Hub from turning a later 404 into an ambiguous destructive failure.
|
|
1357
|
+
*/
|
|
1358
|
+
export async function assertProviderHubCapabilities({ hubUrl, action, fetchImpl = globalThis.fetch } = {}) {
|
|
1359
|
+
return assertProductionHub({
|
|
1360
|
+
hubUrl,
|
|
1361
|
+
requiredCapabilities: PROVIDER_CAPABILITY_REQUIREMENTS[action] ?? PROVIDER_CAPABILITY_REQUIREMENTS.list,
|
|
1362
|
+
purpose: 'provider lifecycle',
|
|
1363
|
+
fetchImpl,
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Call the isolated 3210 provider lifecycle API. The CLI never parses or
|
|
1369
|
+
* edits Harness YAML itself; the Hub remains the sole mutation authority.
|
|
1370
|
+
*/
|
|
1371
|
+
export async function npxProviders({
|
|
1372
|
+
args = [],
|
|
1373
|
+
planId,
|
|
1374
|
+
expectedRevision,
|
|
1375
|
+
replacementDefault,
|
|
1376
|
+
confirm = false,
|
|
1377
|
+
purgeOrphanCredentials = false,
|
|
1378
|
+
log = console.log,
|
|
1379
|
+
fetchImpl = globalThis.fetch,
|
|
1380
|
+
readConfig = realInstaller.readGlobalConfig,
|
|
1381
|
+
} = {}) {
|
|
1382
|
+
const configuredHubUrl = String(readConfig()?.hub_url ?? PRODUCTION_HUB_URL).replace(/\/$/, '');
|
|
1383
|
+
const hubUrl = configuredHubUrl;
|
|
1384
|
+
const base = `${hubUrl}/_dsh/dsh-crew/providers`;
|
|
1385
|
+
const action = args[0] ?? 'list';
|
|
1386
|
+
const id = args[1];
|
|
1387
|
+
const options = new Map();
|
|
1388
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
1389
|
+
const value = args[index];
|
|
1390
|
+
if (!value?.startsWith('--')) continue;
|
|
1391
|
+
const [name, inline] = value.split('=', 2);
|
|
1392
|
+
if (name === '--confirm' || name === '--purge-orphan-credentials') options.set(name === '--confirm' ? 'confirm' : name, true);
|
|
1393
|
+
else if (inline !== undefined) options.set(name, inline);
|
|
1394
|
+
else if (args[index + 1] !== undefined) options.set(name, args[++index]);
|
|
1395
|
+
}
|
|
1396
|
+
const resolvedPlan = planId ?? options.get('--plan');
|
|
1397
|
+
const resolvedRevision = expectedRevision ?? options.get('--expected-revision');
|
|
1398
|
+
const resolvedReplacement = replacementDefault ?? options.get('--replacement-default');
|
|
1399
|
+
const resolvedConfirm = confirm === true || options.get('confirm') === true;
|
|
1400
|
+
if (purgeOrphanCredentials === true || options.get('--purge-orphan-credentials') === true) {
|
|
1401
|
+
throw new Error('credential purge requires a separate explicit confirmation flow');
|
|
1402
|
+
}
|
|
1403
|
+
await assertProviderHubCapabilities({ hubUrl, action, fetchImpl });
|
|
1404
|
+
let url = base;
|
|
1405
|
+
let init = { headers: { accept: 'application/json' } };
|
|
1406
|
+
if (action === 'list') {
|
|
1407
|
+
// keep defaults
|
|
1408
|
+
} else if (action === 'migration-status') {
|
|
1409
|
+
url = `${base}/migration-status`;
|
|
1410
|
+
} else if (action === 'migrate-plan') {
|
|
1411
|
+
if (!id) throw new Error('providers migrate-plan requires a provider id');
|
|
1412
|
+
url = `${base}/${encodeURIComponent(id)}/migrate-plan`;
|
|
1413
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: '{}' };
|
|
1414
|
+
} else if (action === 'migrate') {
|
|
1415
|
+
if (!id) throw new Error('providers migrate requires a provider id');
|
|
1416
|
+
if (!resolvedPlan || !resolvedConfirm) throw new Error('providers migrate requires --plan and --confirm');
|
|
1417
|
+
url = `${base}/${encodeURIComponent(id)}/migrate`;
|
|
1418
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ plan_id: resolvedPlan, confirm: true }) };
|
|
1419
|
+
} else if (action === 'verify-migration') {
|
|
1420
|
+
if (!id) throw new Error('providers verify-migration requires a provider id');
|
|
1421
|
+
if (!resolvedPlan || !resolvedConfirm) throw new Error('providers verify-migration requires --plan and --confirm');
|
|
1422
|
+
url = `${base}/${encodeURIComponent(id)}/verify-migration`;
|
|
1423
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }) };
|
|
1424
|
+
} else if (action === 'rollback-migration') {
|
|
1425
|
+
if (!id) throw new Error('providers rollback-migration requires a provider id');
|
|
1426
|
+
if (!resolvedPlan || !resolvedConfirm) throw new Error('providers rollback-migration requires --plan and --confirm');
|
|
1427
|
+
url = `${base}/${encodeURIComponent(id)}/rollback-migration`;
|
|
1428
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }) };
|
|
1429
|
+
} else if (action === 'verify-rollback-migration') {
|
|
1430
|
+
if (!id) throw new Error('providers verify-rollback-migration requires a provider id');
|
|
1431
|
+
if (!resolvedPlan || !resolvedConfirm) throw new Error('providers verify-rollback-migration requires --plan and --confirm');
|
|
1432
|
+
url = `${base}/${encodeURIComponent(id)}/verify-rollback-migration`;
|
|
1433
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }) };
|
|
1434
|
+
} else if (action === 'delete-plan') {
|
|
1435
|
+
if (!id) throw new Error('providers delete-plan requires a provider id');
|
|
1436
|
+
url = `${base}/${encodeURIComponent(id)}/delete-plan`;
|
|
1437
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ ...(resolvedReplacement ? { replacement_default: resolvedReplacement } : {}) }) };
|
|
1438
|
+
} else if (action === 'delete') {
|
|
1439
|
+
if (!id) throw new Error('providers delete requires a provider id');
|
|
1440
|
+
if (!resolvedPlan || !resolvedRevision || !resolvedConfirm) throw new Error('providers delete requires --plan, --expected-revision and --confirm');
|
|
1441
|
+
url = `${base}/${encodeURIComponent(id)}`;
|
|
1442
|
+
init = {
|
|
1443
|
+
method: 'DELETE',
|
|
1444
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1445
|
+
body: JSON.stringify({ plan_id: resolvedPlan, expected_revision: resolvedRevision, confirm: true }),
|
|
1446
|
+
};
|
|
1447
|
+
} else if (action === 'probe') {
|
|
1448
|
+
if (!id) throw new Error('providers probe requires a provider id');
|
|
1449
|
+
url = `${base}/${encodeURIComponent(id)}/probe`;
|
|
1450
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: '{}' };
|
|
1451
|
+
} else if (action === 'rollback') {
|
|
1452
|
+
if (!id) throw new Error('providers rollback requires a provider id');
|
|
1453
|
+
if (!resolvedPlan || !resolvedConfirm) throw new Error('providers rollback requires --plan and --confirm');
|
|
1454
|
+
url = `${base}/${encodeURIComponent(id)}/rollback`;
|
|
1455
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }) };
|
|
1456
|
+
} else {
|
|
1457
|
+
throw new Error(`unknown providers action: ${action}`);
|
|
1458
|
+
}
|
|
1459
|
+
const response = await fetchImpl(url, init);
|
|
1460
|
+
let body = await response.json();
|
|
1461
|
+
if (!response.ok || body?.ok === false) throw new Error(body?.code ?? body?.error ?? 'Crew provider API unavailable');
|
|
1462
|
+
if (action === 'delete' && body?.restart_required === true && body?.result?.state === 'RESTART_PENDING') {
|
|
1463
|
+
const restartResponse = await fetchImpl('http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart', {
|
|
1464
|
+
method: 'POST',
|
|
1465
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1466
|
+
body: JSON.stringify({ confirm: true }),
|
|
1467
|
+
});
|
|
1468
|
+
const restartBody = await restartResponse.json();
|
|
1469
|
+
if (!restartResponse.ok || restartBody?.ok !== true) throw new Error(restartBody?.code ?? restartBody?.error ?? 'Crew 3210 restart failed');
|
|
1470
|
+
const verifyUrl = `${base}/${encodeURIComponent(id)}/verify-delete`;
|
|
1471
|
+
const verifyResponse = await fetchImpl(verifyUrl, {
|
|
1472
|
+
method: 'POST',
|
|
1473
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1474
|
+
body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }),
|
|
1475
|
+
});
|
|
1476
|
+
const verifyBody = await verifyResponse.json();
|
|
1477
|
+
if (!verifyResponse.ok || verifyBody?.ok !== true) throw new Error(verifyBody?.code ?? verifyBody?.error ?? 'Crew provider deletion verification failed');
|
|
1478
|
+
body = { ...body, restart: restartBody, verification: verifyBody };
|
|
1479
|
+
}
|
|
1480
|
+
if (action === 'migrate' && body?.restart_required === true && body?.result?.state === 'RESTART_PENDING') {
|
|
1481
|
+
const restartResponse = await fetchImpl('http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart', {
|
|
1482
|
+
method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
|
|
1483
|
+
});
|
|
1484
|
+
const restartBody = await restartResponse.json();
|
|
1485
|
+
if (!restartResponse.ok || restartBody?.ok !== true) throw new Error(restartBody?.code ?? restartBody?.error ?? 'Crew 3210 restart failed');
|
|
1486
|
+
const verifyResponse = await fetchImpl(`${base}/${encodeURIComponent(id)}/verify-migration`, {
|
|
1487
|
+
method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }),
|
|
1488
|
+
});
|
|
1489
|
+
const verifyBody = await verifyResponse.json();
|
|
1490
|
+
if (!verifyResponse.ok || verifyBody?.ok !== true) throw new Error(verifyBody?.code ?? verifyBody?.error ?? 'Crew provider migration verification failed');
|
|
1491
|
+
body = { ...body, restart: restartBody, verification: verifyBody };
|
|
1492
|
+
}
|
|
1493
|
+
if (action === 'rollback-migration' && body?.restart_required === true && body?.state === 'ROLLBACK_RESTART_PENDING') {
|
|
1494
|
+
const restartResponse = await fetchImpl('http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart', {
|
|
1495
|
+
method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ confirm: true }),
|
|
1496
|
+
});
|
|
1497
|
+
const restartBody = await restartResponse.json();
|
|
1498
|
+
if (!restartResponse.ok || restartBody?.ok !== true) throw new Error(restartBody?.code ?? restartBody?.error ?? 'Crew 3210 restart failed');
|
|
1499
|
+
const verifyResponse = await fetchImpl(`${base}/${encodeURIComponent(id)}/verify-rollback-migration`, {
|
|
1500
|
+
method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }),
|
|
1501
|
+
});
|
|
1502
|
+
const verifyBody = await verifyResponse.json();
|
|
1503
|
+
if (!verifyResponse.ok || verifyBody?.ok !== true) throw new Error(verifyBody?.code ?? verifyBody?.error ?? 'Crew provider migration rollback verification failed');
|
|
1504
|
+
body = { ...body, restart: restartBody, verification: verifyBody };
|
|
1505
|
+
}
|
|
1506
|
+
if (action === 'rollback' && body?.restart_required === true && body?.state === 'ROLLBACK_PENDING') {
|
|
1507
|
+
const restartResponse = await fetchImpl('http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart', {
|
|
1508
|
+
method: 'POST',
|
|
1509
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1510
|
+
body: JSON.stringify({ confirm: true }),
|
|
1511
|
+
});
|
|
1512
|
+
const restartBody = await restartResponse.json();
|
|
1513
|
+
if (!restartResponse.ok || restartBody?.ok !== true) throw new Error(restartBody?.code ?? restartBody?.error ?? 'Crew 3210 restart failed');
|
|
1514
|
+
const verifyUrl = `${base}/${encodeURIComponent(id)}/verify-rollback`;
|
|
1515
|
+
const verifyResponse = await fetchImpl(verifyUrl, {
|
|
1516
|
+
method: 'POST',
|
|
1517
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
1518
|
+
body: JSON.stringify({ transaction_id: resolvedPlan, confirm: true }),
|
|
1519
|
+
});
|
|
1520
|
+
const verifyBody = await verifyResponse.json();
|
|
1521
|
+
if (!verifyResponse.ok || verifyBody?.ok !== true) throw new Error(verifyBody?.code ?? verifyBody?.error ?? 'Crew provider rollback verification failed');
|
|
1522
|
+
body = { ...body, restart: restartBody, verification: verifyBody };
|
|
1523
|
+
}
|
|
1524
|
+
log(JSON.stringify(body, null, 2));
|
|
1525
|
+
return { ok: true, body };
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/** Independent credential-reference inventory and irreversible purge CLI. */
|
|
1529
|
+
export async function npxCredentials({
|
|
1530
|
+
args = [],
|
|
1531
|
+
planId,
|
|
1532
|
+
expectedRevision,
|
|
1533
|
+
confirm = false,
|
|
1534
|
+
log = console.log,
|
|
1535
|
+
fetchImpl = globalThis.fetch,
|
|
1536
|
+
readConfig = realInstaller.readGlobalConfig,
|
|
1537
|
+
} = {}) {
|
|
1538
|
+
const hubUrl = String(readConfig()?.hub_url ?? PRODUCTION_HUB_URL).replace(/\/$/, '');
|
|
1539
|
+
const action = args[0] ?? 'list';
|
|
1540
|
+
const id = args[1];
|
|
1541
|
+
const requiredCapabilities = CREDENTIAL_CAPABILITY_REQUIREMENTS[action];
|
|
1542
|
+
if (!requiredCapabilities) throw new Error(`unknown credentials action: ${action}`);
|
|
1543
|
+
await assertProductionHub({ hubUrl, requiredCapabilities, purpose: 'credential', fetchImpl });
|
|
1544
|
+
const base = `${hubUrl}${CREW_ROUTE_BASE}/credential-references`;
|
|
1545
|
+
let url = base;
|
|
1546
|
+
let init = { headers: { accept: 'application/json' } };
|
|
1547
|
+
if (action === 'purge-plan') {
|
|
1548
|
+
if (!id) throw new Error('credentials purge-plan requires a reference id');
|
|
1549
|
+
url = `${base}/${encodeURIComponent(id)}/purge-plan`;
|
|
1550
|
+
init = { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ ...(expectedRevision ? { expected_revision: expectedRevision } : {}) }) };
|
|
1551
|
+
} else if (action === 'purge') {
|
|
1552
|
+
if (!id) throw new Error('credentials purge requires a reference id');
|
|
1553
|
+
if (!planId || !expectedRevision || confirm !== true) throw new Error('credentials purge requires --plan, --expected-revision and --confirm');
|
|
1554
|
+
url = `${base}/${encodeURIComponent(id)}`;
|
|
1555
|
+
init = { method: 'DELETE', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ plan_id: planId, expected_revision: expectedRevision, confirm: true }) };
|
|
1556
|
+
}
|
|
1557
|
+
const response = await fetchImpl(url, init);
|
|
1558
|
+
const body = await response.json();
|
|
1559
|
+
if (!response.ok || body?.ok === false) throw new Error(body?.code ?? body?.error ?? 'Crew credential API unavailable');
|
|
1560
|
+
log(JSON.stringify(body, null, 2));
|
|
1561
|
+
return { ok: true, body };
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function normalizeCommand(argv) {
|
|
1565
|
+
const flags = argv.slice(1);
|
|
1566
|
+
let candidate;
|
|
1567
|
+
let after = 0;
|
|
1568
|
+
let detail = 'compact';
|
|
1569
|
+
let request;
|
|
1570
|
+
let planId;
|
|
1571
|
+
let expectedRevision;
|
|
1572
|
+
let replacementDefault;
|
|
1573
|
+
let confirm = false;
|
|
1574
|
+
let purgeOrphanCredentials = false;
|
|
1517
1575
|
for (let index = 0; index < flags.length; index += 1) {
|
|
1518
1576
|
if (flags[index] === '--candidate') {
|
|
1519
1577
|
candidate = flags[index + 1];
|
|
@@ -1535,34 +1593,34 @@ function normalizeCommand(argv) {
|
|
|
1535
1593
|
after = Number(flags[index].slice('--after='.length)); flags.splice(index, 1); index -= 1;
|
|
1536
1594
|
} else if (flags[index]?.startsWith('--detail=')) {
|
|
1537
1595
|
detail = flags[index].slice('--detail='.length); flags.splice(index, 1); index -= 1;
|
|
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
|
-
}
|
|
1596
|
+
} else if (flags[index]?.startsWith('--request=')) {
|
|
1597
|
+
request = flags[index].slice('--request='.length); flags.splice(index, 1); index -= 1;
|
|
1598
|
+
} else if (flags[index] === '--plan' || flags[index] === '--expected-revision' || flags[index] === '--replacement-default') {
|
|
1599
|
+
const name = flags[index];
|
|
1600
|
+
const value = flags[index + 1];
|
|
1601
|
+
if (name === '--plan') planId = value;
|
|
1602
|
+
if (name === '--expected-revision') expectedRevision = value;
|
|
1603
|
+
if (name === '--replacement-default') replacementDefault = value;
|
|
1604
|
+
flags.splice(index, 2); index -= 1;
|
|
1605
|
+
} else if (flags[index]?.startsWith('--plan=')) {
|
|
1606
|
+
planId = flags[index].slice('--plan='.length); flags.splice(index, 1); index -= 1;
|
|
1607
|
+
} else if (flags[index]?.startsWith('--expected-revision=')) {
|
|
1608
|
+
expectedRevision = flags[index].slice('--expected-revision='.length); flags.splice(index, 1); index -= 1;
|
|
1609
|
+
} else if (flags[index]?.startsWith('--replacement-default=')) {
|
|
1610
|
+
replacementDefault = flags[index].slice('--replacement-default='.length); flags.splice(index, 1); index -= 1;
|
|
1611
|
+
} else if (flags[index] === '--confirm') {
|
|
1612
|
+
confirm = true; flags.splice(index, 1); index -= 1;
|
|
1613
|
+
} else if (flags[index] === '--purge-orphan-credentials') {
|
|
1614
|
+
purgeOrphanCredentials = true; flags.splice(index, 1); index -= 1;
|
|
1615
|
+
}
|
|
1558
1616
|
}
|
|
1559
1617
|
const knownFlags = new Set(['--purge']);
|
|
1560
1618
|
const unknown = flags.filter((f) => f.startsWith('--') && !knownFlags.has(f));
|
|
1561
1619
|
const args = flags.filter((f) => !f.startsWith('--'));
|
|
1562
1620
|
if (!Number.isInteger(after) || after < 0) unknown.push('--after');
|
|
1563
1621
|
if (!['compact', 'full'].includes(detail)) unknown.push('--detail');
|
|
1564
|
-
return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown };
|
|
1565
|
-
}
|
|
1622
|
+
return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown };
|
|
1623
|
+
}
|
|
1566
1624
|
|
|
1567
1625
|
/**
|
|
1568
1626
|
* CLI dispatcher used by bin/dsh-crew.mjs. Returns a process exit code.
|
|
@@ -1573,7 +1631,7 @@ export async function runNpxCli({
|
|
|
1573
1631
|
error = console.error,
|
|
1574
1632
|
commands = {},
|
|
1575
1633
|
} = {}) {
|
|
1576
|
-
const { command, purge, candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown } = normalizeCommand(argv);
|
|
1634
|
+
const { command, purge, candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown } = normalizeCommand(argv);
|
|
1577
1635
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
1578
1636
|
log(USAGE);
|
|
1579
1637
|
return 0;
|
|
@@ -1582,7 +1640,7 @@ export async function runNpxCli({
|
|
|
1582
1640
|
error(USAGE);
|
|
1583
1641
|
return 1;
|
|
1584
1642
|
}
|
|
1585
|
-
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'inspect', 'jobs', 'providers', 'credentials', 'releases', 'rollback', 'update', 'uninstall'].includes(command)) {
|
|
1643
|
+
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'inspect', 'jobs', 'providers', 'credentials', 'releases', 'rollback', 'update', 'uninstall'].includes(command)) {
|
|
1586
1644
|
error(`unknown command: ${command ?? '<none>'}\n\n${USAGE}`);
|
|
1587
1645
|
return 1;
|
|
1588
1646
|
}
|
|
@@ -1592,23 +1650,23 @@ export async function runNpxCli({
|
|
|
1592
1650
|
integrate: commands.integrate ?? npxIntegrate,
|
|
1593
1651
|
detach: commands.detach ?? npxDetach,
|
|
1594
1652
|
status: commands.status ?? npxStatus,
|
|
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,
|
|
1653
|
+
inspect: commands.inspect ?? npxInspect,
|
|
1654
|
+
jobs: commands.jobs ?? npxJobs,
|
|
1655
|
+
providers: commands.providers ?? npxProviders,
|
|
1656
|
+
credentials: commands.credentials ?? npxCredentials,
|
|
1657
|
+
releases: commands.releases ?? npxReleases,
|
|
1658
|
+
rollback: commands.rollback ?? npxRollback,
|
|
1601
1659
|
update: commands.update ?? npxUpdate,
|
|
1602
1660
|
uninstall: commands.uninstall ?? npxUninstall,
|
|
1603
1661
|
};
|
|
1604
1662
|
let result;
|
|
1605
1663
|
if (command === 'uninstall') result = await actions.uninstall({ purge, 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 });
|
|
1664
|
+
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
1665
|
+
else if (command === 'jobs') result = await actions.jobs({ args, after, detail, request, log });
|
|
1666
|
+
else if (command === 'providers') result = await actions.providers({ args, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, log });
|
|
1667
|
+
else if (command === 'credentials') result = await actions.credentials({ args, planId, expectedRevision, confirm, log });
|
|
1668
|
+
else if (command === 'releases') result = await actions.releases({ args, log });
|
|
1669
|
+
else if (command === 'rollback') result = await actions.rollback({ version: args?.[0], args, log });
|
|
1612
1670
|
else result = await actions[command]({ log });
|
|
1613
1671
|
return result?.ok === false ? 1 : 0;
|
|
1614
1672
|
} catch (err) {
|