nx 23.3.0-beta.2 → 23.3.0-beta.3

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 (58) hide show
  1. package/dist/bin/init-local.js +7 -1
  2. package/dist/src/command-line/add/add.js +5 -1
  3. package/dist/src/command-line/init/implementation/utils.js +6 -1
  4. package/dist/src/command-line/migrate/agentic/capture-generator-output.d.ts +25 -0
  5. package/dist/src/command-line/migrate/agentic/capture-generator-output.js +121 -5
  6. package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +21 -0
  7. package/dist/src/command-line/migrate/agentic/close-agent-session.js +126 -0
  8. package/dist/src/command-line/migrate/agentic/definitions.d.ts +24 -0
  9. package/dist/src/command-line/migrate/agentic/definitions.js +5 -2
  10. package/dist/src/command-line/migrate/agentic/handoff.d.ts +23 -0
  11. package/dist/src/command-line/migrate/agentic/handoff.js +62 -3
  12. package/dist/src/command-line/migrate/agentic/master/invocations.d.ts +12 -0
  13. package/dist/src/command-line/migrate/agentic/master/invocations.js +54 -0
  14. package/dist/src/command-line/migrate/agentic/master/run-master-session.d.ts +12 -0
  15. package/dist/src/command-line/migrate/agentic/master/run-master-session.js +99 -0
  16. package/dist/src/command-line/migrate/agentic/master/spawn-master.d.ts +31 -0
  17. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +217 -0
  18. package/dist/src/command-line/migrate/agentic/prompts/fragments.js +3 -0
  19. package/dist/src/command-line/migrate/agentic/runner.d.ts +0 -21
  20. package/dist/src/command-line/migrate/agentic/runner.js +12 -249
  21. package/dist/src/command-line/migrate/agentic/terminal-repair.d.ts +1 -0
  22. package/dist/src/command-line/migrate/agentic/terminal-repair.js +27 -0
  23. package/dist/src/command-line/migrate/agentic/windows-cmd.d.ts +33 -0
  24. package/dist/src/command-line/migrate/agentic/windows-cmd.js +88 -0
  25. package/dist/src/command-line/migrate/deferred-output.d.ts +44 -0
  26. package/dist/src/command-line/migrate/deferred-output.js +92 -0
  27. package/dist/src/command-line/migrate/execute-migration.d.ts +9 -3
  28. package/dist/src/command-line/migrate/execute-migration.js +53 -16
  29. package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -4
  30. package/dist/src/command-line/migrate/migrate-commits.js +7 -6
  31. package/dist/src/command-line/migrate/migrate.js +28 -13
  32. package/dist/src/command-line/migrate/run/broker.d.ts +138 -0
  33. package/dist/src/command-line/migrate/run/broker.js +507 -0
  34. package/dist/src/command-line/migrate/run/clean-retry.d.ts +12 -0
  35. package/dist/src/command-line/migrate/run/clean-retry.js +98 -0
  36. package/dist/src/command-line/migrate/run/index.d.ts +4 -3
  37. package/dist/src/command-line/migrate/run/index.js +7 -1
  38. package/dist/src/command-line/migrate/run/issues.d.ts +8 -1
  39. package/dist/src/command-line/migrate/run/issues.js +40 -6
  40. package/dist/src/command-line/migrate/run/orchestrator.d.ts +18 -1
  41. package/dist/src/command-line/migrate/run/orchestrator.js +409 -377
  42. package/dist/src/command-line/migrate/run/run-state.d.ts +16 -0
  43. package/dist/src/command-line/migrate/run/run-state.js +33 -2
  44. package/dist/src/command-line/migrate/run/runbook.js +8 -4
  45. package/dist/src/command-line/migrate/run/state-machine.d.ts +24 -1
  46. package/dist/src/command-line/migrate/run/state-machine.js +112 -21
  47. package/dist/src/command-line/migrate/run/util.d.ts +2 -1
  48. package/dist/src/command-line/migrate/run/util.js +3 -3
  49. package/dist/src/command-line/migrate/run/worker.js +170 -164
  50. package/dist/src/core/graph/main.js +1 -1
  51. package/dist/src/daemon/server/server.js +11 -8
  52. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  53. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  54. package/dist/src/tasks-runner/run-command.js +3 -0
  55. package/dist/src/utils/git-utils.d.ts +12 -0
  56. package/dist/src/utils/git-utils.js +76 -30
  57. package/dist/src/utils/package-json.js +8 -0
  58. package/package.json +11 -11
@@ -1,29 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WINDOWS_COMMAND_LINE_BUDGET = void 0;
4
3
  exports.runAgentic = runAgentic;
5
- exports.adaptSpawnForWindowsShim = adaptSpawnForWindowsShim;
6
4
  const tslib_1 = require("tslib");
7
5
  const child_process_1 = require("child_process");
8
- const path_1 = require("path");
9
6
  const pc = tslib_1.__importStar(require("picocolors"));
10
7
  const logger_1 = require("../../../utils/logger");
