@ran-sh/dsh-crew 2.1.8 → 2.2.1
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/docs/job-contracts.md +14 -1
- package/docs/readiness-matrix.md +20 -1
- package/lib/client.js +9 -8
- package/package.json +1 -1
- package/scripts/setup.mjs +6 -27
- package/scripts/verify-npm-install.mjs +2 -2
- package/skills/dsh-crew/SKILL.md +24 -15
- package/src/adaptive-routing.mjs +2 -2
- package/src/ci-evidence.mjs +1 -1
- package/src/client/history-panel.tsx +6 -5
- package/src/client/quick-panel.tsx +0 -1
- 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 +136 -0
- package/src/delivery.mjs +2 -2
- package/src/dsh-cli-runtime.mjs +18 -22
- package/src/extension-contract.mjs +1 -1
- package/src/failure-classification.mjs +0 -10
- package/src/history/cleanup-plan.mjs +15 -5
- package/src/hub/entry.mjs +1 -1
- package/src/hub/index.mjs +45 -19
- package/src/i18n.mjs +0 -2
- package/src/install/crew-skill.mjs +1 -1
- package/src/install/install-legacy.mjs +3 -3
- package/src/install/npx-lifecycle.mjs +60 -112
- 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 +13 -6
- package/src/jobs.mjs +3 -4
- package/src/mcp-runtime.mjs +17 -12
- package/src/model-routing.mjs +3 -4
- package/src/model-schedule.mjs +0 -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/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 +71 -4
- package/src/workspace-lock.mjs +134 -0
- 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
package/src/dsh-cli-runtime.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
// Re-exported so existing importers keep working while the cohort value now
|
|
36
36
|
// lives in exactly one place (src/dsh-cohort.mjs).
|
|
37
37
|
export { DSH_CLI_PACKAGE, TARGET_DSH_VERSION, TARGET_DSH_SPEC };
|
|
38
|
-
|
|
38
|
+
const CREW_DSH_RUNTIME_DIRNAME = 'runtime';
|
|
39
39
|
const CREW_PROFILE_DEFAULT_BUNDLES = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'];
|
|
40
40
|
const PROFILE_PATCH_TEMPLATE = '[]\n';
|
|
41
41
|
const PROFILE_PNPM_WORKSPACE = 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n';
|
|
@@ -73,11 +73,6 @@ export function crewDshRuntimeVersionDir({ home = homedir(), version = TARGET_DS
|
|
|
73
73
|
return join(crewDshHome({ home }), `runtime-${version}`);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function crewDshRuntimeEntry({ home = homedir(), platform = process.platform } = {}) {
|
|
77
|
-
const suffix = platform === 'win32' ? '.cmd' : '';
|
|
78
|
-
return join(crewDshRuntimeRoot({ home }), 'node_modules', '.bin', `dsh${suffix}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
76
|
export function crewDshRuntimeModule({ home = homedir() } = {}) {
|
|
82
77
|
return join(crewDshRuntimeRoot({ home }), 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js');
|
|
83
78
|
}
|
|
@@ -736,35 +731,36 @@ export async function restoreRetainedRuntime({
|
|
|
736
731
|
}
|
|
737
732
|
|
|
738
733
|
// GC retained runtimes that no release pins. A retained cohort is needed only
|
|
739
|
-
// while some managed release
|
|
740
|
-
//
|
|
741
|
-
|
|
734
|
+
// while some managed release resolves to it, so the caller passes the resolved
|
|
735
|
+
// cohort versions (see resolveReleaseCohort: manifest pin, then the
|
|
736
|
+
// release-cohort.json sidecar). Best-effort; never throws — a root that is
|
|
737
|
+
// missing, unreadable or not a directory prunes nothing rather than failing
|
|
738
|
+
// the update that just committed.
|
|
739
|
+
export function gcRetainedRuntimes({ home = homedir(), cohorts = [], log = () => {} } = {}) {
|
|
742
740
|
const root = retainedRuntimesRoot({ home });
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const spec = payloadDshSpec(release);
|
|
747
|
-
if (spec && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(spec)) needed.add(spec);
|
|
748
|
-
}
|
|
741
|
+
const needed = new Set(cohorts.filter((spec) => typeof spec === 'string' && EXACT_COHORT_RE.test(spec)));
|
|
742
|
+
let names;
|
|
743
|
+
try { names = readdirSync(root); } catch { return []; }
|
|
749
744
|
const removed = [];
|
|
750
|
-
for (const name of
|
|
745
|
+
for (const name of names) {
|
|
746
|
+
if (needed.has(name)) continue;
|
|
751
747
|
const dir = join(root, name);
|
|
752
|
-
|
|
753
|
-
try { rmSync(dir, { recursive: true, force: true }); removed.push(name); } catch { /* best effort */ }
|
|
754
|
-
}
|
|
748
|
+
try { rmSync(dir, { recursive: true, force: true }); removed.push(name); } catch { /* best effort */ }
|
|
755
749
|
}
|
|
756
750
|
return removed;
|
|
757
751
|
}
|
|
758
752
|
|
|
753
|
+
const EXACT_COHORT_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
754
|
+
|
|
759
755
|
// Read the exact @deepseek-ai/dsh pin from a payload manifest (dependencies
|
|
760
756
|
// first, then peerDependencies). Exact pins only: a range or absence yields
|
|
761
757
|
// null so callers fail closed instead of guessing a cohort.
|
|
762
758
|
function payloadDshSpec(manifest) {
|
|
763
759
|
if (!manifest || typeof manifest !== 'object') return null;
|
|
764
760
|
const direct = manifest.dependencies?.['@deepseek-ai/dsh'];
|
|
765
|
-
if (typeof direct === 'string' &&
|
|
761
|
+
if (typeof direct === 'string' && EXACT_COHORT_RE.test(direct)) return direct;
|
|
766
762
|
const peer = manifest.peerDependencies?.['@deepseek-ai/dsh'];
|
|
767
|
-
if (typeof peer === 'string' &&
|
|
763
|
+
if (typeof peer === 'string' && EXACT_COHORT_RE.test(peer)) return peer;
|
|
768
764
|
return null;
|
|
769
765
|
}
|
|
770
766
|
|
|
@@ -964,7 +960,7 @@ function ensureDirectoryLink(linkPath, targetRoot) {
|
|
|
964
960
|
* command. The profile manifest and one loader-visible directory link are the
|
|
965
961
|
* only state changed; no dependency resolution or policy bypass is attempted.
|
|
966
962
|
*/
|
|
967
|
-
|
|
963
|
+
function ensurePluginRegistration({
|
|
968
964
|
profileRoot,
|
|
969
965
|
root,
|
|
970
966
|
name,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { validateModelCallabilityV2 } from './model-callability-contract.mjs';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const EXTENSION_CONTRACT_SCHEMA_VERSION = 1;
|
|
8
8
|
|
|
9
9
|
function row(matrix, id) {
|
|
10
10
|
return Array.isArray(matrix?.rows) ? matrix.rows.find((entry) => entry?.id === id) : undefined;
|
|
@@ -3,16 +3,6 @@
|
|
|
3
3
|
// Classification consumes structured status/codes/outcomes only. It never
|
|
4
4
|
// parses raw exception text, provider responses, credentials, quotas or logs.
|
|
5
5
|
|
|
6
|
-
export const FAILURE_CATEGORIES = Object.freeze([
|
|
7
|
-
'none',
|
|
8
|
-
'policy',
|
|
9
|
-
'compatibility',
|
|
10
|
-
'provider',
|
|
11
|
-
'runtime',
|
|
12
|
-
'verification',
|
|
13
|
-
'cancelled',
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
6
|
export const FAILURE_REASON_CODES = Object.freeze({
|
|
17
7
|
NONE: 'NONE',
|
|
18
8
|
CANCELLED: 'CANCELLED',
|
|
@@ -31,14 +31,24 @@ function records(values) {
|
|
|
31
31
|
/**
|
|
32
32
|
* Pure preview only. No filesystem or project-path operations are performed.
|
|
33
33
|
*
|
|
34
|
-
* `scope` defaults to `
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* `scope` defaults to `worktree` — Crew's own workspaces — because that is the
|
|
35
|
+
* range an operator is actually asking for when they clear a Crew project out,
|
|
36
|
+
* and it still cannot remove a session the operator opened themselves or one
|
|
37
|
+
* Crew ran in someone else's directory. Widening to `all` is always explicit.
|
|
38
|
+
*
|
|
39
|
+
* The value is named `worktree` for compatibility and now means workspaces: the
|
|
40
|
+
* pair a project's jobs run in, plus the per-job trees earlier releases left
|
|
41
|
+
* behind, so those stay cleanable instead of being stranded by the change.
|
|
37
42
|
*/
|
|
38
|
-
export function planHistoryCleanup(snapshot, { operation = 'archive', scope = '
|
|
43
|
+
export function planHistoryCleanup(snapshot, { operation = 'archive', scope = 'worktree', before } = {}) {
|
|
39
44
|
if (!['archive', 'delete'].includes(operation) || !['all', 'crew', 'worktree', 'before'].includes(scope)) throw new Error('HISTORY_INVALID_OPTIONS');
|
|
40
|
-
|
|
45
|
+
// `worktree` takes the same time window as `before`, but optionally: the panel
|
|
46
|
+
// supplies one when the operator picks a date, and its absence means "everything
|
|
47
|
+
// in these workspaces" rather than an error. A date that was supplied and is
|
|
48
|
+
// malformed still fails, so a typo cannot silently widen the range.
|
|
49
|
+
const cutoff = (scope === 'before' || scope === 'worktree') ? instant(before) : null;
|
|
41
50
|
if (scope === 'before' && (typeof before !== 'string' || cutoff === null)) throw new Error('HISTORY_INVALID_CUTOFF');
|
|
51
|
+
if (scope === 'worktree' && before !== undefined && cutoff === null) throw new Error('HISTORY_INVALID_CUTOFF');
|
|
42
52
|
const workspaces = records(snapshot.workspaces);
|
|
43
53
|
const sessions = records(snapshot.sessions);
|
|
44
54
|
const active = ids(snapshot.activeSessionIds);
|
package/src/hub/entry.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export function recordAdaptiveJobOutcome(job, store = getProcessAdaptiveHealthSt
|
|
|
68
68
|
* a chained promise preserves all legacy completion/waiter behavior and runs
|
|
69
69
|
* this observer only after the Hub's own finalizer populated endedAt/status.
|
|
70
70
|
*/
|
|
71
|
-
|
|
71
|
+
function installAdaptiveHealthObserver() {
|
|
72
72
|
if (WorkerRegistry.prototype[ADAPTIVE_OBSERVER_INSTALLED] === true) return false;
|
|
73
73
|
const originalSpawn = WorkerRegistry.prototype.spawn;
|
|
74
74
|
Object.defineProperty(WorkerRegistry.prototype, ADAPTIVE_OBSERVER_INSTALLED, {
|
package/src/hub/index.mjs
CHANGED
|
@@ -27,10 +27,11 @@ import { boundedMachineCodeFromError } from '../structured-error-code.mjs';
|
|
|
27
27
|
import { createCanonicalJobEvent, projectWorkflowView } from '../job-contracts.mjs';
|
|
28
28
|
import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
|
|
29
29
|
import { loadCiEvidence } from '../ci-evidence.mjs';
|
|
30
|
+
import { ensureCrewWorkspace, releaseCrewWorkspace } from '../crew-workspaces.mjs';
|
|
30
31
|
import { loadRoleProfiles, resolveRoleProfile, saveRoleProfiles } from '../role-profiles.mjs';
|
|
31
32
|
import { addContextReferences, buildWorkspaceTask, isSafeBranchName, loadWorkspaceContexts, resolveWorkspaceContext, saveWorkspaceContexts } from '../workspace-context.mjs';
|
|
32
33
|
import { buildExtensionContract } from '../extension-contract.mjs';
|
|
33
|
-
import { cleanupIsolatedWorkspace,
|
|
34
|
+
import { cleanupIsolatedWorkspace, isCrewWorktreeName } from '../workspace-isolation.mjs';
|
|
34
35
|
import { jobDisplayName } from '../job-identity.mjs';
|
|
35
36
|
import { assessWorkspaceReadiness } from '../workspace-readiness.mjs';
|
|
36
37
|
import { buildConfigReadinessMatrix } from '../config-readiness.mjs';
|
|
@@ -118,7 +119,6 @@ export const QUICK_CONFIG_KEYS = Object.freeze([
|
|
|
118
119
|
const LEGACY_TIER_MODELS = { flash: 'deepseek-v4-flash', pro: 'deepseek-v4-pro' };
|
|
119
120
|
// Local copy (the hub must not import jobs.mjs, which pulls the DSH SDK into
|
|
120
121
|
// the profile realm): a valid dispatch role set.
|
|
121
|
-
const ROLES = { worker: true, reviewer: true };
|
|
122
122
|
const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
|
|
123
123
|
const CREDENTIAL_PURGE_STATE_FILE = join(CONFIG_DIR, 'credential-purge-lifecycle.json');
|
|
124
124
|
const RECOVERY_ACTION_PATTERN = /^[a-f0-9]{32}$/u;
|
|
@@ -861,26 +861,45 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
861
861
|
let executionCwd = cwd;
|
|
862
862
|
let isolatedWorkspace = null;
|
|
863
863
|
if ((requested_isolation === 'worktree' && jobRole === 'worker') || requested_isolation === 'readonly') {
|
|
864
|
-
|
|
864
|
+
// This project's stable worker/reviewer workspace, reset to the job's base
|
|
865
|
+
// revision and held for the job's duration, so the Harness groups these
|
|
866
|
+
// sessions into two entries instead of one per job.
|
|
867
|
+
const created = await ensureCrewWorkspace({ cwd, role: jobRole, baseRevision: workspace_branch });
|
|
865
868
|
if (!created.ok) throw Object.assign(new Error(created.error ?? created.reason), { code: created.reason });
|
|
866
869
|
executionCwd = created.worktreePath;
|
|
867
|
-
isolatedWorkspace = { worktreePath: created.worktreePath, repoRoot: created.repoRoot };
|
|
870
|
+
isolatedWorkspace = { worktreePath: created.worktreePath, repoRoot: created.repoRoot, stable: true, release: created.release };
|
|
868
871
|
}
|
|
869
872
|
// The Harness titles the session from the opening words of the prompt the
|
|
870
|
-
// agent receives, so the prompt opens with the job's Crew name
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
873
|
+
// agent receives, so the prompt opens with the job's Crew name. An allocated
|
|
874
|
+
// worktree's name wins, so a collision suffix stays consistent between the
|
|
875
|
+
// two. A stable workspace deliberately does *not* win: its directory name is
|
|
876
|
+
// shared by every job that uses it, so naming jobs after it would give them
|
|
877
|
+
// all one title and leave an operator unable to tell them apart. The
|
|
878
|
+
// workspace groups; the job name distinguishes.
|
|
879
|
+
let jobName;
|
|
880
|
+
let workerPrompt;
|
|
881
|
+
let sessionId;
|
|
882
|
+
try {
|
|
883
|
+
jobName = isCrewWorktreeName(basename(executionCwd))
|
|
884
|
+
? basename(executionCwd)
|
|
885
|
+
: jobDisplayName({ purpose: jobRole });
|
|
886
|
+
workerPrompt = appendDeliveryInstructions(prependJobIdentity(task, { name: jobName, role: jobRole }), { tier: effTier, role: jobRole, isReview: delivery === 'review' || jobRole === 'reviewer' });
|
|
887
|
+
|
|
888
|
+
sessionId = `session-${randomUUID()}`;
|
|
889
|
+
// Record provenance while it is still knowable: a session header carries no
|
|
890
|
+
// field naming who asked for the session, so a later Crew-scoped cleanup can
|
|
891
|
+
// only tell Crew's own work apart from the operator's by this ledger.
|
|
892
|
+
appendSessionOrigin({ sessionId, role: jobRole, jobId: id });
|
|
893
|
+
} catch (error) {
|
|
894
|
+
// Everything below this point runs inside the job's own lifecycle, whose
|
|
895
|
+
// `finally` releases the workspace lock. A throw here happens before that
|
|
896
|
+
// job exists, so nothing would release a lock already taken — and it would
|
|
897
|
+
// then block the workspace until the age backstop, which is hours. The
|
|
898
|
+
// window is small (`appendSessionOrigin` writes a file) but the cost of
|
|
899
|
+
// missing it is a workspace no job can use.
|
|
900
|
+
releaseCrewWorkspace(isolatedWorkspace);
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
884
903
|
const job = {
|
|
885
904
|
id, client_job_id: client_job_id ?? null, sessionId, role: jobRole, attempt, tier: effTier, provider: selection.provider, model: selection.model,
|
|
886
905
|
selection_source: selection.source, selection_trace: selection.selection_trace ?? null,
|
|
@@ -1158,7 +1177,14 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
1158
1177
|
review: job.review,
|
|
1159
1178
|
workspaceDiff: job.workspaceDiff,
|
|
1160
1179
|
});
|
|
1161
|
-
if (job.isolatedWorkspace) {
|
|
1180
|
+
if (job.isolatedWorkspace?.stable === true) {
|
|
1181
|
+
// A stable workspace is not disposed of — it is the directory the next
|
|
1182
|
+
// job and every session it produced belong to. Only the lock is dropped.
|
|
1183
|
+
const released = releaseCrewWorkspace(job.isolatedWorkspace);
|
|
1184
|
+
job.workspace_retained = false;
|
|
1185
|
+
const workspaceCleanupWarning = released ? null : 'workspace lock was not held at release';
|
|
1186
|
+
job.cleanup_warning = [handleCleanupWarning, workspaceCleanupWarning].filter(Boolean).join('; ') || null;
|
|
1187
|
+
} else if (job.isolatedWorkspace) {
|
|
1162
1188
|
const cleanup = await cleanupIsolatedWorkspace(job.isolatedWorkspace).catch((error) => ({ ok: false, error: error?.message ?? String(error) }));
|
|
1163
1189
|
job.workspace_retained = cleanup.ok !== true;
|
|
1164
1190
|
const workspaceCleanupWarning = cleanup.ok === true ? null : cleanup.error ?? 'worktree cleanup failed';
|
package/src/i18n.mjs
CHANGED
|
@@ -42,7 +42,7 @@ export function crewSkillFiles({ home = homedir(), env = process.env } = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** The skill template shipped in the payload. */
|
|
45
|
-
|
|
45
|
+
function crewSkillSource({ root }) {
|
|
46
46
|
return join(root, 'skills', CREW_SKILL_NAME, 'SKILL.md');
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -57,7 +57,7 @@ const REGEX_MAY_FOLLOW = new Set([
|
|
|
57
57
|
* from the raw text, so a keyword inside a comment or a string is not a
|
|
58
58
|
* declaration and a quote inside a regex is not a string.
|
|
59
59
|
*/
|
|
60
|
-
|
|
60
|
+
function tokenizeModuleSource(source) {
|
|
61
61
|
const tokens = [];
|
|
62
62
|
const text = String(source ?? '');
|
|
63
63
|
let i = 0;
|
|
@@ -748,7 +748,7 @@ export function pickClaudeCommand(candidates, { platform = process.platform } =
|
|
|
748
748
|
?? list[0];
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
-
|
|
751
|
+
function resolveClaudeCommand({ platform = process.platform } = {}) {
|
|
752
752
|
const probe = spawnSync(platform === 'win32' ? 'where' : 'which', ['claude'], {
|
|
753
753
|
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
754
754
|
});
|
|
@@ -1019,7 +1019,7 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
1019
1019
|
}
|
|
1020
1020
|
|
|
1021
1021
|
// Keep CLI transaction ordering testable without changing the operator's home.
|
|
1022
|
-
|
|
1022
|
+
async function refreshClaudePlugin({ home, root, actions = [], claudeCommand, runStep = runClaudeStep }) {
|
|
1023
1023
|
// Each step runs to completion — including ending the whole process tree on a
|
|
1024
1024
|
// timeout — before the next one starts, and the settle window is the widest any
|
|
1025
1025
|
// step asks for. Two things this replaces: an `uninstall` left running past its
|
|
@@ -44,9 +44,10 @@ import { samePayloadContent, capturePayloadContent } from './payload-content.mjs
|
|
|
44
44
|
import { crewDshHome, crewProfileDir, claudeIntegrationLine } from './install.mjs';
|
|
45
45
|
import { releaseClaimsState } from '../release-in-use.mjs';
|
|
46
46
|
import { compareProcessToken, processStartToken } from '../process-identity.mjs';
|
|
47
|
+
import { RELEASE_COHORT_FILENAME } from '../dsh-cohort.mjs';
|
|
47
48
|
import { renameTree } from './tree-move.mjs';
|
|
48
49
|
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';
|
|
50
|
+
import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, gcRetainedRuntimes, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
|
|
50
51
|
import {
|
|
51
52
|
ensureOfficialWebIntegration,
|
|
52
53
|
officialWebIntegrationStatus,
|
|
@@ -61,15 +62,15 @@ import {
|
|
|
61
62
|
} from './windows-supervisor-adapter.mjs';
|
|
62
63
|
|
|
63
64
|
export const CREW_APP_DIRNAME = 'app';
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const RELEASES_DIRNAME = 'releases';
|
|
66
|
+
const CURRENT_POINTER_FILENAME = 'current.json';
|
|
67
|
+
const KEEP_RELEASES = 2;
|
|
67
68
|
export const INCOMPLETE_MARKER = '.dsh-crew-incomplete';
|
|
68
69
|
const CREW_ROUTE_BASE = '/_dsh/dsh-crew';
|
|
69
70
|
// Exact DSH cohort version: dotted numeric with optional -prerelease suffix.
|
|
70
71
|
// Anything else (ranges, "../..", paths) is NOT an authorized cohort value
|
|
71
72
|
// and must never reach rename/rmSync authority via retained-runtimes keys.
|
|
72
|
-
|
|
73
|
+
const EXACT_DSH_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
73
74
|
|
|
74
75
|
export function npmCliInvocation(args, {
|
|
75
76
|
platform = process.platform,
|
|
@@ -242,8 +243,8 @@ function writeCurrentPointer({ home, name, version, path }) {
|
|
|
242
243
|
return pointer;
|
|
243
244
|
}
|
|
244
245
|
|
|
245
|
-
|
|
246
|
-
|
|
246
|
+
const UPDATE_JOURNAL_FILENAME = 'update-journal.json';
|
|
247
|
+
const UPDATE_LOCK_FILENAME = 'update-in-progress.lock';
|
|
247
248
|
|
|
248
249
|
export function updateJournalFile({ home = homedir() } = {}) {
|
|
249
250
|
return join(crewAppRoot({ home }), UPDATE_JOURNAL_FILENAME);
|
|
@@ -453,7 +454,7 @@ function writeUpdateJournal({ home, stage, prior = null, candidate = null, verif
|
|
|
453
454
|
// identity matches the expected stage/prior/candidate. A crash between
|
|
454
455
|
// verification success and this write leaves the journal unverified, so
|
|
455
456
|
// reconcile refuses to finalize it.
|
|
456
|
-
|
|
457
|
+
function markJournalVerified({ home = homedir(), stage, prior = null, candidate = null, runtime = null } = {}) {
|
|
457
458
|
const current = readUpdateJournal({ home });
|
|
458
459
|
if (!current || current.malformed) return { ok: false, code: 'JOURNAL_NOT_FOUND' };
|
|
459
460
|
const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
@@ -481,82 +482,6 @@ function compensateActivationSync({ home, prior, manifest, log, installer }) {
|
|
|
481
482
|
return { ok: true, version: manifest.version, path: prior.path };
|
|
482
483
|
}
|
|
483
484
|
|
|
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
485
|
// Canonical containment validation for a journal's runtime segment. Recovery
|
|
561
486
|
// may rename/recursively-delete these roots, so liveRoot and retainedRoot
|
|
562
487
|
// must be EXACTLY the canonical Crew-owned paths (resolved, never a suffix
|
|
@@ -760,6 +685,7 @@ export async function reconcileUpdateJournal({ home = homedir(), log = () => {},
|
|
|
760
685
|
}
|
|
761
686
|
clearUpdateJournal({ home });
|
|
762
687
|
gcOldReleases({ home, protect: journal.prior?.path ?? null });
|
|
688
|
+
gcRetainedCohorts({ home });
|
|
763
689
|
log(`- recovered update journal at stage ${journal.stage}: candidate ${candidateIdent.version} already committed, finalized`);
|
|
764
690
|
return { ok: true, reconciled: true, stage: journal.stage, committed: true };
|
|
765
691
|
}
|
|
@@ -803,6 +729,7 @@ export async function reconcileUpdateJournal({ home = homedir(), log = () => {},
|
|
|
803
729
|
writeCurrentPointer({ home, name: candidateIdent.name, version: candidateIdent.version, path: candidateDir });
|
|
804
730
|
clearUpdateJournal({ home });
|
|
805
731
|
gcOldReleases({ home, protect: journal.prior?.path ?? null });
|
|
732
|
+
gcRetainedCohorts({ home });
|
|
806
733
|
log(`- recovered update journal at stage ${journal.stage}: candidate ${candidateIdent.version} was verified and started, so the commit was completed`);
|
|
807
734
|
return { ok: true, reconciled: true, stage: journal.stage, committed: true };
|
|
808
735
|
}
|
|
@@ -1304,15 +1231,48 @@ export function commitActivatedRelease({ stageDir, manifest, home, prior = null
|
|
|
1304
1231
|
writeCurrentPointer({ home, name: manifest.name, version: manifest.version, path: stageDir });
|
|
1305
1232
|
clearUpdateJournal({ home });
|
|
1306
1233
|
gcOldReleases({ home, protect: prior?.path ?? null });
|
|
1234
|
+
gcRetainedCohorts({ home });
|
|
1307
1235
|
return stageDir;
|
|
1308
1236
|
}
|
|
1309
1237
|
|
|
1310
|
-
function commitStagedRelease({ stageDir, manifest, home, prior = null }) {
|
|
1311
|
-
return commitActivatedRelease({ stageDir, manifest, home, prior });
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
1238
|
const STALE_INCOMPLETE_MS = 24 * 60 * 60 * 1000;
|
|
1315
1239
|
|
|
1240
|
+
// A retained runtime cohort is needed only while some surviving release
|
|
1241
|
+
// resolves to it, so this runs next to release pruning at the commit points
|
|
1242
|
+
// that call gcOldReleases: dropping a release is what stops its cohort being
|
|
1243
|
+
// pinned. Resolution goes through resolveReleaseCohort, the same chain every
|
|
1244
|
+
// other caller uses (manifest pin, then the release-cohort.json sidecar), so a
|
|
1245
|
+
// release that names its cohort only in the sidecar keeps its runtime.
|
|
1246
|
+
//
|
|
1247
|
+
// Fail closed: the cohort set is trusted only when EVERY directory under
|
|
1248
|
+
// releases/ resolved, because "nothing pins it" and "could not tell" must not
|
|
1249
|
+
// prune the same way. Pruning is best-effort on top of that — it must never
|
|
1250
|
+
// turn an update that already committed its pointer into a failed one.
|
|
1251
|
+
//
|
|
1252
|
+
// performCoordinatedCohortUpdate commits without gcOldReleases and is
|
|
1253
|
+
// deliberately not covered here either; its prior tree is still the rollback
|
|
1254
|
+
// target at that point.
|
|
1255
|
+
function gcRetainedCohorts({ home }) {
|
|
1256
|
+
try {
|
|
1257
|
+
const releasesDir = crewReleasesDir({ home });
|
|
1258
|
+
if (!existsSync(releasesDir)) return [];
|
|
1259
|
+
const entries = readdirSync(releasesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
1260
|
+
const cohorts = [];
|
|
1261
|
+
for (const entry of entries) {
|
|
1262
|
+
const resolved = resolveReleaseCohort({ releaseDir: join(releasesDir, entry.name) });
|
|
1263
|
+
if (!resolved.ok) {
|
|
1264
|
+
process.emitWarning(`dsh-crew: release "${entry.name}" does not resolve to an exact cohort (${resolved.code ?? 'unknown'}); skipping retained-runtime pruning this pass`);
|
|
1265
|
+
return [];
|
|
1266
|
+
}
|
|
1267
|
+
cohorts.push(resolved.dshVersion);
|
|
1268
|
+
}
|
|
1269
|
+
return gcRetainedRuntimes({ home, cohorts });
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
process.emitWarning(`dsh-crew: retained-runtime pruning skipped: ${error?.message ?? error}`);
|
|
1272
|
+
return [];
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1316
1276
|
function gcOldReleases({ home, keep = KEEP_RELEASES, protect = null }) {
|
|
1317
1277
|
const pointer = readCurrentPointer({ home });
|
|
1318
1278
|
const releasesDir = crewReleasesDir({ home });
|
|
@@ -1362,7 +1322,7 @@ function gcOldReleases({ home, keep = KEEP_RELEASES, protect = null }) {
|
|
|
1362
1322
|
return removed;
|
|
1363
1323
|
}
|
|
1364
1324
|
|
|
1365
|
-
|
|
1325
|
+
function listManagedReleases({ home = homedir() } = {}) {
|
|
1366
1326
|
const pointer = readCurrentPointer({ home });
|
|
1367
1327
|
const root = crewReleasesDir({ home });
|
|
1368
1328
|
if (!existsSync(root)) return [];
|
|
@@ -1390,7 +1350,7 @@ export function listManagedReleases({ home = homedir() } = {}) {
|
|
|
1390
1350
|
// Windows supervisor executes it and writes a VERIFIED result. This polls
|
|
1391
1351
|
// until the result arrives (or the timeout elapses). NEVER talks to the
|
|
1392
1352
|
// legacy 3080 supervisor endpoint.
|
|
1393
|
-
|
|
1353
|
+
async function requestCrewRuntimeRestart({
|
|
1394
1354
|
reason = null,
|
|
1395
1355
|
fetchImpl = globalThis.fetch,
|
|
1396
1356
|
pollIntervalMs = 1_000,
|
|
@@ -1442,18 +1402,6 @@ export async function requestCrewRuntimeRestart({
|
|
|
1442
1402
|
}
|
|
1443
1403
|
}
|
|
1444
1404
|
|
|
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
1405
|
// Cohort verifier: compares the TARGET DSH cohort against the Hub's
|
|
1458
1406
|
// dsh_version domain (the installed @deepseek-ai/dsh package), NEVER
|
|
1459
1407
|
// against Crew's own runtime_version (the dsh-crew release). A null
|
|
@@ -1509,7 +1457,7 @@ export async function verifyCrewRuntimeIdentity(version, fetchImpl = globalThis.
|
|
|
1509
1457
|
// AND the expected DSH cohort (dsh_version domain), alongside full 3210
|
|
1510
1458
|
// identity. A stale old-Crew process on the right cohort (or vice versa)
|
|
1511
1459
|
// fails closed instead of reporting rollback success.
|
|
1512
|
-
|
|
1460
|
+
async function verifyRollbackTarget({ crewVersion, dshVersion, fetchImpl = globalThis.fetch } = {}) {
|
|
1513
1461
|
let body = null;
|
|
1514
1462
|
try {
|
|
1515
1463
|
const response = await fetchImpl('http://127.0.0.1:3210/_dsh/dsh-crew/extension', { headers: { accept: 'application/json' } });
|
|
@@ -1910,10 +1858,10 @@ async function finishLifecycleAfterUpdateLock({
|
|
|
1910
1858
|
// Lifecycle-owned cohort sidecar for a managed release directory. Historical
|
|
1911
1859
|
// releases (pre-1.0.4) do not pin @deepseek-ai/dsh in their immutable
|
|
1912
1860
|
// manifest; this records the cohort fact the lifecycle observed for them.
|
|
1913
|
-
const cohortFile = (releaseDir) => join(releaseDir,
|
|
1861
|
+
const cohortFile = (releaseDir) => join(releaseDir, RELEASE_COHORT_FILENAME);
|
|
1914
1862
|
|
|
1915
1863
|
// Exact DSH cohort version helper: delegates to the module-level regex.
|
|
1916
|
-
|
|
1864
|
+
function isExactDshVersion(value) {
|
|
1917
1865
|
return typeof value === 'string' && EXACT_DSH_VERSION_RE.test(value);
|
|
1918
1866
|
}
|
|
1919
1867
|
|
|
@@ -2242,7 +2190,7 @@ function registrationHealthy({ home, name, releaseDir }) {
|
|
|
2242
2190
|
* config resolution. Package-manager children must see the user's real
|
|
2243
2191
|
* environment, not our execution context.
|
|
2244
2192
|
*/
|
|
2245
|
-
|
|
2193
|
+
function sanitizedPackageManagerEnv(baseEnv = process.env) {
|
|
2246
2194
|
const env = {};
|
|
2247
2195
|
for (const [key, value] of Object.entries(baseEnv)) {
|
|
2248
2196
|
if (/^npm_(config_|lifecycle|package_|execpath$|node_execpath$)/i.test(key)) continue;
|
|
@@ -2818,15 +2766,15 @@ async function npxInstallInner({ home, log, sourceRoot, installer, ensureRuntime
|
|
|
2818
2766
|
return { ok: true, version: manifest.version, path: releaseDir };
|
|
2819
2767
|
}
|
|
2820
2768
|
|
|
2821
|
-
|
|
2822
|
-
|
|
2769
|
+
const UPDATE_PACKAGE_NAME = '@ran-sh/dsh-crew';
|
|
2770
|
+
const UPDATE_DEFAULT_SPEC = `${UPDATE_PACKAGE_NAME}@latest`;
|
|
2823
2771
|
|
|
2824
2772
|
/**
|
|
2825
2773
|
* Extract a packed npm tarball with the platform `tar` binary (bsdtar ships
|
|
2826
2774
|
* with Windows 10+, macOS, and Linux) into destDir; npm tarballs always root
|
|
2827
2775
|
* at `package/`.
|
|
2828
2776
|
*/
|
|
2829
|
-
|
|
2777
|
+
function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } = {}) {
|
|
2830
2778
|
mkdirSync(destDir, { recursive: true });
|
|
2831
2779
|
// GNU tar interprets a `C:\...` argument as a remote rsh host ("Cannot
|
|
2832
2780
|
// connect to C:"), so anchor the invocation inside destDir and pass the
|
|
@@ -3449,9 +3397,9 @@ const CREDENTIAL_CAPABILITY_REQUIREMENTS = Object.freeze({
|
|
|
3449
3397
|
'purge-plan': Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
|
|
3450
3398
|
purge: Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
|
|
3451
3399
|
});
|
|
3452
|
-
|
|
3400
|
+
const PRODUCTION_HUB_URL = 'http://127.0.0.1:3210';
|
|
3453
3401
|
|
|
3454
|
-
|
|
3402
|
+
async function assertProductionHub({ hubUrl, requiredCapabilities = [], purpose = 'command', fetchImpl = globalThis.fetch } = {}) {
|
|
3455
3403
|
if (hubUrl !== PRODUCTION_HUB_URL) throw new Error(`${purpose} requires the isolated 3210 Crew Hub`);
|
|
3456
3404
|
const response = await fetchImpl(`${hubUrl}${CREW_ROUTE_BASE}/runtime`, { headers: { accept: 'application/json' } });
|
|
3457
3405
|
let body;
|
|
@@ -3474,7 +3422,7 @@ export async function assertProductionHub({ hubUrl, requiredCapabilities = [], p
|
|
|
3474
3422
|
* surface before sending a provider request. This prevents a same-port stale
|
|
3475
3423
|
* Hub from turning a later 404 into an ambiguous destructive failure.
|
|
3476
3424
|
*/
|
|
3477
|
-
|
|
3425
|
+
async function assertProviderHubCapabilities({ hubUrl, action, fetchImpl = globalThis.fetch } = {}) {
|
|
3478
3426
|
return assertProductionHub({
|
|
3479
3427
|
hubUrl,
|
|
3480
3428
|
requiredCapabilities: PROVIDER_CAPABILITY_REQUIREMENTS[action] ?? PROVIDER_CAPABILITY_REQUIREMENTS.list,
|
|
@@ -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) {
|