@ran-sh/dsh-crew 2.2.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/official-web-bridge/lib/client.js +1 -1
- package/official-web-bridge/overlay-entry.mjs +5 -1
- package/official-web-bridge/package.json +1 -1
- package/package.json +1 -1
- package/scripts/setup.mjs +39 -33
- package/scripts/verify-npm-install.mjs +2 -2
- package/src/adaptive-routing.mjs +2 -2
- package/src/ci-evidence.mjs +1 -1
- package/src/client/quick-panel.tsx +6 -2
- package/src/credential-lifecycle.mjs +0 -3
- package/src/credential-purge-state.mjs +1 -5
- package/src/credential-reference.mjs +0 -8
- package/src/crew-workspaces.mjs +1 -1
- package/src/delivery.mjs +2 -2
- package/src/dsh-cli-runtime.mjs +227 -30
- package/src/extension-contract.mjs +1 -1
- package/src/failure-classification.mjs +0 -10
- package/src/hub/entry.mjs +1 -1
- package/src/hub/index.mjs +57 -7
- package/src/i18n.mjs +0 -2
- package/src/install/crew-paths.mjs +16 -11
- package/src/install/crew-skill.mjs +1 -1
- package/src/install/desktop-profile.mjs +168 -0
- package/src/install/harness-config-import.mjs +204 -0
- package/src/install/install-legacy.mjs +3 -3
- package/src/install/npx-lifecycle.mjs +210 -125
- package/src/install/official-web.mjs +1 -1
- package/src/install/windows-startup.mjs +5 -5
- package/src/install/windows-supervisor-adapter.mjs +3 -2
- package/src/install/windows-supervisor-lifecycle.mjs +2 -2
- package/src/install/zcode.mjs +0 -2
- package/src/job-contracts.mjs +1 -1
- package/src/job-identity.mjs +2 -2
- package/src/jobs.mjs +3 -4
- package/src/model-routing.mjs +3 -4
- package/src/model-schedule.mjs +0 -1
- package/src/official-web-bridge.mjs +3 -1
- package/src/policy-legacy.mjs +10 -12
- package/src/provider-inventory.mjs +1 -1
- package/src/provider-layer-migration.mjs +0 -2
- package/src/provider-settings-store.mjs +19 -1
- package/src/readiness-matrix.mjs +1 -1
- package/src/release-in-use.mjs +1 -1
- package/src/role-profiles.mjs +2 -2
- package/src/runtime-controls.mjs +1 -1
- package/src/runtime-identity.mjs +1 -1
- package/src/status-shard.mjs +1 -1
- package/src/supervisor/restart-request.mjs +4 -4
- package/src/workflow-runtime.mjs +1 -2
- package/src/workspace-context.mjs +2 -2
- package/src/workspace-isolation.mjs +4 -4
- package/src/workspace-lock.mjs +2 -3
- package/scripts/policy-probe.mjs +0 -101
- package/scripts/verify-crew-ui-polish.mjs +0 -145
- package/scripts/verify-history-ui.mjs +0 -98
- package/scripts/verify-installer-fix.mjs +0 -26
|
@@ -42,11 +42,15 @@ import { homedir } from 'node:os';
|
|
|
42
42
|
import * as realInstaller from './install.mjs';
|
|
43
43
|
import { samePayloadContent, capturePayloadContent } from './payload-content.mjs';
|
|
44
44
|
import { crewDshHome, crewProfileDir, claudeIntegrationLine } from './install.mjs';
|
|
45
|
+
import { integrationRoot } from './crew-paths.mjs';
|
|
45
46
|
import { releaseClaimsState } from '../release-in-use.mjs';
|
|
46
47
|
import { compareProcessToken, processStartToken } from '../process-identity.mjs';
|
|
48
|
+
import { RELEASE_COHORT_FILENAME } from '../dsh-cohort.mjs';
|
|
47
49
|
import { renameTree } from './tree-move.mjs';
|
|
48
50
|
import { checkRuntimeAdvance, normalizeRuntimeState, runtimeStateMayHaveStarted } from './runtime-lifecycle.mjs';
|
|
49
|
-
import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
|
|
51
|
+
import { ensureCrewDshRuntime, ensureCrewPluginRegistration, ensureCrewWebProfile, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, gcRetainedRuntimes, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
|
|
52
|
+
import { desktopAttach, desktopDetach, desktopStatus } from './desktop-profile.mjs';
|
|
53
|
+
import { mirrorOfficialHarnessConfig, officialConfigMirrorStatus } from './harness-config-import.mjs';
|
|
50
54
|
import {
|
|
51
55
|
ensureOfficialWebIntegration,
|
|
52
56
|
officialWebIntegrationStatus,
|
|
@@ -61,15 +65,15 @@ import {
|
|
|
61
65
|
} from './windows-supervisor-adapter.mjs';
|
|
62
66
|
|
|
63
67
|
export const CREW_APP_DIRNAME = 'app';
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const RELEASES_DIRNAME = 'releases';
|
|
69
|
+
const CURRENT_POINTER_FILENAME = 'current.json';
|
|
70
|
+
const KEEP_RELEASES = 2;
|
|
67
71
|
export const INCOMPLETE_MARKER = '.dsh-crew-incomplete';
|
|
68
72
|
const CREW_ROUTE_BASE = '/_dsh/dsh-crew';
|
|
69
73
|
// Exact DSH cohort version: dotted numeric with optional -prerelease suffix.
|
|
70
74
|
// Anything else (ranges, "../..", paths) is NOT an authorized cohort value
|
|
71
75
|
// and must never reach rename/rmSync authority via retained-runtimes keys.
|
|
72
|
-
|
|
76
|
+
const EXACT_DSH_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
73
77
|
|
|
74
78
|
export function npmCliInvocation(args, {
|
|
75
79
|
platform = process.platform,
|
|
@@ -242,8 +246,8 @@ function writeCurrentPointer({ home, name, version, path }) {
|
|
|
242
246
|
return pointer;
|
|
243
247
|
}
|
|
244
248
|
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
const UPDATE_JOURNAL_FILENAME = 'update-journal.json';
|
|
250
|
+
const UPDATE_LOCK_FILENAME = 'update-in-progress.lock';
|
|
247
251
|
|
|
248
252
|
export function updateJournalFile({ home = homedir() } = {}) {
|
|
249
253
|
return join(crewAppRoot({ home }), UPDATE_JOURNAL_FILENAME);
|
|
@@ -453,7 +457,7 @@ function writeUpdateJournal({ home, stage, prior = null, candidate = null, verif
|
|
|
453
457
|
// identity matches the expected stage/prior/candidate. A crash between
|
|
454
458
|
// verification success and this write leaves the journal unverified, so
|
|
455
459
|
// reconcile refuses to finalize it.
|
|
456
|
-
|
|
460
|
+
function markJournalVerified({ home = homedir(), stage, prior = null, candidate = null, runtime = null } = {}) {
|
|
457
461
|
const current = readUpdateJournal({ home });
|
|
458
462
|
if (!current || current.malformed) return { ok: false, code: 'JOURNAL_NOT_FOUND' };
|
|
459
463
|
const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
@@ -481,82 +485,6 @@ function compensateActivationSync({ home, prior, manifest, log, installer }) {
|
|
|
481
485
|
return { ok: true, version: manifest.version, path: prior.path };
|
|
482
486
|
}
|
|
483
487
|
|
|
484
|
-
// Undo a first-install candidate's activation surfaces: remove its profile
|
|
485
|
-
// registration (dependency + bundle + junction) ONLY when each surface
|
|
486
|
-
// still references THIS journal's candidate. The dependency must resolve
|
|
487
|
-
// to exactly link:<candidateRealPath> (normalized slashes); a later
|
|
488
|
-
// legitimate registration pointing elsewhere is never removed. Mixed or
|
|
489
|
-
// unjudgeable state fails closed with the journal retained. When the
|
|
490
|
-
// profile manifest is missing/unreadable the junction is still handled
|
|
491
|
-
// independently so a "junction created, manifest never written" crash
|
|
492
|
-
// cannot leave a dangling junction.
|
|
493
|
-
function undoCandidateActivationSync({ home, candidateDir, candidateName }) {
|
|
494
|
-
if (!candidateDir || !candidateName) return { ok: true, undone: false };
|
|
495
|
-
const profileRoot = crewProfileDir({ home });
|
|
496
|
-
const profileFile = join(profileRoot, 'package.json');
|
|
497
|
-
let manifest = null;
|
|
498
|
-
let manifestReadable = true;
|
|
499
|
-
try { manifest = JSON.parse(readFileSync(profileFile, 'utf8')); } catch { manifest = null; manifestReadable = false; }
|
|
500
|
-
const linkPath = join(profileRoot, 'node_modules', ...candidateName.split('/'));
|
|
501
|
-
let linked = null;
|
|
502
|
-
try {
|
|
503
|
-
if (lstatSync(linkPath).isSymbolicLink()) linked = realpathSync(linkPath);
|
|
504
|
-
} catch { linked = null; }
|
|
505
|
-
let candidateReal = null;
|
|
506
|
-
try { candidateReal = realpathSync(candidateDir); } catch { candidateReal = candidateDir; }
|
|
507
|
-
const expectedDep = `link:${String(candidateReal).replace(/\\/g, '/')}`;
|
|
508
|
-
const rawDep = manifest?.dependencies?.[candidateName];
|
|
509
|
-
const depPointsAtCandidate = typeof rawDep === 'string'
|
|
510
|
-
&& rawDep.replace(/\\/g, '/') === expectedDep;
|
|
511
|
-
const bundleNamesCandidate = Array.isArray(manifest?.dsh?.profile?.bundles) && manifest.dsh.profile.bundles.includes(candidateName);
|
|
512
|
-
const linkPointsAtCandidate = linked !== null && linked === candidateReal;
|
|
513
|
-
// Bundle carries no path: it NEVER grants deletion authority by itself.
|
|
514
|
-
// It is removed only when dependency or junction proves THIS journal's
|
|
515
|
-
// candidate still owns the registration. A re-pointed dep/junction with
|
|
516
|
-
// a leftover same-name bundle fails closed (journal retained).
|
|
517
|
-
const identityEvidence = depPointsAtCandidate || linkPointsAtCandidate;
|
|
518
|
-
const bundlePointsAtCandidate = bundleNamesCandidate && identityEvidence;
|
|
519
|
-
// Manifest unreadable but junction dangles at candidate: remove junction.
|
|
520
|
-
if (!manifestReadable) {
|
|
521
|
-
if (!linkPointsAtCandidate) return { ok: true, undone: false };
|
|
522
|
-
try { rmSync(linkPath, { force: true }); } catch (error) {
|
|
523
|
-
return { ok: false, code: 'CANDIDATE_UNDO_FAILED', error: String(error?.message ?? error) };
|
|
524
|
-
}
|
|
525
|
-
return { ok: true, undone: true };
|
|
526
|
-
}
|
|
527
|
-
if (!manifest) return { ok: true, undone: false };
|
|
528
|
-
// Same-name package re-pointed elsewhere (dep or junction references a
|
|
529
|
-
// different target) while the bundle still names it: mixed state that
|
|
530
|
-
// cannot prove THIS candidate owns the registration. Fail closed,
|
|
531
|
-
// retain the journal for operator inspection.
|
|
532
|
-
const depPointsElsewhere = typeof rawDep === 'string' && !depPointsAtCandidate;
|
|
533
|
-
const linkPointsElsewhere = linked !== null && !linkPointsAtCandidate;
|
|
534
|
-
if ((depPointsElsewhere || linkPointsElsewhere) && bundleNamesCandidate) {
|
|
535
|
-
return { ok: false, code: 'CANDIDATE_UNDO_AMBIGUOUS', error: 'same-name package re-pointed elsewhere; refusing bundle removal' };
|
|
536
|
-
}
|
|
537
|
-
if (!depPointsAtCandidate && !bundlePointsAtCandidate && !linkPointsAtCandidate) {
|
|
538
|
-
return { ok: true, undone: false };
|
|
539
|
-
}
|
|
540
|
-
// Partial registration: only undo the surfaces that reference THIS
|
|
541
|
-
// candidate; leave anything already re-pointed elsewhere untouched.
|
|
542
|
-
try {
|
|
543
|
-
const next = { ...manifest };
|
|
544
|
-
if (depPointsAtCandidate) {
|
|
545
|
-
next.dependencies = { ...manifest.dependencies };
|
|
546
|
-
delete next.dependencies[candidateName];
|
|
547
|
-
if (Object.keys(next.dependencies).length === 0) delete next.dependencies;
|
|
548
|
-
}
|
|
549
|
-
if (bundlePointsAtCandidate) {
|
|
550
|
-
next.dsh = { ...manifest.dsh, profile: { ...manifest.dsh.profile, bundles: manifest.dsh.profile.bundles.filter((b) => b !== candidateName) } };
|
|
551
|
-
}
|
|
552
|
-
writeFileAtomic(profileFile, JSON.stringify(next, null, 2) + '\n');
|
|
553
|
-
if (linkPointsAtCandidate) rmSync(linkPath, { force: true });
|
|
554
|
-
} catch (error) {
|
|
555
|
-
return { ok: false, code: 'CANDIDATE_UNDO_FAILED', error: String(error?.message ?? error) };
|
|
556
|
-
}
|
|
557
|
-
return { ok: true, undone: true };
|
|
558
|
-
}
|
|
559
|
-
|
|
560
488
|
// Canonical containment validation for a journal's runtime segment. Recovery
|
|
561
489
|
// may rename/recursively-delete these roots, so liveRoot and retainedRoot
|
|
562
490
|
// must be EXACTLY the canonical Crew-owned paths (resolved, never a suffix
|
|
@@ -760,6 +688,7 @@ export async function reconcileUpdateJournal({ home = homedir(), log = () => {},
|
|
|
760
688
|
}
|
|
761
689
|
clearUpdateJournal({ home });
|
|
762
690
|
gcOldReleases({ home, protect: journal.prior?.path ?? null });
|
|
691
|
+
gcRetainedCohorts({ home });
|
|
763
692
|
log(`- recovered update journal at stage ${journal.stage}: candidate ${candidateIdent.version} already committed, finalized`);
|
|
764
693
|
return { ok: true, reconciled: true, stage: journal.stage, committed: true };
|
|
765
694
|
}
|
|
@@ -803,6 +732,7 @@ export async function reconcileUpdateJournal({ home = homedir(), log = () => {},
|
|
|
803
732
|
writeCurrentPointer({ home, name: candidateIdent.name, version: candidateIdent.version, path: candidateDir });
|
|
804
733
|
clearUpdateJournal({ home });
|
|
805
734
|
gcOldReleases({ home, protect: journal.prior?.path ?? null });
|
|
735
|
+
gcRetainedCohorts({ home });
|
|
806
736
|
log(`- recovered update journal at stage ${journal.stage}: candidate ${candidateIdent.version} was verified and started, so the commit was completed`);
|
|
807
737
|
return { ok: true, reconciled: true, stage: journal.stage, committed: true };
|
|
808
738
|
}
|
|
@@ -1043,6 +973,28 @@ function defaultNpmInstaller(stageRoot, log) {
|
|
|
1043
973
|
return true;
|
|
1044
974
|
}
|
|
1045
975
|
|
|
976
|
+
// Exact-version specs only (the pinned-cohort shape). Ranges stay with normal
|
|
977
|
+
// resolution: a range override would both fight the resolver and violate the
|
|
978
|
+
// verbatim-spec rule npm applies when a direct dependency shares the name.
|
|
979
|
+
const EXACT_SPEC_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Lock every exact-pinned dependency to its own version across the whole
|
|
983
|
+
* staged tree. The npm fallback install has no flag channel (sanitized env,
|
|
984
|
+
* fixed args), so a drifting transitive peer — e.g. a projection package
|
|
985
|
+
* peer-requiring a newer cohort than the payload's pin — otherwise aborts
|
|
986
|
+
* staging with ERESOLVE. npm v11 rejects an override whose spec differs from
|
|
987
|
+
* the direct dependency, so each override reuses the dependency's exact spec
|
|
988
|
+
* verbatim.
|
|
989
|
+
*/
|
|
990
|
+
export function exactSpecOverrides(dependencies) {
|
|
991
|
+
const overrides = {};
|
|
992
|
+
for (const [name, spec] of Object.entries(dependencies ?? {})) {
|
|
993
|
+
if (typeof spec === 'string' && EXACT_SPEC_RE.test(spec)) overrides[name] = spec;
|
|
994
|
+
}
|
|
995
|
+
return Object.keys(overrides).length > 0 ? overrides : null;
|
|
996
|
+
}
|
|
997
|
+
|
|
1046
998
|
// ---- payload staging ---------------------------------------------------------
|
|
1047
999
|
|
|
1048
1000
|
/**
|
|
@@ -1102,6 +1054,8 @@ export function stageCandidatePayload({
|
|
|
1102
1054
|
...(manifest.peerDependencies ?? {}),
|
|
1103
1055
|
...(manifest.dependencies ?? {}),
|
|
1104
1056
|
};
|
|
1057
|
+
const overrides = exactSpecOverrides(stagedManifest.dependencies);
|
|
1058
|
+
if (overrides) stagedManifest.overrides = overrides;
|
|
1105
1059
|
writeFileSync(join(stageDir, 'package.json'), JSON.stringify(stagedManifest, null, 2) + '\n');
|
|
1106
1060
|
|
|
1107
1061
|
// Materialize dependencies: prefer replicating the exact bits the candidate
|
|
@@ -1304,15 +1258,48 @@ export function commitActivatedRelease({ stageDir, manifest, home, prior = null
|
|
|
1304
1258
|
writeCurrentPointer({ home, name: manifest.name, version: manifest.version, path: stageDir });
|
|
1305
1259
|
clearUpdateJournal({ home });
|
|
1306
1260
|
gcOldReleases({ home, protect: prior?.path ?? null });
|
|
1261
|
+
gcRetainedCohorts({ home });
|
|
1307
1262
|
return stageDir;
|
|
1308
1263
|
}
|
|
1309
1264
|
|
|
1310
|
-
function commitStagedRelease({ stageDir, manifest, home, prior = null }) {
|
|
1311
|
-
return commitActivatedRelease({ stageDir, manifest, home, prior });
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
1265
|
const STALE_INCOMPLETE_MS = 24 * 60 * 60 * 1000;
|
|
1315
1266
|
|
|
1267
|
+
// A retained runtime cohort is needed only while some surviving release
|
|
1268
|
+
// resolves to it, so this runs next to release pruning at the commit points
|
|
1269
|
+
// that call gcOldReleases: dropping a release is what stops its cohort being
|
|
1270
|
+
// pinned. Resolution goes through resolveReleaseCohort, the same chain every
|
|
1271
|
+
// other caller uses (manifest pin, then the release-cohort.json sidecar), so a
|
|
1272
|
+
// release that names its cohort only in the sidecar keeps its runtime.
|
|
1273
|
+
//
|
|
1274
|
+
// Fail closed: the cohort set is trusted only when EVERY directory under
|
|
1275
|
+
// releases/ resolved, because "nothing pins it" and "could not tell" must not
|
|
1276
|
+
// prune the same way. Pruning is best-effort on top of that — it must never
|
|
1277
|
+
// turn an update that already committed its pointer into a failed one.
|
|
1278
|
+
//
|
|
1279
|
+
// performCoordinatedCohortUpdate commits without gcOldReleases and is
|
|
1280
|
+
// deliberately not covered here either; its prior tree is still the rollback
|
|
1281
|
+
// target at that point.
|
|
1282
|
+
function gcRetainedCohorts({ home }) {
|
|
1283
|
+
try {
|
|
1284
|
+
const releasesDir = crewReleasesDir({ home });
|
|
1285
|
+
if (!existsSync(releasesDir)) return [];
|
|
1286
|
+
const entries = readdirSync(releasesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
1287
|
+
const cohorts = [];
|
|
1288
|
+
for (const entry of entries) {
|
|
1289
|
+
const resolved = resolveReleaseCohort({ releaseDir: join(releasesDir, entry.name) });
|
|
1290
|
+
if (!resolved.ok) {
|
|
1291
|
+
process.emitWarning(`dsh-crew: release "${entry.name}" does not resolve to an exact cohort (${resolved.code ?? 'unknown'}); skipping retained-runtime pruning this pass`);
|
|
1292
|
+
return [];
|
|
1293
|
+
}
|
|
1294
|
+
cohorts.push(resolved.dshVersion);
|
|
1295
|
+
}
|
|
1296
|
+
return gcRetainedRuntimes({ home, cohorts });
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
process.emitWarning(`dsh-crew: retained-runtime pruning skipped: ${error?.message ?? error}`);
|
|
1299
|
+
return [];
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1316
1303
|
function gcOldReleases({ home, keep = KEEP_RELEASES, protect = null }) {
|
|
1317
1304
|
const pointer = readCurrentPointer({ home });
|
|
1318
1305
|
const releasesDir = crewReleasesDir({ home });
|
|
@@ -1362,7 +1349,7 @@ function gcOldReleases({ home, keep = KEEP_RELEASES, protect = null }) {
|
|
|
1362
1349
|
return removed;
|
|
1363
1350
|
}
|
|
1364
1351
|
|
|
1365
|
-
|
|
1352
|
+
function listManagedReleases({ home = homedir() } = {}) {
|
|
1366
1353
|
const pointer = readCurrentPointer({ home });
|
|
1367
1354
|
const root = crewReleasesDir({ home });
|
|
1368
1355
|
if (!existsSync(root)) return [];
|
|
@@ -1390,7 +1377,7 @@ export function listManagedReleases({ home = homedir() } = {}) {
|
|
|
1390
1377
|
// Windows supervisor executes it and writes a VERIFIED result. This polls
|
|
1391
1378
|
// until the result arrives (or the timeout elapses). NEVER talks to the
|
|
1392
1379
|
// legacy 3080 supervisor endpoint.
|
|
1393
|
-
|
|
1380
|
+
async function requestCrewRuntimeRestart({
|
|
1394
1381
|
reason = null,
|
|
1395
1382
|
fetchImpl = globalThis.fetch,
|
|
1396
1383
|
pollIntervalMs = 1_000,
|
|
@@ -1442,18 +1429,6 @@ export async function requestCrewRuntimeRestart({
|
|
|
1442
1429
|
}
|
|
1443
1430
|
}
|
|
1444
1431
|
|
|
1445
|
-
async function restartOwnedRuntime(fetchImpl = globalThis.fetch) {
|
|
1446
|
-
return requestCrewRuntimeRestart({ fetchImpl, reason: 'lifecycle restart' });
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
async function verifyRuntimeVersion(version, fetchImpl = globalThis.fetch) {
|
|
1450
|
-
const response = await fetchImpl('http://127.0.0.1:3210/_dsh/dsh-crew/runtime', { headers: { accept: 'application/json' } });
|
|
1451
|
-
const body = await response.json();
|
|
1452
|
-
return response.ok && body?.ok === true && body.runtime_version === version
|
|
1453
|
-
? { ok: true, runtime_version: body.runtime_version }
|
|
1454
|
-
: { ok: false, code: 'RUNTIME_VERSION_MISMATCH' };
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
1432
|
// Cohort verifier: compares the TARGET DSH cohort against the Hub's
|
|
1458
1433
|
// dsh_version domain (the installed @deepseek-ai/dsh package), NEVER
|
|
1459
1434
|
// against Crew's own runtime_version (the dsh-crew release). A null
|
|
@@ -1509,7 +1484,7 @@ export async function verifyCrewRuntimeIdentity(version, fetchImpl = globalThis.
|
|
|
1509
1484
|
// AND the expected DSH cohort (dsh_version domain), alongside full 3210
|
|
1510
1485
|
// identity. A stale old-Crew process on the right cohort (or vice versa)
|
|
1511
1486
|
// fails closed instead of reporting rollback success.
|
|
1512
|
-
|
|
1487
|
+
async function verifyRollbackTarget({ crewVersion, dshVersion, fetchImpl = globalThis.fetch } = {}) {
|
|
1513
1488
|
let body = null;
|
|
1514
1489
|
try {
|
|
1515
1490
|
const response = await fetchImpl('http://127.0.0.1:3210/_dsh/dsh-crew/extension', { headers: { accept: 'application/json' } });
|
|
@@ -1910,10 +1885,10 @@ async function finishLifecycleAfterUpdateLock({
|
|
|
1910
1885
|
// Lifecycle-owned cohort sidecar for a managed release directory. Historical
|
|
1911
1886
|
// releases (pre-1.0.4) do not pin @deepseek-ai/dsh in their immutable
|
|
1912
1887
|
// manifest; this records the cohort fact the lifecycle observed for them.
|
|
1913
|
-
const cohortFile = (releaseDir) => join(releaseDir,
|
|
1888
|
+
const cohortFile = (releaseDir) => join(releaseDir, RELEASE_COHORT_FILENAME);
|
|
1914
1889
|
|
|
1915
1890
|
// Exact DSH cohort version helper: delegates to the module-level regex.
|
|
1916
|
-
|
|
1891
|
+
function isExactDshVersion(value) {
|
|
1917
1892
|
return typeof value === 'string' && EXACT_DSH_VERSION_RE.test(value);
|
|
1918
1893
|
}
|
|
1919
1894
|
|
|
@@ -2242,7 +2217,7 @@ function registrationHealthy({ home, name, releaseDir }) {
|
|
|
2242
2217
|
* config resolution. Package-manager children must see the user's real
|
|
2243
2218
|
* environment, not our execution context.
|
|
2244
2219
|
*/
|
|
2245
|
-
|
|
2220
|
+
function sanitizedPackageManagerEnv(baseEnv = process.env) {
|
|
2246
2221
|
const env = {};
|
|
2247
2222
|
for (const [key, value] of Object.entries(baseEnv)) {
|
|
2248
2223
|
if (/^npm_(config_|lifecycle|package_|execpath$|node_execpath$)/i.test(key)) continue;
|
|
@@ -2269,7 +2244,7 @@ export function buildProductionMigration({ home, log = () => {}, stopOwned, star
|
|
|
2269
2244
|
});
|
|
2270
2245
|
}
|
|
2271
2246
|
|
|
2272
|
-
async function ensureRuntimeStep({ home, log, ensureRuntime, migrateRuntime, stopOwned, startOwned, verifyOwned, supervisorFactory = crewSupervisor }) { const ensure = ensureRuntime ?? ((opts) => {
|
|
2247
|
+
async function ensureRuntimeStep({ home, log, ensureRuntime, migrateRuntime, ensureWebProfile, stopOwned, startOwned, verifyOwned, supervisorFactory = crewSupervisor }) { const ensure = ensureRuntime ?? ((opts) => {
|
|
2273
2248
|
const r = ensureCrewDshRuntime({ ...opts, env: sanitizedPackageManagerEnv() });
|
|
2274
2249
|
if (!r.ok && r.stderrTail) {
|
|
2275
2250
|
log(` (runtime installer said: ${r.stderrTail})`);
|
|
@@ -2277,17 +2252,37 @@ async function ensureRuntimeStep({ home, log, ensureRuntime, migrateRuntime, sto
|
|
|
2277
2252
|
if (!r.ok && r.code === 'DSH_RUNTIME_COHORT_MISMATCH') {
|
|
2278
2253
|
return { ok: false, code: r.code, error: r.error, installed: r.installed, target: r.target, needsMigration: true };
|
|
2279
2254
|
}
|
|
2280
|
-
return r.ok
|
|
2255
|
+
return r.ok
|
|
2256
|
+
? { ok: true, version: r.cli?.version ?? null, cohortRepaired: r.cohortRepaired === true }
|
|
2257
|
+
: { ok: false, error: r.error ?? r.code ?? 'runtime bootstrap failed' };
|
|
2281
2258
|
});
|
|
2282
2259
|
// Production migration wiring: ONE supervisor instance per migration,
|
|
2283
2260
|
// reused by stop/start/verify/rollback so child identity stays coherent.
|
|
2284
2261
|
// No legacy bridge involved. Missing callbacks fail closed inside
|
|
2285
2262
|
// migrateCrewDshRuntime.
|
|
2286
2263
|
const migrate = migrateRuntime ?? buildProductionMigration({ home, log, stopOwned, startOwned, verifyOwned, supervisorFactory });
|
|
2264
|
+
// The runtime alone cannot boot a managed 3210/3080: the launcher preflight
|
|
2265
|
+
// and the supervisor require <DSH_HOME>/profiles/web. Materialize it here —
|
|
2266
|
+
// before any supervisor convergence, on every path that ends in "runtime
|
|
2267
|
+
// ready" — so a fresh install cannot deadlock on a heartbeat that a
|
|
2268
|
+
// profile-less service can never publish.
|
|
2269
|
+
const convergeWebProfile = () => {
|
|
2270
|
+
const profile = (ensureWebProfile ?? ensureCrewWebProfile)({ home });
|
|
2271
|
+
if (!profile.ok) {
|
|
2272
|
+
log(`✗ Crew web profile bootstrap failed (${profile.code ?? 'unknown error'})`);
|
|
2273
|
+
return false;
|
|
2274
|
+
}
|
|
2275
|
+
if (profile.created) log(`✓ Crew web profile scaffold created (${profile.profileRoot})`);
|
|
2276
|
+
return true;
|
|
2277
|
+
};
|
|
2287
2278
|
const r = await ensure({ home });
|
|
2288
2279
|
if (r?.ok) {
|
|
2289
2280
|
log(`✓ reusable Crew DSH runtime${r.version ? ` (@${r.version})` : ''}`);
|
|
2290
|
-
|
|
2281
|
+
// The install locks the whole family when the registry had published a newer
|
|
2282
|
+
// prerelease than the pinned cohort; say so, because a silent repair is
|
|
2283
|
+
// indistinguishable from a registry that never drifted.
|
|
2284
|
+
if (r.cohortRepaired) log(' the DSH family had drifted from the pinned cohort; installed the pinned family instead');
|
|
2285
|
+
return convergeWebProfile();
|
|
2291
2286
|
}
|
|
2292
2287
|
if (r?.needsMigration === true) {
|
|
2293
2288
|
log(`- runtime cohort stale (${r.installed} -> ${r.target}); migrating via staged transaction`);
|
|
@@ -2297,7 +2292,7 @@ async function ensureRuntimeStep({ home, log, ensureRuntime, migrateRuntime, sto
|
|
|
2297
2292
|
return false;
|
|
2298
2293
|
}
|
|
2299
2294
|
log(`✓ runtime cohort migrated (@${m.version})`);
|
|
2300
|
-
return
|
|
2295
|
+
return convergeWebProfile();
|
|
2301
2296
|
}
|
|
2302
2297
|
log(`✗ reusable Crew DSH runtime unavailable: ${r?.error ?? 'unknown error'}`);
|
|
2303
2298
|
return false;
|
|
@@ -2676,9 +2671,18 @@ async function activateRelease({ home, releaseDir, manifest, log, installer, sup
|
|
|
2676
2671
|
// leave four configurations naming a directory that is gone. Writing the
|
|
2677
2672
|
// release path directly is what made crash recovery unable to delete a
|
|
2678
2673
|
// candidate without breaking Codex, ZCode and Claude Code.
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2674
|
+
//
|
|
2675
|
+
// The root is derived through the SAME integrationRoot() the readiness
|
|
2676
|
+
// checks use, not blindly from registration.linkPath: write and validate
|
|
2677
|
+
// must be one function of (home, releaseDir, name). In the healthy case the
|
|
2678
|
+
// gate passes right after registration re-pointed the link, so this is
|
|
2679
|
+
// still the link path; when the gate fails, both sides fall back to the
|
|
2680
|
+
// release directory together instead of the writer using the link while the
|
|
2681
|
+
// validator reports the release — the disagreement that made every clean
|
|
2682
|
+
// install read back as "needs repair".
|
|
2683
|
+
const integrationWriteRoot = integrationRoot({ home, root: releaseDir, name: manifest.name });
|
|
2684
|
+
|
|
2685
|
+
const codex = installer.installCodex({ home, root: integrationWriteRoot });
|
|
2682
2686
|
if (codex.ok === false) {
|
|
2683
2687
|
log(`✗ Codex Desktop integration failed: ${(codex.actions ?? []).join('; ')}`);
|
|
2684
2688
|
return false;
|
|
@@ -2686,7 +2690,7 @@ async function activateRelease({ home, releaseDir, manifest, log, installer, sup
|
|
|
2686
2690
|
log('✓ Codex Desktop integration');
|
|
2687
2691
|
|
|
2688
2692
|
if (installer.installZCode) {
|
|
2689
|
-
const zcode = installer.installZCode({ home, root:
|
|
2693
|
+
const zcode = installer.installZCode({ home, root: integrationWriteRoot });
|
|
2690
2694
|
if (zcode.ok === false) {
|
|
2691
2695
|
log(`✗ ZCode integration failed (${zcode.code ?? 'unknown'})`);
|
|
2692
2696
|
return false;
|
|
@@ -2701,7 +2705,7 @@ async function activateRelease({ home, releaseDir, manifest, log, installer, sup
|
|
|
2701
2705
|
}
|
|
2702
2706
|
if (startup?.supported) log('✓ Windows login startup');
|
|
2703
2707
|
|
|
2704
|
-
const claude = await installer.installClaudeCode({ home, root:
|
|
2708
|
+
const claude = await installer.installClaudeCode({ home, root: integrationWriteRoot });
|
|
2705
2709
|
if (claude.ok === false) {
|
|
2706
2710
|
log(claudeIntegrationLine(claude));
|
|
2707
2711
|
return false;
|
|
@@ -2740,6 +2744,59 @@ export async function npxDetach({ home = homedir(), log = console.log } = {}) {
|
|
|
2740
2744
|
return { ok: false, error: 'OFFICIAL_WEB_PROFILE_READ_ONLY' };
|
|
2741
2745
|
}
|
|
2742
2746
|
|
|
2747
|
+
// Desktop-client integration. The official desktop app boots its Harness with the
|
|
2748
|
+
// app-exclusive `desktop` profile, so the Crew panel can only reach it through
|
|
2749
|
+
// that profile's own patch layer — the one official-home write Crew performs, and
|
|
2750
|
+
// only because an operator asked for it by name. It never runs during install,
|
|
2751
|
+
// update or supervision.
|
|
2752
|
+
export async function npxDesktop({ home = homedir(), args = [], dryRun = false, log = console.log } = {}) {
|
|
2753
|
+
const action = args[0] ?? 'status';
|
|
2754
|
+
if (!['attach', 'detach', 'status'].includes(action)) {
|
|
2755
|
+
log(`✗ unknown desktop action: ${action}`);
|
|
2756
|
+
return { ok: false, error: 'usage: dsh-crew desktop <attach|detach|status> [--dry-run]' };
|
|
2757
|
+
}
|
|
2758
|
+
if (action === 'status') {
|
|
2759
|
+
const status = desktopStatus({ home });
|
|
2760
|
+
if (!status.file_present) log(`- desktop patch layer: absent (${status.path})`);
|
|
2761
|
+
else if (!status.attached) log(`- desktop patch layer: present, no Crew bridge (${status.path})`);
|
|
2762
|
+
else if (status.current) log(`✓ desktop patch layer carries the current Crew bridge (revision ${status.bridge?.revision})`);
|
|
2763
|
+
else log(`! desktop patch layer carries a stale Crew bridge (current revision ${status.bridge?.revision ?? 'unavailable'})`);
|
|
2764
|
+
if (!status.bridge) log(` (${status.bridge_error})`);
|
|
2765
|
+
log(JSON.stringify(status, null, 2));
|
|
2766
|
+
return { ok: true, status };
|
|
2767
|
+
}
|
|
2768
|
+
const result = action === 'attach'
|
|
2769
|
+
? desktopAttach({ home, dryRun, log })
|
|
2770
|
+
: desktopDetach({ home, dryRun, log });
|
|
2771
|
+
if (!result.ok) log(`✗ desktop ${action} failed: ${result.error ?? result.code}`);
|
|
2772
|
+
return result;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
// Bring the operator's user-level DSH configuration across from the official home
|
|
2776
|
+
// into Crew's home: the Crew hub then runs the same models, providers, policy and
|
|
2777
|
+
// MCP servers as the desktop app. Read-only on ~/.dsh, no secrets copied (provider
|
|
2778
|
+
// entries keep their apiKeyEnv references, and those variables are inherited).
|
|
2779
|
+
export async function npxConfig({ home = homedir(), args = [], dryRun = false, log = console.log } = {}) {
|
|
2780
|
+
const action = args[0] ?? 'status';
|
|
2781
|
+
if (!['import', 'status'].includes(action)) {
|
|
2782
|
+
log(`✗ unknown config action: ${action}`);
|
|
2783
|
+
return { ok: false, error: 'usage: dsh-crew config <import|status> [--dry-run]' };
|
|
2784
|
+
}
|
|
2785
|
+
if (action === 'status') {
|
|
2786
|
+
const status = officialConfigMirrorStatus({ home });
|
|
2787
|
+
for (const layer of [status.home_layer, status.profile_layer]) {
|
|
2788
|
+
log(`- ${layer.label}: ${!layer.source_present ? `no official source (${layer.source})` : layer.mirrored ? 'mirrored' : 'not imported yet (run: dsh-crew config import)'}`);
|
|
2789
|
+
}
|
|
2790
|
+
log(JSON.stringify(status, null, 2));
|
|
2791
|
+
return { ok: true, status };
|
|
2792
|
+
}
|
|
2793
|
+
const result = mirrorOfficialHarnessConfig({ home, dryRun, log });
|
|
2794
|
+
if (result.ok && result.changed && !dryRun) {
|
|
2795
|
+
log(' the Crew harness hot-watches its patch layers; the hub picks the imported configuration up without a restart');
|
|
2796
|
+
}
|
|
2797
|
+
return result;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2743
2800
|
export async function npxInstall({
|
|
2744
2801
|
home = homedir(),
|
|
2745
2802
|
log = console.log,
|
|
@@ -2818,15 +2875,15 @@ async function npxInstallInner({ home, log, sourceRoot, installer, ensureRuntime
|
|
|
2818
2875
|
return { ok: true, version: manifest.version, path: releaseDir };
|
|
2819
2876
|
}
|
|
2820
2877
|
|
|
2821
|
-
|
|
2822
|
-
|
|
2878
|
+
const UPDATE_PACKAGE_NAME = '@ran-sh/dsh-crew';
|
|
2879
|
+
const UPDATE_DEFAULT_SPEC = `${UPDATE_PACKAGE_NAME}@latest`;
|
|
2823
2880
|
|
|
2824
2881
|
/**
|
|
2825
2882
|
* Extract a packed npm tarball with the platform `tar` binary (bsdtar ships
|
|
2826
2883
|
* with Windows 10+, macOS, and Linux) into destDir; npm tarballs always root
|
|
2827
2884
|
* at `package/`.
|
|
2828
2885
|
*/
|
|
2829
|
-
|
|
2886
|
+
function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } = {}) {
|
|
2830
2887
|
mkdirSync(destDir, { recursive: true });
|
|
2831
2888
|
// GNU tar interprets a `C:\...` argument as a remote rsh host ("Cannot
|
|
2832
2889
|
// connect to C:"), so anchor the invocation inside destDir and pass the
|
|
@@ -3248,6 +3305,16 @@ export function npxStatus({
|
|
|
3248
3305
|
}
|
|
3249
3306
|
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
|
|
3250
3307
|
log(`Official 3080 UI bridge: ${officialWeb}`);
|
|
3308
|
+
// The desktop app carries its own bridge entry, which pins a revision: after a
|
|
3309
|
+
// payload update that entry is stale until it is re-pointed, so say which of the
|
|
3310
|
+
// three states the machine is in instead of leaving the panel silently old.
|
|
3311
|
+
const desktop = desktopStatus({ home });
|
|
3312
|
+
log(`Desktop app integration: ${
|
|
3313
|
+
!desktop.file_present ? 'not attached (run: dsh-crew desktop attach)'
|
|
3314
|
+
: desktop.attached && desktop.current ? 'attached'
|
|
3315
|
+
: desktop.attached ? 'attached to a stale bridge revision (run: dsh-crew desktop attach)'
|
|
3316
|
+
: 'not attached (run: dsh-crew desktop attach)'
|
|
3317
|
+
}`);
|
|
3251
3318
|
log(`Codex Desktop integration: ${codex}`);
|
|
3252
3319
|
log(`ZCode integration: ${zcode}`);
|
|
3253
3320
|
log(`Claude Code integration: ${claude}`);
|
|
@@ -3343,6 +3410,16 @@ Commands:
|
|
|
3343
3410
|
install persist the candidate package into Crew-owned state and register it
|
|
3344
3411
|
integrate disabled: the official web profile is read-only (legacy bridge retired)
|
|
3345
3412
|
detach disabled: the official web profile is read-only (legacy bridge retired)
|
|
3413
|
+
desktop the official DSH desktop app: attach|detach|status the Crew panel in its
|
|
3414
|
+
own patch layer (~/.dsh/profiles/desktop/cordis.patch.yml, hot-watched
|
|
3415
|
+
by the Harness). The only official-home write Crew performs, and only
|
|
3416
|
+
when this command is run; attach is idempotent and follows the current
|
|
3417
|
+
bridge revision, detach removes exactly the block it wrote
|
|
3418
|
+
config import|status the operator's user-level DSH configuration (models,
|
|
3419
|
+
providers, policy, MCP servers) from the official home into Crew's own
|
|
3420
|
+
home, so the Crew hub matches the desktop app. Read-only on ~/.dsh; no
|
|
3421
|
+
secrets are copied (provider entries keep their apiKeyEnv references);
|
|
3422
|
+
entries naming packages absent from Crew's runtime cohort are skipped
|
|
3346
3423
|
status read-only report of launcher/installed versions and integrations
|
|
3347
3424
|
inspect print the machine-readable extension capability/readiness contract
|
|
3348
3425
|
jobs machine-first job API: list|get|watch|cancel|submit
|
|
@@ -3364,6 +3441,7 @@ Options:
|
|
|
3364
3441
|
--replacement-default <id> with providers delete-plan: replacement Harness Default provider
|
|
3365
3442
|
--confirm with providers delete: confirm the destructive mutation
|
|
3366
3443
|
--purge with uninstall: also remove ~/.config/dsh-crew config/backups (destructive)
|
|
3444
|
+
--dry-run with desktop attach|detach: report the change without writing
|
|
3367
3445
|
--help show this help
|
|
3368
3446
|
|
|
3369
3447
|
Primary install: npm install -g @ran-sh/dsh-crew (then run: dsh-crew install)
|
|
@@ -3449,9 +3527,9 @@ const CREDENTIAL_CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
|
3449
3527
|
'purge-plan': Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
|
|
3450
3528
|
purge: Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
|
|
3451
3529
|
});
|
|
3452
|
-
|
|
3530
|
+
const PRODUCTION_HUB_URL = 'http://127.0.0.1:3210';
|
|
3453
3531
|
|
|
3454
|
-
|
|
3532
|
+
async function assertProductionHub({ hubUrl, requiredCapabilities = [], purpose = 'command', fetchImpl = globalThis.fetch } = {}) {
|
|
3455
3533
|
if (hubUrl !== PRODUCTION_HUB_URL) throw new Error(`${purpose} requires the isolated 3210 Crew Hub`);
|
|
3456
3534
|
const response = await fetchImpl(`${hubUrl}${CREW_ROUTE_BASE}/runtime`, { headers: { accept: 'application/json' } });
|
|
3457
3535
|
let body;
|
|
@@ -3474,7 +3552,7 @@ export async function assertProductionHub({ hubUrl, requiredCapabilities = [], p
|
|
|
3474
3552
|
* surface before sending a provider request. This prevents a same-port stale
|
|
3475
3553
|
* Hub from turning a later 404 into an ambiguous destructive failure.
|
|
3476
3554
|
*/
|
|
3477
|
-
|
|
3555
|
+
async function assertProviderHubCapabilities({ hubUrl, action, fetchImpl = globalThis.fetch } = {}) {
|
|
3478
3556
|
return assertProductionHub({
|
|
3479
3557
|
hubUrl,
|
|
3480
3558
|
requiredCapabilities: PROVIDER_CAPABILITY_REQUIREMENTS[action] ?? PROVIDER_CAPABILITY_REQUIREMENTS.list,
|
|
@@ -3674,6 +3752,7 @@ function normalizeCommand(argv) {
|
|
|
3674
3752
|
let expectedRevision;
|
|
3675
3753
|
let replacementDefault;
|
|
3676
3754
|
let confirm = false;
|
|
3755
|
+
let dryRun = false;
|
|
3677
3756
|
let purgeOrphanCredentials = false;
|
|
3678
3757
|
for (let index = 0; index < flags.length; index += 1) {
|
|
3679
3758
|
if (flags[index] === '--candidate') {
|
|
@@ -3713,16 +3792,18 @@ function normalizeCommand(argv) {
|
|
|
3713
3792
|
replacementDefault = flags[index].slice('--replacement-default='.length); flags.splice(index, 1); index -= 1;
|
|
3714
3793
|
} else if (flags[index] === '--confirm') {
|
|
3715
3794
|
confirm = true; flags.splice(index, 1); index -= 1;
|
|
3795
|
+
} else if (flags[index] === '--dry-run') {
|
|
3796
|
+
dryRun = true; flags.splice(index, 1); index -= 1;
|
|
3716
3797
|
} else if (flags[index] === '--purge-orphan-credentials') {
|
|
3717
3798
|
purgeOrphanCredentials = true; flags.splice(index, 1); index -= 1;
|
|
3718
3799
|
}
|
|
3719
3800
|
}
|
|
3720
|
-
const knownFlags = new Set(['--purge']);
|
|
3801
|
+
const knownFlags = new Set(['--purge', '--dry-run']);
|
|
3721
3802
|
const unknown = flags.filter((f) => f.startsWith('--') && !knownFlags.has(f));
|
|
3722
3803
|
const args = flags.filter((f) => !f.startsWith('--'));
|
|
3723
3804
|
if (!Number.isInteger(after) || after < 0) unknown.push('--after');
|
|
3724
3805
|
if (!['compact', 'full'].includes(detail)) unknown.push('--detail');
|
|
3725
|
-
return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown };
|
|
3806
|
+
return { command: argv[0], purge: flags.includes('--purge'), candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, dryRun, purgeOrphanCredentials, args, unknown };
|
|
3726
3807
|
}
|
|
3727
3808
|
|
|
3728
3809
|
/**
|
|
@@ -3734,7 +3815,7 @@ export async function runNpxCli({
|
|
|
3734
3815
|
error = console.error,
|
|
3735
3816
|
commands = {},
|
|
3736
3817
|
} = {}) {
|
|
3737
|
-
const { command, purge, candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, args, unknown } = normalizeCommand(argv);
|
|
3818
|
+
const { command, purge, candidate, after, detail, request, planId, expectedRevision, replacementDefault, confirm, dryRun, purgeOrphanCredentials, args, unknown } = normalizeCommand(argv);
|
|
3738
3819
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
3739
3820
|
log(USAGE);
|
|
3740
3821
|
return 0;
|
|
@@ -3743,7 +3824,7 @@ export async function runNpxCli({
|
|
|
3743
3824
|
error(USAGE);
|
|
3744
3825
|
return 1;
|
|
3745
3826
|
}
|
|
3746
|
-
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'inspect', 'jobs', 'providers', 'credentials', 'releases', 'rollback', 'update', 'uninstall'].includes(command)) {
|
|
3827
|
+
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'desktop', 'config', 'status', 'inspect', 'jobs', 'providers', 'credentials', 'releases', 'rollback', 'update', 'uninstall'].includes(command)) {
|
|
3747
3828
|
error(`unknown command: ${command ?? '<none>'}\n\n${USAGE}`);
|
|
3748
3829
|
return 1;
|
|
3749
3830
|
}
|
|
@@ -3752,6 +3833,8 @@ export async function runNpxCli({
|
|
|
3752
3833
|
install: commands.install ?? npxInstall,
|
|
3753
3834
|
integrate: commands.integrate ?? npxIntegrate,
|
|
3754
3835
|
detach: commands.detach ?? npxDetach,
|
|
3836
|
+
desktop: commands.desktop ?? npxDesktop,
|
|
3837
|
+
config: commands.config ?? npxConfig,
|
|
3755
3838
|
status: commands.status ?? npxStatus,
|
|
3756
3839
|
inspect: commands.inspect ?? npxInspect,
|
|
3757
3840
|
jobs: commands.jobs ?? npxJobs,
|
|
@@ -3765,6 +3848,8 @@ export async function runNpxCli({
|
|
|
3765
3848
|
let result;
|
|
3766
3849
|
if (command === 'uninstall') result = await actions.uninstall({ purge, log });
|
|
3767
3850
|
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
3851
|
+
else if (command === 'desktop') result = await actions.desktop({ args, dryRun, log });
|
|
3852
|
+
else if (command === 'config') result = await actions.config({ args, dryRun, log });
|
|
3768
3853
|
else if (command === 'jobs') result = await actions.jobs({ args, after, detail, request, log });
|
|
3769
3854
|
else if (command === 'providers') result = await actions.providers({ args, planId, expectedRevision, replacementDefault, confirm, purgeOrphanCredentials, log });
|
|
3770
3855
|
else if (command === 'credentials') result = await actions.credentials({ args, planId, expectedRevision, confirm, log });
|
|
@@ -10,7 +10,7 @@ import { join } from 'node:path';
|
|
|
10
10
|
export const OFFICIAL_BRIDGE_PACKAGE = '@ran-sh/dsh-crew-web-bridge';
|
|
11
11
|
const STATE_FILENAME = 'official-web.json';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
function officialWebProfileDir({ home = homedir() } = {}) {
|
|
14
14
|
return join(home, '.dsh', 'profiles', 'web');
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -12,11 +12,11 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import { installOfficialFrontendAssets, officialFrontendAssetsReady } from './official-frontend-assets.mjs';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const WINDOWS_STARTUP_FILENAME = 'DSH Crew.vbs';
|
|
16
|
+
const WINDOWS_LAUNCHER_FILENAME = 'start-dsh-crew.cmd';
|
|
17
|
+
const WINDOWS_HELPER_FILENAME = 'start-dsh-crew.ps1';
|
|
18
|
+
const WINDOWS_CONTROL_FILENAME = 'supervisor-control.ps1';
|
|
19
|
+
const WINDOWS_SUPERVISOR_ASSET_MANIFEST = 'supervisor-assets.json';
|
|
20
20
|
|
|
21
21
|
function defaultStartupDir({ home, env }) {
|
|
22
22
|
if (home === homedir() && env.APPDATA) {
|