11
8
  const output_1 = require("../../../utils/output");
12
9
  const migrate_analytics_1 = require("../migrate-analytics");
13
10
  const safe_prompt_1 = require("../safe-prompt");
11
+ const close_agent_session_1 = require("./close-agent-session");
14
12
  const handoff_1 = require("./handoff");
15
- // How long to wait for the agent to exit gracefully after sending SIGINT once
16
- // a valid handoff has been written. Long enough for an interactive agent to
17
- // finish its current render and clean up; short enough that a frozen child
18
- // still gets escalated to SIGKILL in a sensible time.
19
- const AGENT_GRACEFUL_EXIT_MS = 5_000;
13
+ const terminal_repair_1 = require("./terminal-repair");
14
+ const windows_cmd_1 = require("./windows-cmd");
20
15
  /**
21
16
  * Spawns the selected agent with `stdio: 'inherit'`, swallows SIGINT while the
22
17
  * child is alive, waits for it to exit, and resolves the run's outcome from
23
18
  * the handoff file (or the user when the file is missing).
24
19
  */
25
20
  async function runAgentic(args) {
26
- const { detected, definition, invocationContext, handoffFilePath, handoffsDir, handoffPollIntervalMs, gracefulExitMs = AGENT_GRACEFUL_EXIT_MS, forceKillWaitMs = FORCE_KILL_WAIT_MS, } = args;
21
+ const { detected, definition, invocationContext, handoffFilePath, handoffsDir, handoffPollIntervalMs, gracefulExitMs = close_agent_session_1.AGENT_GRACEFUL_EXIT_MS, forceKillWaitMs = close_agent_session_1.FORCE_KILL_WAIT_MS, } = args;
27
22
  const adapted = adaptWithinCommandLineBudget(detected, definition, invocationContext);
28
23
  let child;
29
24
  // Local alias so `@nx/workspace-require-windows-hide` recognizes the
@@ -50,7 +45,7 @@ async function runAgentic(args) {
50
45
  };
51
46
  process.on('SIGINT', swallowSigint);
52
47
  const handoffWatchAbort = new AbortController();
53
- const exitPromise = waitForExit(child);
48
+ const exitPromise = (0, close_agent_session_1.waitForExit)(child);
54
49
  const handoffPromise = (0, handoff_1.waitForValidHandoff)(handoffFilePath, handoffsDir, {
55
50
  signal: handoffWatchAbort.signal,
56
51
  intervalMs: handoffPollIntervalMs,
@@ -68,27 +63,20 @@ async function runAgentic(args) {
68
63
  handoffPromise.then(() => 'handoff', () => 'exit'),
69
64
  ]);
70
65
  if (winner === 'handoff') {
71
- await closeAgentSession(child, exitPromise, gracefulExitMs, forceKillWaitMs);
66
+ await (0, close_agent_session_1.closeAgentSession)(child, exitPromise, gracefulExitMs, forceKillWaitMs);
72
67
  // Extra safety bound so the orchestrator can't hang if the child
73
68
  // stays stuck after SIGKILL / taskkill (e.g. D-state on a hung NFS
74
69
  // read, or taskkill returning before the process actually exits).
75
- await raceWithTimeout(exitPromise, forceKillWaitMs);
70
+ await (0, close_agent_session_1.raceWithTimeout)(exitPromise, forceKillWaitMs);
76
71
  }
77
72
  }
78
73
  finally {
79
74
  handoffWatchAbort.abort();
80
75
  process.removeListener('SIGINT', swallowSigint);
81
- restoreTermiosAfterAgent();
76
+ (0, terminal_repair_1.restoreTermiosAfterAgent)();
82
77
  }
83
78
  return resolveFromHandoffOrPrompt(handoffFilePath, handoffsDir, userInterruptCount > 0, exitInfoToCause(exitInfo));
84
79
  }
