nx 23.0.0-beta.19 → 23.0.0-beta.20

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 (62) hide show
  1. package/dist/src/adapter/ngcli-adapter.d.ts +2 -1
  2. package/dist/src/adapter/ngcli-adapter.js +25 -2
  3. package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.js +1 -0
  4. package/dist/src/command-line/init/implementation/utils.js +7 -0
  5. package/dist/src/command-line/migrate/agentic/capture-generator-output.d.ts +22 -0
  6. package/dist/src/command-line/migrate/agentic/capture-generator-output.js +100 -0
  7. package/dist/src/command-line/migrate/agentic/cli-args.d.ts +12 -0
  8. package/dist/src/command-line/migrate/agentic/cli-args.js +38 -0
  9. package/dist/src/command-line/migrate/agentic/definitions.d.ts +6 -0
  10. package/dist/src/command-line/migrate/agentic/definitions.js +98 -0
  11. package/dist/src/command-line/migrate/agentic/detect-installed.d.ts +10 -0
  12. package/dist/src/command-line/migrate/agentic/detect-installed.js +68 -0
  13. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +46 -0
  14. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +87 -0
  15. package/dist/src/command-line/migrate/agentic/handoff.d.ts +63 -0
  16. package/dist/src/command-line/migrate/agentic/handoff.js +183 -0
  17. package/dist/src/command-line/migrate/agentic/inception.d.ts +9 -0
  18. package/dist/src/command-line/migrate/agentic/inception.js +15 -0
  19. package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.d.ts +22 -0
  20. package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.js +50 -0
  21. package/dist/src/command-line/migrate/agentic/prompts/generic-validation.d.ts +51 -0
  22. package/dist/src/command-line/migrate/agentic/prompts/generic-validation.js +73 -0
  23. package/dist/src/command-line/migrate/agentic/prompts/hybrid-prompt-migration.d.ts +44 -0
  24. package/dist/src/command-line/migrate/agentic/prompts/hybrid-prompt-migration.js +60 -0
  25. package/dist/src/command-line/migrate/agentic/prompts/prompt-migration.d.ts +21 -0
  26. package/dist/src/command-line/migrate/agentic/prompts/prompt-migration.js +29 -0
  27. package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.d.ts +9 -0
  28. package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.js +87 -0
  29. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.d.ts +46 -0
  30. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.js +88 -0
  31. package/dist/src/command-line/migrate/agentic/run-step.d.ts +51 -0
  32. package/dist/src/command-line/migrate/agentic/run-step.js +121 -0
  33. package/dist/src/command-line/migrate/agentic/runner.d.ts +33 -0
  34. package/dist/src/command-line/migrate/agentic/runner.js +439 -0
  35. package/dist/src/command-line/migrate/agentic/select.d.ts +14 -0
  36. package/dist/src/command-line/migrate/agentic/select.js +150 -0
  37. package/dist/src/command-line/migrate/agentic/types.d.ts +102 -0
  38. package/dist/src/command-line/migrate/agentic/types.js +2 -0
  39. package/dist/src/command-line/migrate/command-object.d.ts +1 -0
  40. package/dist/src/command-line/migrate/command-object.js +21 -6
  41. package/dist/src/command-line/migrate/migrate-commits.d.ts +50 -0
  42. package/dist/src/command-line/migrate/migrate-commits.js +102 -0
  43. package/dist/src/command-line/migrate/migrate-output.d.ts +121 -0
  44. package/dist/src/command-line/migrate/migrate-output.js +241 -0
  45. package/dist/src/command-line/migrate/migrate.d.ts +73 -9
  46. package/dist/src/command-line/migrate/migrate.js +577 -95
  47. package/dist/src/command-line/migrate/migration-shape.d.ts +8 -0
  48. package/dist/src/command-line/migrate/migration-shape.js +13 -0
  49. package/dist/src/command-line/migrate/multi-major.js +2 -2
  50. package/dist/src/command-line/migrate/run-migration-process.js +20 -6
  51. package/dist/src/command-line/migrate/safe-prompt.d.ts +28 -0
  52. package/dist/src/command-line/migrate/safe-prompt.js +49 -0
  53. package/dist/src/config/misc-interfaces.d.ts +16 -1
  54. package/dist/src/devkit-exports.d.ts +1 -1
  55. package/dist/src/migrations/update-23-0-0/add-migrate-runs-to-git-ignore.d.ts +2 -0
  56. package/dist/src/migrations/update-23-0-0/add-migrate-runs-to-git-ignore.js +16 -0
  57. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  58. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  59. package/dist/src/utils/git-utils.d.ts +14 -0
  60. package/dist/src/utils/git-utils.js +73 -0
  61. package/migrations.json +5 -0
  62. package/package.json +15 -13
