nx 23.3.0 → 24.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +3 -6
  2. package/dist/nx-cloud-internals.d.ts +1 -0
  3. package/dist/nx-cloud-internals.js +7 -1
  4. package/dist/src/adapter/ngcli-adapter.js +1 -0
  5. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +0 -12
  6. package/dist/src/command-line/graph/graph.js +3 -5
  7. package/dist/src/command-line/init/command-object.js +0 -12
  8. package/dist/src/command-line/init/init-v2.js +0 -2
  9. package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +1 -1
  10. package/dist/src/command-line/migrate/agentic/close-agent-session.js +4 -3
  11. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +76 -15
  12. package/dist/src/command-line/migrate/execute-migration.d.ts +2 -2
  13. package/dist/src/command-line/migrate/execute-migration.js +18 -10
  14. package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -1
  15. package/dist/src/command-line/migrate/migrate-commits.js +10 -3
  16. package/dist/src/command-line/migrate/migrate.js +43 -6
  17. package/dist/src/command-line/migrate/run/agent-output.d.ts +1 -0
  18. package/dist/src/command-line/migrate/run/agent-output.js +5 -0
  19. package/dist/src/command-line/migrate/run/broker.d.ts +14 -5
  20. package/dist/src/command-line/migrate/run/broker.js +68 -20
  21. package/dist/src/command-line/migrate/run/index.d.ts +4 -3
  22. package/dist/src/command-line/migrate/run/index.js +8 -1
  23. package/dist/src/command-line/migrate/run/orchestrator.js +72 -13
  24. package/dist/src/command-line/migrate/run/runbook.js +19 -0
  25. package/dist/src/command-line/migrate/run/state-machine.js +4 -1
  26. package/dist/src/command-line/migrate/run/util.d.ts +3 -0
  27. package/dist/src/command-line/migrate/run/util.js +7 -1
  28. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +17 -12
  29. package/dist/src/command-line/release/utils/remote-release-clients/github.js +5 -6
  30. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +5 -6
  31. package/dist/src/command-line/run/executor-utils.js +1 -0
  32. package/dist/src/command-line/run/run-one.d.ts +20 -1
  33. package/dist/src/command-line/run/run-one.js +153 -5
  34. package/dist/src/command-line/show/show-target/info.js +1 -1
  35. package/dist/src/command-line/show/show-target/utils.js +1 -0
  36. package/dist/src/command-line/watch/command-object.js +3 -22
  37. package/dist/src/command-line/watch/watch.d.ts +0 -7
  38. package/dist/src/command-line/watch/watch.js +1 -1
  39. package/dist/src/config/misc-interfaces.d.ts +12 -0
  40. package/dist/src/config/task-graph.d.ts +1 -1
  41. package/dist/src/config/workspace-json-project-json.d.ts +65 -0
  42. package/dist/src/core/graph/main.js +1 -1
  43. package/dist/src/daemon/client/client.d.ts +9 -0
  44. package/dist/src/daemon/client/client.js +89 -37
  45. package/dist/src/daemon/server/shutdown-utils.js +0 -3
  46. package/dist/src/daemon/socket-utils.d.ts +9 -4
  47. package/dist/src/daemon/socket-utils.js +12 -49
  48. package/dist/src/daemon/tmp-dir.d.ts +19 -14
  49. package/dist/src/daemon/tmp-dir.js +142 -354
  50. package/dist/src/devkit-exports.d.ts +1 -1
  51. package/dist/src/devkit-internals.d.ts +1 -1
  52. package/dist/src/devkit-internals.js +1 -0
  53. package/dist/src/hasher/check-task-files.js +4 -4
  54. package/dist/src/hasher/file-hasher.js +9 -7
  55. package/dist/src/hasher/hash-task.js +4 -0
  56. package/dist/src/hasher/native-task-hasher-impl.js +3 -1
  57. package/dist/src/migrations/update-24-0-0/find-custom-hashers.d.ts +3 -0
  58. package/dist/src/migrations/update-24-0-0/find-custom-hashers.js +100 -0
  59. package/dist/src/migrations/update-24-0-0/find-custom-hashers.md +63 -0
  60. package/dist/src/migrations/update-24-0-0/move-custom-hashers-to-inputs.md +145 -0
  61. package/dist/src/native/index.d.ts +180 -12
  62. package/dist/src/native/native-bindings.js +8 -0
  63. package/dist/src/native/nx.wasi-browser.js +1 -0
  64. package/dist/src/native/nx.wasi.cjs +1 -0
  65. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  66. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  67. package/dist/src/nx-cloud/update-manager.d.ts +7 -0
  68. package/dist/src/nx-cloud/update-manager.js +237 -50
  69. package/dist/src/project-graph/affected/affected-tasks.js +1 -0
  70. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +7 -0
  71. package/dist/src/tasks-runner/create-task-graph.d.ts +8 -7
  72. package/dist/src/tasks-runner/create-task-graph.js +47 -26
  73. package/dist/src/tasks-runner/init-tasks-runner.js +13 -0
  74. package/dist/src/tasks-runner/life-cycle.d.ts +4 -1
  75. package/dist/src/tasks-runner/life-cycle.js +7 -0
  76. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +5 -2
  77. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +13 -7
  78. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +4 -3
  79. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +9 -2
  80. package/dist/src/tasks-runner/readiness/probes.d.ts +9 -0
  81. package/dist/src/tasks-runner/readiness/probes.js +62 -0
  82. package/dist/src/tasks-runner/readiness/ready-when.d.ts +38 -0
  83. package/dist/src/tasks-runner/readiness/ready-when.js +190 -0
  84. package/dist/src/tasks-runner/run-command.js +5 -4
  85. package/dist/src/tasks-runner/running-tasks/node-child-process.js +5 -4
  86. package/dist/src/tasks-runner/task-env.d.ts +10 -0
  87. package/dist/src/tasks-runner/task-env.js +26 -5
  88. package/dist/src/tasks-runner/task-graph-utils.d.ts +9 -6
  89. package/dist/src/tasks-runner/task-graph-utils.js +17 -12
  90. package/dist/src/tasks-runner/task-orchestrator.d.ts +37 -8
  91. package/dist/src/tasks-runner/task-orchestrator.js +460 -76
  92. package/dist/src/tasks-runner/tasks-schedule.d.ts +18 -1
  93. package/dist/src/tasks-runner/tasks-schedule.js +64 -6
  94. package/dist/src/tasks-runner/utils.d.ts +20 -5
  95. package/dist/src/tasks-runner/utils.js +70 -35
  96. package/dist/src/ultracache/overrides.js +4 -0
  97. package/dist/src/utils/child-process.js +1 -0
  98. package/dist/src/utils/git-utils.d.ts +6 -1
  99. package/dist/src/utils/git-utils.js +26 -5
  100. package/dist/src/utils/owned-private-dir.d.ts +1 -17
  101. package/dist/src/utils/owned-private-dir.js +10 -62
  102. package/dist/src/utils/perf-hooks.d.ts +1 -0
  103. package/dist/src/utils/plugins/core-plugins.js +1 -1
  104. package/dist/src/utils/spawn-without-terminal.d.ts +27 -0
  105. package/dist/src/utils/spawn-without-terminal.js +110 -0
  106. package/dist/src/utils/string-similarity.d.ts +38 -0
  107. package/dist/src/utils/string-similarity.js +108 -0
  108. package/migrations.json +7 -0
  109. package/package.json +11 -20
  110. package/readme-template.md +3 -3
  111. package/schemas/nx-schema.json +70 -0
  112. package/schemas/project-schema.json +67 -0
  113. package/dist/src/tasks-runner/legacy-depends-on-warning.d.ts +0 -18
  114. package/dist/src/tasks-runner/legacy-depends-on-warning.js +0 -109
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
- <p style="text-align: center;">
1
+ <p align="center">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-dark.svg">
4
- <img alt="Nx - Smart Repos · Fast Builds" src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-light.svg" width="100%">
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo-light.svg">
4
+ <img alt="Nx Logo" src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.svg" width="140">
5
5
  </picture>