85
- // "The maximum length of the string that you can use at the command prompt is
86
- // 8191 characters".
87
- // https://learn.microsoft.com/troubleshoot/windows-client/shell-experience/command-line-string-limitation
88
- const WINDOWS_COMMAND_LINE_LIMIT = 8191;
89
- // Deliberate headroom below the documented limit for argument growth.
90
- const WINDOWS_COMMAND_LINE_RESERVE = 1000;
91
- exports.WINDOWS_COMMAND_LINE_BUDGET = WINDOWS_COMMAND_LINE_LIMIT - WINDOWS_COMMAND_LINE_RESERVE;
92
80
  /**
93
81
  * Builds Windows shim arguments within budget, trying the shorter context
94
82
  * before aborting. Rejects overflow to avoid truncated instructions.
@@ -97,7 +85,7 @@ exports.WINDOWS_COMMAND_LINE_BUDGET = WINDOWS_COMMAND_LINE_LIMIT - WINDOWS_COMMA
97
85
  function adaptWithinCommandLineBudget(detected, definition, invocationContext) {
98
86
  const adapt = (ctx) => {
99
87
  const spec = definition.buildInteractive(ctx);
100
- return adaptSpawnForWindowsShim(detected.binary, spec.args, {
88
+ return (0, windows_cmd_1.adaptSpawnForWindowsShim)(detected.binary, spec.args, {
101
89
  stdio: 'inherit',
102
90
  cwd: spec.cwd ?? ctx.workspaceRoot,
103
91
  env: spec.env ? { ...process.env, ...spec.env } : process.env,
@@ -105,21 +93,21 @@ function adaptWithinCommandLineBudget(detected, definition, invocationContext) {
105
93
  });
106
94
  };
107
95
  const adapted = adapt(invocationContext);
108
- if (withinCommandLineBudget(adapted)) {
96
+ if ((0, windows_cmd_1.withinCommandLineBudget)(adapted)) {
109
97
  return adapted;
110
98
  }
111
99
  const reduced = adapt({
112
100
  ...invocationContext,
113
101
  inlineSystemContext: invocationContext.inlineSystemContextFallback,
114
102
  });
115
- if (withinCommandLineBudget(reduced)) {
103
+ if ((0, windows_cmd_1.withinCommandLineBudget)(reduced)) {
116
104
  logger_1.logger.info(pc.dim(` Passing the agent a reduced system context. The full one does not fit on a Windows command line alongside these paths.`));
117
105
  return reduced;
118
106
  }
119
107
  output_1.output.error({
120
108
  title: `${detected.displayName} cannot be started for this migration step`,
121
109
  bodyLines: [
122
- `Launching it needs a ${reduced.commandLineLength}-character command line. cmd.exe runs at most ${WINDOWS_COMMAND_LINE_LIMIT} characters, and nx stops at ${exports.WINDOWS_COMMAND_LINE_BUDGET} to leave room for what it cannot measure from here.`,
110
+ `Launching it needs a ${reduced.commandLineLength}-character command line. cmd.exe runs at most ${windows_cmd_1.WINDOWS_COMMAND_LINE_LIMIT} characters, and nx stops at ${windows_cmd_1.WINDOWS_COMMAND_LINE_BUDGET} to leave room for what it cannot measure from here.`,
123
111
  `The length is paths: the workspace root, at ${invocationContext.workspaceRoot.length} characters, and the migration's package and name, each repeated across several of them.`,
124
112
  ``,
125
113
  `Re-run with \`--agentic=false\` to apply the remaining migrations yourself. Moving the workspace to a shorter path can help too, but only for agents that put it on the command line.`,
@@ -127,10 +115,6 @@ function adaptWithinCommandLineBudget(detected, definition, invocationContext) {
127
115
  });
128
116
  throw new Error(`Cannot start ${detected.displayName}: the command line exceeds the Windows limit.`);
129
117
  }
130
- function withinCommandLineBudget(adapted) {
131
- return (adapted.commandLineLength === undefined ||
132
- adapted.commandLineLength <= exports.WINDOWS_COMMAND_LINE_BUDGET);
133
- }
134
118
  function exitInfoToCause(info) {
135
119
  const cause = {};
136
120
  // Include `code === 0` so the prompt can distinguish "agent exited cleanly
@@ -148,161 +132,6 @@ function exitInfoToCause(info) {
148
132
  cause.exitError = info.error.message;
149
133
  return cause;
150
134
  }
151
- function restoreTermiosAfterAgent() {
152
- if (process.platform === 'win32')
153
- return;
154
- if (!process.stdin.isTTY)
155
- return;
156
- try {
157
- // `stty sane` resets termios to a known cooked state via the kernel —
158
- // independent of Node's libuv mode tracking (Node's setRawMode(false)
159
- // short-circuits when libuv's per-handle mode is already NORMAL, even
160
- // if the OS-level termios was changed out-of-band by the agent).
161
- (0, child_process_1.execSync)('stty sane < /dev/tty', {
162
- stdio: ['ignore', 'ignore', 'ignore'],
163
- windowsHide: true,
164
- });
165
- // Carriage-return + clear to end of screen, to wipe any agent TUI
166
- // cells below our row that subsequent log lines won't overwrite
167
- // (e.g. a status footer past where our text wraps).
168
- process.stdout.write('\r\x1B[J');
169
- }
170
- catch {
171
- // best-effort — if stty isn't on PATH or /dev/tty isn't accessible,
172
- // the worst case is the pre-existing staircase + cell-bleed output.
173
- }
174
- }
175
- // Safety bound after force-kill. SIGKILL normally reaps in microseconds;
176
- // the bound exists for uninterruptible kernel calls or taskkill returning
177
- // before the process actually exits.
178
- const FORCE_KILL_WAIT_MS = 500;
179
- /**
180
- * Stops the agent process after a successful handoff.
181
- *
182
- * POSIX: SIGINT, then SIGKILL after a bounded wait. SIGTERM is skipped so a
183
- * child that ignored the first graceful signal does not get a second one.
184
- *
185
- * Windows: `taskkill /T /F`. `child.kill` is a `TerminateProcess` call whatever
186
- * the signal name, which on the `cmd.exe` shim path would kill the shim and
187
- * orphan the agent. `taskkill` failures are swallowed.
188
- */
189
- async function closeAgentSession(child, exitPromise, gracefulExitMs, forceKillWaitMs) {
190
- if (child.exitCode !== null || child.signalCode !== null)
191
- return;
192
- if (process.platform === 'win32') {
193
- await forceKillWindowsTree(child, exitPromise, forceKillWaitMs);
194
- return;
195
- }
196
- // POSIX path.
197
- try {
198
- child.kill('SIGINT');
199
- }
200
- catch {
201
- // child already gone between the check above and here
202
- return;
203
- }
204
- let escalation;
205
- try {
206
- await Promise.race([
207
- exitPromise,
208
- new Promise((resolve) => {
209
- escalation = setTimeout(resolve, gracefulExitMs);
210
- }),
211
- ]);
212
- }
213
- finally {
214
- if (escalation)
215
- clearTimeout(escalation);
216
- }
217
- if (child.exitCode !== null || child.signalCode !== null)
218
- return;
219
- // Graceful timeout elapsed without the agent exiting. SIGKILL is
220
- // uncatchable; bound the post-kill wait so a pathological uninterruptible
221
- // syscall can't hang us forever.
222
- try {
223
- child.kill('SIGKILL');
224
- }
225
- catch {
226
- /* child already gone */
227
- }
228
- await raceWithTimeout(exitPromise, forceKillWaitMs);
229
- }
230
- async function forceKillWindowsTree(child, exitPromise, forceKillWaitMs) {
231
- const pid = child.pid;
232
- // `child.pid` is undefined only when spawn itself failed; the early-return
233
- // guard in `closeAgentSession` should short-circuit that path. Reaching
234
- // here without a pid means a narrow race between handoff-detection and
235
- // error-event propagation — without a pid we can't taskkill, so wait
236
- // briefly and return.
237
- if (pid !== undefined) {
238
- try {
239
- (0, child_process_1.execSync)(`taskkill /T /F /PID ${pid}`, {
240
- stdio: 'ignore',
241
- windowsHide: true,
242
- // Bound so a hung Windows shell can't block the orchestrator.
243
- timeout: 2_000,
244
- });
245
- }
246
- catch {
247
- /* taskkill missing, pid already dead, or timed out — fall through */
248
- }
249
- }
250
- await raceWithTimeout(exitPromise, forceKillWaitMs);
251
- }
252
- async function raceWithTimeout(promise, timeoutMs) {
253
- let timer;
254
- try {
255
- await Promise.race([
256
- promise,
257
- new Promise((resolve) => {
258
- timer = setTimeout(resolve, timeoutMs);
259
- }),
260
- ]);
261
- }
262
- finally {
263
- if (timer)
264
- clearTimeout(timer);
265
- }
266
- }
267
- // Merge window so a paired exit + error both land in the same ExitInfo
268
- // (e.g. error from IPC followed by exit when the process actually
269
- // terminates). For error-only paths like spawn ENOENT — where Node fires
270
- // error but never exit — this timer is the SOLE settlement mechanism.
271
- const EXIT_MERGE_WINDOW_MS = 10;
272
- function waitForExit(child) {
273
- return new Promise((resolve) => {
274
- const info = {};
275
- let pending = null;
276
- let settled = false;
277
- const settle = () => {
278
- if (settled)
279
- return;
280
- settled = true;
281
- if (pending)
282
- clearTimeout(pending);
283
- resolve(info);
284
- };
285
- const onFirst = () => {
286
- if (settled || pending)
287
- return;
288
- pending = setTimeout(settle, EXIT_MERGE_WINDOW_MS);
289
- };
290
- child.on('exit', (code, signal) => {
291
- info.code = code;
292
- info.signal = signal;
293
- onFirst();
294
- });
295
- // `error` fires when spawn itself fails (e.g. binary disappeared between
296
- // detection and run) OR alongside `exit` when the process started but
297
- // emitted an error event later. Treat both as exit with no handoff so
298
- // the ambiguous flow kicks in; field-merge so we don't drop the loser's
299
- // contribution when both fire.
300
- child.on('error', (error) => {
301
- info.error = error;
302
- onFirst();
303
- });
304
- });
305
- }
306
135
  async function resolveFromHandoffOrPrompt(handoffFilePath, handoffsDir, userInterrupted = false, cause = {}) {
307
136
  const read = (0, handoff_1.readHandoffWithReason)(handoffFilePath, handoffsDir);
308
137
  if (read.ok === true) {
@@ -346,72 +175,6 @@ async function resolveFromHandoffOrPrompt(handoffFilePath, handoffsDir, userInte
346
175
  }
347
176
  return promptAmbiguous(fullCause);
348
177
  }
349
- /**
350
- * Node's `spawn` cannot directly execute `.cmd` / `.bat` shims on Windows;
351
- * `which` resolves to those when an agent was installed via npm. Wrap them in
352
- * a `cmd.exe /c` invocation with `windowsVerbatimArguments` so quoting follows
353
- * the cmd.exe convention rather than Node's default cooking.
354
- *
355
- * On non-Windows or for non-shim binaries this is a passthrough.
356
- */
357
- function adaptSpawnForWindowsShim(binary, args, options) {
358
- if (process.platform !== 'win32') {
359
- return { binary, args: [...args], options };
360
- }
361
- const ext = (0, path_1.extname)(binary).toLowerCase();
362
- if (ext !== '.cmd' && ext !== '.bat') {
363
- return { binary, args: [...args], options };
364
- }
365
- assertNoLineBreaks(binary, args);
366
- const cmdLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArg)].join(' ');
367
- const comspec = process.env.comspec || 'cmd.exe';
368
- // Both modes are set rather than inherited, since a machine-wide registry
369
- // setting can flip either: `/e:on` for the `%cd:~,%` substring, `/v:off` so a
370
- // `!` stays literal. The outer quotes stop `cmd.exe /c` stripping the inner
371
- // ones around the binary path.
372
- const cmdArgs = ['/e:on', '/v:off', '/d', '/s', '/c', `"${cmdLine}"`];
373
- return {
374
- binary: comspec,
375
- args: cmdArgs,
376
- options: { ...options, windowsVerbatimArguments: true },
377
- // `windowsVerbatimArguments` makes the command line the argv joined by
378
- // single spaces, so this is what CreateProcess and then cmd.exe see.
379
- commandLineLength: [comspec, ...cmdArgs].join(' ').length,
380
- };
381
- }
382
- /** Rejects line breaks in shim commands to avoid truncated instructions. */
383
- function assertNoLineBreaks(binary, args) {
384
- const offending = [binary, ...args].find((value) => /[\r\n]/.test(value));
385
- if (offending !== undefined) {
386
- throw new Error(`Cannot pass a multi-line argument to "${binary}" on Windows: cmd.exe truncates the command line at the line break. Offending argument: ${JSON.stringify(offending.slice(0, 120))}`);
387
- }
388
- }
389
- const CMD_META_CHARS = /([()\][!^"`<>&|;, ])/g;
390
- function escapeCmdArg(arg) {
391
- return neutralizePercent(caretEscape(quoteCmdArg(arg)));
392
- }
393
- function escapeCmdCommand(arg) {
394
- // cmd.exe interprets the command portion through an extra parsing pass;
395
- // apply the caret-escape twice so the .cmd shim sees the original.
396
- return neutralizePercent(caretEscape(caretEscape(quoteCmdArg(arg))));
397
- }
398
- // Double backslashes before quotes for the MS C runtime argv parser.
399
- function quoteCmdArg(arg) {
400
- return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, '$1$1')}"`;
401
- }
402
- function caretEscape(quoted) {
403
- return quoted.replace(CMD_META_CHARS, '^$1');
404
- }
405
- /**
406
- * Stops cmd.exe expanding `%VAR%` inside an argument. A caret cannot escape
407
- * `%`, since expansion runs before caret processing, so each `%` becomes
408
- * `%%cd:~,%`: `%cd:~,%` is a zero-length substring of a built-in and expands to
409
- * nothing, leaving the leading `%`. Runs after the caret passes so the `,` it
410
- * introduces stays uncareted, which the substring syntax requires.
411
- */
412
- function neutralizePercent(escaped) {
413
- return escaped.replace(/%/g, '%%cd:~,%');
414
- }
415
178
  async function promptAmbiguous(cause) {
416
179
  // stderr blank line so the spacer lands in the same stream as output.warn
417
180
  // and the prompt — buffered stdout could otherwise reorder it and glue the
@@ -0,0 +1 @@
1
+ export declare function restoreTermiosAfterAgent(): void;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.restoreTermiosAfterAgent = restoreTermiosAfterAgent;
4
+ const child_process_1 = require("child_process");
5
+ function restoreTermiosAfterAgent() {
6
+ if (process.platform === 'win32')
7
+ return;
8
+ if (!process.stdin.isTTY)
9
+ return;
10
+ try {
11
+ // Not setRawMode(false): it short-circuits when libuv's per-handle mode
12
+ // is already NORMAL, even after the agent changed termios out-of-band.
13
+ // `stty sane` goes through the kernel instead.
14
+ (0, child_process_1.execSync)('stty sane < /dev/tty', {
15
+ stdio: ['ignore', 'ignore', 'ignore'],
16
+ windowsHide: true,
17
+ });
18
+ // Carriage-return + clear to end of screen, to wipe any agent TUI
19
+ // cells below our row that subsequent log lines won't overwrite
20
+ // (e.g. a status footer past where our text wraps).
21
+ process.stdout.write('\r\x1B[J');
22
+ }
23
+ catch {
24
+ // Best-effort: if stty isn't on PATH or /dev/tty isn't accessible, the
25
+ // worst case is the pre-existing staircase + cell-bleed output.
26
+ }
27
+ }
@@ -0,0 +1,33 @@
1
+ import type { SpawnOptions } from 'child_process';
2
+ export declare function quoteCmdArg(arg: string): string;
3
+ export declare function caretEscape(quoted: string): string;
4
+ /**
5
+ * Stops cmd.exe expanding `%VAR%` inside an argument. A caret cannot escape
6
+ * `%`, since expansion runs before caret processing, so each `%` becomes
7
+ * `%%cd:~,%`: `%cd:~,%` is a zero-length substring of a built-in and expands to
8
+ * nothing, leaving the leading `%`. Runs after the caret passes so the `,` it
9
+ * introduces stays uncareted, which the substring syntax requires.
10
+ */
11
+ export declare function neutralizePercent(escaped: string): string;
12
+ export declare const WINDOWS_COMMAND_LINE_LIMIT = 8191;
13
+ export declare const WINDOWS_COMMAND_LINE_BUDGET: number;
14
+ export interface AdaptedSpawn {
15
+ binary: string;
16
+ args: string[];
17
+ options: SpawnOptions;
18
+ /**
19
+ * Length of the command line Windows will receive. Set only on the `cmd.exe`
20
+ * wrapper path.
21
+ */
22
+ commandLineLength?: number;
23
+ }
24
+ export declare function withinCommandLineBudget(adapted: AdaptedSpawn): boolean;
25
+ /**
26
+ * Node's `spawn` cannot directly execute `.cmd` / `.bat` shims on Windows;
27
+ * `which` resolves to those when an agent was installed via npm. Wrap them in
28
+ * a `cmd.exe /c` invocation with `windowsVerbatimArguments` so quoting follows
29
+ * the cmd.exe convention rather than Node's default cooking.
30
+ *
31
+ * On non-Windows or for non-shim binaries this is a passthrough.
32
+ */
33
+ export declare function adaptSpawnForWindowsShim(binary: string, args: readonly string[], options: SpawnOptions): AdaptedSpawn;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WINDOWS_COMMAND_LINE_BUDGET = exports.WINDOWS_COMMAND_LINE_LIMIT = void 0;
4
+ exports.quoteCmdArg = quoteCmdArg;
5
+ exports.caretEscape = caretEscape;
6
+ exports.neutralizePercent = neutralizePercent;
7
+ exports.withinCommandLineBudget = withinCommandLineBudget;
8
+ exports.adaptSpawnForWindowsShim = adaptSpawnForWindowsShim;
9
+ const path_1 = require("path");
10
+ // Backslash-escape embedded quotes per MS C runtime convention, then wrap in
11
+ // quotes. Callers caret-escape cmd.exe metacharacters on top.
12
+ function quoteCmdArg(arg) {
13
+ return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, '$1$1')}"`;
14
+ }
15
+ const CMD_META_CHARS = /([()\][!^"`<>&|;, ])/g;
16
+ function caretEscape(quoted) {
17
+ return quoted.replace(CMD_META_CHARS, '^$1');
18
+ }
19
+ /**
20
+ * Stops cmd.exe expanding `%VAR%` inside an argument. A caret cannot escape
21
+ * `%`, since expansion runs before caret processing, so each `%` becomes
22
+ * `%%cd:~,%`: `%cd:~,%` is a zero-length substring of a built-in and expands to
23
+ * nothing, leaving the leading `%`. Runs after the caret passes so the `,` it
24
+ * introduces stays uncareted, which the substring syntax requires.
25
+ */
26
+ function neutralizePercent(escaped) {
27
+ return escaped.replace(/%/g, '%%cd:~,%');
28
+ }
29
+ // "The maximum length of the string that you can use at the command prompt is
30
+ // 8191 characters".
31
+ // https://learn.microsoft.com/troubleshoot/windows-client/shell-experience/command-line-string-limitation
32
+ exports.WINDOWS_COMMAND_LINE_LIMIT = 8191;
33
+ // Absorbs cmd.exe's own accounting of the string, which cannot be measured
34
+ // from here.
35
+ const WINDOWS_COMMAND_LINE_RESERVE = 1000;
36
+ exports.WINDOWS_COMMAND_LINE_BUDGET = exports.WINDOWS_COMMAND_LINE_LIMIT - WINDOWS_COMMAND_LINE_RESERVE;
37
+ function withinCommandLineBudget(adapted) {
38
+ return (adapted.commandLineLength === undefined ||
39
+ adapted.commandLineLength <= exports.WINDOWS_COMMAND_LINE_BUDGET);
40
+ }
41
+ /**
42
+ * Node's `spawn` cannot directly execute `.cmd` / `.bat` shims on Windows;
43
+ * `which` resolves to those when an agent was installed via npm. Wrap them in
44
+ * a `cmd.exe /c` invocation with `windowsVerbatimArguments` so quoting follows
45
+ * the cmd.exe convention rather than Node's default cooking.
46
+ *
47
+ * On non-Windows or for non-shim binaries this is a passthrough.
48
+ */
49
+ function adaptSpawnForWindowsShim(binary, args, options) {
50
+ if (process.platform !== 'win32') {
51
+ return { binary, args: [...args], options };
52
+ }
53
+ const ext = (0, path_1.extname)(binary).toLowerCase();
54
+ if (ext !== '.cmd' && ext !== '.bat') {
55
+ return { binary, args: [...args], options };
56
+ }
57
+ assertNoLineBreaks(binary, args);
58
+ const cmdLine = [escapeCmdCommand(binary), ...args.map(escapeCmdArg)].join(' ');
59
+ const comspec = process.env.comspec || 'cmd.exe';
60
+ // Both modes are set rather than inherited, since a machine-wide registry
61
+ // setting can flip either: `/e:on` for the `%cd:~,%` substring, `/v:off` so a
62
+ // `!` stays literal. The outer quotes stop `cmd.exe /c` stripping the inner
63
+ // ones around the binary path.
64
+ const cmdArgs = ['/e:on', '/v:off', '/d', '/s', '/c', `"${cmdLine}"`];
65
+ return {
66
+ binary: comspec,
67
+ args: cmdArgs,
68
+ options: { ...options, windowsVerbatimArguments: true },
69
+ // `windowsVerbatimArguments` makes the command line the argv joined by
70
+ // single spaces, so this is what CreateProcess and then cmd.exe see.
71
+ commandLineLength: [comspec, ...cmdArgs].join(' ').length,
72
+ };
73
+ }
74
+ // No escaping reproduces a line break on the other side, and cmd.exe truncates
75
+ // the command line at it, so the call is refused.
76
+ function assertNoLineBreaks(binary, args) {
77
+ const offending = [binary, ...args].find((value) => /[\r\n]/.test(value));
78
+ if (offending !== undefined) {
79
+ throw new Error(`Cannot pass a multi-line argument to "${binary}" on Windows: cmd.exe truncates the command line at the line break. Offending argument: ${JSON.stringify(offending.slice(0, 120))}`);
80
+ }
81
+ }
82
+ function escapeCmdArg(arg) {
83
+ return neutralizePercent(caretEscape(quoteCmdArg(arg)));
84
+ }
85
+ // cmd.exe parses the command portion twice, so it is caret-escaped twice.
86
+ function escapeCmdCommand(arg) {
87
+ return neutralizePercent(caretEscape(caretEscape(quoteCmdArg(arg))));
88
+ }
@@ -0,0 +1,44 @@
1
+ export type NoticeLevel = 'log' | 'warn' | 'error';
2
+ export type LineColor = 'dim' | 'yellow' | 'red';
3
+ export interface MigrateOutputSink {
4
+ notice(level: NoticeLevel, message: {
5
+ title: string;
6
+ bodyLines?: string[];
7
+ }): void;
8
+ line(color: LineColor, text: string): void;
9
+ /** A chunk of a package manager's own output. */
10
+ raw(chunk: string): void;
11
+ }
12
+ export declare const terminalOutput: MigrateOutputSink;
13
+ export type DeferredOutputRecord = {
14
+ kind: 'notice';
15
+ level: NoticeLevel;
16
+ title: string;
17
+ bodyLines?: string[];
18
+ } | {
19
+ kind: 'line';
20
+ color: LineColor;
21
+ text: string;
22
+ } | {
23
+ kind: 'raw';
24
+ text: string;
25
+ };
26
+ /**
27
+ * Collects the output as records that `replayDeferredOutput` prints later.
28
+ * The notices and lines are always kept; package manager output is bounded
29
+ * like generator output and rendered only on request. Render once the output
30
+ * is complete: a rendered collector accepts nothing more.
31
+ */
32
+ export declare class DeferredOutputCollector implements MigrateOutputSink {
33
+ private readonly records;
34
+ private rendered;
35
+ private assertOpen;
36
+ notice(level: NoticeLevel, message: {
37
+ title: string;
38
+ bodyLines?: string[];
39
+ }): void;
40
+ line(color: LineColor, text: string): void;
41
+ raw(chunk: string): void;
42
+ render(rawOutput: 'keep' | 'drop'): DeferredOutputRecord[];
43
+ }
44
+ export declare function replayDeferredOutput(records: DeferredOutputRecord[], to?: MigrateOutputSink): void;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeferredOutputCollector = exports.terminalOutput = void 0;
4
+ exports.replayDeferredOutput = replayDeferredOutput;
5
+ const tslib_1 = require("tslib");
6
+ const pc = tslib_1.__importStar(require("picocolors"));
7
+ const logger_1 = require("../../utils/logger");
8
+ const output_1 = require("../../utils/output");
9
+ exports.terminalOutput = {
10
+ notice: (level, message) => output_1.output[level](message),
11
+ line: (color, text) => logger_1.logger.info(pc[color](text)),
12
+ raw: (chunk) => process.stdout.write(chunk),
13
+ };
14
+ /**
15
+ * Collects the output as records that `replayDeferredOutput` prints later.
16
+ * The notices and lines are always kept; package manager output is bounded
17
+ * like generator output and rendered only on request. Render once the output
18
+ * is complete: a rendered collector accepts nothing more.
19
+ */
20
+ class DeferredOutputCollector {
21
+ constructor() {
22
+ this.records = [];
23
+ this.rendered = false;
24
+ }
25
+ assertOpen() {
26
+ if (this.rendered) {
27
+ throw new Error('The deferred output was already rendered.');
28
+ }
29
+ }
30
+ notice(level, message) {
31
+ this.assertOpen();
32
+ this.records.push({ kind: 'notice', level, ...message });
33
+ }
34
+ line(color, text) {
35
+ this.assertOpen();
36
+ this.records.push({ kind: 'line', color, text });
37
+ }
38
+ raw(chunk) {
39
+ this.assertOpen();
40
+ let block = this.records[this.records.length - 1];
41
+ if (block?.kind !== 'raw-block') {
42
+ // Lazy: the non-agentic migrate path must not load the agentic chain.
43
+ const { BoundedChunks } = require('./agentic/capture-generator-output');
44
+ block = { kind: 'raw-block', buffer: new BoundedChunks() };
45
+ this.records.push(block);
46
+ }
47
+ block.buffer.append(chunk);
48
+ }
49
+ render(rawOutput) {
50
+ this.rendered = true;
51
+ const records = [];
52
+ for (const record of this.records) {
53
+ if (record.kind !== 'raw-block') {
54
+ records.push(record);
55
+ continue;
56
+ }
57
+ if (rawOutput === 'drop')
58
+ continue;
59
+ // The replay adds the final newline back.
60
+ const text = record.buffer.render();
61
+ records.push({
62
+ kind: 'raw',
63
+ text: text.endsWith('\n') ? text.slice(0, -1) : text,
64
+ });
65
+ }
66
+ return records;
67
+ }
68
+ }
69
+ exports.DeferredOutputCollector = DeferredOutputCollector;
70
+ function replayDeferredOutput(records, to = exports.terminalOutput) {
71
+ for (const record of records) {
72
+ switch (record.kind) {
73
+ case 'notice':
74
+ to.notice(record.level, {
75
+ title: record.title,
76
+ bodyLines: record.bodyLines,
77
+ });
78
+ break;
79
+ case 'line':
80
+ to.line(record.color, record.text);
81
+ break;
82
+ case 'raw':
83
+ if (record.text)
84
+ to.raw(`${record.text}\n`);
85
+ break;
86
+ default: {
87
+ const exhaustive = record;
88
+ throw new Error(`Unhandled output record: ${JSON.stringify(exhaustive)}`);
89
+ }
90
+ }
91
+ }
92
+ }
@@ -1,12 +1,18 @@
1
1
  import { MigrationsJson } from '../../config/misc-interfaces';
2
2
  import { FileChange } from '../../generators/tree';
3
3
  import { ArrayPackageGroup, NxMigrationsConfiguration, PackageJson } from '../../utils/package-json';
4
+ import { type MigrateOutputSink } from './deferred-output';
4
5
  interface PackageMigrationConfig extends NxMigrationsConfiguration {
5
6
  packageJson: PackageJson;
6
7
  packageGroup: ArrayPackageGroup;
7
8
  }
8
9
  export declare function readPackageMigrationConfig(packageName: string, dir: string): PackageMigrationConfig;
9
- export declare function runInstall(nxWorkspaceRoot?: string, phase?: MigrationInstallPhase, rerunCommand?: string): Promise<void>;
10
+ /**
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.
14
+ */
15
+ export declare function runInstall(nxWorkspaceRoot?: string, phase?: MigrationInstallPhase, rerunCommand?: string, sink?: MigrateOutputSink): Promise<void>;
10
16
  export type MigrationInstallPhase = 'pre-migration' | 'post-migration';
11
17
  export declare class NpmPeerDepsInstallError extends Error {
12
18
  constructor();
@@ -20,8 +26,8 @@ export declare class NpmPeerDepsInstallError extends Error {
20
26
  */
21
27
  export declare function isNpmPeerDepsError(stderr: string): boolean;
22
28
  export declare function formatSingleMigrationRerunCommand(migrationId: string): string;
23
- export declare function logNpmPeerDepsError(phase: MigrationInstallPhase, rerunCommand?: string): void;
24
- export declare function logSkippedPostMigrationInstall(root: string): void;
29
+ export declare function logNpmPeerDepsError(phase: MigrationInstallPhase, rerunCommand?: string, out?: MigrateOutputSink): void;
30
+ export declare function logSkippedPostMigrationInstall(root: string, out?: MigrateOutputSink): void;
25
31
  export declare class ChangedDepInstaller {
26
32
  private readonly root;
27
33
  private readonly shouldSkipInstall;