nx 23.2.0-beta.4 → 23.2.0-beta.5

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 (81) hide show
  1. package/dist/bin/nx.js +1 -0
  2. package/dist/src/command-line/affected/affected.js +1 -0
  3. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +2 -2
  4. package/dist/src/command-line/init/init-v2.js +2 -2
  5. package/dist/src/command-line/migrate/agentic/definitions.js +2 -2
  6. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +15 -19
  7. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +19 -31
  8. package/dist/src/command-line/migrate/agentic/handoff.d.ts +4 -6
  9. package/dist/src/command-line/migrate/agentic/handoff.js +9 -8
  10. package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.d.ts +7 -12
  11. package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.js +7 -0
  12. package/dist/src/command-line/migrate/agentic/run-step.d.ts +10 -0
  13. package/dist/src/command-line/migrate/agentic/select.d.ts +10 -2
  14. package/dist/src/command-line/migrate/agentic/select.js +10 -2
  15. package/dist/src/command-line/migrate/agentic/types.d.ts +17 -3
  16. package/dist/src/command-line/migrate/agentic/types.js +26 -0
  17. package/dist/src/command-line/migrate/command-object.d.ts +3 -0
  18. package/dist/src/command-line/migrate/command-object.js +6 -1
  19. package/dist/src/command-line/migrate/execute-migration.d.ts +14 -9
  20. package/dist/src/command-line/migrate/execute-migration.js +82 -18
  21. package/dist/src/command-line/migrate/migrate-analytics.d.ts +7 -0
  22. package/dist/src/command-line/migrate/migrate-analytics.js +14 -0
  23. package/dist/src/command-line/migrate/migrate-commits.d.ts +32 -8
  24. package/dist/src/command-line/migrate/migrate-commits.js +77 -9
  25. package/dist/src/command-line/migrate/migrate-config.d.ts +11 -12
  26. package/dist/src/command-line/migrate/migrate-config.js +14 -14
  27. package/dist/src/command-line/migrate/migrate-output.d.ts +45 -7
  28. package/dist/src/command-line/migrate/migrate-output.js +52 -8
  29. package/dist/src/command-line/migrate/migrate-ui-api.d.ts +2 -1
  30. package/dist/src/command-line/migrate/migrate-ui-api.js +16 -7
  31. package/dist/src/command-line/migrate/migrate.d.ts +17 -68
  32. package/dist/src/command-line/migrate/migrate.js +385 -220
  33. package/dist/src/command-line/migrate/migration-shape.d.ts +8 -0
  34. package/dist/src/command-line/migrate/run/index.d.ts +2 -0
  35. package/dist/src/command-line/migrate/run/index.js +5 -0
  36. package/dist/src/command-line/migrate/run/util.d.ts +1 -0
  37. package/dist/src/command-line/migrate/run/util.js +16 -0
  38. package/dist/src/command-line/migrate/run/worker.d.ts +15 -0
  39. package/dist/src/command-line/migrate/run/worker.js +419 -0
  40. package/dist/src/command-line/migrate/run-migration-process.js +6 -5
  41. package/dist/src/command-line/migrate/sort-migrations.d.ts +16 -0
  42. package/dist/src/command-line/migrate/sort-migrations.js +32 -0
  43. package/dist/src/command-line/migrate/version-skew-guard.d.ts +59 -0
  44. package/dist/src/command-line/migrate/version-skew-guard.js +156 -0
  45. package/dist/src/command-line/nx-cloud/start-nx-agents/command-object.d.ts +2 -0
  46. package/dist/src/command-line/nx-cloud/start-nx-agents/command-object.js +16 -0
  47. package/dist/src/command-line/nx-cloud/start-nx-agents/start-nx-agents.d.ts +4 -0
  48. package/dist/src/command-line/nx-cloud/start-nx-agents/start-nx-agents.js +13 -0
  49. package/dist/src/command-line/nx-commands.js +26 -24
  50. package/dist/src/command-line/release/changelog.js +5 -0
  51. package/dist/src/command-line/release/utils/git.js +1 -1
  52. package/dist/src/command-line/release/utils/release-graph.d.ts +1 -0
  53. package/dist/src/command-line/release/utils/release-graph.js +15 -3
  54. package/dist/src/command-line/release/version/version-actions.d.ts +8 -0
  55. package/dist/src/command-line/release/version/version-actions.js +8 -0
  56. package/dist/src/command-line/release/version.js +9 -5
  57. package/dist/src/config/misc-interfaces.d.ts +19 -7
  58. package/dist/src/core/graph/main.js +1 -1
  59. package/dist/src/daemon/server/latest-nx.js +5 -2
  60. package/dist/src/devkit-internals.d.ts +91 -2
  61. package/dist/src/devkit-internals.js +181 -3
  62. package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.d.ts +5 -0
  63. package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.js +9 -2
  64. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  65. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  66. package/dist/src/nx-cloud/utilities/get-cloud-options.js +5 -2
  67. package/dist/src/nx-cloud/utilities/onboarding.js +5 -2
  68. package/dist/src/nx-cloud/utilities/url-shorten.d.ts +1 -4
  69. package/dist/src/nx-cloud/utilities/url-shorten.js +9 -8
  70. package/dist/src/plugins/js/lock-file/project-graph-pruning.js +14 -8
  71. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +2 -0
  72. package/dist/src/project-graph/affected/affected-project-graph-models.d.ts +1 -1
  73. package/dist/src/project-graph/affected/affected-project-graph.d.ts +1 -1
  74. package/dist/src/project-graph/affected/affected-project-graph.js +2 -2
  75. package/dist/src/project-graph/affected/locators/project-glob-changes.js +5 -2
  76. package/dist/src/tasks-runner/life-cycles/performance-report.d.ts +12 -0
  77. package/dist/src/tasks-runner/life-cycles/performance-report.js +62 -3
  78. package/dist/src/tasks-runner/run-command.js +4 -0
  79. package/dist/src/utils/assert-workspace-validity.js +7 -2
  80. package/dist/src/utils/child-process.js +2 -0
  81. package/package.json +12 -12
@@ -7,16 +7,13 @@ exports.resolveInclude = resolveInclude;
7
7
  exports.parseMigrationsOptions = parseMigrationsOptions;
8
8
  exports.createFetcher = createFetcher;
9
9
  exports.generateMigrationsJsonAndUpdatePackageJson = generateMigrationsJsonAndUpdatePackageJson;
10
- exports.resolveAgenticRunId = resolveAgenticRunId;
11
10
  exports.formatSkippedPromptsNextStep = formatSkippedPromptsNextStep;
12
- exports.resolveCreateCommits = resolveCreateCommits;
13
- exports.confirmCommitsOnDefaultBranch = confirmCommitsOnDefaultBranch;
14
- exports.resolveShouldRunValidation = resolveShouldRunValidation;
15
11
  exports.executeMigrations = executeMigrations;
12
+ exports.isSingleMigrationInvocation = isSingleMigrationInvocation;
16
13
  exports.migrate = migrate;
14
+ exports.readLocalNxVersion = readLocalNxVersion;
17
15
  exports.runMigration = runMigration;
18
16
  exports.resolveMigrationForRun = resolveMigrationForRun;