6
6
  </p>
7
7
 
@@ -60,6 +60,3 @@ npx nx@latest init
60
60
  - [Official Nx YouTube Channel](https://www.youtube.com/@NxDevtools)
61
61
  - [Blog Posts About Nx](https://nx.dev/blog)
62
62
 
63
- <p style="text-align: center;"><a href="https://nx.dev/#learning-materials" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-courses-and-videos.svg"
64
- width="100%" alt="Nx - Smart Monorepos · Fast Builds"></a></p>
65
-
@@ -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;
@@ -991,6 +991,7 @@ async function getWrappedWorkspaceNodeModulesArchitectHost(workspace, root, proj
991
991
  const batchImplementationFactory = executorConfig.batchImplementation
992
992
  ? this.getImplementationFactory(executorConfig.batchImplementation, executorsDir, nodeModule)
993
993
  : null;
994
+ // TODO(v25): remove custom hasher support.
994
995
  const hasherFactory = executorConfig.hasher
995
996
  ? this.getImplementationFactory(executorConfig.hasher, executorsDir, nodeModule)
996
997
  : null;
@@ -64,18 +64,6 @@ async function configureAiAgentsHandler(args, inner = false) {
64
64
  await resetDaemonAgentStatus();
65
65
  }
66
66
  async function configureAiAgentsHandlerImpl(options) {
67
- // Node 24's stricter readline throws ERR_USE_AFTER_CLOSE when a prompt
68
- // library operates on a closed interface. Added for enquirer, which nx no
69
- // longer uses; verify against Node 24 before removing.
70
- // TODO(v24): drop if @clack/prompts proves not to need it.
71
- process.on('uncaughtException', (error) => {
72
- if (error &&
73
- typeof error === 'object' &&
74
- 'code' in error &&
75
- error['code'] === 'ERR_USE_AFTER_CLOSE')
76
- return;
77
- throw error;
78
- });
79
67
  const normalizedOptions = normalizeOptions(options);
80
68
  const { nonConfiguredAgents, partiallyConfiguredAgents, fullyConfiguredAgents, disabledAgents, } = await (0, utils_1.getAgentConfigurations)(normalizedOptions.agents, workspace_root_1.workspaceRoot);
81
69
  if (disabledAgents.length > 0) {
@@ -372,11 +372,9 @@ async function generateGraph(args, affectedProjects) {
372
372
  title: `Project graph started at ${url.toString()}`,
373
373
  });
374
374
  if (args.open) {
375
- new Function('return import("open")')()
376
- .then((m) => m.default(url.toString()))
377
- .catch(() => {
378
- // Ignore errors when opening browser (e.g. no browser available)
379
- });
375
+ // Best-effort: openUrl never throws, returning false if no opener could be
376
+ // spawned. The URL is printed above, so a failure is silent.
377
+ (0, native_1.openUrl)(url.toString());
380
378
  }
381
379
  return new Promise((res) => {
382
380
  app.once('close', res);
@@ -17,18 +17,6 @@ exports.yargsInitCommand = {
17
17
  return withInitOptions(yargs);
18
18
  },
19
19
  handler: async (args) => {
20
- // Node 24's stricter readline throws ERR_USE_AFTER_CLOSE when a prompt
21
- // library operates on a closed interface. Added for enquirer, which nx no
22
- // longer uses; verify against Node 24 before removing.
23
- // TODO(v24): drop if @clack/prompts proves not to need it.
24
- process.on('uncaughtException', (error) => {
25
- if (error &&
26
- typeof error === 'object' &&
27
- 'code' in error &&
28
- error['code'] === 'ERR_USE_AFTER_CLOSE')
29
- return;
30
- throw error;
31
- });
32
20
  const useV2 = await isInitV2();
33
21
  if (useV2) {
34
22
  await require('./init-v2').initHandler(args);
@@ -442,7 +442,6 @@ function getPluginReason(plugin) {
442
442
  '@nx/next': 'next.js detected in dependencies',
443
443
  '@nx/nuxt': 'nuxt detected in dependencies',
444
444
  '@nx/react-native': 'react-native detected in dependencies',
445
- '@nx/remix': '@remix-run/dev detected in dependencies',
446
445
  '@nx/rsbuild': '@rsbuild/core detected in dependencies',
447
446
  '@nx/react': '@react-router/dev detected in dependencies',
448
447
  '@nx/gradle': 'gradlew detected in workspace',
@@ -493,7 +492,6 @@ const npmPackageToPluginMap = {
493
492
  next: '@nx/next',
494
493
  nuxt: '@nx/nuxt',
495
494
  'react-native': '@nx/react-native',
496
- '@remix-run/dev': '@nx/remix',
497
495
  '@rsbuild/core': '@nx/rsbuild',
498
496
  '@react-router/dev': '@nx/react',
499
497
  };
@@ -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
  }
@@ -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,6 +16,12 @@ 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");
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;
16
25
  // The wrapper's local re-exec sets the first two for its own hop and the user
17
26
  // sets the third to reach this path; inherited, they would change install or
18
27
  // routing behavior for every `nx migrate` the agent runs. CI exports the last
@@ -80,9 +89,26 @@ async function spawnMasterSession(input) {
80
89
  broker?.close();
81
90
  return { kind: 'spawn-failed', error: toError(error) };
82
91
  }
83
- // Ctrl+C belongs to the agent from the moment it exists.
84
- const swallowSigint = () => { };
85
- process.on('SIGINT', swallowSigint);
92
+ // Ctrl+C belongs to the agent from the moment it exists. The operations the
93
+ // broker runs have no terminal, so nx passes on what would have reached them
94
+ // through it, and ends them when it goes away itself.
95
+ const ctrlC = new events_1.EventEmitter();
96
+ let lastPressAt = -Infinity;
97
+ const forwardSigint = () => {
98
+ const now = Date.now();
99
+ if (now - lastPressAt < SAME_PRESS_MS)
100
+ return;
101
+ lastPressAt = now;
102
+ (0, spawn_without_terminal_1.signalCommandsWithoutTerminal)('SIGINT');
103
+ ctrlC.emit('press');
104
+ };
105
+ const exitOnSignal = (signal) => {
106
+ (0, spawn_without_terminal_1.signalCommandsWithoutTerminal)(signal);
107
+ process.exit((0, exit_codes_1.signalToCode)(signal));
108
+ };
109
+ process.on('SIGINT', forwardSigint);
110
+ process.on('SIGHUP', exitOnSignal);
111
+ process.on('SIGTERM', exitOnSignal);
86
112
  const sentinelWatch = new AbortController();
87
113
  let brokerFailure;
88
114
  // Settles when the poll aborts and the request in flight is answered, or
@@ -133,17 +159,33 @@ async function spawnMasterSession(input) {
133
159
  finally {
134
160
  sentinelWatch.abort();
135
161
  // 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.
162
+ // agent that left it raw would keep a Ctrl+C from reaching nx as a signal.
138
163
  const exited = child.exitCode !== null || child.signalCode !== null;
164
+ let stoppedWaiting = false;
139
165
  if (started && exited) {
140
166
  restoreTerminal();
141
- warnOperationInFlight(broker);
167
+ const inFlight = warnOperationInFlight(broker);
168
+ if (inFlight && (await interruptedTwice(brokerDone, ctrlC))) {
169
+ (0, spawn_without_terminal_1.signalCommandsWithoutTerminal)('SIGKILL');
170
+ if (!(await (0, close_agent_session_1.raceWithTimeout)(brokerDone, OPERATION_KILL_WAIT_MS))) {
171
+ stoppedWaiting = true;
172
+ output_1.output.warn({
173
+ title: `Stopped waiting for ${(0, run_1.treeOperationLabel)(inFlight.request, inFlight.stepName)}.`,
174
+ bodyLines: [
175
+ 'A process it started may still be running. Resume the run once it has exited.',
176
+ ],
177
+ });
178
+ }
179
+ }
142
180
  }
143
- // The request in flight settles before the lock is released.
144
- await brokerDone;
181
+ // The request in flight settles before the lock is released, unless nx
182
+ // stopped waiting on it above.
183
+ if (!stoppedWaiting)
184
+ await brokerDone;
145
185
  broker.close();
146
- process.removeListener('SIGINT', swallowSigint);
186
+ process.removeListener('SIGINT', forwardSigint);
187
+ process.removeListener('SIGHUP', exitOnSignal);
188
+ process.removeListener('SIGTERM', exitOnSignal);
147
189
  if (started && !exited)
148
190
  restoreTerminal();
149
191
  }
@@ -155,15 +197,34 @@ function restoreTerminal() {
155
197
  (0, terminal_repair_1.restoreTermiosAfterAgent)();
156
198
  (0, migrate_output_1.resetSgrAfterAgent)();
157
199
  }
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.
200
+ // With the agent gone nothing else tells the user an operation still runs.
160
201
  function warnOperationInFlight(broker) {
161
- const request = broker.requestInFlight;
162
- if (!request)
163
- return;
202
+ const inFlight = broker.requestInFlight;
203
+ if (!inFlight)
204
+ return null;
164
205
  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.`,
206
+ 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.`,
207
+ });
208
+ return inFlight;
209
+ }
210
+ // The first Ctrl+C reaches the operation as usual; the second ends whatever
211
+ // outlived it.
212
+ async function interruptedTwice(done, ctrlC) {
213
+ let countPress;
214
+ const secondPress = new Promise((resolve) => {
215
+ let presses = 0;
216
+ countPress = () => {
217
+ if (++presses === 2)
218
+ resolve(true);
219
+ };
166
220
  });
221
+ ctrlC.on('press', countPress);
222
+ try {
223
+ return await Promise.race([done.then(() => false), secondPress]);
224
+ }
225
+ finally {
226
+ ctrlC.removeListener('press', countPress);
227
+ }
167
228
  }
168
229
  async function serviceBrokerUntilAborted(broker, intervalMs, signal) {
169
230
  while (!signal.aborted) {
@@ -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');
@@ -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
@@ -20,6 +20,9 @@ const deferred_output_1 = require("./deferred-output");
20
20
  // ignore rule that normally hides it goes missing mid-run (a checkout, a
21
21
  // .gitignore edit, or the migration's own changes).
22
22
  const MIGRATE_COMMIT_EXCLUDES = [types_1.MIGRATE_RUNS_RELATIVE_DIR];
23
+ // Without it the agent continues as the absorb guidance says, and a cause that
24
+ // fails every commit leaves the whole run uncommitted.
25
+ const AGENT_SESSION_COMMIT_FAILURE = "Commits in this session cannot ask for input, such as a signing passphrase or a hook's prompt, so later commits may fail the same way until the cause is fixed. Before the next step, tell the user what failed and ask whether to fix the cause first or to continue as is. Either way nx tries every later commit, and the first one that lands includes these changes.";
23
26
  /**
24
27
  * `pendingMigrations` are listed in the commit body so a `git log -p` reader
25
28
  * can see which earlier migrations' diffs this commit absorbed (their own
@@ -28,10 +31,14 @@ const MIGRATE_COMMIT_EXCLUDES = [types_1.MIGRATE_RUNS_RELATIVE_DIR];
28
31
  * The default `failureGuidance` describes the classic loop's absorb-and-recap
29
32
  * behavior; a caller with no later commit or recap to absorb the diff (the
30
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.
31
37
  */
32
- async function commitMigrationIfRequested(root, migration, shouldCreateCommits, commitPrefix, installDepsIfChanged, pendingMigrations = [], failureGuidance = 'Any uncommitted changes will be included in the next successful commit, which will reference this migration; if they remain uncommitted, the end-of-run output will list this migration so you can commit or revert them manually.', out = deferred_output_1.terminalOutput) {
38
+ async function commitMigrationIfRequested(root, migration, shouldCreateCommits, commitPrefix, installDepsIfChanged, pendingMigrations = [], failureGuidance = 'Any uncommitted changes will be included in the next successful commit, which will reference this migration; if they remain uncommitted, the end-of-run output will list this migration so you can commit or revert them manually.', sink) {
33
39
  if (!shouldCreateCommits)
34
40
  return { status: 'disabled' };
41
+ const out = sink ?? deferred_output_1.terminalOutput;
35
42
  await installDepsIfChanged();
36
43
  // Generator may have only touched gitignored paths or the excluded scratch
37
44
  // dir, or the prompt half made no change: log neutrally, not as an error.
@@ -42,7 +49,7 @@ async function commitMigrationIfRequested(root, migration, shouldCreateCommits,
42
49
  }
43
50
  const commitMessage = buildCommitMessage(`${commitPrefix}${migration.name}`, pendingMigrations);
44
51
  try {
45
- const sha = await (0, git_utils_1.tryCommitChangesAsync)(commitMessage, root, MIGRATE_COMMIT_EXCLUDES);
52
+ const sha = await (0, git_utils_1.tryCommitChangesAsync)(commitMessage, root, MIGRATE_COMMIT_EXCLUDES, { withoutTerminal: sink !== undefined });
46
53
  if (sha)
47
54
  return { status: 'committed', sha };
48
55
  // null = commit landed but `git rev-parse HEAD` failed (see
@@ -52,7 +59,7 @@ async function commitMigrationIfRequested(root, migration, shouldCreateCommits,
52
59
  }
53
60
  catch (err) {
54
61
  const reason = err instanceof Error ? err.message : String(err);
55
- out.line('red', `The commit for ${migration.name} failed:\n${reason}\nCheck \`git status\` and \`git log\`; the commit may have landed despite this error. ${failureGuidance}`);
62
+ out.line('red', `The commit for ${migration.name} failed:\n${reason}\nCheck \`git status\` and \`git log\`; the commit may have landed despite this error. ${failureGuidance}${sink ? ` ${AGENT_SESSION_COMMIT_FAILURE}` : ''}`);
56
63
  return { status: 'failed', reason };
57
64
  }
58
65
  }
@@ -37,6 +37,7 @@ const package_manager_1 = require("../../utils/package-manager");
37
37
  const errors_1 = require("../../utils/min-release-age/errors");
38
38
  const resolve_package_version_1 = require("./resolve-package-version");
39
39
  const handle_errors_1 = require("../../utils/handle-errors");
40
+ const exit_codes_1 = require("../../utils/exit-codes");
40
41
  const connect_to_nx_cloud_1 = require("../nx-cloud/connect/connect-to-nx-cloud");
41
42
  const output_1 = require("../../utils/output");
42
43
  const fs_1 = require("fs");
@@ -68,6 +69,7 @@ Object.defineProperty(exports, "normalizeVersion", { enumerable: true, get: func
68
69
  const execute_migration_1 = require("./execute-migration");
69
70
  const step_actions_1 = require("./step-actions");
70
71
  const sort_migrations_1 = require("./sort-migrations");
72
+ const text_1 = require("./text");
71
73
  const inception_1 = require("./agentic/inception");
72
74
  const version_skew_guard_1 = require("./version-skew-guard");
73
75
  const versions_1 = require("../../utils/versions");
@@ -88,20 +90,27 @@ function formatCommandFailure(command, error) {
88
90
  .trim();
89
91
  return [`Command failed: ${command}`, ...(details ? [details] : [])].join('\n');
90
92
  }
93
+ // Ctrl+C is left to the child. Killed by it, this process would hand the
94
+ // terminal back to the shell while the child still runs.
91
95
  function runOrReturnExitCode(run) {
96
+ const leaveSigintToChild = () => { };
97
+ process.on('SIGINT', leaveSigintToChild);
92
98
  try {
93
99
  run();
94
100
  return 0;
95
101
  }
96
102
  catch (e) {
97
- if (typeof e === 'object' &&
98
- e !== null &&
99
- 'status' in e &&
100
- typeof e.status === 'number') {
103
+ if (typeof e !== 'object' || e === null)
104
+ throw e;
105
+ if ('signal' in e && e.signal === 'SIGINT')
106
+ (0, exit_codes_1.exitAsInterrupted)();
107
+ if ('status' in e && typeof e.status === 'number')
101
108
  return e.status;
102
- }
103
109
  throw e;
104
110
  }
111
+ finally {
112
+ process.removeListener('SIGINT', leaveSigintToChild);
113
+ }
105
114
  }
106
115
  function cleanSemver(version) {
107
116
  return (0, semver_1.clean)(version) ?? (0, semver_1.coerce)(version);
@@ -2161,7 +2170,35 @@ async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, c
2161
2170
  activeRunToReplace(root, opts.runId);
2162
2171
  }
2163
2172
  if (!shouldSkipInstall && !process.env.NX_MIGRATE_SKIP_INSTALL) {
2164
- await (0, execute_migration_1.runInstall)();
2173
+ // The run must resume while the cause of a step's failed install persists.
2174
+ const failedInstallSteps = continued?.steps.filter((s) => s.installFailed === true) ?? [];
2175
+ if (failedInstallSteps.length > 0) {
2176
+ const { pmInstallCommand, stepLabel, TERMINAL_STEP_STATUSES } = require('./run');
2177
+ // A settled step keeps its mark, but no step action can retry it.
2178
+ const settled = failedInstallSteps.filter((s) => TERMINAL_STEP_STATUSES.has(s.status));
2179
+ const retryable = failedInstallSteps.filter((s) => !settled.includes(s));
2180
+ const notCompleted = (steps) => `The dependency install of ${steps
2181
+ .map(stepLabel)
2182
+ .join(', ')} did not complete earlier in this run.`;
2183
+ output_1.output.warn({
2184
+ title: 'Skipping the dependency install',
2185
+ bodyLines: [
2186
+ ...(retryable.length > 0
2187
+ ? [
2188
+ `${notCompleted(retryable)} Retrying that step installs again; any other choice leaves the install to you.`,
2189
+ ]
2190
+ : []),
2191
+ ...(settled.length > 0
2192
+ ? [
2193
+ `${notCompleted(settled)} Run \`${pmInstallCommand(root)}\` once the cause of the failed install is fixed.`,
2194
+ ]
2195
+ : []),
2196
+ ].map(text_1.singleLine),
2197
+ });
2198
+ }
2199
+ else {
2200
+ await (0, execute_migration_1.runInstall)();
2201
+ }
2165
2202
  }
2166
2203
  if (!__dirname.startsWith(workspace_root_1.workspaceRoot)) {
2167
2204
  // we are running from a temp installation with nx latest, switch to running
@@ -16,6 +16,7 @@ export interface AgentMessage {
16
16
  export declare function safeLines(lines: readonly string[]): string[];
17
17
  export declare function logToAgent(message: AgentMessage): void;
18
18
  export declare function warnToAgent(message: AgentMessage): void;
19
+ export declare function logLineToAgent(line: string): void;
19
20
  export declare function emitStepBlock(runId: string, stepId: string, action: string, payload: object): void;
20
21
  export declare function emitPromptBlock(migrationId: string, payload: object): void;
21
22
  export declare function emitStepPromptBlock(stepId: string, payload: object): void;
@@ -13,10 +13,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.safeLines = safeLines;
14
14
  exports.logToAgent = logToAgent;
15
15
  exports.warnToAgent = warnToAgent;
16
+ exports.logLineToAgent = logLineToAgent;
16
17
  exports.emitStepBlock = emitStepBlock;
17
18
  exports.emitPromptBlock = emitPromptBlock;
18
19
  exports.emitStepPromptBlock = emitStepPromptBlock;
19
20
  exports.emitRunbookBlock = emitRunbookBlock;
21
+ const logger_1 = require("../../../utils/logger");
20
22
  const output_1 = require("../../../utils/output");
21
23
  const print_dropped_agent_context_1 = require("../agentic/print-dropped-agent-context");
22
24
  const text_1 = require("../text");
@@ -40,6 +42,9 @@ function logToAgent(message) {
40
42
  function warnToAgent(message) {
41
43
  output_1.output.warn(safe(message));
42
44
  }
45
+ function logLineToAgent(line) {
46
+ logger_1.logger.info((0, text_1.singleLine)(line));
47
+ }
43
48
  /**
44
49
  * Serializes a block payload. `<` becomes its JSON unicode escape so a raw one
45
50
  * in a value cannot forge the closing tag, and the result stays valid JSON for
@@ -91,8 +91,8 @@ export declare function acquireTreeOperation(dir: string, request: TreeOperation
91
91
  export declare function releaseTreeOperation(dir: string, owner: string, markedStepId?: string): void;
92
92
  /** The reservation a live process other than `owner` holds, if any. */
93
93
  export declare function liveTreeOperation(state: MigrateRunState, owner?: string): MigrateTreeOperation | undefined;
94
- export declare function treeBusyMessage(held: MigrateTreeOperation): string;
95
- export declare function treeOperationLabel(held: Pick<MigrateTreeOperation, 'kind' | 'stepId'>): string;
94
+ export declare function treeBusyMessage(held: MigrateTreeOperation, stepName?: string): string;
95
+ export declare function treeOperationLabel(held: Pick<MigrateTreeOperation, 'kind' | 'stepId'>, stepName?: string): string;
96
96
  export declare function brokerDir(runDirPath: string): string;
97
97
  /**
98
98
  * Runs the step's install and commit where they can land: in this process
@@ -119,6 +119,15 @@ export declare function resetStepTree(dir: string, step: MigrateStep, resetInPro
119
119
  * reservation it runs with, so the caller writes no transition of its own.
120
120
  */
121
121
  export declare function giveUpStepTree(root: string, dir: string, step: MigrateStep, skipInstall: boolean, reconcileCommand: string, scope: TreeScope): Promise<GiveUpOutcome>;
122
+ export declare const COMMIT_INSTALLS_FIRST = "A commit first installs any dependency changes.";
123
+ export declare function commitsStep(kind: MigrateTreeOperation['kind']): boolean;
124
+ export declare function sessionOperationWayOut(operation: string): string;
125
+ export declare function formatElapsed(ms: number): string;
126
+ export interface InFlightOperation {
127
+ request: BrokerRequest;
128
+ startedAt: number;
129
+ stepName: string;
130
+ }
122
131
  /**
123
132
  * The parent side. Holds one exclusive lock for the session's lifetime so a
124
133
  * waiting step can tell a slow parent from a dead one, answers each request
@@ -138,13 +147,13 @@ export declare class MigrateCommitBroker {
138
147
  private inFlight;
139
148
  private readonly lock;
140
149
  constructor(root: string, dir: string, reconcileCommand: string, policy: MigrateRunPolicy);
141
- /** The request whose operation this process is running right now. */
142
- get requestInFlight(): BrokerRequest | null;
150
+ /** The request whose operation this process is running right now, and since when. */
151
+ get requestInFlight(): InFlightOperation | null;
143
152
  /** Answers this session's unanswered requests, one at a time. */
144
153
  service(): Promise<void>;
145
154
  private record;
146
155
  private answer;
147
- /** Releases the lock; call after the last `service` settled. */
156
+ /** Releases the lock; call after the last `service` settled, or once giving up on it. */
148
157
  close(): void;
149
158
  private notADirectory;
150
159
  }