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.
- package/dist/src/adapter/ngcli-adapter.d.ts +2 -1
- package/dist/src/adapter/ngcli-adapter.js +25 -2
- package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.js +1 -0
- package/dist/src/command-line/init/implementation/utils.js +7 -0
- package/dist/src/command-line/migrate/agentic/capture-generator-output.d.ts +22 -0
- package/dist/src/command-line/migrate/agentic/capture-generator-output.js +100 -0
- package/dist/src/command-line/migrate/agentic/cli-args.d.ts +12 -0
- package/dist/src/command-line/migrate/agentic/cli-args.js +38 -0
- package/dist/src/command-line/migrate/agentic/definitions.d.ts +6 -0
- package/dist/src/command-line/migrate/agentic/definitions.js +98 -0
- package/dist/src/command-line/migrate/agentic/detect-installed.d.ts +10 -0
- package/dist/src/command-line/migrate/agentic/detect-installed.js +68 -0
- package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +46 -0
- package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +87 -0
- package/dist/src/command-line/migrate/agentic/handoff.d.ts +63 -0
- package/dist/src/command-line/migrate/agentic/handoff.js +183 -0
- package/dist/src/command-line/migrate/agentic/inception.d.ts +9 -0
- package/dist/src/command-line/migrate/agentic/inception.js +15 -0
- package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.d.ts +22 -0
- package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.js +50 -0
- package/dist/src/command-line/migrate/agentic/prompts/generic-validation.d.ts +51 -0
- package/dist/src/command-line/migrate/agentic/prompts/generic-validation.js +73 -0
- package/dist/src/command-line/migrate/agentic/prompts/hybrid-prompt-migration.d.ts +44 -0
- package/dist/src/command-line/migrate/agentic/prompts/hybrid-prompt-migration.js +60 -0
- package/dist/src/command-line/migrate/agentic/prompts/prompt-migration.d.ts +21 -0
- package/dist/src/command-line/migrate/agentic/prompts/prompt-migration.js +29 -0
- package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.d.ts +9 -0
- package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.js +87 -0
- package/dist/src/command-line/migrate/agentic/prompts/system-prompt.d.ts +46 -0
- package/dist/src/command-line/migrate/agentic/prompts/system-prompt.js +88 -0
- package/dist/src/command-line/migrate/agentic/run-step.d.ts +51 -0
- package/dist/src/command-line/migrate/agentic/run-step.js +121 -0
- package/dist/src/command-line/migrate/agentic/runner.d.ts +33 -0
- package/dist/src/command-line/migrate/agentic/runner.js +439 -0
- package/dist/src/command-line/migrate/agentic/select.d.ts +14 -0
- package/dist/src/command-line/migrate/agentic/select.js +150 -0
- package/dist/src/command-line/migrate/agentic/types.d.ts +102 -0
- package/dist/src/command-line/migrate/agentic/types.js +2 -0
- package/dist/src/command-line/migrate/command-object.d.ts +1 -0
- package/dist/src/command-line/migrate/command-object.js +21 -6
- package/dist/src/command-line/migrate/migrate-commits.d.ts +50 -0
- package/dist/src/command-line/migrate/migrate-commits.js +102 -0
- package/dist/src/command-line/migrate/migrate-output.d.ts +121 -0
- package/dist/src/command-line/migrate/migrate-output.js +241 -0
- package/dist/src/command-line/migrate/migrate.d.ts +73 -9
- package/dist/src/command-line/migrate/migrate.js +577 -95
- package/dist/src/command-line/migrate/migration-shape.d.ts +8 -0
- package/dist/src/command-line/migrate/migration-shape.js +13 -0
- package/dist/src/command-line/migrate/multi-major.js +2 -2
- package/dist/src/command-line/migrate/run-migration-process.js +20 -6
- package/dist/src/command-line/migrate/safe-prompt.d.ts +28 -0
- package/dist/src/command-line/migrate/safe-prompt.js +49 -0
- package/dist/src/config/misc-interfaces.d.ts +16 -1
- package/dist/src/devkit-exports.d.ts +1 -1
- package/dist/src/migrations/update-23-0-0/add-migrate-runs-to-git-ignore.d.ts +2 -0
- package/dist/src/migrations/update-23-0-0/add-migrate-runs-to-git-ignore.js +16 -0
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/utils/git-utils.d.ts +14 -0
- package/dist/src/utils/git-utils.js +73 -0
- package/migrations.json +5 -0
- 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 = {
|
|
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 = {
|
|
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
|
}
|
|
@@ -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>;
|