19
- exports.resolveDocumentationFileToWorkspacePath = resolveDocumentationFileToWorkspacePath;
20
17
  exports.nxCliPath = nxCliPath;
21
18
  const tslib_1 = require("tslib");
22
19
  const pc = tslib_1.__importStar(require("picocolors"));
@@ -58,7 +55,6 @@ const multi_major_1 = require("./multi-major");
58
55
  const prompt_files_1 = require("./prompt-files");
59
56
  const command_object_1 = require("./command-object");
60
57
  const migrate_config_1 = require("./migrate-config");
61
- const handoff_gitignore_1 = require("./agentic/handoff-gitignore");
62
58
  const migrate_commits_1 = require("./migrate-commits");
63
59
  const migrate_output_1 = require("./migrate-output");
64
60
  const migration_shape_1 = require("./migration-shape");
@@ -69,6 +65,9 @@ Object.defineProperty(exports, "filterDowngradedUpdates", { enumerable: true, ge
69
65
  const version_utils_1 = require("./version-utils");
70
66
  Object.defineProperty(exports, "normalizeVersion", { enumerable: true, get: function () { return version_utils_1.normalizeVersion; } });
71
67
  const execute_migration_1 = require("./execute-migration");
68
+ const sort_migrations_1 = require("./sort-migrations");
69
+ const version_skew_guard_1 = require("./version-skew-guard");
70
+ const versions_1 = require("../../utils/versions");
72
71
  tslib_1.__exportStar(require("./execute-migration"), exports);
73
72
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
74
73
  function formatCommandFailure(command, error) {
@@ -671,6 +670,32 @@ async function parseTargetPackageAndVersion(args) {
671
670
  return { targetPackage: args, targetVersion: 'latest' };
672
671
  }
673
672
  async function parseMigrationsOptions(options, fetch) {
673
+ if (options.runMigration !== undefined) {
674
+ if (options.runMigration === '') {
675
+ throw new Error(`Error: '--run-migration' requires a migration id, e.g. '--run-migration=@nx/js:my-migration'.`);
676
+ }
677
+ if (options.runMigrations !== undefined) {
678
+ throw new Error(`Error: '--run-migration' (run a single migration) cannot be combined with '--run-migrations' (run the whole migrations file).`);
679
+ }
680
+ if (options.include) {
681
+ throw new Error(`Error: '--run-migration' cannot be combined with '--include'.`);
682
+ }
683
+ if (options.multiMajorMode) {
684
+ throw new Error(`Error: '--run-migration' cannot be combined with '--multi-major-mode'.`);
685
+ }
686
+ // `--if-exists` only applies to a whole-file run, so an explicit "on"
687
+ // conflicts. Its yargs default (false) is always present and is tolerated.
688
+ if (options.ifExists === true) {
689
+ throw new Error(`Error: '--run-migration' cannot be combined with '--if-exists'.`);
690
+ }
691
+ return {
692
+ type: 'runSingleMigration',
693
+ runMigration: options.runMigration,
694
+ agentic: options.agentic,
695
+ validate: options.validate,
696
+ interactive: options.interactive,
697
+ };
698
+ }
674
699
  if (options.runMigrations === '') {
675
700
  options.runMigrations = 'migrations.json';
676
701
  }
@@ -1593,136 +1618,22 @@ function showConnectToCloudMessage() {
1593
1618
  return false;
1594
1619
  }
1595
1620
  }
1596
- function resolveAgenticRunId(migrations) {
1597
- return (0, semver_1.rsort)(migrations.map((m) => (0, version_utils_1.normalizeVersion)(m.version)))[0];
1598
- }
1599
1621
  function formatSkippedPromptsNextStep(skipped) {
1600
1622
  return [
1601
1623
  'Some prompt migrations were skipped. Review and apply each of the following prompt files to the workspace, in the listed order:',
1602
1624
  ...skipped.map((m) => ` - ${m.prompt}`),
1603
1625
  ].join('\n');
1604
1626
  }
1605
- /**
1606
- * Resolves the effective `--create-commits` state once the agentic flow has
1607
- * been resolved. The agent's outer prompt only embeds the impl-phase file list
1608
- * when per-migration commits isolate each migration's diff, so the diff-context
1609
- * flag returned here gates that section.
1610
- */
1611
- function resolveCreateCommits(args) {
1612
- const { createCommits, agenticKind, isGitRepo, commitPrefixIsCustom } = args;
1613
- // Explicit `--create-commits` without git is a hard error — the user asked
1614
- // for something we cannot deliver.
1615
- if (createCommits === true && !isGitRepo) {
1616
- return {
1617
- effective: false,
1618
- agenticHasDiffContext: false,
1619
- error: '`--create-commits` requires a git repository. Run `git init` first, or omit the flag.',
1620
- };
1621
- }
1622
- if (agenticKind === 'enabled') {
1623
- if (createCommits === false) {
1624
- return {
1625
- effective: false,
1626
- agenticHasDiffContext: false,
1627
- warning: "--no-create-commits was passed alongside --agentic. Without per-migration commits, the agent can't isolate the current migration's changes from earlier migrations in this run. Drop --no-create-commits for accurate per-migration review." +
1628
- (commitPrefixIsCustom
1629
- ? ' Note: the custom --commit-prefix value will have no effect because commits are disabled.'
1630
- : ''),
1631
- };
1632
- }
1633
- // Without git we cannot soft-force commits the user didn't explicitly
1634
- // opt into. Degrade rather than error: continue the agentic run, but
1635
- // without per-file diff context (which depends on per-migration commits).
1636
- if (!isGitRepo) {
1637
- return {
1638
- effective: false,
1639
- agenticHasDiffContext: false,
1640
- warning: '`--agentic` enables per-migration commits by default, but the workspace is not a git repository. Continuing without commits — the agent will not receive per-file diff context. Run `git init` to enable.' +
1641
- (commitPrefixIsCustom
1642
- ? ' The custom --commit-prefix value will have no effect.'
1643
- : ''),
1644
- };
1645
- }
1646
- return { effective: true, agenticHasDiffContext: true };
1647
- }
1648
- // Commits aren't enabled here. A custom prefix only reaches this path via
1649
- // nx.json (e.g. `migrate.commitPrefix` + `migrate.agentic` when the agentic
1650
- // flow resolves to disabled); surface that it has no effect rather than
1651
- // dropping it silently.
1652
- return {
1653
- effective: createCommits === true,
1654
- agenticHasDiffContext: false,
1655
- warning: commitPrefixIsCustom && createCommits !== true
1656
- ? 'A custom migrate commit prefix is configured, but commits are not enabled for this run, so it has no effect. Set `migrate.createCommits` to `true` (or pass `--create-commits`) to create a commit per migration.'
1657
- : undefined,
1658
- };
1659
- }
1660
- /**
1661
- * Confirms before creating per-migration commits while the user sits on the
1662
- * repository's default branch, so migrations don't silently commit there (most
1663
- * relevant since `--agentic` enables commits by default). Only the default
1664
- * branch triggers a prompt; a detached HEAD, a different branch, or an
1665
- * unresolved default branch all proceed untouched. Callers gate this on
1666
- * commits being effective and prompting being possible, so non-interactive
1667
- * runs (CI, `--no-interactive`) never reach here.
1668
- */
1669
- async function confirmCommitsOnDefaultBranch(args) {
1670
- const { currentBranch, defaultBranch } = args;
1671
- if (!currentBranch || !defaultBranch || currentBranch !== defaultBranch) {
1672
- return true;
1673
- }
1674
- const { proceed } = await (0, safe_prompt_1.migratePrompt)([
1675
- {
1676
- name: 'proceed',
1677
- type: 'confirm',
1678
- message: `You're on the default branch '${currentBranch}'. nx migrate will create a commit for each migration on this branch. Continue?`,
1679
- initial: false,
1680
- },
1681
- ]);
1682
- return proceed;
1683
- }
1684
- /**
1685
- * Resolves whether the framework-owned generic-validation agent step should run
1686
- * after generator-only migrations.
1687
- *
1688
- * Default-on when the agentic flow resolved to `enabled`; silently ignored
1689
- * otherwise (no warning emitted) — `--validate` requires an active agent
1690
- * session by definition. An explicit `--no-validate` (`validate === false`)
1691
- * opts out even when agentic is enabled.
1692
- */
1693
- function resolveShouldRunValidation(args) {
1694
- return args.validate !== false && args.agenticKind === 'enabled';
1695
- }
1696
1627
  async function executeMigrations(root, migrations, isVerbose, shouldCreateCommits, commitPrefix, shouldSkipInstall = false, agentic, agenticHasDiffContext = false, shouldRunValidation = false) {
1697
1628
  const changedDepInstaller = new execute_migration_1.ChangedDepInstaller(root, shouldSkipInstall);
1698
1629
  const migrationsWithNoChanges = [];
1699
- const sortedMigrations = migrations.sort((a, b) => {
1700
- // Under `--agentic`, hoist the v23 migration that ignores
1701
- // `.nx/migrate-runs` to position 0 so its .gitignore update lands
1702
- // before any per-migration commit absorbs the run's handoff scratch.
1703
- // See `agentic/handoff-gitignore.ts` for the full rationale and the
1704
- // inline-fallback path that covers intra-pre-v23 agentic runs.
1705
- if (agentic?.kind === 'enabled') {
1706
- if ((0, handoff_gitignore_1.isHandoffGitignoreMigration)(a))
1707
- return -1;
1708
- if ((0, handoff_gitignore_1.isHandoffGitignoreMigration)(b))
1709
- return 1;
1710
- }
1711
- // special case for the split configuration migration to run first
1712
- if (a.name === '15-7-0-split-configuration-into-project-json-files') {
1713
- return -1;
1714
- }
1715
- if (b.name === '15-7-0-split-configuration-into-project-json-files') {
1716
- return 1;
1717
- }
1718
- return (0, semver_1.lt)((0, version_utils_1.normalizeVersion)(a.version), (0, version_utils_1.normalizeVersion)(b.version))
1719
- ? -1
1720
- : 1;
1630
+ const sortedMigrations = (0, sort_migrations_1.sortMigrations)(migrations, {
1631
+ hoistHandoffGitignore: agentic?.kind === 'enabled',
1721
1632
  });
1722
1633
  // Lazy-load the agentic chain so non-agentic runs don't pay its startup cost.
1723
1634
  let agenticRun;
1724
1635
  if (agentic?.kind === 'enabled' && sortedMigrations.length > 0) {
1725
- const { initRunDir } = require('./agentic/handoff');
1636
+ const { initRunDir, resolveAgenticRunId } = require('./agentic/handoff');
1726
1637
  const { runAgenticPromptStep } = require('./agentic/run-step');
1727
1638
  agenticRun = {
1728
1639
  agentic,
@@ -1810,6 +1721,23 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1810
1721
  // as a commit failure.
1811
1722
  return { kind: 'none' };
1812
1723
  }
1724
+ // Records what a migration's deterministic phase produced. A migration that
1725
+ // changed nothing must not attempt a commit: the absorbing `git add -A`
1726
+ // would build a commit subject naming this no-op migration even though its
1727
+ // content is entirely prior pending diffs, confusing `git log` / `git blame`
1728
+ // attribution. Pending stays pending and the next change-producing migration
1729
+ // absorbs it.
1730
+ async function commitOrRecordNoChanges(m, madeChanges) {
1731
+ if (!madeChanges) {
1732
+ migrationsWithNoChanges.push(m);
1733
+ return { kind: 'none' };
1734
+ }
1735
+ const commit = await attemptMigrationCommit(m);
1736
+ if (commit.kind === 'landed' && commit.sha) {
1737
+ logger_1.logger.info(pc.dim(`Committed as ${commit.sha}`));
1738
+ }
1739
+ return commit;
1740
+ }
1813
1741
  const totalMigrations = sortedMigrations.length;
1814
1742
  let migrationIndex = 0;
1815
1743
  for (const m of sortedMigrations) {
@@ -1830,6 +1758,11 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1830
1758
  const { resolvedCollection, documentationPath } = resolveMigrationForRun(root, m, !!agenticRun);
1831
1759
  let outcome;
1832
1760
  let commit = { kind: 'none' };
1761
+ // Set when the migration returned `skipAgentic: true` and something was
1762
+ // actually waived, so the end-of-run recaps can report it. A hybrid's
1763
+ // prompt is owed in every agentic mode, so waiving it always counts; a
1764
+ // generator-only migration only counts when validation would have run.
1765
+ let waivedAgenticStep = false;
1833
1766
  if ((0, migration_shape_1.isPromptOnlyMigration)(m)) {
1834
1767
  if (agenticRun) {
1835
1768
  inAgenticStep = true;
@@ -1854,10 +1787,19 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1854
1787
  }
1855
1788
  }
1856
1789
  else if ((0, migration_shape_1.isHybridMigration)(m)) {
1857
- const { changes, nextSteps, agentContext, logs, madeChanges } = await (0, execute_migration_1.runNxOrAngularMigration)(root, m, isVerbose,
1790
+ const { changes, nextSteps, agentContext, skipAgentic, logs, madeChanges, } = await (0, execute_migration_1.runNxOrAngularMigration)(root, m, isVerbose,
1858
1791
  /* captureGeneratorOutput: */ !!agenticRun, resolvedCollection);
1859
1792
  migrationEmittedNextSteps.push(...nextSteps);
1860
- if (agenticRun) {
1793
+ if (skipAgentic) {
1794
+ // The generator reported the prompt half unnecessary, so nothing is
1795
+ // owed: no agent run, and no next-steps entry telling the user to
1796
+ // run the prompt themselves. Runs in all three agentic modes.
1797
+ (0, migrate_output_1.logWaivedAgenticStep)(m, agentContext);
1798
+ waivedAgenticStep = true;
1799
+ commit = await commitOrRecordNoChanges(m, madeChanges);
1800
+ outcome = madeChanges ? 'applied' : 'no-changes';
1801
+ }
1802
+ else if (agenticRun) {
1861
1803
  // Install any deps the deterministic phase added/bumped before the
1862
1804
  // agent runs — the prompt half may depend on them being present in
1863
1805
  // node_modules.
@@ -1896,21 +1838,7 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1896
1838
  printDroppedAgentContext({ migration: m, agentContext });
1897
1839
  }
1898
1840
  skippedPrompts.push(m);
1899
- if (!madeChanges) {
1900
- migrationsWithNoChanges.push(m);
1901
- }
1902
- // Only attempt a commit when this migration's deterministic
1903
- // phase actually produced changes. Otherwise the absorbing
1904
- // `git add -A` would build a commit subject naming this no-op
1905
- // migration even though its content is entirely prior pending
1906
- // diffs — confusing `git log` / `git blame` attribution. Pending
1907
- // stays pending and the next change-producing migration absorbs.
1908
- if (madeChanges) {
1909
- commit = await attemptMigrationCommit(m);
1910
- }
1911
- if (commit.kind === 'landed' && commit.sha) {
1912
- logger_1.logger.info(pc.dim(`Committed as ${commit.sha}`));
1913
- }
1841
+ commit = await commitOrRecordNoChanges(m, madeChanges);
1914
1842
  outcome = 'deferred';
1915
1843
  }
1916
1844
  }
@@ -1918,10 +1846,14 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1918
1846
  // Defer commit until validation succeeds; failed validation leaves
1919
1847
  // changes uncommitted in the working tree for the user to review.
1920
1848
  const validationRun = agenticRun && shouldRunValidation ? agenticRun : undefined;
1921
- const { changes, nextSteps, agentContext, logs, madeChanges } = await (0, execute_migration_1.runNxOrAngularMigration)(root, m, isVerbose,
1849
+ const { changes, nextSteps, agentContext, skipAgentic, logs, madeChanges, } = await (0, execute_migration_1.runNxOrAngularMigration)(root, m, isVerbose,
1922
1850
  /* captureGeneratorOutput: */ !!validationRun, resolvedCollection);
1923
1851
  migrationEmittedNextSteps.push(...nextSteps);
1924
- const canRunValidation = !!validationRun && changes.length > 0;
1852
+ // Whether a validation step was on the table at all. `skipAgentic`
1853
+ // waives nothing when it wasn't, so the whole waived path hangs off
1854
+ // this, not just the log line.
1855
+ const validationApplies = !!validationRun && changes.length > 0;
1856
+ const canRunValidation = validationApplies && !skipAgentic;
1925
1857
  if (canRunValidation) {
1926
1858
  // Install any deps the deterministic phase added/bumped before the
1927
1859
  // validation agent runs — the agent may run tasks that need them.
@@ -1952,22 +1884,17 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1952
1884
  outcome = 'applied';
1953
1885
  }
1954
1886
  else {
1955
- // Inner validation step didn't run. Surface `agentContext` under
1956
- // `inside-agent` so the outer driving agent can ingest it.
1957
- if (printDroppedAgentContext && agentContext.length > 0) {
1958
- printDroppedAgentContext({ migration: m, agentContext });
1887
+ if (skipAgentic && validationApplies) {
1888
+ (0, migrate_output_1.logWaivedAgenticStep)(m, agentContext);
1889
+ waivedAgenticStep = true;
1959
1890
  }
1960
- if (!madeChanges) {
1961
- migrationsWithNoChanges.push(m);
1962
- outcome = 'no-changes';
1963
- }
1964
- else {
1965
- commit = await attemptMigrationCommit(m);
1966
- if (commit.kind === 'landed' && commit.sha) {
1967
- logger_1.logger.info(pc.dim(`Committed as ${commit.sha}`));
1968
- }
1969
- outcome = 'applied';
1891
+ else if (printDroppedAgentContext && agentContext.length > 0) {
1892
+ // Inner validation step didn't run. Surface `agentContext` under
1893
+ // `inside-agent` so the outer driving agent can ingest it.
1894
+ printDroppedAgentContext({ migration: m, agentContext });
1970
1895
  }
1896
+ commit = await commitOrRecordNoChanges(m, madeChanges);
1897
+ outcome = madeChanges ? 'applied' : 'no-changes';
1971
1898
  }
1972
1899
  }
1973
1900
  outcomes.push({
@@ -1975,6 +1902,7 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
1975
1902
  status: 'completed',
1976
1903
  kind: outcome,
1977
1904
  commit,
1905
+ waivedAgenticStep,
1978
1906
  });
1979
1907
  logger_1.logger.info('');
1980
1908
  }
@@ -2037,7 +1965,7 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
2037
1965
  await installDepsIfChanged();
2038
1966
  }
2039
1967
  if (changedDepInstaller.skippedInstall) {
2040
- logSkippedPostMigrationInstall(root);
1968
+ (0, execute_migration_1.logSkippedPostMigrationInstall)(root);
2041
1969
  }
2042
1970
  // Combined-view next-steps array kept for back-compat with repair.ts, which
2043
1971
  // consumes the single `nextSteps` field.
@@ -2053,6 +1981,7 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
2053
1981
  skippedPrompts,
2054
1982
  migrationEmittedNextSteps,
2055
1983
  committedShasCount: (0, migrate_output_1.countLandedCommits)(outcomes),
1984
+ waivedAgenticStepsCount: (0, migrate_output_1.countWaivedAgenticSteps)(outcomes),
2056
1985
  // Migrations whose commits failed and never got absorbed by a later
2057
1986
  // commit. The caller surfaces them so a successful run doesn't claim
2058
1987
  // "up to date" while leaving uncommitted diffs in the working tree.
@@ -2060,13 +1989,21 @@ async function executeMigrations(root, migrations, isVerbose, shouldCreateCommit
2060
1989
  retainedAtSuccess: (0, migrate_output_1.retainedMigrations)(outcomes).map((p) => `${p.package}: ${p.name}`),
2061
1990
  };
2062
1991
  }
2063
- function logSkippedPostMigrationInstall(root) {
2064
- const packageManager = (0, package_manager_1.detectPackageManager)(root);
2065
- const installCommand = (0, package_manager_1.getPackageManagerCommand)(packageManager, root).install;
2066
- output_1.output.warn({
2067
- title: 'Migrations updated your dependencies, but the install was skipped',
2068
- bodyLines: [`Run "${installCommand}" to install the updated dependencies.`],
2069
- });
1992
+ // Forwards this invocation's raw argv to the workspace-local nx, used from
1993
+ // each run path's temp-installation check (`!__dirname.startsWith(workspaceRoot)`)
1994
+ // right after its gated `runInstall` pre-install. `runNxSync` resolves nx
1995
+ // through the workspace's package manager at spawn time, so the child runs
1996
+ // the bytes that pre-install put in place.
1997
+ function handOffToLocalNx(args) {
1998
+ const exitCode = runOrReturnExitCode(() => (0, child_process_2.runNxSync)(`migrate ${args.join(' ')}`, {
1999
+ stdio: ['inherit', 'inherit', 'inherit'],
2000
+ env: {
2001
+ ...process.env,
2002
+ NX_MIGRATE_SKIP_INSTALL: 'true',
2003
+ NX_MIGRATE_USE_LOCAL: 'true',
2004
+ },
2005
+ }));
2006
+ return exitCode !== 0 ? exitCode : undefined;
2070
2007
  }
2071
2008
  async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, commitPrefix, shouldSkipInstall = false) {
2072
2009
  if (!shouldSkipInstall && !process.env.NX_MIGRATE_SKIP_INSTALL) {
@@ -2075,18 +2012,7 @@ async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, c
2075
2012
  if (!__dirname.startsWith(workspace_root_1.workspaceRoot)) {
2076
2013
  // we are running from a temp installation with nx latest, switch to running
2077
2014
  // from local installation
2078
- const exitCode = runOrReturnExitCode(() => (0, child_process_2.runNxSync)(`migrate ${args.join(' ')}`, {
2079
- stdio: ['inherit', 'inherit', 'inherit'],
2080
- env: {
2081
- ...process.env,
2082
- NX_MIGRATE_SKIP_INSTALL: 'true',
2083
- NX_MIGRATE_USE_LOCAL: 'true',
2084
- },
2085
- }));
2086
- if (exitCode !== 0) {
2087
- return exitCode;
2088
- }
2089
- return;
2015
+ return handOffToLocalNx(args);
2090
2016
  }
2091
2017
  const migrationsExists = (0, fileutils_1.fileExists)(opts.runMigrations);
2092
2018
  if (opts.ifExists && !migrationsExists) {
@@ -2098,12 +2024,13 @@ async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, c
2098
2024
  else if (!opts.ifExists && !migrationsExists) {
2099
2025
  throw new Error(`File '${opts.runMigrations}' doesn't exist, can't run migrations. Use flag --if-exists to run migrations only if the file exists`);
2100
2026
  }
2101
- const migrations = (0, fileutils_1.readJsonFile)((0, path_1.join)(root, opts.runMigrations)).migrations;
2027
+ const migrationsJson = (0, fileutils_1.readJsonFile)((0, path_1.join)(root, opts.runMigrations));
2028
+ const migrations = migrationsJson.migrations;
2102
2029
  (0, migrate_analytics_1.reportMigrateRunStart)({
2103
2030
  createCommits: shouldCreateCommits ?? false,
2104
2031
  migrationCount: migrations.length,
2105
2032
  });
2106
- const { resolveAgentic } = require('./agentic/select');
2033
+ const { resolveAgentic, resolveShouldRunValidation } = require('./agentic/select');
2107
2034
  let agentic;
2108
2035
  try {
2109
2036
  agentic = await resolveAgentic({
@@ -2116,7 +2043,7 @@ async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, c
2116
2043
  (0, migrate_analytics_1.reportMigrateRunError)({ code: 'agentic', error: e });
2117
2044
  throw e;
2118
2045
  }
2119
- const { effective: effectiveCreateCommits, agenticHasDiffContext, warning: createCommitsWarning, error: createCommitsError, } = resolveCreateCommits({
2046
+ const { effective: effectiveCreateCommits, agenticHasDiffContext, warning: createCommitsWarning, error: createCommitsError, } = (0, migrate_commits_1.resolveCreateCommits)({
2120
2047
  createCommits: shouldCreateCommits,
2121
2048
  agenticKind: agentic.kind,
2122
2049
  isGitRepo: (0, git_utils_1.isGitRepository)(root),
@@ -2133,7 +2060,7 @@ async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, c
2133
2060
  // `getBaseRef` may carry an `origin/` prefix (set by the CI-workflow
2134
2061
  // generator); compare against the local branch name.
2135
2062
  const defaultBranch = (0, command_line_utils_1.getBaseRef)((0, configuration_1.readNxJson)(root)).replace(/^origin\//, '');
2136
- const proceed = await confirmCommitsOnDefaultBranch({
2063
+ const proceed = await (0, migrate_commits_1.confirmCommitsOnDefaultBranch)({
2137
2064
  currentBranch,
2138
2065
  defaultBranch,
2139
2066
  });
@@ -2161,8 +2088,9 @@ async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, c
2161
2088
  (0, migrate_commits_1.commitCheckpointBeforeMigrations)(root, commitPrefix);
2162
2089
  }
2163
2090
  if (agentic.kind === 'enabled') {
2091
+ const { applyAgenticHandoffGitignoreFallback } = require('./agentic/handoff-gitignore');
2164
2092
  const { packageJson: nxPackageJson } = (0, package_json_1.readModulePackageJson)('nx', (0, installation_directory_1.getNxRequirePaths)(root));
2165
- await (0, handoff_gitignore_1.applyAgenticHandoffGitignoreFallback)({
2093
+ await applyAgenticHandoffGitignoreFallback({
2166
2094
  migrations,
2167
2095
  installedNxVersion: nxPackageJson.version,
2168
2096
  effectiveCreateCommits,
@@ -2170,7 +2098,7 @@ async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, c
2170
2098
  root,
2171
2099
  });
2172
2100
  }
2173
- const { migrationsWithNoChanges, skippedPromptsCount, notRunMigrationsCount, skippedPrompts, migrationEmittedNextSteps, committedShasCount, retainedAtSuccess, } = await executeMigrations(root, migrations, isVerbose, effectiveCreateCommits, commitPrefix, shouldSkipInstall, agentic, agenticHasDiffContext, shouldRunValidation);
2101
+ const { migrationsWithNoChanges, skippedPromptsCount, notRunMigrationsCount, skippedPrompts, migrationEmittedNextSteps, committedShasCount, waivedAgenticStepsCount, retainedAtSuccess, } = await executeMigrations(root, migrations, isVerbose, effectiveCreateCommits, commitPrefix, shouldSkipInstall, agentic, agenticHasDiffContext, shouldRunValidation);
2174
2102
  const ranWithChangesCount = migrations.length - notRunMigrationsCount - migrationsWithNoChanges.length;
2175
2103
  // The "applied" tally counts fully-completed migrations — those that
2176
2104
  // left no deferred work behind. Hybrid migrations whose prompt half was
@@ -2181,6 +2109,7 @@ async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, c
2181
2109
  appliedCount,
2182
2110
  committedShasCount,
2183
2111
  skippedPromptsCount,
2112
+ waivedAgenticStepsCount,
2184
2113
  insideAgent,
2185
2114
  });
2186
2115
  const tallyBody = tallyLine ? [tallyLine] : undefined;
@@ -2262,18 +2191,26 @@ async function runMigrations(root, opts, args, isVerbose, shouldCreateCommits, c
2262
2191
  appliedCount,
2263
2192
  });
2264
2193
  }
2194
+ function isSingleMigrationInvocation(args) {
2195
+ return args.runMigration !== undefined;
2196
+ }
2265
2197
  async function migrate(root, args, rawArgs) {
2266
2198
  await client_1.daemonClient.stop();
2267
2199
  return (0, handle_errors_1.handleErrors)(process.env.NX_VERBOSE_LOGGING === 'true', async () => {
2268
2200
  const mergedArgs = (0, migrate_config_1.applyNxJsonMigrateDefaults)(args, (0, configuration_1.readNxJson)().migrate);
2269
- (0, migrate_config_1.assertCommitPrefixHasCommits)(mergedArgs);
2201
+ // A single-migration invocation resolves its commit config downstream via
2202
+ // resolveCreateCommits, so this assert must not fire for it.
2203
+ const singleMigration = isSingleMigrationInvocation(mergedArgs);
2204
+ if (!singleMigration) {
2205
+ (0, migrate_config_1.assertCommitPrefixHasCommits)(mergedArgs);
2206
+ }
2270
2207
  // One fetcher (registry-first, install fallback) shared by the `--include`
2271
2208
  // eligibility gate and the migration cascade so package metadata is fetched
2272
2209
  // at most once per package/version.
2273
2210
  const fetch = createFetcher((0, package_manager_1.getPackageManagerCommand)());
2274
- // `--run-migrations` without a value parses as '' - undefined means the
2275
- // generate phase.
2276
- const isGenerateInvocation = mergedArgs['runMigrations'] === undefined;
2211
+ // `--run-migrations` without a value parses as '', so only undefined (and
2212
+ // no single-migration flag) means the generate phase.
2213
+ const isGenerateInvocation = mergedArgs['runMigrations'] === undefined && !singleMigration;
2277
2214
  let opts;
2278
2215
  try {
2279
2216
  opts = await parseMigrationsOptions(mergedArgs, fetch);
@@ -2282,36 +2219,282 @@ async function migrate(root, args, rawArgs) {
2282
2219
  if (isGenerateInvocation) {
2283
2220
  (0, migrate_analytics_1.reportMigrateGenerateError)('resolve_version', e);
2284
2221
  }
2222
+ else if (singleMigration) {
2223
+ (0, migrate_analytics_1.reportMigrateRunError)({ code: 'other', error: e });
2224
+ }
2285
2225
  throw e;
2286
2226
  }
2287
- if (opts.type === 'generateMigrations') {
2288
- await generateMigrationsJsonAndUpdatePackageJson(root, opts, fetch);
2227
+ switch (opts.type) {
2228
+ case 'generateMigrations':
2229
+ await generateMigrationsJsonAndUpdatePackageJson(root, opts, fetch);
2230
+ return;
2231
+ case 'runSingleMigration':
2232
+ try {
2233
+ return await runSingleMigrationFromCli(root, opts, rawArgs, mergedArgs);
2234
+ }
2235
+ catch (e) {
2236
+ // `runInstall` already logged the remediation; returning instead of
2237
+ // rethrowing keeps `handleErrors` from printing a stack over it.
2238
+ if (e instanceof execute_migration_1.NpmPeerDepsInstallError) {
2239
+ (0, migrate_analytics_1.reportMigrateRunError)({ code: 'npm_install', error: e });
2240
+ return 1;
2241
+ }
2242
+ (0, migrate_analytics_1.reportMigrateRunError)({ code: 'other', error: e });
2243
+ throw e;
2244
+ }
2245
+ case 'runMigrations':
2246
+ try {
2247
+ return await runMigrations(root, opts, rawArgs, mergedArgs['verbose'], mergedArgs['createCommits'], mergedArgs['commitPrefix'] ?? command_object_1.DEFAULT_MIGRATION_COMMIT_PREFIX, mergedArgs['skipInstall']);
2248
+ }
2249
+ catch (e) {
2250
+ if (e instanceof execute_migration_1.NpmPeerDepsInstallError) {
2251
+ (0, migrate_analytics_1.reportMigrateRunError)({ code: 'npm_install', error: e });
2252
+ return 1;
2253
+ }
2254
+ (0, migrate_analytics_1.reportMigrateRunError)({ code: 'other', error: e });
2255
+ throw e;
2256
+ }
2257
+ default: {
2258
+ // `unhandled: never` makes a new invocation type a compile error here
2259
+ // rather than a silent fall-through.
2260
+ const unhandled = opts;
2261
+ throw new Error(`Unhandled migrate invocation type: ${JSON.stringify(unhandled)}`);
2262
+ }
2289
2263
  }
2290
- else {
2291
- try {
2292
- return await runMigrations(root, opts, rawArgs, mergedArgs['verbose'], mergedArgs['createCommits'], mergedArgs['commitPrefix'] ?? command_object_1.DEFAULT_MIGRATION_COMMIT_PREFIX, mergedArgs['skipInstall']);
2264
+ });
2265
+ }
2266
+ // Evaluated manifest code can throw any value: a null-prototype object defeats
2267
+ // String() (the Object.prototype.toString rung catches it), and a revoked
2268
+ // proxy defeats that too. Interpolating either in a catch would throw again
2269
+ // and escape the catch.
2270
+ function stringifyCaught(e) {
2271
+ try {
2272
+ return String(e);
2273
+ }
2274
+ catch {
2275
+ try {
2276
+ return Object.prototype.toString.call(e);
2277
+ }
2278
+ catch {
2279
+ return '<unprintable value>';
2280
+ }
2281
+ }
2282
+ }
2283
+ // A resolver-based lookup (including `resolvePackageJsonWithoutCachePollution`,
2284
+ // which does defeat Node's package self-reference) is the wrong tool here:
2285
+ // resolvers fall back to NODE_PATH after the explicit paths, and NODE_PATH
2286
+ // names the temp installation when this runs there, while the hand-off spawn
2287
+ // locates nx through the package manager's bin lookup, which ignores
2288
+ // NODE_PATH. The PnP branch below stays bespoke either way: a resolver cannot
2289
+ // see into the zip cache without the PnP runtime.
2290
+ function readLocalNxVersion(root) {
2291
+ // A PnP manifest is consulted only when yarn drives the hand-off:
2292
+ // `getRunNxBaseCommand` builds the hand-off command through this same
2293
+ // detectPackageManager call (which answers from nx.json's
2294
+ // cli.packageManager first, before any lockfile), and yarn is the one
2295
+ // package manager that executes the manifest's nx. That mirror alone
2296
+ // cannot see a switch off yarn, because yarn.lock survives one and
2297
+ // outranks all but bun's lockfiles in the detection order, while .pnp.cjs
2298
+ // lingers only on a switch to another package manager (yarn removes it
2299
+ // when moving to node_modules linking). A rival lockfile beside the
2300
+ // manifest is therefore read as evidence of such a switch, though not
2301
+ // proof, since `pnpm install --lockfile-only` writes one without
2302
+ // installing: on a scan miss the manifest still answers rather than
2303
+ // failing the guards, and a rival install over a still-live PnP
2304
+ // workspace is read over the PnP nx the yarn spawn executes, because
2305
+ // preferring the manifest would judge the dead install that a completed
2306
+ // switch leaves behind and refuse persistently. The known misread left
2307
+ // is yarn classic driving a workspace with a leftover Berry manifest
2308
+ // and no other lockfile; telling the two yarns apart from disk is not
2309
+ // worth the fragility, and a `yarn node` runtime probe was rejected
2310
+ // because its stdout is forgeable through an inherited NODE_OPTIONS
2311
+ // preload and a hung descendant holding the output pipe outlives a
2312
+ // subprocess timeout on this synchronous path.
2313
+ const pnpManifest = ['.pnp.cjs', '.pnp.js']
2314
+ .map((f) => (0, path_1.join)(root, f))
2315
+ .find((f) => (0, fs_1.existsSync)(f));
2316
+ if (pnpManifest) {
2317
+ const competingLockfile = [
2318
+ 'package-lock.json',
2319
+ 'npm-shrinkwrap.json',
2320
+ 'pnpm-lock.yaml',
2321
+ 'bun.lockb',
2322
+ 'bun.lock',
2323
+ ].some((f) => (0, fs_1.existsSync)((0, path_1.join)(root, f)));
2324
+ if (competingLockfile) {
2325
+ const version = scanNodeModulesForNxVersion(root);
2326
+ if (version) {
2327
+ return version;
2293
2328
  }
2294
- catch (e) {
2295
- // The remediation guidance is already logged by `runInstall`; swallow
2296
- // the error here so `handleErrors` doesn't print a noisy stack after
2297
- // the friendly output.
2298
- if (e instanceof execute_migration_1.NpmPeerDepsInstallError) {
2299
- (0, migrate_analytics_1.reportMigrateRunError)({ code: 'npm_install', error: e });
2300
- return 1;
2329
+ }
2330
+ try {
2331
+ if ((0, package_manager_1.detectPackageManager)(root) === 'yarn') {
2332
+ // Compiled manually rather than required: a require would serve the
2333
+ // stale cached copy the runtime loaded at startup. new Function does
2334
+ // not strip a leading shebang like the CJS loader, so drop it here.
2335
+ const manifestModule = {
2336
+ exports: {},
2337
+ };
2338
+ new Function('module', 'exports', 'require', '__dirname', '__filename', (0, fs_1.readFileSync)(pnpManifest, 'utf-8').replace(/^#!.*/, ''))(manifestModule, manifestModule.exports, require, root, pnpManifest);
2339
+ const packageJsonPath = manifestModule.exports.resolveRequest('nx/package.json', (0, path_1.join)(root, 'package.json'));
2340
+ if (packageJsonPath) {
2341
+ // A resolving manifest names the install the yarn spawn runs, so
2342
+ // every branch below returns rather than falling through to the
2343
+ // ancestor scan.
2344
+ try {
2345
+ const { version } = (0, fileutils_1.readJsonFile)(packageJsonPath);
2346
+ if (typeof version === 'string') {
2347
+ logger_1.logger.verbose(`Read the workspace's nx version ${version} from '${packageJsonPath}'.`);
2348
+ return version;
2349
+ }
2350
+ logger_1.logger.verbose(`'${packageJsonPath}' does not declare a usable version.`);
2351
+ return undefined;
2352
+ }
2353
+ catch (readError) {
2354
+ // A zip-served path (installs with scripts disabled keep nx
2355
+ // zipped) cannot be read without the PnP runtime's zip fs, but
2356
+ // the manifest's own locator still names the version (e.g.
2357
+ // 'virtual:<hash>#npm:23.1.0').
2358
+ let version;
2359
+ try {
2360
+ const reference = manifestModule.exports.findPackageLocator?.(packageJsonPath)?.reference;
2361
+ version =
2362
+ typeof reference === 'string'
2363
+ ? reference.match(/npm:([^#:]+)/)?.[1]
2364
+ : undefined;
2365
+ }
2366
+ catch (locatorError) {
2367
+ logger_1.logger.verbose(`Could not read the nx locator from the PnP manifest '${pnpManifest}': ${stringifyCaught(locatorError)}`);
2368
+ }
2369
+ if (version && (0, semver_1.valid)(version)) {
2370
+ logger_1.logger.verbose(`Read the workspace's nx version ${version} from the locator in the PnP manifest '${pnpManifest}'.`);
2371
+ return version;
2372
+ }
2373
+ logger_1.logger.verbose(`Could not read the workspace's nx version through the PnP manifest '${pnpManifest}': ${stringifyCaught(readError)}`);
2374
+ return undefined;
2375
+ }
2301
2376
  }
2302
- (0, migrate_analytics_1.reportMigrateRunError)({ code: 'other', error: e });
2303
- throw e;
2304
2377
  }
2305
2378
  }
2379
+ catch (e) {
2380
+ // Degrade to the full scan: neither a failed detection nor an
2381
+ // unevaluable manifest can name the spawn's nx.
2382
+ logger_1.logger.verbose(`Could not read the workspace's nx version through the PnP manifest '${pnpManifest}': ${stringifyCaught(e)}`);
2383
+ }
2384
+ }
2385
+ return scanNodeModulesForNxVersion(root);
2386
+ }
2387
+ // The workspace's own install locations first (.nx/installation, root), then
2388
+ // ancestor directories: package-manager executable lookup ascends (npx and
2389
+ // bun always, pnpm and yarn inside an outer workspace) and hoisted layouts
2390
+ // install nx above the workspace root. An unrelated ancestor install the
2391
+ // hand-off's spawn would not run can be read here; the guards then judge that
2392
+ // version rather than failing open. A hand-off issued anyway may not fail
2393
+ // visibly: yarn reports "command not found", but pnpm exec falls back to an
2394
+ // nx on PATH, so the version read here is the readable answer, not
2395
+ // necessarily the executed one.
2396
+ function scanNodeModulesForNxVersion(root) {
2397
+ const searchPaths = [...(0, installation_directory_1.getNxRequirePaths)(root)];
2398
+ for (let dir = (0, path_1.dirname)(root);; dir = (0, path_1.dirname)(dir)) {
2399
+ searchPaths.push(dir);
2400
+ if (dir === (0, path_1.dirname)(dir)) {
2401
+ break;
2402
+ }
2403
+ }
2404
+ return readNxVersionFromNodeModules(searchPaths);
2405
+ }
2406
+ function readNxVersionFromNodeModules(searchPaths) {
2407
+ for (const searchPath of searchPaths) {
2408
+ const packageJsonPath = (0, path_1.join)(searchPath, 'node_modules', 'nx', 'package.json');
2409
+ try {
2410
+ if ((0, fs_1.existsSync)(packageJsonPath)) {
2411
+ const { version } = (0, fileutils_1.readJsonFile)(packageJsonPath);
2412
+ if (typeof version === 'string') {
2413
+ logger_1.logger.verbose(`Resolved nx version ${version} from '${packageJsonPath}'.`);
2414
+ return version;
2415
+ }
2416
+ logger_1.logger.verbose(`'${packageJsonPath}' does not declare a usable version; continuing the search.`);
2417
+ }
2418
+ }
2419
+ catch (e) {
2420
+ logger_1.logger.verbose(`Could not read the workspace's installed nx version from '${packageJsonPath}': ${e}`);
2421
+ }
2422
+ }
2423
+ return undefined;
2424
+ }
2425
+ // Keeps `--run-migration` behaving like `--run-migrations` when invoked from
2426
+ // a temp `nx@latest` install: same pre-install, same local-nx hand-off.
2427
+ async function runSingleMigrationFromCli(root, opts, args, mergedArgs) {
2428
+ const shouldSkipInstall = mergedArgs['skipInstall'] ?? false;
2429
+ if (!shouldSkipInstall && !process.env.NX_MIGRATE_SKIP_INSTALL) {
2430
+ await (0, execute_migration_1.runInstall)(undefined, 'pre-migration', (0, execute_migration_1.formatSingleMigrationRerunCommand)(opts.runMigration));
2431
+ }
2432
+ if (!__dirname.startsWith(workspace_root_1.workspaceRoot)) {
2433
+ // Must stay after the pre-install: it reads the version that install may
2434
+ // have just updated, so checking earlier would falsely refuse a workspace
2435
+ // whose package.json bump was not installed yet.
2436
+ (0, version_skew_guard_1.assertWorkspaceNxSupportsNewMigrateFlags)({
2437
+ argv: args,
2438
+ // `workspaceRoot`, not `root`: the migrate flow's `root` is the
2439
+ // invocation directory, which may sit below the workspace the hand-off
2440
+ // resolves nx in.
2441
+ readLocalNxVersion: () => readLocalNxVersion(workspace_root_1.workspaceRoot),
2442
+ });
2443
+ return handOffToLocalNx(args);
2444
+ }
2445
+ // The worker resolves the agentic flow, the effective commit config, and
2446
+ // the default-branch confirmation itself; hand it the raw flags.
2447
+ // Lazy-load run/ so plain migrate and repair runs don't pay for the agentic
2448
+ // selection chain its barrel pulls in eagerly.
2449
+ const { runSingleMigrationWorker } = require('./run');
2450
+ await runSingleMigrationWorker({
2451
+ root,
2452
+ runMigration: opts.runMigration,
2453
+ agentic: opts.agentic,
2454
+ validate: opts.validate,
2455
+ createCommits: mergedArgs['createCommits'],
2456
+ commitPrefix: mergedArgs['commitPrefix'] ??
2457
+ command_object_1.DEFAULT_MIGRATION_COMMIT_PREFIX,
2458
+ interactive: opts.interactive,
2459
+ skipInstall: shouldSkipInstall,
2460
+ isVerbose: mergedArgs['verbose'] ?? false,
2306
2461
  });
2307
2462
  }
2308
2463
  async function runMigration() {
2309
2464
  return (0, handle_errors_1.handleErrors)(process.env.NX_VERBOSE_LOGGING === 'true', async () => {
2465
+ // Spawning `nx _migrate` re-resolves the workspace's nx, normally the
2466
+ // very installation running this code: bin/nx.ts delegated the outer
2467
+ // `migrate` command to it, while `_migrate` skips that delegation. A
2468
+ // new-flag invocation reaching this fallback therefore lands on an nx
2469
+ // that parsed the flags, so it needs no capability check. Two lookups
2470
+ // can still diverge from the running install, both properties of the
2471
+ // spawn machinery rather than of this fallback: a `.nx/installation`
2472
+ // beside a root package.json, and a spawn that misses the workspace
2473
+ // install and falls back to an nx on PATH (pnpm exec).
2310
2474
  const runLocalMigrate = () => runOrReturnExitCode(() => (0, child_process_2.runNxSync)(`_migrate ${process.argv.slice(3).join(' ')}`, {
2311
2475
  stdio: ['inherit', 'inherit', 'inherit'],
2312
2476
  }));
2313
2477
  if (process.env.NX_USE_LOCAL !== 'true' &&
2314
2478
  process.env.NX_MIGRATE_USE_LOCAL === undefined) {
2479
+ // The version spec mirrors nxCliPath's; keep the two in sync.
2480
+ const cliVersionSpec = process.env.NX_MIGRATE_CLI_VERSION || 'latest';
2481
+ const target = await (0, version_skew_guard_1.resolveNewMigrateFlagsRunTarget)({
2482
+ argv: process.argv.slice(3),
2483
+ cliVersionSpec,
2484
+ fromEnvOverride: !!process.env.NX_MIGRATE_CLI_VERSION,
2485
+ ownNxVersion: versions_1.nxVersion,
2486
+ resolveVersion: (spec) =>
2487
+ // A probe must not prompt, write pnpm excludes, or install; a
2488
+ // minimum-release-age violation surfaces as a rejection and
2489
+ // routes local.
2490
+ (0, resolve_package_version_1.resolvePackageVersionRespectingMinReleaseAge)('nx', spec, {
2491
+ applySideEffects: false,
2492
+ }),
2493
+ readLocalNxVersion: () => readLocalNxVersion(workspace_root_1.workspaceRoot),
2494
+ });
2495
+ if (target === 'local-nx') {
2496
+ return runLocalMigrate();
2497
+ }
2315
2498
  const p = await nxCliPath();
2316
2499
  if (p === null) {
2317
2500
  return runLocalMigrate();
@@ -2365,7 +2548,7 @@ function resolveMigrationForRun(root, migration, resolveDocumentation) {
2365
2548
  let documentationPath;
2366
2549
  if (resolveDocumentation && migration.documentation) {
2367
2550
  documentationPath = resolvedCollection
2368
- ? resolveDocumentationFileToWorkspacePath(root, (0, path_1.dirname)(resolvedCollection.collectionPath), migration.documentation)
2551
+ ? (0, execute_migration_1.resolveDocumentationFileToWorkspacePath)(root, (0, path_1.dirname)(resolvedCollection.collectionPath), migration.documentation)
2369
2552
  : undefined;
2370
2553
  if (!documentationPath) {
2371
2554
  logger_1.logger.warn(`Could not resolve the "documentation" file "${migration.documentation}" declared for migration "${migration.package}: ${migration.name}". It will be skipped as additional context for the AI agent.`);
@@ -2373,24 +2556,6 @@ function resolveMigrationForRun(root, migration, resolveDocumentation) {
2373
2556
  }
2374
2557
  return { resolvedCollection, documentationPath };
2375
2558
  }
2376
- // Resolves a `documentation` path (relative to the package's migrations dir) to
2377
- // a workspace-relative path - or the absolute path when it resolves outside the
2378
- // workspace (unusual hoisted/symlinked layouts). The agent runs with cwd =
2379
- // workspace root, so the workspace-relative form is preferred. Returns
2380
- // undefined when the file can't be resolved.
2381
- function resolveDocumentationFileToWorkspacePath(root, migrationsDir, documentation) {
2382
- let documentationFile;
2383
- try {
2384
- documentationFile = require.resolve(documentation, {
2385
- paths: [migrationsDir],
2386
- });
2387
- }
2388
- catch {
2389
- return undefined;
2390
- }
2391
- const relativePath = (0, path_1.relative)(root, documentationFile);
2392
- return relativePath.startsWith('..') ? documentationFile : relativePath;
2393
- }
2394
2559
  async function nxCliPath(nxWorkspaceRoot) {
2395
2560
  const version = process.env.NX_MIGRATE_CLI_VERSION || 'latest';
2396
2561
  const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
@@ -2441,7 +2606,7 @@ async function nxCliPath(nxWorkspaceRoot) {
2441
2606
  return (0, path_1.join)(tmpDir, `node_modules`, '.bin', 'nx');
2442
2607
  }
2443
2608
  catch (e) {
2444
- console.error(`Failed to install the ${version} version of the migration script. Using the current version.`);
2609
+ console.error(`Failed to install the ${version} version of the migration script. Falling back to the workspace's installed nx.`);
2445
2610
  if (isVerbose) {
2446
2611
  console.error(e);
2447
2612
  }