nx 24.0.0-beta.0 → 24.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/nx-cloud-internals.d.ts +1 -0
  2. package/dist/nx-cloud-internals.js +7 -1
  3. package/dist/src/command-line/graph/graph.js +2 -1
  4. package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +1 -1
  5. package/dist/src/command-line/migrate/agentic/close-agent-session.js +4 -3
  6. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +19 -23
  7. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +16 -19
  8. package/dist/src/command-line/migrate/agentic/master/run-master-session.js +25 -13
  9. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +82 -22
  10. package/dist/src/command-line/migrate/agentic/select.js +3 -3
  11. package/dist/src/command-line/migrate/agentic/types.js +1 -3
  12. package/dist/src/command-line/migrate/command-object.js +5 -9
  13. package/dist/src/command-line/migrate/execute-migration.d.ts +2 -2
  14. package/dist/src/command-line/migrate/execute-migration.js +18 -10
  15. package/dist/src/command-line/migrate/migrate-analytics.d.ts +6 -0
  16. package/dist/src/command-line/migrate/migrate-analytics.js +12 -0
  17. package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -1
  18. package/dist/src/command-line/migrate/migrate-commits.js +10 -3
  19. package/dist/src/command-line/migrate/migrate-config.d.ts +6 -6
  20. package/dist/src/command-line/migrate/migrate-config.js +12 -6
  21. package/dist/src/command-line/migrate/migrate.d.ts +3 -30
  22. package/dist/src/command-line/migrate/migrate.js +206 -237
  23. package/dist/src/command-line/migrate/run/agent-output.d.ts +1 -0
  24. package/dist/src/command-line/migrate/run/agent-output.js +5 -0
  25. package/dist/src/command-line/migrate/run/broker.d.ts +14 -5
  26. package/dist/src/command-line/migrate/run/broker.js +68 -20
  27. package/dist/src/command-line/migrate/run/existing-run-report.d.ts +1 -1
  28. package/dist/src/command-line/migrate/run/existing-run-report.js +7 -5
  29. package/dist/src/command-line/migrate/run/index.d.ts +6 -5
  30. package/dist/src/command-line/migrate/run/index.js +12 -1
  31. package/dist/src/command-line/migrate/run/orchestrator.d.ts +21 -0
  32. package/dist/src/command-line/migrate/run/orchestrator.js +149 -32
  33. package/dist/src/command-line/migrate/run/run-state.d.ts +13 -0
  34. package/dist/src/command-line/migrate/run/run-state.js +21 -0
  35. package/dist/src/command-line/migrate/run/runbook.js +19 -0
  36. package/dist/src/command-line/migrate/run/state-machine.js +4 -1
  37. package/dist/src/command-line/migrate/run/util.d.ts +3 -0
  38. package/dist/src/command-line/migrate/run/util.js +7 -1
  39. package/dist/src/command-line/migrate/sort-migrations.d.ts +1 -7
  40. package/dist/src/command-line/migrate/sort-migrations.js +1 -12
  41. package/dist/src/command-line/show/show-target/info.js +1 -1
  42. package/dist/src/config/nx-json.d.ts +13 -4
  43. package/dist/src/config/task-graph.d.ts +1 -1
  44. package/dist/src/config/workspace-json-project-json.d.ts +72 -0
  45. package/dist/src/core/graph/main.js +1 -1
  46. package/dist/src/devkit-exports.d.ts +1 -1
  47. package/dist/src/hasher/check-task-files.js +4 -4
  48. package/dist/src/hasher/native-task-hasher-impl.js +3 -1
  49. package/dist/src/hasher/task-hasher.d.ts +2 -0
  50. package/dist/src/hasher/task-hasher.js +4 -1
  51. package/dist/src/native/index.d.ts +92 -14
  52. package/dist/src/native/native-bindings.js +3 -0
  53. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  54. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  55. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +6 -1
  56. package/dist/src/project-graph/utils/retrieve-workspace-files.js +27 -6
  57. package/dist/src/tasks-runner/create-task-graph.d.ts +7 -16
  58. package/dist/src/tasks-runner/create-task-graph.js +43 -57
  59. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  60. package/dist/src/tasks-runner/init-tasks-runner.js +13 -0
  61. package/dist/src/tasks-runner/life-cycle.d.ts +4 -1
  62. package/dist/src/tasks-runner/life-cycle.js +7 -0
  63. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +5 -2
  64. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +13 -7
  65. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +4 -3
  66. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +9 -2
  67. package/dist/src/tasks-runner/readiness/probes.d.ts +9 -0
  68. package/dist/src/tasks-runner/readiness/probes.js +62 -0
  69. package/dist/src/tasks-runner/readiness/ready-when.d.ts +38 -0
  70. package/dist/src/tasks-runner/readiness/ready-when.js +190 -0
  71. package/dist/src/tasks-runner/run-command.js +7 -6
  72. package/dist/src/tasks-runner/running-tasks/batch-process.js +6 -0
  73. package/dist/src/tasks-runner/running-tasks/node-child-process.js +5 -4
  74. package/dist/src/tasks-runner/task-graph-utils.d.ts +9 -6
  75. package/dist/src/tasks-runner/task-graph-utils.js +17 -12
  76. package/dist/src/tasks-runner/task-orchestrator.d.ts +37 -10
  77. package/dist/src/tasks-runner/task-orchestrator.js +459 -79
  78. package/dist/src/tasks-runner/tasks-runner.d.ts +1 -1
  79. package/dist/src/tasks-runner/tasks-schedule.d.ts +18 -1
  80. package/dist/src/tasks-runner/tasks-schedule.js +64 -6
  81. package/dist/src/tasks-runner/utils.js +6 -22
  82. package/dist/src/utils/child-process.js +1 -0
  83. package/dist/src/utils/git-utils.d.ts +6 -1
  84. package/dist/src/utils/git-utils.js +26 -5
  85. package/dist/src/utils/plugins/core-plugins.js +4 -0
  86. package/dist/src/utils/spawn-without-terminal.d.ts +27 -0
  87. package/dist/src/utils/spawn-without-terminal.js +110 -0
  88. package/package.json +12 -11
  89. package/schemas/nx-schema.json +93 -4
  90. package/schemas/project-schema.json +84 -2