@@ -3,7 +3,7 @@ import { FileBuffer } from '@angular-devkit/core/src/virtual-fs/host/interface';
3
3
  import { Observable } from 'rxjs';
4
4
  import type { GenerateOptions } from '../command-line/generate/generate';
5
5
  import { ProjectConfiguration } from '../config/workspace-json-project-json';
6
- import { Tree } from '../generators/tree';
6
+ import { FileChange, Tree } from '../generators/tree';
7
7
  import type { ProjectGraph } from '../config/project-graph';
8
8
  import { ExecutorContext, GeneratorCallback } from '../config/misc-interfaces';
9
9
  export declare function createBuilderContext(builderInfo: {
@@ -59,6 +59,7 @@ export declare class NxScopeHostUsedForWrappedSchematics extends NxScopedHost {
59
59
  export declare function generate(root: string, opts: GenerateOptions, projects: Record<string, ProjectConfiguration>, verbose: boolean, projectGraph: ProjectGraph): Promise<number>;
60
60
  export declare function runMigration(root: string, packageName: string, migrationName: string, projects: Record<string, ProjectConfiguration>, isVerbose: boolean, projectGraph: ProjectGraph): Promise<{
61
61
  loggingQueue: string[];
62
+ changes: FileChange[];
62
63
  madeChanges: boolean;
63
64
  }>;
64
65
  /**
@@ -239,20 +239,38 @@ async function createRecorder(host, record, logger) {
239
239
  }
240
240
  else if (event.kind === 'update') {
241
241
  record.loggingQueue.push(core_1.tags.oneLine `${pc.white('UPDATE')} ${eventPath}`);
242
+ record.changes?.push(emptyFileChange('UPDATE', eventPath));
242
243
  }
243
244
  else if (event.kind === 'create') {
244
245
  record.loggingQueue.push(core_1.tags.oneLine `${pc.green('CREATE')} ${eventPath}`);
246
+ record.changes?.push(emptyFileChange('CREATE', eventPath));
245
247
  }
246
248
  else if (event.kind === 'delete') {
247
249
  record.loggingQueue.push(`${pc.yellow('DELETE')} ${eventPath}`);
250
+ record.changes?.push({ type: 'DELETE', path: eventPath, content: null });
248
251
  }
249
252
  else if (event.kind === 'rename') {
250
253
  record.loggingQueue.push(`${pc.blue('RENAME')} ${eventPath} => ${event.to}`);
254
+ // Surface as DELETE source + CREATE destination so downstream consumers
255
+ // (e.g. the agentic validation prompt's `<files_changed>` block) see
256
+ // both endpoints.
257
+ const toPath = event.to.startsWith('/') ? event.to.slice(1) : event.to;
258
+ record.changes?.push({ type: 'DELETE', path: eventPath, content: null });
259
+ record.changes?.push(emptyFileChange('CREATE', toPath));
251
260
  }
252
261
  };
253
262
  }
263
+ // Empty content for non-DELETE FileChange entries: the Angular workflow has
264
+ // already flushed bytes to disk, and our only downstream consumer (agentic
265
+ // prompt builders) reads `path` + `type` only. `null` is reserved for DELETE.
266
+ function emptyFileChange(type, path) {
267
+ return { type, path, content: Buffer.alloc(0) };
268
+ }
254
269
  async function runSchematic(host, root, workflow, logger, opts, schematic, printDryRunMessage = true, recorder = null) {
255
- const record = { loggingQueue: [], error: false };
270
+ const record = {
271
+ loggingQueue: [],
272
+ error: false,
273
+ };
256
274
  workflow.reporter.subscribe(recorder || (await createRecorder(host, record, logger)));
257
275
  try {
258
276
  await workflow
@@ -643,7 +661,11 @@ async function runMigration(root, packageName, migrationName, projects, isVerbos
643
661
  const fsHost = new NxScopeHostUsedForWrappedSchematics(root, new tree_1.FsTree(root, isVerbose, `ng-cli migration: ${packageName}:${migrationName}`), projectGraph);
644
662
  const workflow = createWorkflow(fsHost, root, {}, projects);
645
663
  const collection = resolveMigrationsCollection(packageName);
646
- const record = { loggingQueue: [], error: false };
664
+ const record = {
665
+ loggingQueue: [],
666
+ changes: [],
667
+ error: false,
668
+ };
647
669
  workflow.reporter.subscribe(await createRecorder(fsHost, record, logger));
648
670
  await workflow
649
671
  .execute({
@@ -656,6 +678,7 @@ async function runMigration(root, packageName, migrationName, projects, isVerbos
656
678
  .toPromise();
657
679
  return {
658
680
  loggingQueue: record.loggingQueue,
681
+ changes: record.changes,
659
682
  madeChanges: record.loggingQueue.length > 0,
660
683
  };
661
684
  }
@@ -102,6 +102,7 @@ function updateGitIgnore(host) {
102
102
  '.nx/cache',
103
103
  '.nx/workspace-data',
104
104
  '.nx/self-healing',
105
+ '.nx/migrate-runs',
105
106
  ].forEach((file) => {
106
107
  if (!contents.includes(file)) {
107
108
  contents = [contents, file].join('\n');
@@ -217,6 +217,13 @@ function updateGitIgnore(root) {
217
217
  }
218
218
  lines.push('.nx/workspace-data');
219
219
  }
220
+ if (!contents.includes('.nx/migrate-runs')) {
221
+ if (!sepIncluded) {
222
+ lines.push('\n');
223
+ sepIncluded = true;
224
+ }
225
+ lines.push('.nx/migrate-runs');
226
+ }
220
227
  (0, fs_1.writeFileSync)(ignorePath, lines.join('\n'), 'utf-8');
221
228
  }
222
229
  catch { }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Tees `console.{log,warn,error,info,debug}` into an internal buffer while
3
+ * preserving the original behavior. Does not intercept
4
+ * `process.{stdout,stderr}.write` — those bypass `console` and would also
5
+ * pick up unrelated framework output. Restoration is idempotent.
6
+ */
7
+ export interface GeneratorOutputCapture {
8
+ flush(): string;
9
+ restore(): void;
10
+ }
11
+ export declare function installGeneratorOutputCapture(): GeneratorOutputCapture;
12
+ /**
13
+ * Convenience wrapper that installs the capture, runs `fn`, restores on
14
+ * completion or throw, and returns the captured logs alongside `fn`'s value.
15
+ * Throws from `fn` propagate with the captured logs attached as
16
+ * `(err as any).capturedLogs` — the most useful diagnostic when a generator
17
+ * crashes mid-output.
18
+ */
19
+ export declare function withGeneratorOutputCapture<T>(fn: () => Promise<T> | T): Promise<{
20
+ result: T;
21
+ logs: string;
22
+ }>;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installGeneratorOutputCapture = installGeneratorOutputCapture;
4
+ exports.withGeneratorOutputCapture = withGeneratorOutputCapture;
5
+ const node_util_1 = require("node:util");
6
+ const logger_1 = require("../../../utils/logger");
7
+ const CONSOLE_METHODS = [
8
+ 'log',
9
+ 'warn',
10
+ 'error',
11
+ 'info',
12
+ 'debug',
13
+ ];
14
+ // Marks `console[method]` as a wrapper installed by this module. Seeing it on
15
+ // entry means the previous install never restored — refuse rather than layer,
16
+ // otherwise the leak compounds silently into a wrapper-wrapping-a-wrapper.
17
+ const CAPTURED_MARKER = Symbol.for('nx-migrate.generator-output-captured');
18
+ const NOOP_CAPTURE = {
19
+ flush: () => '',
20
+ restore: () => { },
21
+ };
22
+ function installGeneratorOutputCapture() {
23
+ // Refuse to layer if the previous install never restored. Returns a noop
24
+ // handle so callers' `flush()` / `restore()` calls remain safe.
25
+ for (const method of CONSOLE_METHODS) {
26
+ if (console[method][CAPTURED_MARKER]) {
27
+ logger_1.logger.verbose(`nx migrate: refusing to layer a second generator-output capture; the previous one was not restored. This typically means a caller skipped its \`try/finally\`. The inner caller's \`flush()\` will return empty, but its console output is still being captured by the outer install.`);
28
+ return NOOP_CAPTURE;
29
+ }
30
+ }
31
+ const buffer = [];
32
+ const originals = new Map();
33
+ for (const method of CONSOLE_METHODS) {
34
+ originals.set(method, console[method]);
35
+ const original = console[method].bind(console);
36
+ const wrapper = ((...args) => {
37
+ original(...args);
38
+ try {
39
+ buffer.push((0, node_util_1.format)(...args));
40
+ }
41
+ catch {
42
+ // `format` is robust against the common pathologies but a user arg
43
+ // with a throwing `toString()` would otherwise turn a benign
44
+ // `console.log(...)` into a generator crash.
45
+ }
46
+ });
47
+ Object.defineProperty(wrapper, CAPTURED_MARKER, {
48
+ value: true,
49
+ enumerable: false,
50
+ configurable: true,
51
+ writable: false,
52
+ });
53
+ console[method] = wrapper;
54
+ }
55
+ let restored = false;
56
+ return {
57
+ flush() {
58
+ return buffer.join('\n');
59
+ },
60
+ restore() {
61
+ if (restored)
62
+ return;
63
+ restored = true;
64
+ for (const [method, fn] of originals) {
65
+ console[method] = fn;
66
+ }
67
+ },
68
+ };
69
+ }
70
+ /**
71
+ * Convenience wrapper that installs the capture, runs `fn`, restores on
72
+ * completion or throw, and returns the captured logs alongside `fn`'s value.
73
+ * Throws from `fn` propagate with the captured logs attached as
74
+ * `(err as any).capturedLogs` — the most useful diagnostic when a generator
75
+ * crashes mid-output.
76
+ */
77
+ async function withGeneratorOutputCapture(fn) {
78
+ const capture = installGeneratorOutputCapture();
79
+ try {
80
+ const result = await fn();
81
+ return { result, logs: capture.flush() };
82
+ }
83
+ catch (err) {
84
+ if (err && typeof err === 'object') {
85
+ // A frozen / sealed / non-extensible error would make this throw a
86
+ // TypeError under TS-emitted strict-mode code, masking the original
87
+ // generator error. Swallow that failure; the diagnostic is best-effort.
88
+ try {
89
+ err.capturedLogs = capture.flush();
90
+ }
91
+ catch {
92
+ /* attachment failed; preserve the original error */
93
+ }
94
+ }
95
+ throw err;
96
+ }
97
+ finally {
98
+ capture.restore();
99
+ }
100
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Canonical list of agent ids for the migrate agentic flow. Used by the yargs
3
+ * layer for `--agentic` validation and by the runtime as the source of truth
4
+ * for the {@link AgentId} union.
5
+ */
6
+ export declare const AGENT_IDS: readonly ["claude-code", "codex", "opencode"];
7
+ export type AgentId = (typeof AGENT_IDS)[number];
8
+ /**
9
+ * Shape-only normalization of `--agentic`; validation of agent-id strings is
10
+ * done upstream in the yargs `.check()` chain.
11
+ */
12
+ export declare function coerceAgenticArg(value: unknown): string | boolean | undefined;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ // Zero-dep helpers for --agentic — keeps the agentic chain out of every
3
+ // nx CLI startup.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.AGENT_IDS = void 0;
6
+ exports.coerceAgenticArg = coerceAgenticArg;
7
+ /**
8
+ * Canonical list of agent ids for the migrate agentic flow. Used by the yargs
9
+ * layer for `--agentic` validation and by the runtime as the source of truth
10
+ * for the {@link AgentId} union.
11
+ */
12
+ exports.AGENT_IDS = ['claude-code', 'codex', 'opencode'];
13
+ /**
14
+ * Shape-only normalization of `--agentic`; validation of agent-id strings is
15
+ * done upstream in the yargs `.check()` chain.
16
+ */
17
+ function coerceAgenticArg(value) {
18
+ if (value === undefined)
19
+ return undefined;
20
+ // yargs collects repeated occurrences into an array; error rather than
21
+ // silently picking last/first. `--agentic` is single-value by intent.
22
+ if (Array.isArray(value)) {
23
+ const received = value
24
+ .map((v) => (typeof v === 'string' ? `--agentic=${v}` : '--agentic'))
25
+ .join(' ');
26
+ throw new Error(`Error: --agentic was passed more than once (received: ${received}). Specify --agentic at most one time.`);
27
+ }
28
+ if (value === true || value === '' || value === 'true' || value === 'yes') {
29
+ return true;
30
+ }
31
+ if (value === false || value === 'false' || value === 'no') {
32
+ return false;
33
+ }
34
+ if (typeof value === 'string') {
35
+ return value;
36
+ }
37
+ return undefined;
38
+ }
@@ -0,0 +1,6 @@
1
+ import { AgentDefinition, AgentId } from './types';
2
+ export declare const claudeCodeDefinition: AgentDefinition;
3
+ export declare const codexDefinition: AgentDefinition;
4
+ export declare const opencodeDefinition: AgentDefinition;
5
+ export declare const AGENT_DEFINITIONS: readonly AgentDefinition[];
6
+ export declare function getAgentDefinition(id: AgentId): AgentDefinition | undefined;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AGENT_DEFINITIONS = exports.opencodeDefinition = exports.codexDefinition = exports.claudeCodeDefinition = void 0;
4
+ exports.getAgentDefinition = getAgentDefinition;
5
+ const os_1 = require("os");
6
+ const path_1 = require("path");
7
+ // --- Claude Code ---------------------------------------------------------
8
+ function claudeCodeWellKnownPaths() {
9
+ if (process.platform === 'win32') {
10
+ const home = process.env.USERPROFILE;
11
+ return home ? [(0, path_1.join)(home, '.local', 'bin', 'claude.exe')] : [];
12
+ }
13
+ return [(0, path_1.join)((0, os_1.homedir)(), '.claude', 'local', 'claude')];
14
+ }
15
+ function claudeCodeBuildInteractive(ctx) {
16
+ return {
17
+ args: ['--system-prompt', ctx.systemContext, ctx.userPrompt],
18
+ cwd: ctx.workspaceRoot,
19
+ };
20
+ }
21
+ exports.claudeCodeDefinition = {
22
+ id: 'claude-code',
23
+ displayName: 'Claude Code',
24
+ binaryNames: ['claude'],
25
+ wellKnownPaths: claudeCodeWellKnownPaths,
26
+ buildInteractive: claudeCodeBuildInteractive,
27
+ };
28
+ // --- OpenAI Codex --------------------------------------------------------
29
+ function codexWellKnownPaths() {
30
+ return [];
31
+ }
32
+ function codexBuildInteractive(ctx) {
33
+ return {
34
+ args: ['-c', `developer_instructions=${ctx.systemContext}`, ctx.userPrompt],
35
+ cwd: ctx.workspaceRoot,
36
+ };
37
+ }
38
+ exports.codexDefinition = {
39
+ id: 'codex',
40
+ displayName: 'OpenAI Codex',
41
+ binaryNames: ['codex'],
42
+ wellKnownPaths: codexWellKnownPaths,
43
+ buildInteractive: codexBuildInteractive,
44
+ };
45
+ // --- OpenCode ------------------------------------------------------------
46
+ const OPENCODE_TRANSIENT_AGENT_NAME = 'nx-migrate';
47
+ function opencodeWellKnownPaths() {
48
+ if (process.platform === 'win32') {
49
+ return [];
50
+ }
51
+ const candidates = [];
52
+ const home = (0, os_1.homedir)();
53
+ const installDir = process.env.OPENCODE_INSTALL_DIR;
54
+ const xdgBinDir = process.env.XDG_BIN_DIR;
55
+ if (installDir) {
56
+ candidates.push((0, path_1.join)(installDir, 'opencode'));
57
+ }
58
+ if (xdgBinDir) {
59
+ candidates.push((0, path_1.join)(xdgBinDir, 'opencode'));
60
+ }
61
+ candidates.push((0, path_1.join)(home, 'bin', 'opencode'));
62
+ candidates.push((0, path_1.join)(home, '.opencode', 'bin', 'opencode'));
63
+ return candidates;
64
+ }
65
+ function opencodeBuildInteractive(ctx) {
66
+ const config = {
67
+ agent: {
68
+ [OPENCODE_TRANSIENT_AGENT_NAME]: { prompt: ctx.systemContext },
69
+ },
70
+ };
71
+ return {
72
+ args: [
73
+ '--agent',
74
+ OPENCODE_TRANSIENT_AGENT_NAME,
75
+ '--prompt',
76
+ ctx.userPrompt,
77
+ ],
78
+ env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(config) },
79
+ cwd: ctx.workspaceRoot,
80
+ };
81
+ }
82
+ exports.opencodeDefinition = {
83
+ id: 'opencode',
84
+ displayName: 'OpenCode',
85
+ binaryNames: ['opencode'],
86
+ wellKnownPaths: opencodeWellKnownPaths,
87
+ buildInteractive: opencodeBuildInteractive,
88
+ };
89
+ // --- Registry ------------------------------------------------------------
90
+ exports.AGENT_DEFINITIONS = [
91
+ exports.claudeCodeDefinition,
92
+ exports.codexDefinition,
93
+ exports.opencodeDefinition,
94
+ ];
95
+ const byId = new Map(exports.AGENT_DEFINITIONS.map((definition) => [definition.id, definition]));
96
+ function getAgentDefinition(id) {
97
+ return byId.get(id);
98
+ }
@@ -0,0 +1,10 @@
1
+ import { AgentDefinition, DetectedInstalledAgent } from './types';
2
+ /**
3
+ * Probes each given agent definition for an installed binary on the user's
4
+ * machine. PATH is checked first via `which` (which handles Windows PATHEXT),
5
+ * then the per-agent well-known fallback paths. Probes run in parallel.
6
+ *
7
+ * Returns only the agents that were found, preserving the order of the input
8
+ * `definitions` argument for callers that rely on it for picker presentation.
9
+ */
10
+ export declare function detectInstalledAgents(definitions: readonly AgentDefinition[]): Promise<DetectedInstalledAgent[]>;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectInstalledAgents = detectInstalledAgents;
4
+ const tslib_1 = require("tslib");
5
+ const promises_1 = require("fs/promises");
6
+ const which_1 = tslib_1.__importDefault(require("which"));
7
+ const logger_1 = require("../../../utils/logger");
8
+ async function isExecutable(path) {
9
+ try {
10
+ await (0, promises_1.access)(path, promises_1.constants.X_OK);
11
+ return true;
12
+ }
13
+ catch (err) {
14
+ // EACCES on an existing path means "this looks installed but we can't
15
+ // execute it" — surface so the user can answer "why isn't my agent
16
+ // detected?" without grep-debugging the filesystem.
17
+ const code = err?.code;
18
+ if (code && code !== 'ENOENT') {
19
+ logger_1.logger.verbose(`Agent detection: cannot probe ${path} (${code}).`);
20
+ }
21
+ return false;
22
+ }
23
+ }
24
+ async function findOnPath(binaryNames) {
25
+ for (const name of binaryNames) {
26
+ // `{ nothrow: true }` avoids the throw-per-missing-binary overhead;
27
+ // EACCES on existing-but-unexecutable paths is handled by `isExecutable`.
28
+ const found = await (0, which_1.default)(name, { nothrow: true });
29
+ if (typeof found === 'string') {
30
+ return found;
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ async function detectOne(definition) {
36
+ const onPath = await findOnPath(definition.binaryNames);
37
+ if (onPath) {
38
+ return {
39
+ id: definition.id,
40
+ displayName: definition.displayName,
41
+ binary: onPath,
42
+ source: 'path',
43
+ };
44
+ }
45
+ for (const candidate of definition.wellKnownPaths()) {
46
+ if (await isExecutable(candidate)) {
47
+ return {
48
+ id: definition.id,
49
+ displayName: definition.displayName,
50
+ binary: candidate,
51
+ source: 'well-known',
52
+ };
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Probes each given agent definition for an installed binary on the user's
59
+ * machine. PATH is checked first via `which` (which handles Windows PATHEXT),
60
+ * then the per-agent well-known fallback paths. Probes run in parallel.
61
+ *
62
+ * Returns only the agents that were found, preserving the order of the input
63
+ * `definitions` argument for callers that rely on it for picker presentation.
64
+ */
65
+ async function detectInstalledAgents(definitions) {
66
+ const results = await Promise.all(definitions.map(detectOne));
67
+ return results.filter((result) => result !== null);
68
+ }
@@ -0,0 +1,46 @@
1
+ export declare function isHandoffGitignoreMigration(m: {
2
+ package: string;
3
+ name: string;
4
+ }): boolean;
5
+ /**
6
+ * Under `--agentic`, the runner writes per-run scratch under
7
+ * `.nx/migrate-runs/<run-id>/`. The v23 migration
8
+ * `23-0-0-add-migrate-runs-to-git-ignore` adds `.nx/migrate-runs` to
9
+ * `.gitignore`; without intervention it would run in its declared slot
10
+ * (typically late), so earlier per-migration commits would absorb the
11
+ * scratch into the user-visible diff.
12
+ *
13
+ * Two paths cover the leak, with no overlap:
14
+ *
15
+ * 1. HOIST — handled by the sort comparator in `executeMigrations`. When
16
+ * the v23 migration is in the queue, it sorts to position 0 and runs
17
+ * first via the normal migration runner (with its own log line and
18
+ * commit). Fully traceable in `git log`.
19
+ *
20
+ * 2. INLINE FALLBACK — this function. When the migration is NOT in the
21
+ * queue AND the highest target version is < v23 (intra-pre-v23
22
+ * `--agentic` run), the migration won't run at all. Apply its body
23
+ * inline against an `FsTree` and commit it as a standalone preflight
24
+ * commit (or leave in the working tree under `--no-create-commits`).
25
+ *
26
+ * When the migration is not in the queue AND target >= v23, the user is
27
+ * past v23 already. They had the entry historically; if it's gone, that's
28
+ * a conscious removal we respect.
29
+ */
30
+ export declare function applyAgenticHandoffGitignoreFallback({ migrations, installedNxVersion, effectiveCreateCommits, commitPrefix, root, }: {
31
+ migrations: ReadonlyArray<{
32
+ package: string;
33
+ name: string;
34
+ }>;
35
+ /**
36
+ * The version of `nx` currently installed in the workspace. After
37
+ * `nx migrate latest` runs (the step before `--run-migrations`), this is
38
+ * the target nx version. We use it as the v23 cutoff instead of walking
39
+ * the migration list: any third-party plugin migration with a `23.x`
40
+ * version is irrelevant to whether `nx` itself crossed v23.
41
+ */
42
+ installedNxVersion: string;
43
+ effectiveCreateCommits: boolean;
44
+ commitPrefix: string;
45
+ root: string;
46
+ }): Promise<void>;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isHandoffGitignoreMigration = isHandoffGitignoreMigration;
4
+ exports.applyAgenticHandoffGitignoreFallback = applyAgenticHandoffGitignoreFallback;
5
+ const tslib_1 = require("tslib");
6
+ const semver_1 = require("semver");
7
+ const pc = tslib_1.__importStar(require("picocolors"));
8
+ const add_migrate_runs_to_git_ignore_1 = tslib_1.__importDefault(require("../../../migrations/update-23-0-0/add-migrate-runs-to-git-ignore"));
9
+ const tree_1 = require("../../../generators/tree");
10
+ const git_utils_1 = require("../../../utils/git-utils");
11
+ const logger_1 = require("../../../utils/logger");
12
+ /**
13
+ * Composite identity of the v23 migration that adds `.nx/migrate-runs` to
14
+ * `.gitignore`. Hard-coded because the agentic preflight is a deliberate
15
+ * one-off coupling: this exact migration owns the entry that keeps
16
+ * `.nx/migrate-runs/<run-id>/...` scratch out of per-migration commits. If
17
+ * the migration is ever renamed, this entry must move with it.
18
+ */
19
+ const HANDOFF_GITIGNORE_MIGRATION_PACKAGE = 'nx';
20
+ const HANDOFF_GITIGNORE_MIGRATION_NAME = '23-0-0-add-migrate-runs-to-git-ignore';
21
+ function isHandoffGitignoreMigration(m) {
22
+ return (m.package === HANDOFF_GITIGNORE_MIGRATION_PACKAGE &&
23
+ m.name === HANDOFF_GITIGNORE_MIGRATION_NAME);
24
+ }
25
+ /**
26
+ * Under `--agentic`, the runner writes per-run scratch under
27
+ * `.nx/migrate-runs/<run-id>/`. The v23 migration
28
+ * `23-0-0-add-migrate-runs-to-git-ignore` adds `.nx/migrate-runs` to
29
+ * `.gitignore`; without intervention it would run in its declared slot
30
+ * (typically late), so earlier per-migration commits would absorb the
31
+ * scratch into the user-visible diff.
32
+ *
33
+ * Two paths cover the leak, with no overlap:
34
+ *
35
+ * 1. HOIST — handled by the sort comparator in `executeMigrations`. When
36
+ * the v23 migration is in the queue, it sorts to position 0 and runs
37
+ * first via the normal migration runner (with its own log line and
38
+ * commit). Fully traceable in `git log`.
39
+ *
40
+ * 2. INLINE FALLBACK — this function. When the migration is NOT in the
41
+ * queue AND the highest target version is < v23 (intra-pre-v23
42
+ * `--agentic` run), the migration won't run at all. Apply its body
43
+ * inline against an `FsTree` and commit it as a standalone preflight
44
+ * commit (or leave in the working tree under `--no-create-commits`).
45
+ *
46
+ * When the migration is not in the queue AND target >= v23, the user is
47
+ * past v23 already. They had the entry historically; if it's gone, that's
48
+ * a conscious removal we respect.
49
+ */
50
+ async function applyAgenticHandoffGitignoreFallback({ migrations, installedNxVersion, effectiveCreateCommits, commitPrefix, root, }) {
51
+ if (migrations.some(isHandoffGitignoreMigration)) {
52
+ // The hoist path handles this via the sort comparator.
53
+ return;
54
+ }
55
+ if ((0, semver_1.major)(installedNxVersion) >= 23) {
56
+ // User is past v23. Respect their `.gitignore` state — if the entry
57
+ // is missing, that's a conscious removal.
58
+ return;
59
+ }
60
+ const tree = new tree_1.FsTree(root, false);
61
+ await (0, add_migrate_runs_to_git_ignore_1.default)(tree);
62
+ const changes = tree.listChanges();
63
+ if (changes.length === 0) {
64
+ // Migration body short-circuited (no `.gitignore`, Lerna without nx.json,
65
+ // or the entry is already covered by an existing pattern).
66
+ return;
67
+ }
68
+ (0, tree_1.flushChanges)(root, changes);
69
+ logger_1.logger.info(pc.dim(`- Added .nx/migrate-runs to .gitignore so this --agentic run's handoff scratch is ignored.`));
70
+ if (!effectiveCreateCommits)
71
+ return;
72
+ if (!(0, git_utils_1.hasUncommittedChanges)(root))
73
+ return;
74
+ try {
75
+ const sha = (0, git_utils_1.tryCommitChanges)(`${commitPrefix}add .nx/migrate-runs to .gitignore`, root);
76
+ if (sha) {
77
+ logger_1.logger.info(pc.dim(` Commit: ${sha}`));
78
+ }
79
+ // `null` return = commit landed but `git rev-parse HEAD` raced. The
80
+ // diff cleared from the working tree; nothing more to say.
81
+ }
82
+ catch (err) {
83
+ const reason = err instanceof Error ? err.message : String(err);
84
+ logger_1.logger.info(pc.yellow(`Could not create the agentic preflight commit:\n${reason}\n` +
85
+ `The .gitignore change remains in the working tree; commit it manually or it will be absorbed into the first per-migration commit.`));
86
+ }
87
+ }
@@ -0,0 +1,63 @@
1
+ import { HandoffFile } from './types';
2
+ /** Returns the run directory for a given workspace + run id (target version). */
3
+ export declare function runDirPath(workspaceRoot: string, runId: string): string;
4
+ /**
5
+ * `mkdir -p` with a contextual error wrapper. Without this, the raw
6
+ * ENOSPC/EACCES/EROFS surfaces with no indication of which directory the
7
+ * migrate orchestrator was trying to create.
8
+ */
9
+ export declare function mkdirSafely(dir: string, purpose: string): void;
10
+ /**
11
+ * Wipes any prior contents for this run id and recreates an empty directory.
12
+ *
13
+ * Scope of the wipe is intentionally narrow (only `<run-id>/`) so that handoff
14
+ * artifacts from prior runs targeting different versions remain on disk for
15
+ * inspection.
16
+ */
17
+ export declare function initRunDir(workspaceRoot: string, runId: string): string;
18
+ /**
19
+ * Absolute path of the handoff file for a migration step within a run.
20
+ * The package's scope (if any) becomes a real subdirectory so the package name
21
+ * stays readable; two packages can ship a migration with the same name without
22
+ * colliding because they land in different package subdirectories. Each
23
+ * segment is sanitized so the path is always writable on every platform.
24
+ */
25
+ export declare function stepHandoffPath(runDir: string, migration: {
26
+ package: string;
27
+ name: string;
28
+ }): string;
29
+ export type HandoffReadFailureReason = 'missing' | 'read-error' | 'parse-error' | 'shape-mismatch';
30
+ export type HandoffReadResult = {
31
+ ok: true;
32
+ handoff: HandoffFile;
33
+ } | {
34
+ ok: false;
35
+ reason: HandoffReadFailureReason;
36
+ detail?: string;
37
+ };
38
+ /**
39
+ * Reads and validates a handoff file written by an agent. Returns a tagged
40
+ * result so callers (the in-loop poller and the post-exit resolver) can
41
+ * distinguish "file not yet written" from "file written but garbage" — the
42
+ * latter is surfaced to the user instead of being collapsed into the same
43
+ * generic ambiguous-outcome prompt.
44
+ */
45
+ export declare function readHandoffWithReason(filePath: string): HandoffReadResult;
46
+ /**
47
+ * Convenience wrapper preserving the original null-on-any-failure contract.
48
+ * Used by the polling loop (`waitForValidHandoff`) where every failure mode
49
+ * is "keep waiting" — the file may be missing, mid-write, or being rewritten.
50
+ */
51
+ export declare function readHandoff(filePath: string): HandoffFile | null;
52
+ /**
53
+ * Polls for a valid handoff file. Resolves once `readHandoff` accepts the
54
+ * file's contents. Used to detect when the agent has finished its work so the
55
+ * orchestrator can close the agent's session without depending on the agent
56
+ * exiting on its own.
57
+ *
58
+ * Rejects with the abort reason when `options.signal` is aborted.
59
+ */
60
+ export declare function waitForValidHandoff(handoffFilePath: string, options?: {
61
+ intervalMs?: number;
62
+ signal?: AbortSignal;
63
+ }): Promise<void>;