nx 23.3.0-beta.2 → 23.3.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/init-local.js +7 -1
- package/dist/src/command-line/add/add.js +5 -1
- package/dist/src/command-line/init/implementation/utils.js +6 -1
- package/dist/src/command-line/migrate/agentic/capture-generator-output.d.ts +25 -0
- package/dist/src/command-line/migrate/agentic/capture-generator-output.js +121 -5
- package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +21 -0
- package/dist/src/command-line/migrate/agentic/close-agent-session.js +126 -0
- package/dist/src/command-line/migrate/agentic/definitions.d.ts +24 -0
- package/dist/src/command-line/migrate/agentic/definitions.js +5 -2
- package/dist/src/command-line/migrate/agentic/handoff.d.ts +23 -0
- package/dist/src/command-line/migrate/agentic/handoff.js +62 -3
- package/dist/src/command-line/migrate/agentic/master/invocations.d.ts +12 -0
- package/dist/src/command-line/migrate/agentic/master/invocations.js +54 -0
- package/dist/src/command-line/migrate/agentic/master/run-master-session.d.ts +12 -0
- package/dist/src/command-line/migrate/agentic/master/run-master-session.js +99 -0
- package/dist/src/command-line/migrate/agentic/master/spawn-master.d.ts +31 -0
- package/dist/src/command-line/migrate/agentic/master/spawn-master.js +217 -0
- package/dist/src/command-line/migrate/agentic/prompts/fragments.js +3 -0
- package/dist/src/command-line/migrate/agentic/runner.d.ts +0 -21
- package/dist/src/command-line/migrate/agentic/runner.js +12 -249
- package/dist/src/command-line/migrate/agentic/terminal-repair.d.ts +1 -0
- package/dist/src/command-line/migrate/agentic/terminal-repair.js +27 -0
- package/dist/src/command-line/migrate/agentic/windows-cmd.d.ts +33 -0
- package/dist/src/command-line/migrate/agentic/windows-cmd.js +88 -0
- package/dist/src/command-line/migrate/deferred-output.d.ts +44 -0
- package/dist/src/command-line/migrate/deferred-output.js +92 -0
- package/dist/src/command-line/migrate/execute-migration.d.ts +9 -3
- package/dist/src/command-line/migrate/execute-migration.js +53 -16
- package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -4
- package/dist/src/command-line/migrate/migrate-commits.js +7 -6
- package/dist/src/command-line/migrate/migrate.js +28 -13
- package/dist/src/command-line/migrate/run/broker.d.ts +138 -0
- package/dist/src/command-line/migrate/run/broker.js +507 -0
- package/dist/src/command-line/migrate/run/clean-retry.d.ts +12 -0
- package/dist/src/command-line/migrate/run/clean-retry.js +98 -0
- package/dist/src/command-line/migrate/run/index.d.ts +4 -3
- package/dist/src/command-line/migrate/run/index.js +7 -1
- package/dist/src/command-line/migrate/run/issues.d.ts +8 -1
- package/dist/src/command-line/migrate/run/issues.js +40 -6
- package/dist/src/command-line/migrate/run/orchestrator.d.ts +18 -1
- package/dist/src/command-line/migrate/run/orchestrator.js +409 -377
- package/dist/src/command-line/migrate/run/run-state.d.ts +16 -0
- package/dist/src/command-line/migrate/run/run-state.js +33 -2
- package/dist/src/command-line/migrate/run/runbook.js +8 -4
- package/dist/src/command-line/migrate/run/state-machine.d.ts +24 -1
- package/dist/src/command-line/migrate/run/state-machine.js +112 -21
- package/dist/src/command-line/migrate/run/util.d.ts +2 -1
- package/dist/src/command-line/migrate/run/util.js +3 -3
- package/dist/src/command-line/migrate/run/worker.js +170 -164
- package/dist/src/core/graph/main.js +1 -1
- package/dist/src/daemon/server/server.js +11 -8
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/tasks-runner/run-command.js +3 -0
- package/dist/src/utils/git-utils.d.ts +12 -0
- package/dist/src/utils/git-utils.js +76 -30
- package/dist/src/utils/package-json.js +8 -0
- 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
|
-
|
|
16
|
-
|
|
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 ${
|
|
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
|
-
|
|
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;
|