@@ -1 +1,2 @@
1
1
  export { importUltracacheConfigurations, openUltracacheConfigurations, } from './src/ultracache/store';
2
+ export declare const TASK_GRAPH_FORMAT = 2;
@@ -3,7 +3,13 @@
3
3
  // a public API. The client probes these exports to tell what this nx supports,
4
4
  // so keep them stable.
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.openUltracacheConfigurations = exports.importUltracacheConfigurations = void 0;
6
+ exports.TASK_GRAPH_FORMAT = exports.openUltracacheConfigurations = exports.importUltracacheConfigurations = void 0;
7
7
  var store_1 = require("./src/ultracache/store");
8
8
  Object.defineProperty(exports, "importUltracacheConfigurations", { enumerable: true, get: function () { return store_1.importUltracacheConfigurations; } });
9
9
  Object.defineProperty(exports, "openUltracacheConfigurations", { enumerable: true, get: function () { return store_1.openUltracacheConfigurations; } });
10
+ // Nx Cloud reads it to pick the task-graph shape it hands this nx. Bump it when
11
+ // the shape changes in a way Cloud must handle.
12
+ // 1: `dependencies` and `continuousDependencies` edges are task ids (implied
13
+ // when the export is absent)
14
+ // 2: edges are objects (`{ id, waitFor?, inputs? }`)
15
+ exports.TASK_GRAPH_FORMAT = 2;
@@ -967,7 +967,8 @@ function expandInputs(inputs, project, allWorkspaceFiles, depGraphClientResponse
967
967
  }
968
968
  });
969
969
  const workspaceRootsExpanded = getExpandedWorkspaceRoots(workspaceRootInputs, allWorkspaceFiles);
