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.
Files changed (131) hide show
  1. package/dist/bin/init-local.js +3 -5
  2. package/dist/release/changelog-renderer/index.js +4 -5
  3. package/dist/src/adapter/ngcli-adapter.js +60 -1
  4. package/dist/src/command-line/completion/command-object.js +7 -12
  5. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +17 -23
  6. package/dist/src/command-line/generate/generate.js +35 -35
  7. package/dist/src/command-line/import/import.js +18 -40
  8. package/dist/src/command-line/init/ai-agent-prompts.js +5 -11
  9. package/dist/src/command-line/init/command-object.js +4 -2
  10. package/dist/src/command-line/init/implementation/add-nx-to-monorepo.js +12 -32
  11. package/dist/src/command-line/init/implementation/add-nx-to-nest.js +8 -21
  12. package/dist/src/command-line/init/implementation/add-nx-to-npm-repo.js +8 -21
  13. package/dist/src/command-line/init/implementation/angular/index.js +6 -15
  14. package/dist/src/command-line/init/init-v1.js +8 -18
  15. package/dist/src/command-line/init/init-v2.js +28 -56
  16. package/dist/src/command-line/migrate/agentic/definitions.js +35 -8
  17. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +22 -3
  18. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +13 -10
  19. package/dist/src/command-line/migrate/agentic/handoff.d.ts +3 -1
  20. package/dist/src/command-line/migrate/agentic/handoff.js +4 -2
  21. package/dist/src/command-line/migrate/agentic/run-step.js +1 -0
  22. package/dist/src/command-line/migrate/agentic/runner.js +24 -24
  23. package/dist/src/command-line/migrate/agentic/select.js +20 -30
  24. package/dist/src/command-line/migrate/agentic/types.d.ts +19 -3
  25. package/dist/src/command-line/migrate/agentic/types.js +13 -4
  26. package/dist/src/command-line/migrate/command-object.d.ts +3 -0
  27. package/dist/src/command-line/migrate/command-object.js +13 -0
  28. package/dist/src/command-line/migrate/execute-migration.d.ts +14 -0
  29. package/dist/src/command-line/migrate/execute-migration.js +30 -9
  30. package/dist/src/command-line/migrate/migrate-analytics.d.ts +28 -0
  31. package/dist/src/command-line/migrate/migrate-analytics.js +51 -1
  32. package/dist/src/command-line/migrate/migrate-commits.d.ts +8 -2
  33. package/dist/src/command-line/migrate/migrate-commits.js +66 -19
  34. package/dist/src/command-line/migrate/migrate-config.d.ts +4 -2
  35. package/dist/src/command-line/migrate/migrate-config.js +12 -4
  36. package/dist/src/command-line/migrate/migrate.d.ts +9 -2
  37. package/dist/src/command-line/migrate/migrate.js +179 -56
  38. package/dist/src/command-line/migrate/multi-major.js +7 -10
  39. package/dist/src/command-line/migrate/resolve-package-version.js +4 -8
  40. package/dist/src/command-line/migrate/run/agent-output.d.ts +20 -0
  41. package/dist/src/command-line/migrate/run/agent-output.js +65 -0
  42. package/dist/src/command-line/migrate/run/index.d.ts +7 -0
  43. package/dist/src/command-line/migrate/run/index.js +19 -1
  44. package/dist/src/command-line/migrate/run/orchestrator.d.ts +20 -0
  45. package/dist/src/command-line/migrate/run/orchestrator.js +1200 -0
  46. package/dist/src/command-line/migrate/run/run-id.d.ts +16 -0
  47. package/dist/src/command-line/migrate/run/run-id.js +56 -0
  48. package/dist/src/command-line/migrate/run/run-state.d.ts +157 -0
  49. package/dist/src/command-line/migrate/run/run-state.js +438 -0
  50. package/dist/src/command-line/migrate/run/state-lock.d.ts +25 -0
  51. package/dist/src/command-line/migrate/run/state-lock.js +76 -0
  52. package/dist/src/command-line/migrate/run/state-machine.d.ts +84 -0
  53. package/dist/src/command-line/migrate/run/state-machine.js +315 -0
  54. package/dist/src/command-line/migrate/run/util.d.ts +58 -0
  55. package/dist/src/command-line/migrate/run/util.js +139 -7
  56. package/dist/src/command-line/migrate/run/worker.d.ts +1 -0
  57. package/dist/src/command-line/migrate/run/worker.js +308 -45
  58. package/dist/src/command-line/migrate/safe-prompt.d.ts +14 -26
  59. package/dist/src/command-line/migrate/safe-prompt.js +32 -43
  60. package/dist/src/command-line/migrate/step-actions.d.ts +5 -0
  61. package/dist/src/command-line/migrate/step-actions.js +12 -0
  62. package/dist/src/command-line/migrate/text.d.ts +18 -0
  63. package/dist/src/command-line/migrate/text.js +26 -0
  64. package/dist/src/command-line/migrate/version-skew-guard.d.ts +6 -4
  65. package/dist/src/command-line/migrate/version-skew-guard.js +34 -14
  66. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +16 -20
  67. package/dist/src/command-line/release/changelog.js +6 -17
  68. package/dist/src/command-line/release/plan.js +15 -28
  69. package/dist/src/command-line/release/release.js +6 -17
  70. package/dist/src/command-line/release/utils/remote-release-clients/github.js +16 -22
  71. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +13 -22
  72. package/dist/src/command-line/release/utils/resolve-semver-specifier.js +19 -41
  73. package/dist/src/command-line/release/version/resolve-current-version.js +7 -10
  74. package/dist/src/core/graph/main.js +1 -1
  75. package/dist/src/devkit-internals.d.ts +3 -0
  76. package/dist/src/devkit-internals.js +15 -4
  77. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.d.ts +12 -0
  78. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.js +250 -0
  79. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.md +50 -0
  80. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  81. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  82. package/dist/src/plugins/js/lock-file/bun-parser.js +43 -10
  83. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +128 -2
  84. package/dist/src/tasks-runner/run-command.js +17 -32
  85. package/dist/src/tasks-runner/task-env.js +5 -2
  86. package/dist/src/tasks-runner/utils.js +2 -6
  87. package/dist/src/utils/analytics-prompt.js +4 -7
  88. package/dist/src/utils/exit-codes.d.ts +9 -0
  89. package/dist/src/utils/exit-codes.js +19 -0
  90. package/dist/src/utils/fileutils.d.ts +5 -0
  91. package/dist/src/utils/fileutils.js +7 -2
  92. package/dist/src/utils/git-utils.d.ts +33 -2
  93. package/dist/src/utils/git-utils.js +156 -8
  94. package/dist/src/utils/long-running-target.d.ts +10 -0
  95. package/dist/src/utils/long-running-target.js +19 -0
  96. package/dist/src/utils/min-release-age/behavior/bun.js +7 -11
  97. package/dist/src/utils/min-release-age/behavior/npm.js +5 -2
  98. package/dist/src/utils/min-release-age/packument.js +1 -1
  99. package/dist/src/utils/nx-console-prompt.js +3 -6
  100. package/dist/src/utils/package-manager-config/bunfig.d.ts +14 -0
  101. package/dist/src/utils/package-manager-config/bunfig.js +49 -0
  102. package/dist/src/utils/package-manager-config/npmrc.d.ts +23 -0
  103. package/dist/src/utils/package-manager-config/npmrc.js +124 -0
  104. package/dist/src/utils/package-manager-config/pnpm-config.d.ts +17 -0
  105. package/dist/src/utils/package-manager-config/pnpm-config.js +60 -0
  106. package/dist/src/utils/package-manager.d.ts +21 -2
  107. package/dist/src/utils/package-manager.js +244 -47
  108. package/dist/src/utils/params.d.ts +7 -1
  109. package/dist/src/utils/params.js +64 -6
  110. package/dist/src/utils/plugins/core-plugins.js +4 -0
  111. package/dist/src/utils/prompt-helpers.d.ts +53 -0
  112. package/dist/src/utils/prompt-helpers.js +111 -0
  113. package/dist/src/utils/provenance.js +6 -14
  114. package/dist/src/utils/registry-config/bun.d.ts +2 -0
  115. package/dist/src/utils/registry-config/bun.js +257 -0
  116. package/dist/src/utils/registry-config/index.d.ts +13 -0
  117. package/dist/src/utils/registry-config/index.js +102 -0
  118. package/dist/src/utils/registry-config/pnpm.d.ts +2 -0
  119. package/dist/src/utils/registry-config/pnpm.js +1551 -0
  120. package/dist/src/utils/registry-config/utils.d.ts +195 -0
  121. package/dist/src/utils/registry-config/utils.js +496 -0
  122. package/dist/src/utils/registry-config/yarn-berry.d.ts +2 -0
  123. package/dist/src/utils/registry-config/yarn-berry.js +612 -0
  124. package/dist/src/utils/registry-config/yarn-classic.d.ts +2 -0
  125. package/dist/src/utils/registry-config/yarn-classic.js +1001 -0
  126. package/dist/src/utils/safe-spawn.d.ts +24 -0
  127. package/dist/src/utils/safe-spawn.js +104 -0
  128. package/migrations.json +6 -0
  129. package/package.json +18 -13
  130. package/dist/src/utils/min-release-age/npmrc.d.ts +0 -15
  131. 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
+ }