nx 23.2.0-beta.6 → 23.2.0-beta.8
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/dist/bin/init-local.js +3 -5
- package/dist/release/changelog-renderer/index.js +4 -5
- package/dist/src/adapter/ngcli-adapter.js +60 -1
- package/dist/src/command-line/completion/command-object.js +7 -12
- package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +17 -23
- package/dist/src/command-line/generate/generate.js +35 -35
- package/dist/src/command-line/import/import.js +18 -40
- package/dist/src/command-line/init/ai-agent-prompts.js +5 -11
- package/dist/src/command-line/init/command-object.js +4 -2
- package/dist/src/command-line/init/implementation/add-nx-to-monorepo.js +12 -32
- package/dist/src/command-line/init/implementation/add-nx-to-nest.js +8 -21
- package/dist/src/command-line/init/implementation/add-nx-to-npm-repo.js +8 -21
- package/dist/src/command-line/init/implementation/angular/index.js +6 -15
- package/dist/src/command-line/init/init-v1.js +8 -18
- package/dist/src/command-line/init/init-v2.js +28 -56
- package/dist/src/command-line/migrate/agentic/definitions.js +35 -8
- package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +22 -3
- package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +13 -10
- package/dist/src/command-line/migrate/agentic/handoff.d.ts +3 -1
- package/dist/src/command-line/migrate/agentic/handoff.js +4 -2
- package/dist/src/command-line/migrate/agentic/run-step.js +1 -0
- package/dist/src/command-line/migrate/agentic/runner.js +24 -24
- package/dist/src/command-line/migrate/agentic/select.js +20 -30
- package/dist/src/command-line/migrate/agentic/types.d.ts +19 -3
- package/dist/src/command-line/migrate/agentic/types.js +13 -4
- package/dist/src/command-line/migrate/command-object.d.ts +3 -0
- package/dist/src/command-line/migrate/command-object.js +13 -0
- package/dist/src/command-line/migrate/execute-migration.d.ts +14 -0
- package/dist/src/command-line/migrate/execute-migration.js +30 -9
- package/dist/src/command-line/migrate/migrate-analytics.d.ts +28 -0
- package/dist/src/command-line/migrate/migrate-analytics.js +51 -1
- package/dist/src/command-line/migrate/migrate-commits.d.ts +8 -2
- package/dist/src/command-line/migrate/migrate-commits.js +66 -19
- package/dist/src/command-line/migrate/migrate-config.d.ts +4 -2
- package/dist/src/command-line/migrate/migrate-config.js +12 -4
- package/dist/src/command-line/migrate/migrate.d.ts +9 -2
- package/dist/src/command-line/migrate/migrate.js +179 -56
- package/dist/src/command-line/migrate/multi-major.js +7 -10
- package/dist/src/command-line/migrate/resolve-package-version.js +4 -8
- package/dist/src/command-line/migrate/run/agent-output.d.ts +20 -0
- package/dist/src/command-line/migrate/run/agent-output.js +65 -0
- package/dist/src/command-line/migrate/run/index.d.ts +7 -0
- package/dist/src/command-line/migrate/run/index.js +19 -1
- package/dist/src/command-line/migrate/run/orchestrator.d.ts +20 -0
- package/dist/src/command-line/migrate/run/orchestrator.js +1200 -0
- package/dist/src/command-line/migrate/run/run-id.d.ts +16 -0
- package/dist/src/command-line/migrate/run/run-id.js +56 -0
- package/dist/src/command-line/migrate/run/run-state.d.ts +157 -0
- package/dist/src/command-line/migrate/run/run-state.js +438 -0
- package/dist/src/command-line/migrate/run/state-lock.d.ts +25 -0
- package/dist/src/command-line/migrate/run/state-lock.js +76 -0
- package/dist/src/command-line/migrate/run/state-machine.d.ts +84 -0
- package/dist/src/command-line/migrate/run/state-machine.js +315 -0
- package/dist/src/command-line/migrate/run/util.d.ts +58 -0
- package/dist/src/command-line/migrate/run/util.js +139 -7
- package/dist/src/command-line/migrate/run/worker.d.ts +1 -0
- package/dist/src/command-line/migrate/run/worker.js +308 -45
- package/dist/src/command-line/migrate/safe-prompt.d.ts +14 -26
- package/dist/src/command-line/migrate/safe-prompt.js +32 -43
- package/dist/src/command-line/migrate/step-actions.d.ts +5 -0
- package/dist/src/command-line/migrate/step-actions.js +12 -0
- package/dist/src/command-line/migrate/text.d.ts +18 -0
- package/dist/src/command-line/migrate/text.js +26 -0
- package/dist/src/command-line/migrate/version-skew-guard.d.ts +6 -4
- package/dist/src/command-line/migrate/version-skew-guard.js +34 -14
- package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +16 -20
- package/dist/src/command-line/release/changelog.js +6 -17
- package/dist/src/command-line/release/plan.js +15 -28
- package/dist/src/command-line/release/release.js +6 -17
- package/dist/src/command-line/release/utils/remote-release-clients/github.js +16 -22
- package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +13 -22
- package/dist/src/command-line/release/utils/resolve-semver-specifier.js +19 -41
- package/dist/src/command-line/release/version/resolve-current-version.js +7 -10
- package/dist/src/core/graph/main.js +1 -1
- package/dist/src/devkit-internals.d.ts +3 -0
- package/dist/src/devkit-internals.js +15 -4
- package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.d.ts +12 -0
- package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.js +250 -0
- package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.md +50 -0
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/plugins/js/lock-file/bun-parser.js +43 -10
- package/dist/src/project-graph/utils/project-configuration/target-normalization.js +128 -2
- package/dist/src/tasks-runner/run-command.js +17 -32
- package/dist/src/tasks-runner/task-env.js +5 -2
- package/dist/src/tasks-runner/utils.js +2 -6
- package/dist/src/utils/analytics-prompt.js +4 -7
- package/dist/src/utils/exit-codes.d.ts +9 -0
- package/dist/src/utils/exit-codes.js +19 -0
- package/dist/src/utils/fileutils.d.ts +5 -0
- package/dist/src/utils/fileutils.js +7 -2
- package/dist/src/utils/git-utils.d.ts +33 -2
- package/dist/src/utils/git-utils.js +156 -8
- package/dist/src/utils/long-running-target.d.ts +10 -0
- package/dist/src/utils/long-running-target.js +19 -0
- package/dist/src/utils/min-release-age/behavior/bun.js +7 -11
- package/dist/src/utils/min-release-age/behavior/npm.js +5 -2
- package/dist/src/utils/min-release-age/packument.js +1 -1
- package/dist/src/utils/nx-console-prompt.js +3 -6
- package/dist/src/utils/package-manager-config/bunfig.d.ts +14 -0
- package/dist/src/utils/package-manager-config/bunfig.js +49 -0
- package/dist/src/utils/package-manager-config/npmrc.d.ts +23 -0
- package/dist/src/utils/package-manager-config/npmrc.js +124 -0
- package/dist/src/utils/package-manager-config/pnpm-config.d.ts +17 -0
- package/dist/src/utils/package-manager-config/pnpm-config.js +60 -0
- package/dist/src/utils/package-manager.d.ts +21 -2
- package/dist/src/utils/package-manager.js +244 -47
- package/dist/src/utils/params.d.ts +7 -1
- package/dist/src/utils/params.js +64 -6
- package/dist/src/utils/plugins/core-plugins.js +4 -0
- package/dist/src/utils/prompt-helpers.d.ts +53 -0
- package/dist/src/utils/prompt-helpers.js +111 -0
- package/dist/src/utils/provenance.js +6 -14
- package/dist/src/utils/registry-config/bun.d.ts +2 -0
- package/dist/src/utils/registry-config/bun.js +257 -0
- package/dist/src/utils/registry-config/index.d.ts +13 -0
- package/dist/src/utils/registry-config/index.js +102 -0
- package/dist/src/utils/registry-config/pnpm.d.ts +2 -0
- package/dist/src/utils/registry-config/pnpm.js +1551 -0
- package/dist/src/utils/registry-config/utils.d.ts +195 -0
- package/dist/src/utils/registry-config/utils.js +496 -0
- package/dist/src/utils/registry-config/yarn-berry.d.ts +2 -0
- package/dist/src/utils/registry-config/yarn-berry.js +612 -0
- package/dist/src/utils/registry-config/yarn-classic.d.ts +2 -0
- package/dist/src/utils/registry-config/yarn-classic.js +1001 -0
- package/dist/src/utils/safe-spawn.d.ts +24 -0
- package/dist/src/utils/safe-spawn.js +104 -0
- package/migrations.json +6 -0
- package/package.json +18 -13
- package/dist/src/utils/min-release-age/npmrc.d.ts +0 -15
- package/dist/src/utils/min-release-age/npmrc.js +0 -45
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const RUN_ID_SAFE: RegExp;
|
|
2
|
+
/**
|
|
3
|
+
* Creates a run id: a sortable, filesystem-safe UTC timestamp followed by a
|
|
4
|
+
* random suffix (e.g. `20260715T101530-3f9a1c02`). Never derived from
|
|
5
|
+
* package or Nx versions, so it stays stable across an Nx version bump
|
|
6
|
+
* mid-run.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createRunId(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Hashes a migrations.json plan so a resumed run can detect whether the plan
|
|
11
|
+
* changed since a round was recorded. `nx-console` is stripped first since
|
|
12
|
+
* editors write to it without changing the plan; object keys are sorted
|
|
13
|
+
* recursively (arrays keep their order) so key reordering from a different
|
|
14
|
+
* JSON serializer doesn't change the hash.
|
|
15
|
+
*/
|
|
16
|
+
export declare function computePlanHash(migrationsJsonContent: string | object): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RUN_ID_SAFE = void 0;
|
|
4
|
+
exports.createRunId = createRunId;
|
|
5
|
+
exports.computePlanHash = computePlanHash;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
// Orchestrator-generated run ids always match; anything else could smuggle
|
|
8
|
+
// shell metacharacters into dispensed commands or a path out of the runs dir
|
|
9
|
+
// (the leading alphanumeric also rejects '.' and '..').
|
|
10
|
+
exports.RUN_ID_SAFE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a run id: a sortable, filesystem-safe UTC timestamp followed by a
|
|
13
|
+
* random suffix (e.g. `20260715T101530-3f9a1c02`). Never derived from
|
|
14
|
+
* package or Nx versions, so it stays stable across an Nx version bump
|
|
15
|
+
* mid-run.
|
|
16
|
+
*/
|
|
17
|
+
function createRunId() {
|
|
18
|
+
return `${compactUtcTimestamp(new Date())}-${(0, crypto_1.randomBytes)(4).toString('hex')}`;
|
|
19
|
+
}
|
|
20
|
+
function compactUtcTimestamp(date) {
|
|
21
|
+
// '2026-07-15T10:15:30.123Z' -> '20260715T101530': strips separators and
|
|
22
|
+
// milliseconds so the id is filesystem-safe on every platform.
|
|
23
|
+
return date
|
|
24
|
+
.toISOString()
|
|
25
|
+
.replace(/[-:]/g, '')
|
|
26
|
+
.replace(/\.\d+Z$/, '');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Hashes a migrations.json plan so a resumed run can detect whether the plan
|
|
30
|
+
* changed since a round was recorded. `nx-console` is stripped first since
|
|
31
|
+
* editors write to it without changing the plan; object keys are sorted
|
|
32
|
+
* recursively (arrays keep their order) so key reordering from a different
|
|
33
|
+
* JSON serializer doesn't change the hash.
|
|
34
|
+
*/
|
|
35
|
+
function computePlanHash(migrationsJsonContent) {
|
|
36
|
+
const parsed = (typeof migrationsJsonContent === 'string'
|
|
37
|
+
? JSON.parse(migrationsJsonContent)
|
|
38
|
+
: migrationsJsonContent);
|
|
39
|
+
const withoutNxConsole = Object.fromEntries(Object.entries(parsed).filter(([key]) => key !== 'nx-console'));
|
|
40
|
+
return (0, crypto_1.createHash)('sha256')
|
|
41
|
+
.update(JSON.stringify(canonicalize(withoutNxConsole)))
|
|
42
|
+
.digest('hex');
|
|
43
|
+
}
|
|
44
|
+
function canonicalize(value) {
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return value.map(canonicalize);
|
|
47
|
+
}
|
|
48
|
+
if (value !== null && typeof value === 'object') {
|
|
49
|
+
const sorted = {};
|
|
50
|
+
for (const key of Object.keys(value).sort()) {
|
|
51
|
+
sorted[key] = canonicalize(value[key]);
|
|
52
|
+
}
|
|
53
|
+
return sorted;
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export declare const CURRENT_RUN_STATE_FORMAT_VERSION = 1;
|
|
2
|
+
export declare const RUN_STATE_FILE_NAME = "run.json";
|
|
3
|
+
/**
|
|
4
|
+
* The charset a migration id must stay inside to be interpolated into a
|
|
5
|
+
* dispensed command. The outer agent executes those verbatim, so hostile ids
|
|
6
|
+
* are refused rather than quoted per-platform (POSIX quoting is no defense in
|
|
7
|
+
* cmd.exe). Enforced twice: on the incoming plan at init, so a bad id never
|
|
8
|
+
* starts a run, and here on read, so a run whose persisted ids were tampered
|
|
9
|
+
* with fails closed as corrupt instead of being dispensed.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SHELL_SAFE_VALUE: RegExp;
|
|
12
|
+
declare const MIGRATE_RUN_STATUSES: readonly ['active', 'completed'];
|
|
13
|
+
export type MigrateRunStatus = (typeof MIGRATE_RUN_STATUSES)[number];
|
|
14
|
+
export interface MigrateRunRound {
|
|
15
|
+
index: number;
|
|
16
|
+
planHash: string;
|
|
17
|
+
planSnapshot: string;
|
|
18
|
+
}
|
|
19
|
+
declare const MIGRATE_STEP_STATUSES: readonly ['pending', 'dispensed', 'running', 'awaiting-prompt-outcome', 'succeeded', 'failed', 'skipped', 'died'];
|
|
20
|
+
export type MigrateStepStatus = (typeof MIGRATE_STEP_STATUSES)[number];
|
|
21
|
+
declare const PROMPT_OUTCOME_STATUSES: readonly ['completed', 'skipped', 'failed'];
|
|
22
|
+
export type PromptOutcomeStatus = (typeof PROMPT_OUTCOME_STATUSES)[number];
|
|
23
|
+
export interface MigrateStepOutcome {
|
|
24
|
+
fileChanges?: string[];
|
|
25
|
+
gitRefAfter?: string;
|
|
26
|
+
nextSteps?: string[];
|
|
27
|
+
summary?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface MigrateStepPromptOutcome {
|
|
30
|
+
status: PromptOutcomeStatus;
|
|
31
|
+
summary?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface MigrateStep {
|
|
34
|
+
id: string;
|
|
35
|
+
roundIndex: number;
|
|
36
|
+
migrationId: string;
|
|
37
|
+
status: MigrateStepStatus;
|
|
38
|
+
attempt: number;
|
|
39
|
+
dispenseCount: number;
|
|
40
|
+
hasGenerator?: boolean;
|
|
41
|
+
pid?: number;
|
|
42
|
+
startedAt?: string;
|
|
43
|
+
finishedAt?: string;
|
|
44
|
+
gitRefBefore?: string;
|
|
45
|
+
treeCleanAtDispense?: boolean;
|
|
46
|
+
depsHashAtDispense?: string;
|
|
47
|
+
outcome?: MigrateStepOutcome;
|
|
48
|
+
promptOutcome?: MigrateStepPromptOutcome;
|
|
49
|
+
generatorCompleted?: boolean;
|
|
50
|
+
installFailed?: boolean;
|
|
51
|
+
}
|
|
52
|
+
declare const MIGRATE_COMMIT_KINDS: readonly ['checkpoint', 'landed', 'failed'];
|
|
53
|
+
export type MigrateCommitKind = (typeof MIGRATE_COMMIT_KINDS)[number];
|
|
54
|
+
export interface MigrateCommitLedgerEntry {
|
|
55
|
+
sha?: string;
|
|
56
|
+
kind: MigrateCommitKind;
|
|
57
|
+
stepIds: string[];
|
|
58
|
+
}
|
|
59
|
+
export interface MigrateRunAnalytics {
|
|
60
|
+
startEmitted: boolean;
|
|
61
|
+
completeEmitted: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface MigrateRunState {
|
|
64
|
+
formatVersion: number;
|
|
65
|
+
runId: string;
|
|
66
|
+
createdAt: string;
|
|
67
|
+
nxVersion: string;
|
|
68
|
+
status: MigrateRunStatus;
|
|
69
|
+
createCommits: boolean;
|
|
70
|
+
commitPrefix: string;
|
|
71
|
+
skipInstall?: boolean;
|
|
72
|
+
rounds: MigrateRunRound[];
|
|
73
|
+
steps: MigrateStep[];
|
|
74
|
+
commits: MigrateCommitLedgerEntry[];
|
|
75
|
+
checkpointFailed?: boolean;
|
|
76
|
+
analytics: MigrateRunAnalytics;
|
|
77
|
+
}
|
|
78
|
+
export declare function migrateRunsDir(root: string): string;
|
|
79
|
+
export declare function runDir(root: string, runId: string): string;
|
|
80
|
+
/** See `HANDOFFS_DIR_NAME` for why the subtree exists. */
|
|
81
|
+
export declare function runHandoffsDir(runDirPath: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Thrown when a run.json declares a `formatVersion` newer than this Nx
|
|
84
|
+
* understands. Callers must not treat such a run as absent: an older Nx
|
|
85
|
+
* ignoring a newer active run would start a competing run on top of it.
|
|
86
|
+
*
|
|
87
|
+
* Adding a member to any persisted closed set (run status, step status,
|
|
88
|
+
* prompt-outcome status, commit kind) needs a
|
|
89
|
+
* `CURRENT_RUN_STATE_FORMAT_VERSION` bump: without it, an older Nx reading
|
|
90
|
+
* the new value would reject the run as corrupt (the closed-set validation
|
|
91
|
+
* fails) instead of refusing with this error's ask for a newer Nx.
|
|
92
|
+
*/
|
|
93
|
+
export declare class NewerRunStateFormatError extends Error {
|
|
94
|
+
constructor(message: string);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Reads and validates `run.json` from a run directory.
|
|
98
|
+
*
|
|
99
|
+
* A `formatVersion` newer than {@link CURRENT_RUN_STATE_FORMAT_VERSION} means
|
|
100
|
+
* the run was created by a newer Nx than the one currently running, so the
|
|
101
|
+
* shape may not be interpretable here; this throws rather than attempting a
|
|
102
|
+
* best-effort read. An older `formatVersion` is returned as-is: only v1
|
|
103
|
+
* exists today, so there is nothing to migrate yet.
|
|
104
|
+
*/
|
|
105
|
+
export declare function readRunState(runDirPath: string): MigrateRunState;
|
|
106
|
+
/**
|
|
107
|
+
* Writes `run.json` atomically: serializes to a temp file in the same
|
|
108
|
+
* directory, then renames over the real path. A crash mid-write can only
|
|
109
|
+
* ever leave the stale temp file behind, never a half-written run.json.
|
|
110
|
+
*
|
|
111
|
+
* Rename gives per-write atomicity only. Serializing the read-modify-write
|
|
112
|
+
* sequences that concurrent nx migrate processes run is state-lock.ts's job.
|
|
113
|
+
*/
|
|
114
|
+
export declare function writeRunState(runDirPath: string, state: MigrateRunState): void;
|
|
115
|
+
export declare function hasRunState(runDirPath: string): boolean;
|
|
116
|
+
export interface UninterpretableRunDir {
|
|
117
|
+
dirName: string;
|
|
118
|
+
reason: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Scans for the newest active run. A dir that holds a run.json but could be an
|
|
122
|
+
* active run this caller cannot use is returned as `uninterpretable` instead
|
|
123
|
+
* of being silently skipped: treating it as absent would let a run-starting
|
|
124
|
+
* caller create a competing run that re-applies migrations the first run
|
|
125
|
+
* already applied. That covers unreadable or corrupt content, where whether
|
|
126
|
+
* the run is active cannot be determined, and an active run in a dir whose
|
|
127
|
+
* name fails {@link RUN_ID_SAFE}, which cannot be resumed either.
|
|
128
|
+
*
|
|
129
|
+
* A dir that reads cleanly as a finished run is skipped whatever its name is:
|
|
130
|
+
* it competes with nothing, and reporting it would block every future run
|
|
131
|
+
* with no way for retention to ever clear it.
|
|
132
|
+
*
|
|
133
|
+
* Throws {@link NewerRunStateFormatError} when any run dir holds a
|
|
134
|
+
* newer-format run.json: whether that run is active can't be determined
|
|
135
|
+
* here, and its remediation (a newer Nx) differs from the uninterpretable
|
|
136
|
+
* one (fix or remove).
|
|
137
|
+
*/
|
|
138
|
+
export declare function findActiveRun(root: string): {
|
|
139
|
+
active: {
|
|
140
|
+
runId: string;
|
|
141
|
+
state: MigrateRunState;
|
|
142
|
+
} | null;
|
|
143
|
+
uninterpretable: UninterpretableRunDir[];
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Creates a new run directory and writes its initial state, then prunes old
|
|
147
|
+
* completed runs so `.nx/migrate-runs` doesn't grow unbounded: only the
|
|
148
|
+
* newest {@link MAX_RETAINED_COMPLETED_RUNS} completed runs are kept. Active
|
|
149
|
+
* runs, the run just created, and legacy per-version runner dirs (no
|
|
150
|
+
* run.json) are never pruned.
|
|
151
|
+
*
|
|
152
|
+
* Retention is best effort. A dir it cannot interpret or cannot remove is
|
|
153
|
+
* left in place: the run's state is already written by then, so failing here
|
|
154
|
+
* would abort a run that exists, and every retry would abort the same way.
|
|
155
|
+
*/
|
|
156
|
+
export declare function createRun(root: string, state: MigrateRunState): void;
|
|
157
|
+
export {};
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NewerRunStateFormatError = exports.SHELL_SAFE_VALUE = exports.RUN_STATE_FILE_NAME = exports.CURRENT_RUN_STATE_FORMAT_VERSION = void 0;
|
|
4
|
+
exports.migrateRunsDir = migrateRunsDir;
|
|
5
|
+
exports.runDir = runDir;
|
|
6
|
+
exports.runHandoffsDir = runHandoffsDir;
|
|
7
|
+
exports.readRunState = readRunState;
|
|
8
|
+
exports.writeRunState = writeRunState;
|
|
9
|
+
exports.hasRunState = hasRunState;
|
|
10
|
+
exports.findActiveRun = findActiveRun;
|
|
11
|
+
exports.createRun = createRun;
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
14
|
+
const path_1 = require("path");
|
|
15
|
+
const fileutils_1 = require("../../../utils/fileutils");
|
|
16
|
+
const git_utils_1 = require("../../../utils/git-utils");
|
|
17
|
+
const versions_1 = require("../../../utils/versions");
|
|
18
|
+
const types_1 = require("../agentic/types");
|
|
19
|
+
const run_id_1 = require("./run-id");
|
|
20
|
+
const text_1 = require("../text");
|
|
21
|
+
exports.CURRENT_RUN_STATE_FORMAT_VERSION = 1;
|
|
22
|
+
exports.RUN_STATE_FILE_NAME = 'run.json';
|
|
23
|
+
/**
|
|
24
|
+
* The charset a migration id must stay inside to be interpolated into a
|
|
25
|
+
* dispensed command. The outer agent executes those verbatim, so hostile ids
|
|
26
|
+
* are refused rather than quoted per-platform (POSIX quoting is no defense in
|
|
27
|
+
* cmd.exe). Enforced twice: on the incoming plan at init, so a bad id never
|
|
28
|
+
* starts a run, and here on read, so a run whose persisted ids were tampered
|
|
29
|
+
* with fails closed as corrupt instead of being dispensed.
|
|
30
|
+
*/
|
|
31
|
+
exports.SHELL_SAFE_VALUE = /^[A-Za-z0-9@/:._-]+$/;
|
|
32
|
+
// `new Date().toISOString()`, the only shape Nx writes. Retention and active-run
|
|
33
|
+
// selection compare these lexicographically, and the value is rendered into the
|
|
34
|
+
// stdout the agent scans for blocks, so neither a different notation nor an
|
|
35
|
+
// embedded newline can be tolerated.
|
|
36
|
+
const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
37
|
+
/**
|
|
38
|
+
* A round's snapshot is a file Nx writes next to `run.json`, so the recorded
|
|
39
|
+
* name is a bare `plan-<round>.json`. Pinning the whole name is what keeps a
|
|
40
|
+
* tampered value from resolving outside the run directory when the worker
|
|
41
|
+
* joins it, and from reaching stdout with a line break in it when the worker
|
|
42
|
+
* reports the snapshot missing.
|
|
43
|
+
*/
|
|
44
|
+
const PLAN_SNAPSHOT_NAME = /^plan-\d+\.json$/;
|
|
45
|
+
/**
|
|
46
|
+
* Nx numbers its steps off the plan, so a recorded id is a bare `step-<n>`.
|
|
47
|
+
* The state machine names the id back in the reason it rejects an illegal
|
|
48
|
+
* transition with, and the worker throws that reason, which puts it in front
|
|
49
|
+
* of the agent without passing the block-safe writer.
|
|
50
|
+
*/
|
|
51
|
+
const STEP_ID = /^step-\d+$/;
|
|
52
|
+
// Keeps `.nx/migrate-runs` from growing unbounded across many `nx migrate`
|
|
53
|
+
// invocations over the life of a workspace.
|
|
54
|
+
const MAX_RETAINED_COMPLETED_RUNS = 5;
|
|
55
|
+
// Closed sets are declared as const arrays so the derived types and the
|
|
56
|
+
// runtime validation in `readRunState` cannot drift apart (same pattern as
|
|
57
|
+
// STEP_ACTIONS in step-actions.ts).
|
|
58
|
+
const MIGRATE_RUN_STATUSES = ['active', 'completed'];
|
|
59
|
+
const MIGRATE_STEP_STATUSES = [
|
|
60
|
+
'pending',
|
|
61
|
+
'dispensed',
|
|
62
|
+
'running',
|
|
63
|
+
'awaiting-prompt-outcome',
|
|
64
|
+
'succeeded',
|
|
65
|
+
'failed',
|
|
66
|
+
'skipped',
|
|
67
|
+
'died',
|
|
68
|
+
];
|
|
69
|
+
const PROMPT_OUTCOME_STATUSES = ['completed', 'skipped', 'failed'];
|
|
70
|
+
const MIGRATE_COMMIT_KINDS = ['checkpoint', 'landed', 'failed'];
|
|
71
|
+
const REQUIRED_TOP_LEVEL_FIELDS = [
|
|
72
|
+
'formatVersion',
|
|
73
|
+
'runId',
|
|
74
|
+
'createdAt',
|
|
75
|
+
'nxVersion',
|
|
76
|
+
'status',
|
|
77
|
+
'createCommits',
|
|
78
|
+
'commitPrefix',
|
|
79
|
+
'rounds',
|
|
80
|
+
'steps',
|
|
81
|
+
'commits',
|
|
82
|
+
'analytics',
|
|
83
|
+
];
|
|
84
|
+
function migrateRunsDir(root) {
|
|
85
|
+
return (0, path_1.join)(root, types_1.MIGRATE_RUNS_RELATIVE_DIR);
|
|
86
|
+
}
|
|
87
|
+
function runDir(root, runId) {
|
|
88
|
+
return (0, path_1.join)(migrateRunsDir(root), runId);
|
|
89
|
+
}
|
|
90
|
+
/** See `HANDOFFS_DIR_NAME` for why the subtree exists. */
|
|
91
|
+
function runHandoffsDir(runDirPath) {
|
|
92
|
+
return (0, path_1.join)(runDirPath, types_1.HANDOFFS_DIR_NAME);
|
|
93
|
+
}
|
|
94
|
+
function isPlainObject(value) {
|
|
95
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
const REQUIRED_ARRAY_FIELDS = [
|
|
98
|
+
'rounds',
|
|
99
|
+
'steps',
|
|
100
|
+
'commits',
|
|
101
|
+
];
|
|
102
|
+
const REQUIRED_STRING_FIELDS = [
|
|
103
|
+
'runId',
|
|
104
|
+
'createdAt',
|
|
105
|
+
'nxVersion',
|
|
106
|
+
'status',
|
|
107
|
+
'commitPrefix',
|
|
108
|
+
];
|
|
109
|
+
function isOneOf(values, value) {
|
|
110
|
+
return (typeof value === 'string' && values.includes(value));
|
|
111
|
+
}
|
|
112
|
+
function isOptionalString(value) {
|
|
113
|
+
return value === undefined || typeof value === 'string';
|
|
114
|
+
}
|
|
115
|
+
function isOptionalNumber(value) {
|
|
116
|
+
return value === undefined || typeof value === 'number';
|
|
117
|
+
}
|
|
118
|
+
function isOptionalBoolean(value) {
|
|
119
|
+
return value === undefined || typeof value === 'boolean';
|
|
120
|
+
}
|
|
121
|
+
// A recorded `git rev-parse` output. `RegExp.test` stringifies its argument,
|
|
122
|
+
// so a numeric 1234 would pass the hex test without the type check.
|
|
123
|
+
function isOptionalSha(value) {
|
|
124
|
+
return (value === undefined || (typeof value === 'string' && git_utils_1.GIT_SHA.test(value)));
|
|
125
|
+
}
|
|
126
|
+
function isOptionalStringArray(value) {
|
|
127
|
+
return (value === undefined ||
|
|
128
|
+
(Array.isArray(value) && value.every((item) => typeof item === 'string')));
|
|
129
|
+
}
|
|
130
|
+
function isRoundShape(value) {
|
|
131
|
+
return (isPlainObject(value) &&
|
|
132
|
+
typeof value.index === 'number' &&
|
|
133
|
+
typeof value.planHash === 'string' &&
|
|
134
|
+
typeof value.planSnapshot === 'string' &&
|
|
135
|
+
PLAN_SNAPSHOT_NAME.test(value.planSnapshot));
|
|
136
|
+
}
|
|
137
|
+
function isStepOutcomeShape(value) {
|
|
138
|
+
return (value === undefined ||
|
|
139
|
+
(isPlainObject(value) &&
|
|
140
|
+
isOptionalStringArray(value.fileChanges) &&
|
|
141
|
+
isOptionalSha(value.gitRefAfter) &&
|
|
142
|
+
isOptionalStringArray(value.nextSteps) &&
|
|
143
|
+
isOptionalString(value.summary)));
|
|
144
|
+
}
|
|
145
|
+
function isPromptOutcomeShape(value) {
|
|
146
|
+
return (value === undefined ||
|
|
147
|
+
(isPlainObject(value) &&
|
|
148
|
+
isOneOf(PROMPT_OUTCOME_STATUSES, value.status) &&
|
|
149
|
+
isOptionalString(value.summary)));
|
|
150
|
+
}
|
|
151
|
+
function isStepShape(value) {
|
|
152
|
+
return (isPlainObject(value) &&
|
|
153
|
+
typeof value.id === 'string' &&
|
|
154
|
+
STEP_ID.test(value.id) &&
|
|
155
|
+
typeof value.roundIndex === 'number' &&
|
|
156
|
+
typeof value.migrationId === 'string' &&
|
|
157
|
+
exports.SHELL_SAFE_VALUE.test(value.migrationId) &&
|
|
158
|
+
isOneOf(MIGRATE_STEP_STATUSES, value.status) &&
|
|
159
|
+
typeof value.attempt === 'number' &&
|
|
160
|
+
typeof value.dispenseCount === 'number' &&
|
|
161
|
+
isOptionalBoolean(value.hasGenerator) &&
|
|
162
|
+
isOptionalNumber(value.pid) &&
|
|
163
|
+
isOptionalString(value.startedAt) &&
|
|
164
|
+
isOptionalString(value.finishedAt) &&
|
|
165
|
+
isOptionalSha(value.gitRefBefore) &&
|
|
166
|
+
isOptionalBoolean(value.treeCleanAtDispense) &&
|
|
167
|
+
isOptionalString(value.depsHashAtDispense) &&
|
|
168
|
+
isStepOutcomeShape(value.outcome) &&
|
|
169
|
+
isPromptOutcomeShape(value.promptOutcome) &&
|
|
170
|
+
isOptionalBoolean(value.generatorCompleted) &&
|
|
171
|
+
isOptionalBoolean(value.installFailed) &&
|
|
172
|
+
// A cross-field invariant the rest of the loop relies on: a running step
|
|
173
|
+
// without a pid is never reclassified as died and no step action targets
|
|
174
|
+
// it, so it stalls the run forever.
|
|
175
|
+
(value.status === 'running' ? typeof value.pid === 'number' : true));
|
|
176
|
+
}
|
|
177
|
+
function isCommitLedgerEntryShape(value) {
|
|
178
|
+
return (isPlainObject(value) &&
|
|
179
|
+
isOneOf(MIGRATE_COMMIT_KINDS, value.kind) &&
|
|
180
|
+
Array.isArray(value.stepIds) &&
|
|
181
|
+
value.stepIds.every((id) => typeof id === 'string' && STEP_ID.test(id)) &&
|
|
182
|
+
isOptionalSha(value.sha));
|
|
183
|
+
}
|
|
184
|
+
function isAnalyticsShape(value) {
|
|
185
|
+
return (isPlainObject(value) &&
|
|
186
|
+
typeof value.startEmitted === 'boolean' &&
|
|
187
|
+
typeof value.completeEmitted === 'boolean');
|
|
188
|
+
}
|
|
189
|
+
// A field present with the wrong type must fail here, not reach a
|
|
190
|
+
// `.find`/iteration deep in the worker or orchestrator as a raw TypeError.
|
|
191
|
+
// That includes array elements (`steps: [null]`) and closed-set values: a
|
|
192
|
+
// mangled `status` would otherwise read as neither active nor completed and
|
|
193
|
+
// let a competing run start on top of this one.
|
|
194
|
+
function hasValidRunStateShape(parsed) {
|
|
195
|
+
return (REQUIRED_ARRAY_FIELDS.every((field) => Array.isArray(parsed[field])) &&
|
|
196
|
+
REQUIRED_STRING_FIELDS.every((field) => typeof parsed[field] === 'string') &&
|
|
197
|
+
ISO_TIMESTAMP.test(parsed.createdAt) &&
|
|
198
|
+
typeof parsed.formatVersion === 'number' &&
|
|
199
|
+
typeof parsed.createCommits === 'boolean' &&
|
|
200
|
+
isOneOf(MIGRATE_RUN_STATUSES, parsed.status) &&
|
|
201
|
+
isOptionalBoolean(parsed.checkpointFailed) &&
|
|
202
|
+
isOptionalBoolean(parsed.skipInstall) &&
|
|
203
|
+
parsed.rounds.every(isRoundShape) &&
|
|
204
|
+
parsed.steps.every(isStepShape) &&
|
|
205
|
+
parsed.commits.every(isCommitLedgerEntryShape) &&
|
|
206
|
+
isAnalyticsShape(parsed.analytics));
|
|
207
|
+
}
|
|
208
|
+
function corruptRunStateError(filePath, reason) {
|
|
209
|
+
return new Error(`Corrupt run state at ${filePath}: ${reason}`);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Thrown when a run.json declares a `formatVersion` newer than this Nx
|
|
213
|
+
* understands. Callers must not treat such a run as absent: an older Nx
|
|
214
|
+
* ignoring a newer active run would start a competing run on top of it.
|
|
215
|
+
*
|
|
216
|
+
* Adding a member to any persisted closed set (run status, step status,
|
|
217
|
+
* prompt-outcome status, commit kind) needs a
|
|
218
|
+
* `CURRENT_RUN_STATE_FORMAT_VERSION` bump: without it, an older Nx reading
|
|
219
|
+
* the new value would reject the run as corrupt (the closed-set validation
|
|
220
|
+
* fails) instead of refusing with this error's ask for a newer Nx.
|
|
221
|
+
*/
|
|
222
|
+
class NewerRunStateFormatError extends Error {
|
|
223
|
+
constructor(message) {
|
|
224
|
+
super(message);
|
|
225
|
+
this.name = 'NewerRunStateFormatError';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
exports.NewerRunStateFormatError = NewerRunStateFormatError;
|
|
229
|
+
/**
|
|
230
|
+
* Reads and validates `run.json` from a run directory.
|
|
231
|
+
*
|
|
232
|
+
* A `formatVersion` newer than {@link CURRENT_RUN_STATE_FORMAT_VERSION} means
|
|
233
|
+
* the run was created by a newer Nx than the one currently running, so the
|
|
234
|
+
* shape may not be interpretable here; this throws rather than attempting a
|
|
235
|
+
* best-effort read. An older `formatVersion` is returned as-is: only v1
|
|
236
|
+
* exists today, so there is nothing to migrate yet.
|
|
237
|
+
*/
|
|
238
|
+
function readRunState(runDirPath) {
|
|
239
|
+
const filePath = (0, path_1.join)(runDirPath, exports.RUN_STATE_FILE_NAME);
|
|
240
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
241
|
+
let parsed;
|
|
242
|
+
try {
|
|
243
|
+
parsed = JSON.parse(content);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
throw corruptRunStateError(filePath, 'not valid JSON.');
|
|
247
|
+
}
|
|
248
|
+
if (!isPlainObject(parsed)) {
|
|
249
|
+
throw corruptRunStateError(filePath, 'is missing required fields or has fields of an unexpected type.');
|
|
250
|
+
}
|
|
251
|
+
// Version refusal must precede shape validation: a newer format may change a
|
|
252
|
+
// field's type on purpose, and classifying that as corruption would surface
|
|
253
|
+
// it as a corrupt run to fix or remove, when the real remediation is
|
|
254
|
+
// re-running with the newer Nx that owns it.
|
|
255
|
+
if (typeof parsed.formatVersion === 'number' &&
|
|
256
|
+
parsed.formatVersion > exports.CURRENT_RUN_STATE_FORMAT_VERSION) {
|
|
257
|
+
// This refusal runs before the shape check, so `nxVersion` has not been
|
|
258
|
+
// validated yet and the error carrying it leaves through handleErrors,
|
|
259
|
+
// which prints the message's own lines rather than the block-safe writer.
|
|
260
|
+
const createdBy = typeof parsed.nxVersion === 'string'
|
|
261
|
+
? `Nx ${(0, text_1.singleLine)(parsed.nxVersion)}`
|
|
262
|
+
: 'a newer version of Nx';
|
|
263
|
+
throw new NewerRunStateFormatError(`This migrate run was created with ${createdBy} (run state format v${parsed.formatVersion}), which is newer than the Nx version currently running, ${versions_1.nxVersion} (run state format v${exports.CURRENT_RUN_STATE_FORMAT_VERSION}). Re-run your migrate command with ${createdBy} or later to resume this run.`);
|
|
264
|
+
}
|
|
265
|
+
if (REQUIRED_TOP_LEVEL_FIELDS.some((field) => !(field in parsed)) ||
|
|
266
|
+
!hasValidRunStateShape(parsed)) {
|
|
267
|
+
throw corruptRunStateError(filePath, 'is missing required fields or has fields of an unexpected type.');
|
|
268
|
+
}
|
|
269
|
+
// The directory name is the run id every caller reached this state through,
|
|
270
|
+
// so a run.json naming a different one is not this run: the commands built
|
|
271
|
+
// from the persisted copy would send the agent somewhere else.
|
|
272
|
+
if (parsed.runId !== (0, path_1.basename)(runDirPath)) {
|
|
273
|
+
// Collapsed, then quoted: the rejected value is the untrusted one and this
|
|
274
|
+
// reason reaches the stdout the agent scans for blocks. Quoting alone
|
|
275
|
+
// would not do it, since JSON.stringify leaves the Unicode line separators
|
|
276
|
+
// literal.
|
|
277
|
+
throw corruptRunStateError(filePath, `declares run id ${JSON.stringify((0, text_1.singleLine)(parsed.runId))} but sits in a directory named ${JSON.stringify((0, text_1.singleLine)((0, path_1.basename)(runDirPath)))}.`);
|
|
278
|
+
}
|
|
279
|
+
return parsed;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Writes `run.json` atomically: serializes to a temp file in the same
|
|
283
|
+
* directory, then renames over the real path. A crash mid-write can only
|
|
284
|
+
* ever leave the stale temp file behind, never a half-written run.json.
|
|
285
|
+
*
|
|
286
|
+
* Rename gives per-write atomicity only. Serializing the read-modify-write
|
|
287
|
+
* sequences that concurrent nx migrate processes run is state-lock.ts's job.
|
|
288
|
+
*/
|
|
289
|
+
function writeRunState(runDirPath, state) {
|
|
290
|
+
const filePath = (0, path_1.join)(runDirPath, exports.RUN_STATE_FILE_NAME);
|
|
291
|
+
const tmpPath = `${filePath}~${(0, crypto_1.randomBytes)(4).toString('hex')}`;
|
|
292
|
+
(0, fileutils_1.writeJsonFile)(tmpPath, state);
|
|
293
|
+
(0, fs_1.renameSync)(tmpPath, filePath);
|
|
294
|
+
}
|
|
295
|
+
// ENOENT is the ordinary "no runs yet" answer. Any other failure (EACCES,
|
|
296
|
+
// ENOTDIR) hides runs that may exist, so it propagates rather than reading
|
|
297
|
+
// as an empty directory.
|
|
298
|
+
function readDirEntries(dir) {
|
|
299
|
+
try {
|
|
300
|
+
return (0, fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
if (e?.code === 'ENOENT')
|
|
304
|
+
return [];
|
|
305
|
+
throw e;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Whether a directory holds a run at all. False for a path that doesn't exist
|
|
309
|
+
// (a run id the user made up) and for one that does but holds no run.json (a
|
|
310
|
+
// legacy per-version agentic scratch dir).
|
|
311
|
+
function hasRunState(runDirPath) {
|
|
312
|
+
return (0, fs_1.existsSync)((0, path_1.join)(runDirPath, exports.RUN_STATE_FILE_NAME));
|
|
313
|
+
}
|
|
314
|
+
// Corrupt run.json reads as null; a newer-format run.json propagates so
|
|
315
|
+
// callers can't mistake an incompatible run for an absent one.
|
|
316
|
+
function readRunDirState(candidateDir) {
|
|
317
|
+
if (!hasRunState(candidateDir))
|
|
318
|
+
return null;
|
|
319
|
+
try {
|
|
320
|
+
return readRunState(candidateDir);
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
if (e instanceof NewerRunStateFormatError)
|
|
324
|
+
throw e;
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Scans for the newest active run. A dir that holds a run.json but could be an
|
|
330
|
+
* active run this caller cannot use is returned as `uninterpretable` instead
|
|
331
|
+
* of being silently skipped: treating it as absent would let a run-starting
|
|
332
|
+
* caller create a competing run that re-applies migrations the first run
|
|
333
|
+
* already applied. That covers unreadable or corrupt content, where whether
|
|
334
|
+
* the run is active cannot be determined, and an active run in a dir whose
|
|
335
|
+
* name fails {@link RUN_ID_SAFE}, which cannot be resumed either.
|
|
336
|
+
*
|
|
337
|
+
* A dir that reads cleanly as a finished run is skipped whatever its name is:
|
|
338
|
+
* it competes with nothing, and reporting it would block every future run
|
|
339
|
+
* with no way for retention to ever clear it.
|
|
340
|
+
*
|
|
341
|
+
* Throws {@link NewerRunStateFormatError} when any run dir holds a
|
|
342
|
+
* newer-format run.json: whether that run is active can't be determined
|
|
343
|
+
* here, and its remediation (a newer Nx) differs from the uninterpretable
|
|
344
|
+
* one (fix or remove).
|
|
345
|
+
*/
|
|
346
|
+
function findActiveRun(root) {
|
|
347
|
+
let newest = null;
|
|
348
|
+
const uninterpretable = [];
|
|
349
|
+
for (const entry of readDirEntries(migrateRunsDir(root))) {
|
|
350
|
+
if (!entry.isDirectory())
|
|
351
|
+
continue;
|
|
352
|
+
const dir = (0, path_1.join)(migrateRunsDir(root), entry.name);
|
|
353
|
+
if (!hasRunState(dir))
|
|
354
|
+
continue;
|
|
355
|
+
let state;
|
|
356
|
+
try {
|
|
357
|
+
// Safe for any dir name: the path comes from the directory entry, never
|
|
358
|
+
// from a value interpolated into a command.
|
|
359
|
+
state = readRunState(dir);
|
|
360
|
+
}
|
|
361
|
+
catch (e) {
|
|
362
|
+
if (e instanceof NewerRunStateFormatError)
|
|
363
|
+
throw e;
|
|
364
|
+
uninterpretable.push({
|
|
365
|
+
dirName: entry.name,
|
|
366
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
367
|
+
});
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (state.status !== 'active')
|
|
371
|
+
continue;
|
|
372
|
+
// Run ids are joined into paths and interpolated into dispensed commands,
|
|
373
|
+
// so a dir whose name fails the gate is never trusted as a resumable run.
|
|
374
|
+
if (!run_id_1.RUN_ID_SAFE.test(entry.name)) {
|
|
375
|
+
uninterpretable.push({
|
|
376
|
+
dirName: entry.name,
|
|
377
|
+
reason: 'its name is not a valid run id',
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (!newest || state.createdAt > newest.state.createdAt) {
|
|
382
|
+
newest = { runId: entry.name, state };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return { active: newest, uninterpretable };
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Creates a new run directory and writes its initial state, then prunes old
|
|
389
|
+
* completed runs so `.nx/migrate-runs` doesn't grow unbounded: only the
|
|
390
|
+
* newest {@link MAX_RETAINED_COMPLETED_RUNS} completed runs are kept. Active
|
|
391
|
+
* runs, the run just created, and legacy per-version runner dirs (no
|
|
392
|
+
* run.json) are never pruned.
|
|
393
|
+
*
|
|
394
|
+
* Retention is best effort. A dir it cannot interpret or cannot remove is
|
|
395
|
+
* left in place: the run's state is already written by then, so failing here
|
|
396
|
+
* would abort a run that exists, and every retry would abort the same way.
|
|
397
|
+
*/
|
|
398
|
+
function createRun(root, state) {
|
|
399
|
+
const dir = runDir(root, state.runId);
|
|
400
|
+
// Created up front, and each step's package directory at dispense, so the
|
|
401
|
+
// agent never has to `mkdir -p`: that costs a workspace-permission prompt in
|
|
402
|
+
// agents like Claude Code, on every step.
|
|
403
|
+
(0, fs_1.mkdirSync)(runHandoffsDir(dir), { recursive: true });
|
|
404
|
+
writeRunState(dir, state);
|
|
405
|
+
pruneCompletedRuns(root, state.runId);
|
|
406
|
+
}
|
|
407
|
+
function pruneCompletedRuns(root, justCreatedRunId) {
|
|
408
|
+
const dir = migrateRunsDir(root);
|
|
409
|
+
const completed = [];
|
|
410
|
+
for (const entry of readDirEntries(dir)) {
|
|
411
|
+
if (!entry.isDirectory() || entry.name === justCreatedRunId)
|
|
412
|
+
continue;
|
|
413
|
+
let state;
|
|
414
|
+
try {
|
|
415
|
+
state = readRunDirState((0, path_1.join)(dir, entry.name));
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// A newer-format run belongs to a newer Nx; leave it for that Nx to
|
|
419
|
+
// manage rather than pruning what can't be interpreted here.
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (state?.status === 'completed') {
|
|
423
|
+
completed.push({ runId: entry.name, createdAt: state.createdAt });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
completed
|
|
427
|
+
.sort((a, b) => a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0)
|
|
428
|
+
.slice(MAX_RETAINED_COMPLETED_RUNS)
|
|
429
|
+
.forEach((stale) => {
|
|
430
|
+
try {
|
|
431
|
+
(0, fs_1.rmSync)((0, path_1.join)(dir, stale.runId), { recursive: true, force: true });
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// Guarded per dir so one that cannot be removed (permissions, a file
|
|
435
|
+
// still held open) neither aborts the run nor stops the others.
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|