970
- const filesExpanded = filesInputs.flatMap((globs) => (0, native_1.expandFilesInput)(workspace_root_1.workspaceRoot, globs));
970
+ const projectRoots = depGraphClientResponse.projects.map((p) => p.data.root);
971
+ const filesExpanded = filesInputs.flatMap((globs) => (0, native_1.expandFilesInput)(workspace_root_1.workspaceRoot, globs, projectRoots));
971
972
  const otherInputsExpanded = otherInputs.map((input) => {
972
973
  if (input === 'TsConfig') {
973
974
  return (0, node_path_1.relative)(workspace_root_1.workspaceRoot, (0, typescript_1.getRootTsConfigPath)());
@@ -18,4 +18,4 @@ export declare function waitForExit(child: ChildProcess): Promise<ExitInfo>;
18
18
  * orphan the agent. `taskkill` failures are swallowed.
19
19
  */
20
20
  export declare function closeAgentSession(child: ChildProcess, exitPromise: Promise<ExitInfo>, gracefulExitMs: number, forceKillWaitMs: number): Promise<void>;
21
- export declare function raceWithTimeout(promise: Promise<unknown>, timeoutMs: number): Promise<void>;
21
+ export declare function raceWithTimeout(promise: Promise<unknown>, timeoutMs: number): Promise<boolean>;
@@ -109,13 +109,14 @@ async function forceKillWindowsTree(child, exitPromise, forceKillWaitMs) {
109
109
  }
110
110
  await raceWithTimeout(exitPromise, forceKillWaitMs);
111
111
  }
112
+ // True when `promise` resolves before the timeout.
112
113
  async function raceWithTimeout(promise, timeoutMs) {
113
114
  let timer;
114
115
  try {
115
- await Promise.race([
116
- promise,
116
+ return await Promise.race([
117
+ promise.then(() => true),
117
118
  new Promise((resolve) => {
118
- timer = setTimeout(resolve, timeoutMs);
119
+ timer = setTimeout(() => resolve(false), timeoutMs);
119
120
  }),
120
121
  ]);
121
122
  }
@@ -3,26 +3,23 @@
3
3
  * `.nx/migrate-runs/<run-id>/`: handoff and prompt files in both cases, plus
4
4
  * the durable run state and its plan snapshots for the orchestrator. The v23
5
5
  * migration `23-0-0-add-migrate-runs-to-git-ignore` adds `.nx/migrate-runs` to
6
- * `.gitignore`; in its declared slot (typically late) earlier per-migration
7
- * commits would absorb the scratch into the user-visible diff.
6
+ * `.gitignore`, but the scratch exists before that migration runs, so earlier
7
+ * commits would absorb it into the user-visible diff. Its two callers use this
8
+ * function to add the entry before that can happen:
8
9
  *
9
- * Two paths cover the leak, with no overlap:
10
+ * 1. Orchestrator init calls it with `applyWhenPlanned` and no standalone
11
+ * commit, before it creates the run dir. Its checkpoint commit carries the
12
+ * edit when commits are on and the checkpoint succeeds. A planned v23
13
+ * migration then changes nothing in its own step.
10
14
  *
11
- * 1. HOIST: `sortMigrations`, which `executeMigrations` applies, sorts the
12
- * v23 migration to position 0 when it is in the queue, so it runs first
13
- * through the normal runner with its own log line and commit. A
14
- * single-migration worker run needs no hoisting: the requested migration
15
- * is the entire queue.
15
+ * 2. A single-migration worker run with an agent defers to the requested
16
+ * migration when it is the v23 one. Otherwise, below v23, it applies the
17
+ * entry and commits it as a standalone preflight commit (or leaves it in
18
+ * the working tree under `--no-create-commits`).
16
19
  *
17
- * 2. INLINE FALLBACK, this function. When the migration is NOT in the queue
18
- * AND the highest target version is < v23 (intra-pre-v23 `--agentic`
19
- * run) it will never run, so apply its body inline against an `FsTree`
20
- * and commit it as a standalone preflight commit (or leave it in the
21
- * working tree under `--no-create-commits`).
22
- *
23
- * Not in the queue AND target >= v23 means the user is already past v23. They
24
- * had the entry historically; if it is gone, that is a conscious removal we
25
- * respect.
20
+ * When the v23 migration is not planned and nx is at v23 or later, the user is
21
+ * already past v23. They had the entry historically; if it is gone, that is a
22
+ * conscious removal we respect.
26
23
  */
27
24
  export declare function applyAgenticHandoffGitignoreFallback({ migrations, installedNxVersion, effectiveCreateCommits, commitPrefix, root, applyWhenPlanned, commitStandalone, }: {
28
25
  migrations: ReadonlyArray<{
@@ -41,12 +38,11 @@ export declare function applyAgenticHandoffGitignoreFallback({ migrations, insta
41
38
  commitPrefix: string;
42
39
  root: string;
43
40
  /**
44
- * Apply the entry even when the hoisted migration is in the plan. The
45
- * classic loop can defer to that migration because its run scratch appears
46
- * only after migration 1 has run; the orchestrator creates its run dir at
47
- * init, before any migration, so it needs the entry immediately. A planned
48
- * migration also means the missing entry is not a conscious removal, so the
49
- * v23 cutoff does not apply.
41
+ * Apply the entry even when the v23 migration is in the plan. The
42
+ * orchestrator creates its run dir at init, before any migration runs, so
43
+ * it can't defer to that migration. A planned migration also means the
44
+ * missing entry is not a conscious removal, so the v23 cutoff does not
45
+ * apply.
50
46
  */
51
47
  applyWhenPlanned?: boolean;
52
48
  /**
@@ -14,32 +14,29 @@ const types_1 = require("./types");
14
14
  * `.nx/migrate-runs/<run-id>/`: handoff and prompt files in both cases, plus
15
15
  * the durable run state and its plan snapshots for the orchestrator. The v23
16
16
  * migration `23-0-0-add-migrate-runs-to-git-ignore` adds `.nx/migrate-runs` to
17
- * `.gitignore`; in its declared slot (typically late) earlier per-migration
18
- * commits would absorb the scratch into the user-visible diff.
17
+ * `.gitignore`, but the scratch exists before that migration runs, so earlier
18
+ * commits would absorb it into the user-visible diff. Its two callers use this
19
+ * function to add the entry before that can happen:
19
20
  *
20
- * Two paths cover the leak, with no overlap:
21
+ * 1. Orchestrator init calls it with `applyWhenPlanned` and no standalone
22
+ * commit, before it creates the run dir. Its checkpoint commit carries the
23
+ * edit when commits are on and the checkpoint succeeds. A planned v23
24
+ * migration then changes nothing in its own step.
21
25
  *
22
- * 1. HOIST: `sortMigrations`, which `executeMigrations` applies, sorts the
23
- * v23 migration to position 0 when it is in the queue, so it runs first
24
- * through the normal runner with its own log line and commit. A
25
- * single-migration worker run needs no hoisting: the requested migration
26
- * is the entire queue.
26
+ * 2. A single-migration worker run with an agent defers to the requested
27
+ * migration when it is the v23 one. Otherwise, below v23, it applies the
28
+ * entry and commits it as a standalone preflight commit (or leaves it in
29
+ * the working tree under `--no-create-commits`).
27
30
  *
28
- * 2. INLINE FALLBACK, this function. When the migration is NOT in the queue
29
- * AND the highest target version is < v23 (intra-pre-v23 `--agentic`
30
- * run) it will never run, so apply its body inline against an `FsTree`
31
- * and commit it as a standalone preflight commit (or leave it in the
32
- * working tree under `--no-create-commits`).
33
- *
34
- * Not in the queue AND target >= v23 means the user is already past v23. They
35
- * had the entry historically; if it is gone, that is a conscious removal we
36
- * respect.
31
+ * When the v23 migration is not planned and nx is at v23 or later, the user is
32
+ * already past v23. They had the entry historically; if it is gone, that is a
33
+ * conscious removal we respect.
37
34
  */
38
35
  async function applyAgenticHandoffGitignoreFallback({ migrations, installedNxVersion, effectiveCreateCommits, commitPrefix, root, applyWhenPlanned = false, commitStandalone = true, }) {
39
36
  if (migrations.some(types_1.isHandoffGitignoreMigration)) {
40
37
  if (!applyWhenPlanned) {
41
- // The queue runs it itself: hoisted to the front by the sort comparator
42
- // in a full run, or as the single requested migration in a worker run.
38
+ // The single-migration worker run is that migration, and it adds the
39
+ // entry itself.
43
40
  return;
44
41
  }
45
42
  }
@@ -21,20 +21,29 @@ async function runMasterSession(input) {
21
21
  createCommits: init.createCommits,
22
22
  skipInstall: init.skipInstall,
23
23
  };
24
- const resume = (runId) => (0, run_1.runOrchestratorResume)({
25
- root: init.root,
26
- runId,
27
- policy,
28
- emitAgentInstructions: false,
29
- });
24
+ // A resume refuses when it cannot restore the run's missing runbook; a
25
+ // start refuses when the default-branch commit confirmation is declined.
26
+ let refusal = 'declined_commits';
27
+ const resume = (runId) => {
28
+ refusal = 'runbook_missing';
29
+ return (0, run_1.runOrchestratorResume)({
30
+ root: init.root,
31
+ runId,
32
+ policy,
33
+ emitAgentInstructions: false,
34
+ });
35
+ };
30
36
  // With `replaceRunId`, starts fresh over that run only; the consent covers
31
37
  // the run the user saw, and any other active run is reported instead.
32
- const start = (replaceRunId) => (0, run_1.runOrchestratorInit)({
33
- ...init,
34
- emitAgentInstructions: false,
35
- onExistingRun: replaceRunId === undefined ? 'report' : 'start-fresh',
36
- replaceRunId,
37
- });
38
+ const start = (replaceRunId) => {
39
+ refusal = 'declined_commits';
40
+ return (0, run_1.runOrchestratorInit)({
41
+ ...init,
42
+ emitAgentInstructions: false,
43
+ onExistingRun: replaceRunId === undefined ? 'report' : 'start-fresh',
44
+ replaceRunId,
45
+ });
46
+ };
38
47
  let ready = requestedRunId !== undefined && !startFresh
39
48
  ? resume(requestedRunId)
40
49
  : await start(startFresh ? requestedRunId : undefined);
@@ -44,6 +53,7 @@ async function runMasterSession(input) {
44
53
  output_1.output.log({
45
54
  title: `Leaving migrate run ${ready.runId} as it is. ${continueHint(init.root, agent.id, ready.runId, ready.facts.policy)}`,
46
55
  });
56
+ (0, migrate_analytics_1.reportMigrateRunStopped)('aborted');
47
57
  return;
48
58
  }
49
59
  if (decision === 'continue') {
@@ -56,9 +66,11 @@ async function runMasterSession(input) {
56
66
  }
57
67
  if (ready.kind === 'existing-run') {
58
68
  output_1.output.warn((0, run_1.renderExistingRunReport)(ready.facts, (0, run_1.renderExistingRunCommands)(init.root, ready.facts, init.migrationsPath, agent.id)));
69
+ (0, migrate_analytics_1.reportMigrateRunStopped)('existing_run');
59
70
  return 1;
60
71
  }
61
72
  if (ready.kind === 'refused') {
73
+ (0, migrate_analytics_1.reportMigrateRunStopped)(refusal);
62
74
  return;
63
75
  }
64
76
  const { runId, runRoot, runbookPath, reconcileCommand } = ready;
@@ -145,7 +157,7 @@ async function runMasterSession(input) {
145
157
  }
146
158
  }
147
159
  function continueHint(root, agentId, runId, policy) {
148
- return `Run ${(0, run_1.renderContinueCommand)(root, runId, policy, agentId)}, with NX_MIGRATE_ORCHESTRATOR=true set in the environment, to continue it.`;
160
+ return `Run ${(0, run_1.renderContinueCommand)(root, runId, policy, agentId)} to continue it.`;
149
161
  }
150
162
  async function decideExistingRun(found, root) {
151
163
  output_1.output.log((0, run_1.renderExistingRunReport)(found.facts));
@@ -2,10 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.spawnMasterSession = spawnMasterSession;
4
4
  const child_process_1 = require("child_process");
5
+ const events_1 = require("events");
5
6
  const fs_1 = require("fs");
6
7
  const path_1 = require("path");
8
+ const exit_codes_1 = require("../../../../utils/exit-codes");
7
9
  const logger_1 = require("../../../../utils/logger");
8
10
  const output_1 = require("../../../../utils/output");
11
+ const spawn_without_terminal_1 = require("../../../../utils/spawn-without-terminal");
9
12
  const migrate_output_1 = require("../../migrate-output");
10
13
  const run_1 = require("../../run");
11
14
  const close_agent_session_1 = require("../close-agent-session");
@@ -13,16 +16,21 @@ const handoff_1 = require("../handoff");
13
16
  const terminal_repair_1 = require("../terminal-repair");
14
17
  const windows_cmd_1 = require("../windows-cmd");
15
18
  const invocations_1 = require("./invocations");
16
- // The wrapper's local re-exec sets the first two for its own hop and the user
17
- // sets the third to reach this path; inherited, they would change install or
18
- // routing behavior for every `nx migrate` the agent runs. CI exports the last
19
- // two for its own `nx affected` runs; inherited, they would replace the base
20
- // the run names and drop uncommitted files from the pass's selection. The
21
- // workspace's env files can still set them: every nx command loads those.
19
+ // How long a second Ctrl+C waits for the killed operation to settle before nx
20
+ // stops waiting on it: only a process that left its group can hold it open.
21
+ const OPERATION_KILL_WAIT_MS = 2_000;
22
+ // SIGINTs this close together are one Ctrl+C: `npx` and the package managers
23
+ // pass on to nx the SIGINT the terminal already sent it.
24
+ const SAME_PRESS_MS = 500;
25
+ // The wrapper's local re-exec sets the first two for its own hop; inherited,
26
+ // they would change install or routing behavior for every `nx migrate` the
27
+ // agent runs. CI exports the last two for its own `nx affected` runs;
28
+ // inherited, they would replace the base the run names and drop uncommitted
29
+ // files from the pass's selection. The workspace's env files can still set
30
+ // them: every nx command loads those.
22
31
  const STRIPPED_ENV_VARS = [
23
32
  'NX_MIGRATE_SKIP_INSTALL',
24
33
  'NX_MIGRATE_USE_LOCAL',
25
- 'NX_MIGRATE_ORCHESTRATOR',
26
34
  'NX_BASE',
27
35
  'NX_HEAD',
28
36
  ];
@@ -80,9 +88,26 @@ async function spawnMasterSession(input) {
80
88
  broker?.close();
81
89
  return { kind: 'spawn-failed', error: toError(error) };
82
90
  }
83
- // Ctrl+C belongs to the agent from the moment it exists.
84
- const swallowSigint = () => { };
85
- process.on('SIGINT', swallowSigint);
91
+ // Ctrl+C belongs to the agent from the moment it exists. The operations the
92
+ // broker runs have no terminal, so nx passes on what would have reached them
93
+ // through it, and ends them when it goes away itself.
94
+ const ctrlC = new events_1.EventEmitter();
95
+ let lastPressAt = -Infinity;
96
+ const forwardSigint = () => {
97
+ const now = Date.now();
98
+ if (now - lastPressAt < SAME_PRESS_MS)
99
+ return;
100
+ lastPressAt = now;
101
+ (0, spawn_without_terminal_1.signalCommandsWithoutTerminal)('SIGINT');
102
+ ctrlC.emit('press');
103
+ };
104
+ const exitOnSignal = (signal) => {
105
+ (0, spawn_without_terminal_1.signalCommandsWithoutTerminal)(signal);
106
+ process.exit((0, exit_codes_1.signalToCode)(signal));
107
+ };
108
+ process.on('SIGINT', forwardSigint);
109
+ process.on('SIGHUP', exitOnSignal);
110
+ process.on('SIGTERM', exitOnSignal);
86
111
  const sentinelWatch = new AbortController();
87
112
  let brokerFailure;
88
113
  // Settles when the poll aborts and the request in flight is answered, or
@@ -133,17 +158,33 @@ async function spawnMasterSession(input) {
133
158
  finally {
134
159
  sentinelWatch.abort();
135
160
  // With the agent gone, the terminal is restored before the wait: an
136
- // agent that left it raw would keep a Ctrl+C from reaching the operation
137
- // in flight as a signal.
161
+ // agent that left it raw would keep a Ctrl+C from reaching nx as a signal.
138
162
  const exited = child.exitCode !== null || child.signalCode !== null;
163
+ let stoppedWaiting = false;
139
164
  if (started && exited) {
140
165
  restoreTerminal();
141
- warnOperationInFlight(broker);
166
+ const inFlight = warnOperationInFlight(broker);
167
+ if (inFlight && (await interruptedTwice(brokerDone, ctrlC))) {
168
+ (0, spawn_without_terminal_1.signalCommandsWithoutTerminal)('SIGKILL');
169
+ if (!(await (0, close_agent_session_1.raceWithTimeout)(brokerDone, OPERATION_KILL_WAIT_MS))) {
170
+ stoppedWaiting = true;
171
+ output_1.output.warn({
172
+ title: `Stopped waiting for ${(0, run_1.treeOperationLabel)(inFlight.request, inFlight.stepName)}.`,
173
+ bodyLines: [
174
+ 'A process it started may still be running. Resume the run once it has exited.',
175
+ ],
176
+ });
177
+ }
178
+ }
142
179
  }
143
- // The request in flight settles before the lock is released.
144
- await brokerDone;
180
+ // The request in flight settles before the lock is released, unless nx
181
+ // stopped waiting on it above.
182
+ if (!stoppedWaiting)
183
+ await brokerDone;
145
184
  broker.close();
146
- process.removeListener('SIGINT', swallowSigint);
185
+ process.removeListener('SIGINT', forwardSigint);
186
+ process.removeListener('SIGHUP', exitOnSignal);
187
+ process.removeListener('SIGTERM', exitOnSignal);
147
188
  if (started && !exited)
148
189
  restoreTerminal();
149
190
  }
@@ -155,15 +196,34 @@ function restoreTerminal() {
155
196
  (0, terminal_repair_1.restoreTermiosAfterAgent)();
156
197
  (0, migrate_output_1.resetSgrAfterAgent)();
157
198
  }
158
- // With the agent gone a Ctrl+C reaches the operation's child process, and
159
- // nothing else tells the user one is still running.
199
+ // With the agent gone nothing else tells the user an operation still runs.
160
200
  function warnOperationInFlight(broker) {
161
- const request = broker.requestInFlight;
162
- if (!request)
163
- return;
201
+ const inFlight = broker.requestInFlight;
202
+ if (!inFlight)
203
+ return null;
164
204
  output_1.output.warn({
165
- title: `Still running ${(0, run_1.treeOperationLabel)(request)} for this migrate run. Press Ctrl+C to end it; the run can be resumed afterwards.`,
205
+ title: `Still running ${(0, run_1.treeOperationLabel)(inFlight.request, inFlight.stepName)} for this migrate run (${(0, run_1.formatElapsed)(Date.now() - inFlight.startedAt)} so far).${(0, run_1.commitsStep)(inFlight.request.kind) ? ` ${run_1.COMMIT_INSTALLS_FIRST}` : ''} Press Ctrl+C to end it, and again to stop everything it started. The run can be resumed afterwards.`,
206
+ });
207
+ return inFlight;
208
+ }
209
+ // The first Ctrl+C reaches the operation as usual; the second ends whatever
210
+ // outlived it.
211
+ async function interruptedTwice(done, ctrlC) {
212
+ let countPress;
213
+ const secondPress = new Promise((resolve) => {
214
+ let presses = 0;
215
+ countPress = () => {
216
+ if (++presses === 2)
217
+ resolve(true);
218
+ };
166
219
  });
220
+ ctrlC.on('press', countPress);
221
+ try {
222
+ return await Promise.race([done.then(() => false), secondPress]);
223
+ }
224
+ finally {
225
+ ctrlC.removeListener('press', countPress);
226
+ }
167
227
  }
168
228
  async function serviceBrokerUntilAborted(broker, intervalMs, signal) {
169
229
  while (!signal.aborted) {
@@ -97,7 +97,7 @@ function requireInteractiveOrAbort(isInteractive) {
97
97
  if (isInteractive)
98
98
  return;
99
99
  output_1.output.error({
100
- title: 'The agentic flow is interactive-only in this release.',
100
+ title: 'The agentic flow needs an interactive terminal.',
101
101
  bodyLines: [
102
102
  'Re-run in an interactive terminal, or pass `--agentic=false` to skip the agentic flow.',
103
103
  ],
@@ -106,9 +106,9 @@ function requireInteractiveOrAbort(isInteractive) {
106
106
  }
107
107
  function warnAgenticInteractiveOnly() {
108
108
  output_1.output.warn({
109
- title: 'Skipping the agentic flow: it is interactive-only in this release and this run is non-interactive.',
109
+ title: 'Skipping the agentic flow: the agent session needs an interactive terminal, and this run is non-interactive.',
110
110
  bodyLines: [
111
- 'Continuing the migration without the agentic flow. Re-run in an interactive terminal to use it.',
111
+ 'Continuing without the agentic flow. Generators still run, prompt-based changes are left for you to apply, and no AI validation runs. Re-run in an interactive terminal to use the agentic flow.',
112
112
  ],
113
113
  });
114
114
  }
@@ -28,9 +28,7 @@ exports.PROMPTS_DIR_NAME = 'prompts';
28
28
  * `.gitignore`. Hard-coded because the agentic preflight is a deliberate
29
29
  * one-off coupling: this exact migration owns the entry that keeps
30
30
  * `.nx/migrate-runs/<run-id>/...` scratch out of per-migration commits. If
31
- * the migration is ever renamed, this entry must move with it. It lives here
32
- * rather than in `handoff-gitignore.ts` so `sortMigrations`' hoist check
33
- * doesn't load that module's migration-execution machinery.
31
+ * the migration is ever renamed, this entry must move with it.
34
32
  */
35
33
  const HANDOFF_GITIGNORE_MIGRATION_PACKAGE = 'nx';
36
34
  const HANDOFF_GITIGNORE_MIGRATION_NAME = '23-0-0-add-migrate-runs-to-git-ignore';
@@ -59,10 +59,8 @@ function withMigrationOptions(yargs) {
59
59
  type: 'string',
60
60
  })
61
61
  .option('runId', {
62
- // Hidden with the orchestrator itself.
63
- describe: 'The orchestrated migrate run to record a single migration into, to continue with --run-migrations, or to reconcile when given alone.',
62
+ describe: "The id of a migrate run, as nx prints it. With --run-migrations: continue that run, or replace it with --start-fresh. The AI agent driving a run also passes it with --run-migration to record a migration into the run, and alone to record a step's outcome and get the next step.",
64
63
  type: 'string',
65
- hidden: true,
66
64
  })
67
65
  .option('stepAction', {
68
66
  describe: 'How an orchestrated reconcile resolves its single failed or died step.',
@@ -71,9 +69,8 @@ function withMigrationOptions(yargs) {
71
69
  hidden: true,
72
70
  })
73
71
  .option('startFresh', {
74
- describe: 'With --run-migrations and --run-id=<id> under the orchestrator: delete the record of the active migrate run the id names and start a new run over the whole plan. Migrations the deleted run applied stay applied.',
72
+ describe: 'With --run-migrations and --run-id=<id>: delete the record of the active migrate run the id names, then run the whole plan again, as a new run when orchestrated. Migrations the deleted run applied stay applied.',
75
73
  type: 'boolean',
76
- hidden: true,
77
74
  })
78
75
  .option('ifExists', {
79
76
  describe: `Run migrations only if the migrations file exists, if not continues successfully.`,
@@ -123,17 +120,16 @@ function withMigrationOptions(yargs) {
123
120
  choices: exports.MULTI_MAJOR_MODES,
124
121
  })
125
122
  .option('agentic', {
126
- describe: 'Enable the agentic flow for prompt-based migrations and AI-driven review. Pass `--agentic=<agent>` to pin a specific agent (claude-code, codex, or opencode). Pass `--agentic=false` or `--no-agentic` to disable.',
123
+ describe: 'Use an AI agent to apply prompt-based migrations and review the results. With --run-migrations, one agent session drives the whole run. Pass `--agentic=<agent>` to pin a specific agent (claude-code, codex, or opencode). Pass `--agentic=false` or `--no-agentic` to disable it, including when nx migrate runs inside an AI agent.',
127
124
  coerce: cli_args_1.coerceAgenticArg,
128
125
  })
129
126
  .option('validate', {
130
- describe: 'When `--agentic` resolves to an enabled agent, run agent-driven validation after generator-only migrations that have no `prompt:` field. Defaults to on; pass `--no-validate` to opt out. Has no effect when `--agentic` is disabled, when running inside an outer agent, or when running non-interactively without an explicit agent.',
127
+ describe: 'Have the AI agent driving the run review the changes of generator-only migrations (those with no `prompt:` field). On by default; pass `--no-validate` to opt out. A continued run keeps the setting it started with. Has no effect when no agent drives the run, or with --run-migration inside an AI agent.',
131
128
  type: 'boolean',
132
129
  })
133
130
  .option('finalValidation', {
134
- describe: 'With --run-migrations under the orchestrator: end the run with an agent validation pass over the whole workspace once every migration has run. On by default; pass --no-final-validation to opt out.',
131
+ describe: 'With --run-migrations, when an AI agent drives the run: once every migration has run, the agent checks that the projects the run touched still lint, build, and pass their unit tests. On by default; pass --no-final-validation to opt out. A continued run keeps the setting it started with.',
135
132
  type: 'boolean',
136
- hidden: true,
137
133
  })
138
134
  .check(({ createCommits, commitPrefix, from, excludeAppliedMigrations, include, agentic, }) => {
139
135
  // Only an explicit `--no-create-commits` is decidable here, before the
@@ -9,8 +9,8 @@ interface PackageMigrationConfig extends NxMigrationsConfiguration {
9
9
  export declare function readPackageMigrationConfig(packageName: string, dir: string): PackageMigrationConfig;
10
10
  /**
11
11
  * With a `sink`, stdin is ignored and stdout and stderr are collected for a
12
- * caller sharing the terminal. On POSIX the child stays in nx's process
13
- * group. Without a sink, the install owns the terminal.
12
+ * caller sharing the terminal, and the install runs without the terminal (see
13
+ * `spawnWithoutTerminal`). Without a sink, the install owns the terminal.
14
14
  */
15
15
  export declare function runInstall(nxWorkspaceRoot?: string, phase?: MigrationInstallPhase, rerunCommand?: string, sink?: MigrateOutputSink): Promise<void>;
16
16
  export type MigrationInstallPhase = 'pre-migration' | 'post-migration';
@@ -33,6 +33,7 @@ const package_manager_1 = require("../../utils/package-manager");
33
33
  const fs_1 = require("fs");
34
34
  const installation_directory_1 = require("../../utils/installation-directory");
35
35
  const shell_quoting_1 = require("../../utils/shell-quoting");
36
+ const spawn_without_terminal_1 = require("../../utils/spawn-without-terminal");
36
37
  const project_graph_1 = require("../../project-graph/project-graph");
37
38
  const version_utils_1 = require("./version-utils");
38
39
  const deferred_output_1 = require("./deferred-output");
@@ -64,8 +65,8 @@ function readPackageMigrationConfig(packageName, dir) {
64
65
  }
65
66
  /**
66
67
  * With a `sink`, stdin is ignored and stdout and stderr are collected for a
67
- * caller sharing the terminal. On POSIX the child stays in nx's process
68
- * group. Without a sink, the install owns the terminal.
68
+ * caller sharing the terminal, and the install runs without the terminal (see
69
+ * `spawnWithoutTerminal`). Without a sink, the install owns the terminal.
69
70
  */
70
71
  function runInstall(nxWorkspaceRoot, phase = 'pre-migration', rerunCommand, sink) {
71
72
  const cwd = nxWorkspaceRoot ?? process.cwd();
@@ -81,14 +82,21 @@ function runInstall(nxWorkspaceRoot, phase = 'pre-migration', rerunCommand, sink
81
82
  // Without a sink it is mirrored live and other package managers inherit
82
83
  // stderr directly, since their output is not inspected.
83
84
  const shouldCaptureStderr = packageManager === 'npm';
84
- const child = (0, child_process_1.spawn)(installCommand, {
85
- shell: true,
86
- stdio: sink
87
- ? ['ignore', 'pipe', 'pipe']
88
- : ['inherit', 'inherit', shouldCaptureStderr ? 'pipe' : 'inherit'],
89
- windowsHide: true,
90
- cwd,
91
- });
85
+ const child = sink
86
+ ? (0, spawn_without_terminal_1.spawnWithoutTerminal)(installCommand, {
87
+ stdio: ['ignore', 'pipe', 'pipe'],
88
+ cwd,
89
+ })
90
+ : (0, child_process_1.spawn)(installCommand, {
91
+ shell: true,
92
+ stdio: [
93
+ 'inherit',
94
+ 'inherit',
95
+ shouldCaptureStderr ? 'pipe' : 'inherit',
96
+ ],
97
+ windowsHide: true,
98
+ cwd,
99
+ });
92
100
  // Decoded per stream: a chunk boundary can split a multi-byte character,
93
101
  // and a sequence still incomplete at the end is what `end()` returns.
94
102
  const stdoutText = new string_decoder_1.StringDecoder('utf8');
@@ -18,6 +18,7 @@ export type MigratePromptChoices = {
18
18
  export type MigratePromptName = keyof MigratePromptChoices;
19
19
  export type MigrateGenerateErrorCode = 'resolve_version' | 'fetch_migrations' | 'package_updates';
20
20
  export type MigrateRunErrorCode = 'npm_install' | 'migration_exec' | 'agentic' | 'other';
21
+ export type MigrateRunStopReason = 'existing_run' | 'declined_commits' | 'aborted' | 'runbook_missing';
21
22
  export declare function setMigrateInclude(include: MigrateInclude): void;
22
23
  export declare function setMigrateIncludeSource(source: MigrateIncludeSource): void;
23
24
  export declare function classifyMigrateFetchFallback(error: unknown): MigrateFetchFallbackReason;
@@ -60,6 +61,11 @@ export declare function reportMigrateRunComplete(opts: {
60
61
  migrationCount: number;
61
62
  appliedCount: number;
62
63
  }): void;
64
+ /**
65
+ * Closes a `migrate_run_start` whose invocation stopped before running any
66
+ * migration.
67
+ */
68
+ export declare function reportMigrateRunStopped(reason: MigrateRunStopReason): void;
63
69
  export declare function reportMigrateRunError(opts: {
64
70
  code: MigrateRunErrorCode;
65
71
  migrationPackage?: string;
@@ -10,6 +10,7 @@ exports.reportMigrateGenerateError = reportMigrateGenerateError;
10
10
  exports.reportMigrateRunStart = reportMigrateRunStart;
11
11
  exports.hasMigrateRunStarted = hasMigrateRunStarted;
12
12
  exports.reportMigrateRunComplete = reportMigrateRunComplete;
13
+ exports.reportMigrateRunStopped = reportMigrateRunStopped;
13
14
  exports.reportMigrateRunError = reportMigrateRunError;
14
15
  exports.reportMigrateOrchestratorInit = reportMigrateOrchestratorInit;
15
16
  exports.reportMigrateOrchestratorDispense = reportMigrateOrchestratorDispense;
@@ -165,6 +166,17 @@ function reportMigrateRunComplete(opts) {
165
166
  });
166
167
  });
167
168
  }
169
+ /**
170
+ * Closes a `migrate_run_start` whose invocation stopped before running any
171
+ * migration.
172
+ */
173
+ function reportMigrateRunStopped(reason) {
174
+ safeReport(() => {
175
+ if (!analytics_1.customDimensions)
176
+ return;
177
+ (0, analytics_1.reportEvent)(`migrate_run_stopped_${reason}`);
178
+ });
179
+ }
168
180
  function reportMigrateRunError(opts) {
169
181
  safeReport(() => {
170
182
  if (!analytics_1.customDimensions || runErrorRecorded)
@@ -31,13 +31,16 @@ export type CommitResult = {
31
31
  * The default `failureGuidance` describes the classic loop's absorb-and-recap
32
32
  * behavior; a caller with no later commit or recap to absorb the diff (the
33
33
  * standalone single-migration worker) passes its own.
34
+ *
35
+ * A `sink` collects the output for a caller sharing the terminal with an
36
+ * agent, and the commit then runs without the terminal.
34
37
  */
35
38
  export declare function commitMigrationIfRequested(root: string, migration: {
36
39
  name: string;
37
40
  }, shouldCreateCommits: boolean, commitPrefix: string, installDepsIfChanged: () => Promise<void>, pendingMigrations?: ReadonlyArray<{
38
41
  package: string;
39
42
  name: string;
40
- }>, failureGuidance?: string, out?: MigrateOutputSink): Promise<CommitResult>;
43
+ }>, failureGuidance?: string, sink?: MigrateOutputSink): Promise<CommitResult>;
41
44
  /**
42
45
  * Commits any pre-existing working-tree state into a dedicated "checkpoint"
43
46
  * commit before the first migration runs. Without this, the first migration's