nx 23.2.0-beta.7 → 23.2.0-beta.8

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 (130) hide show
  1. package/dist/bin/init-local.js +3 -5
  2. package/dist/release/changelog-renderer/index.js +4 -5
  3. package/dist/src/adapter/ngcli-adapter.js +60 -1
  4. package/dist/src/command-line/completion/command-object.js +7 -12
  5. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +17 -23
  6. package/dist/src/command-line/generate/generate.js +35 -35
  7. package/dist/src/command-line/import/import.js +18 -40
  8. package/dist/src/command-line/init/ai-agent-prompts.js +5 -11
  9. package/dist/src/command-line/init/command-object.js +4 -2
  10. package/dist/src/command-line/init/implementation/add-nx-to-monorepo.js +12 -32
  11. package/dist/src/command-line/init/implementation/add-nx-to-nest.js +8 -21
  12. package/dist/src/command-line/init/implementation/add-nx-to-npm-repo.js +8 -21
  13. package/dist/src/command-line/init/implementation/angular/index.js +6 -15
  14. package/dist/src/command-line/init/init-v1.js +8 -18
  15. package/dist/src/command-line/init/init-v2.js +26 -56
  16. package/dist/src/command-line/migrate/agentic/definitions.js +35 -8
  17. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +22 -3
  18. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +13 -10
  19. package/dist/src/command-line/migrate/agentic/handoff.d.ts +3 -1
  20. package/dist/src/command-line/migrate/agentic/handoff.js +4 -2
  21. package/dist/src/command-line/migrate/agentic/run-step.js +1 -0
  22. package/dist/src/command-line/migrate/agentic/runner.js +24 -24
  23. package/dist/src/command-line/migrate/agentic/select.js +20 -30
  24. package/dist/src/command-line/migrate/agentic/types.d.ts +19 -3
  25. package/dist/src/command-line/migrate/agentic/types.js +13 -4
  26. package/dist/src/command-line/migrate/command-object.d.ts +3 -0
  27. package/dist/src/command-line/migrate/command-object.js +13 -0
  28. package/dist/src/command-line/migrate/execute-migration.d.ts +14 -0
  29. package/dist/src/command-line/migrate/execute-migration.js +30 -9
  30. package/dist/src/command-line/migrate/migrate-analytics.d.ts +28 -0
  31. package/dist/src/command-line/migrate/migrate-analytics.js +51 -1
  32. package/dist/src/command-line/migrate/migrate-commits.d.ts +8 -2
  33. package/dist/src/command-line/migrate/migrate-commits.js +66 -19
  34. package/dist/src/command-line/migrate/migrate-config.d.ts +4 -2
  35. package/dist/src/command-line/migrate/migrate-config.js +12 -4
  36. package/dist/src/command-line/migrate/migrate.d.ts +9 -2
  37. package/dist/src/command-line/migrate/migrate.js +179 -56
  38. package/dist/src/command-line/migrate/multi-major.js +7 -10
  39. package/dist/src/command-line/migrate/resolve-package-version.js +4 -8
  40. package/dist/src/command-line/migrate/run/agent-output.d.ts +20 -0
  41. package/dist/src/command-line/migrate/run/agent-output.js +65 -0
  42. package/dist/src/command-line/migrate/run/index.d.ts +7 -0
  43. package/dist/src/command-line/migrate/run/index.js +19 -1
  44. package/dist/src/command-line/migrate/run/orchestrator.d.ts +20 -0
  45. package/dist/src/command-line/migrate/run/orchestrator.js +1200 -0
  46. package/dist/src/command-line/migrate/run/run-id.d.ts +16 -0
  47. package/dist/src/command-line/migrate/run/run-id.js +56 -0
  48. package/dist/src/command-line/migrate/run/run-state.d.ts +157 -0
  49. package/dist/src/command-line/migrate/run/run-state.js +438 -0
  50. package/dist/src/command-line/migrate/run/state-lock.d.ts +25 -0
  51. package/dist/src/command-line/migrate/run/state-lock.js +76 -0
  52. package/dist/src/command-line/migrate/run/state-machine.d.ts +84 -0
  53. package/dist/src/command-line/migrate/run/state-machine.js +315 -0
  54. package/dist/src/command-line/migrate/run/util.d.ts +58 -0
  55. package/dist/src/command-line/migrate/run/util.js +139 -7
  56. package/dist/src/command-line/migrate/run/worker.d.ts +1 -0
  57. package/dist/src/command-line/migrate/run/worker.js +308 -45
  58. package/dist/src/command-line/migrate/safe-prompt.d.ts +14 -26
  59. package/dist/src/command-line/migrate/safe-prompt.js +32 -43
  60. package/dist/src/command-line/migrate/step-actions.d.ts +5 -0
  61. package/dist/src/command-line/migrate/step-actions.js +12 -0
  62. package/dist/src/command-line/migrate/text.d.ts +18 -0
  63. package/dist/src/command-line/migrate/text.js +26 -0
  64. package/dist/src/command-line/migrate/version-skew-guard.d.ts +6 -4
  65. package/dist/src/command-line/migrate/version-skew-guard.js +34 -14
  66. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +16 -20
  67. package/dist/src/command-line/release/changelog.js +6 -17
  68. package/dist/src/command-line/release/plan.js +15 -28
  69. package/dist/src/command-line/release/release.js +6 -17
  70. package/dist/src/command-line/release/utils/remote-release-clients/github.js +16 -22
  71. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +13 -22
  72. package/dist/src/command-line/release/utils/resolve-semver-specifier.js +19 -41
  73. package/dist/src/command-line/release/version/resolve-current-version.js +7 -10
  74. package/dist/src/core/graph/main.js +1 -1
  75. package/dist/src/devkit-internals.d.ts +2 -0
  76. package/dist/src/devkit-internals.js +12 -4
  77. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.d.ts +12 -0
  78. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.js +250 -0
  79. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.md +50 -0
  80. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  81. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  82. package/dist/src/plugins/js/lock-file/bun-parser.js +43 -10
  83. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +128 -2
  84. package/dist/src/tasks-runner/run-command.js +17 -32
  85. package/dist/src/tasks-runner/task-env.js +5 -2
  86. package/dist/src/tasks-runner/utils.js +2 -6
  87. package/dist/src/utils/analytics-prompt.js +4 -7
  88. package/dist/src/utils/exit-codes.d.ts +9 -0
  89. package/dist/src/utils/exit-codes.js +19 -0
  90. package/dist/src/utils/fileutils.d.ts +5 -0
  91. package/dist/src/utils/fileutils.js +7 -2
  92. package/dist/src/utils/git-utils.d.ts +33 -2
  93. package/dist/src/utils/git-utils.js +156 -8
  94. package/dist/src/utils/long-running-target.d.ts +10 -0
  95. package/dist/src/utils/long-running-target.js +19 -0
  96. package/dist/src/utils/min-release-age/behavior/bun.js +7 -11
  97. package/dist/src/utils/min-release-age/behavior/npm.js +5 -2
  98. package/dist/src/utils/min-release-age/packument.js +1 -1
  99. package/dist/src/utils/nx-console-prompt.js +3 -6
  100. package/dist/src/utils/package-manager-config/bunfig.d.ts +14 -0
  101. package/dist/src/utils/package-manager-config/bunfig.js +49 -0
  102. package/dist/src/utils/package-manager-config/npmrc.d.ts +23 -0
  103. package/dist/src/utils/package-manager-config/npmrc.js +124 -0
  104. package/dist/src/utils/package-manager-config/pnpm-config.d.ts +17 -0
  105. package/dist/src/utils/package-manager-config/pnpm-config.js +60 -0
  106. package/dist/src/utils/package-manager.d.ts +21 -2
  107. package/dist/src/utils/package-manager.js +244 -47
  108. package/dist/src/utils/params.d.ts +7 -1
  109. package/dist/src/utils/params.js +64 -6
  110. package/dist/src/utils/prompt-helpers.d.ts +53 -0
  111. package/dist/src/utils/prompt-helpers.js +111 -0
  112. package/dist/src/utils/provenance.js +6 -14
  113. package/dist/src/utils/registry-config/bun.d.ts +2 -0
  114. package/dist/src/utils/registry-config/bun.js +257 -0
  115. package/dist/src/utils/registry-config/index.d.ts +13 -0
  116. package/dist/src/utils/registry-config/index.js +102 -0
  117. package/dist/src/utils/registry-config/pnpm.d.ts +2 -0
  118. package/dist/src/utils/registry-config/pnpm.js +1551 -0
  119. package/dist/src/utils/registry-config/utils.d.ts +195 -0
  120. package/dist/src/utils/registry-config/utils.js +496 -0
  121. package/dist/src/utils/registry-config/yarn-berry.d.ts +2 -0
  122. package/dist/src/utils/registry-config/yarn-berry.js +612 -0
  123. package/dist/src/utils/registry-config/yarn-classic.d.ts +2 -0
  124. package/dist/src/utils/registry-config/yarn-classic.js +1001 -0
  125. package/dist/src/utils/safe-spawn.d.ts +24 -0
  126. package/dist/src/utils/safe-spawn.js +104 -0
  127. package/migrations.json +6 -0
  128. package/package.json +17 -13
  129. package/dist/src/utils/min-release-age/npmrc.d.ts +0 -15
  130. package/dist/src/utils/min-release-age/npmrc.js +0 -45
@@ -0,0 +1,25 @@
1
+ import { type MigrateRunState } from './run-state';
2
+ /**
3
+ * Runs `fn` while holding the run's state lock, releasing it afterwards even
4
+ * if `fn` throws. writeRunState's tmp+rename gives per-write atomicity, but a
5
+ * writer that reads state, applies an event, then writes still races a second
6
+ * nx migrate process that read the same state first; this lock serializes
7
+ * those sequences so the event always applies to the freshest on-disk state.
8
+ */
9
+ export declare function withRunStateLock<T>(runDirPath: string, fn: () => T): T;
10
+ /**
11
+ * Serializes active-run discovery and run creation across nx migrate
12
+ * processes. Two concurrent inits could otherwise both observe "no active
13
+ * run" and create competing runs against the same workspace; the per-run
14
+ * state lock cannot cover that window because the run directory does not
15
+ * exist yet. Callers must redo their active-run check inside `fn`: a check
16
+ * done before acquiring the lock may predate a concurrent creation.
17
+ */
18
+ export declare function withRunCreationLock<T>(root: string, fn: () => T): T;
19
+ /**
20
+ * Reads the run state fresh under the lock, hands it to `apply`, and writes the
21
+ * result back. `apply` returning null means "no change" and skips the write.
22
+ * `apply` must be pure and synchronous; a corrupt or newer-format run.json
23
+ * propagates from the read.
24
+ */
25
+ export declare function updateRunState(runDirPath: string, apply: (fresh: MigrateRunState) => MigrateRunState | null): MigrateRunState;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ // Cross-process mutual exclusion for the migrate run flow, built on the
3
+ // native flock-based FileLock (the same primitive the project graph uses to
4
+ // serialize nx processes): acquisition blocks until the holder releases, and
5
+ // the kernel releases a dead holder's lock automatically, so there is no
6
+ // stale-lock state to detect or clean up. Under WASM the native lock is
7
+ // unavailable and sections run unserialized, matching the project graph's
8
+ // IS_WASM guard for this lock. Not part of run/'s public surface (not
9
+ // re-exported via ./index): import directly within run/.
10
+ //
11
+ // Locked sections must stay synchronous. lock() blocks the whole thread, so
12
+ // if a holder parked on an await while a second in-process caller reached
13
+ // lock(), the holder's continuation could never run to release it. Git and
14
+ // child-process side effects belong outside the lock for the same reason.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.withRunStateLock = withRunStateLock;
17
+ exports.withRunCreationLock = withRunCreationLock;
18
+ exports.updateRunState = updateRunState;
19
+ const fs_1 = require("fs");
20
+ const path_1 = require("path");
21
+ const native_1 = require("../../../native");
22
+ const run_state_1 = require("./run-state");
23
+ const STATE_LOCK_FILE_NAME = 'run.json.lock';
24
+ const CREATION_LOCK_FILE_NAME = 'init.lock';
25
+ function withFileLock(lockPath, fn) {
26
+ if (native_1.IS_WASM) {
27
+ return fn();
28
+ }
29
+ const lock = new native_1.FileLock(lockPath);
30
+ lock.lock();
31
+ try {
32
+ return fn();
33
+ }
34
+ finally {
35
+ lock.unlock();
36
+ }
37
+ }
38
+ /**
39
+ * Runs `fn` while holding the run's state lock, releasing it afterwards even
40
+ * if `fn` throws. writeRunState's tmp+rename gives per-write atomicity, but a
41
+ * writer that reads state, applies an event, then writes still races a second
42
+ * nx migrate process that read the same state first; this lock serializes
43
+ * those sequences so the event always applies to the freshest on-disk state.
44
+ */
45
+ function withRunStateLock(runDirPath, fn) {
46
+ return withFileLock((0, path_1.join)(runDirPath, STATE_LOCK_FILE_NAME), fn);
47
+ }
48
+ /**
49
+ * Serializes active-run discovery and run creation across nx migrate
50
+ * processes. Two concurrent inits could otherwise both observe "no active
51
+ * run" and create competing runs against the same workspace; the per-run
52
+ * state lock cannot cover that window because the run directory does not
53
+ * exist yet. Callers must redo their active-run check inside `fn`: a check
54
+ * done before acquiring the lock may predate a concurrent creation.
55
+ */
56
+ function withRunCreationLock(root, fn) {
57
+ const dir = (0, run_state_1.migrateRunsDir)(root);
58
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
59
+ return withFileLock((0, path_1.join)(dir, CREATION_LOCK_FILE_NAME), fn);
60
+ }
61
+ /**
62
+ * Reads the run state fresh under the lock, hands it to `apply`, and writes the
63
+ * result back. `apply` returning null means "no change" and skips the write.
64
+ * `apply` must be pure and synchronous; a corrupt or newer-format run.json
65
+ * propagates from the read.
66
+ */
67
+ function updateRunState(runDirPath, apply) {
68
+ return withRunStateLock(runDirPath, () => {
69
+ const fresh = (0, run_state_1.readRunState)(runDirPath);
70
+ const next = apply(fresh);
71
+ if (next === null)
72
+ return fresh;
73
+ (0, run_state_1.writeRunState)(runDirPath, next);
74
+ return next;
75
+ });
76
+ }
@@ -0,0 +1,84 @@
1
+ import type { CommitResult } from '../migrate-commits';
2
+ import type { MigrateCommitLedgerEntry, MigrateRunState, MigrateStepOutcome, MigrateStepPromptOutcome } from './run-state';
3
+ import type { StepAction } from '../step-actions';
4
+ export type { StepAction };
5
+ export type StepEvent = {
6
+ type: 'dispense';
7
+ stepId: string;
8
+ } | {
9
+ type: 'start';
10
+ stepId: string;
11
+ pid: number;
12
+ startedAt: string;
13
+ } | {
14
+ type: 'succeed';
15
+ stepId: string;
16
+ finishedAt: string;
17
+ outcome?: MigrateStepOutcome;
18
+ } | {
19
+ type: 'fail';
20
+ stepId: string;
21
+ finishedAt: string;
22
+ outcome?: MigrateStepOutcome;
23
+ } | {
24
+ type: 'awaitPromptOutcome';
25
+ stepId: string;
26
+ finishedAt: string;
27
+ } | {
28
+ type: 'foldPromptOutcome';
29
+ stepId: string;
30
+ attempt: number;
31
+ promptOutcome: MigrateStepPromptOutcome;
32
+ } | {
33
+ type: 'markGeneratorCompleted';
34
+ stepId: string;
35
+ } | {
36
+ type: 'markDied';
37
+ stepId: string;
38
+ attempt: number;
39
+ } | {
40
+ type: 'stepAction';
41
+ stepId: string;
42
+ action: StepAction;
43
+ attempt: number;
44
+ };
45
+ export type ApplyStepEventResult = {
46
+ kind: 'ok';
47
+ state: MigrateRunState;
48
+ } | {
49
+ kind: 'error';
50
+ reason: string;
51
+ };
52
+ export declare function applyStepEvent(state: MigrateRunState, event: StepEvent): ApplyStepEventResult;
53
+ /**
54
+ * Records that the run could not install the dependency changes a step left
55
+ * behind. Not a {@link StepEvent}: it annotates a step instead of moving it,
56
+ * and every status can carry it, since the orchestrator marks a step it has
57
+ * just settled while the worker marks one it is about to fail.
58
+ */
59
+ export declare function markInstallFailed(state: MigrateRunState, stepId: string): MigrateRunState;
60
+ export declare function uncoveredFailedStepIds(state: MigrateRunState): string[];
61
+ /**
62
+ * A step has commit debt when a 'failed' ledger entry names it and no later
63
+ * 'landed' entry also names it (checkpoint entries neither create nor cover
64
+ * debt). There is no per-step commit object; debt is always derived from the
65
+ * ledger.
66
+ */
67
+ export declare function hasPendingCommitDebt(state: MigrateRunState): boolean;
68
+ export declare function coveringLandedEntries(state: MigrateRunState, stepId: string): MigrateCommitLedgerEntry[];
69
+ export declare function latestRound(state: MigrateRunState): MigrateRunState['rounds'][number] | undefined;
70
+ export declare function splitMigrationId(id: string): {
71
+ package: string;
72
+ name: string;
73
+ };
74
+ export declare function stepsToPendingMigrations(state: MigrateRunState, stepIds: string[]): {
75
+ package: string;
76
+ name: string;
77
+ }[];
78
+ /**
79
+ * Classifies a commit attempt into the ledger entry to record, or null when
80
+ * there is nothing to record ('no-changes' / 'disabled'). A landed entry
81
+ * covers the absorbed steps too: the commit's `git add -A` captured their
82
+ * diffs. A failed entry records only this step's debt.
83
+ */
84
+ export declare function commitResultToLedgerEntry(result: CommitResult, stepId: string, absorbedStepIds: string[]): MigrateCommitLedgerEntry | null;
@@ -0,0 +1,315 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyStepEvent = applyStepEvent;
4
+ exports.markInstallFailed = markInstallFailed;
5
+ exports.uncoveredFailedStepIds = uncoveredFailedStepIds;
6
+ exports.hasPendingCommitDebt = hasPendingCommitDebt;
7
+ exports.coveringLandedEntries = coveringLandedEntries;
8
+ exports.latestRound = latestRound;
9
+ exports.splitMigrationId = splitMigrationId;
10
+ exports.stepsToPendingMigrations = stepsToPendingMigrations;
11
+ exports.commitResultToLedgerEntry = commitResultToLedgerEntry;
12
+ const PROMPT_OUTCOME_TO_STEP_STATUS = {
13
+ completed: 'succeeded',
14
+ skipped: 'skipped',
15
+ failed: 'failed',
16
+ };
17
+ function applyStepEvent(state, event) {
18
+ const index = state.steps.findIndex((s) => s.id === event.stepId);
19
+ if (index === -1) {
20
+ return {
21
+ kind: 'error',
22
+ reason: `No step with id '${event.stepId}' in run state.`,
23
+ };
24
+ }
25
+ const step = state.steps[index];
26
+ switch (event.type) {
27
+ case 'dispense':
28
+ // 'failed' and 'died' steps are re-armed to 'pending' by a stepAction
29
+ // first; this event never dispenses them directly.
30
+ if (step.status !== 'pending')
31
+ return illegal(step, event.type);
32
+ return commit(state, index, {
33
+ ...step,
34
+ status: 'dispensed',
35
+ dispenseCount: step.dispenseCount + 1,
36
+ });
37
+ case 'start':
38
+ if (step.status !== 'dispensed')
39
+ return illegal(step, event.type);
40
+ return commit(state, index, {
41
+ ...step,
42
+ status: 'running',
43
+ pid: event.pid,
44
+ startedAt: event.startedAt,
45
+ });
46
+ case 'succeed':
47
+ if (step.status !== 'running')
48
+ return illegal(step, event.type);
49
+ return commit(state, index, {
50
+ ...step,
51
+ status: 'succeeded',
52
+ finishedAt: event.finishedAt,
53
+ ...(event.outcome ? { outcome: event.outcome } : {}),
54
+ });
55
+ case 'fail':
56
+ if (step.status !== 'running')
57
+ return illegal(step, event.type);
58
+ return commit(state, index, {
59
+ ...step,
60
+ status: 'failed',
61
+ finishedAt: event.finishedAt,
62
+ ...(event.outcome ? { outcome: event.outcome } : {}),
63
+ });
64
+ case 'awaitPromptOutcome':
65
+ if (step.status !== 'running')
66
+ return illegal(step, event.type);
67
+ return commit(state, index, {
68
+ ...step,
69
+ status: 'awaiting-prompt-outcome',
70
+ finishedAt: event.finishedAt,
71
+ });
72
+ case 'markGeneratorCompleted':
73
+ if (step.status !== 'running')
74
+ return illegal(step, event.type);
75
+ return commit(state, index, { ...step, generatorCompleted: true });
76
+ case 'foldPromptOutcome':
77
+ if (step.status !== 'awaiting-prompt-outcome')
78
+ return illegal(step, event.type);
79
+ if (step.attempt !== event.attempt)
80
+ return staleAttempt(step, event.type, event.attempt);
81
+ return commit(state, index, {
82
+ ...step,
83
+ status: PROMPT_OUTCOME_TO_STEP_STATUS[event.promptOutcome.status],
84
+ promptOutcome: event.promptOutcome,
85
+ });
86
+ case 'markDied':
87
+ // A step awaiting a prompt outcome has no live process left to die;
88
+ // only a running step can.
89
+ if (step.status !== 'running')
90
+ return illegal(step, event.type);
91
+ if (step.attempt !== event.attempt)
92
+ return staleAttempt(step, event.type, event.attempt);
93
+ return commit(state, index, { ...step, status: 'died' });
94
+ case 'stepAction':
95
+ if (step.attempt !== event.attempt)
96
+ return staleAttempt(step, event.type, event.attempt);
97
+ return applyStepAction(state, index, step, event.action);
98
+ default: {
99
+ const exhaustive = event;
100
+ return exhaustive;
101
+ }
102
+ }
103
+ }
104
+ function applyStepAction(state, index, step, action) {
105
+ if (step.status === 'failed') {
106
+ switch (action) {
107
+ case 'retry':
108
+ // Nothing resets the tree, so the generator's changes are still in it.
109
+ // A pre-marker retry is additionally gated at the reconcile acceptance
110
+ // site, which can read the live tree; this pure layer cannot.
111
+ return commit(state, index, rearm(step, true));
112
+ case 'retry-clean':
113
+ // A failed generator can have written before throwing (a direct fs or
114
+ // exec side effect, or a crash mid-flush), so a reset-backed retry is
115
+ // offered under the same guard as for a death.
116
+ return commit(state, index, cleanRearm(state, step));
117
+ case 'skip':
118
+ return commit(state, index, { ...step, status: 'skipped' });
119
+ }
120
+ }
121
+ if (step.status === 'died') {
122
+ switch (action) {
123
+ case 'retry':
124
+ // Same no-reset rearm as from 'failed', and legal only once the
125
+ // generator half is recorded (or the step never had one): that is
126
+ // what leaves the redispensed worker something to do (a prompt, or
127
+ // the install and commit) other than reapplying the generator over
128
+ // its own changes.
129
+ if (step.generatorCompleted === true || step.hasGenerator === false) {
130
+ return commit(state, index, rearm(step, true));
131
+ }
132
+ return {
133
+ kind: 'error',
134
+ reason: `Cannot apply action 'retry' to step '${step.id}': the worker died before recording that its generator ran, so keeping the current tree could apply the migration twice. Use 'retry-clean', 'adopt' or 'skip' instead.`,
135
+ };
136
+ case 'retry-clean':
137
+ return commit(state, index, cleanRearm(state, step));
138
+ case 'adopt':
139
+ return commit(state, index, {
140
+ ...step,
141
+ status: 'succeeded',
142
+ outcome: { ...step.outcome, summary: adoptedSummary(step) },
143
+ });
144
+ case 'skip':
145
+ // Same as skipping a failure: the tree stays as the worker left it.
146
+ return commit(state, index, { ...step, status: 'skipped' });
147
+ }
148
+ }
149
+ return {
150
+ kind: 'error',
151
+ reason: `Cannot apply action '${action}' to step '${step.id}' in status '${step.status}'.`,
152
+ };
153
+ }
154
+ // Re-arms for a retry that resets the tree first. The reset target predates
155
+ // the generator unless a commit of this step's own already carries it, so the
156
+ // marker only survives when such a commit exists; otherwise the reset discards
157
+ // the generator's changes and the retry has to run it again.
158
+ function cleanRearm(state, step) {
159
+ return rearm(step, coveringLandedEntries(state, step.id).length > 0);
160
+ }
161
+ // An adopted death records how far the worker got, since 'succeeded' alone
162
+ // says the migration was applied and cannot say by what.
163
+ function adoptedSummary(step) {
164
+ return step.generatorCompleted === true
165
+ ? "Adopted after the worker died: its generator had run, and the working tree it left was taken as this migration's result."
166
+ : "Adopted after the worker died before recording that its generator had run; the working tree it left was taken as this migration's result.";
167
+ }
168
+ // Re-arms a step for a fresh attempt. Drops every field the previous attempt
169
+ // wrote (pid, timestamps, git ref, tree state, outcomes) so a later success
170
+ // can't carry a stale failure outcome; dispenseCount stays cumulative across
171
+ // attempts.
172
+ // `keepGeneratorCompleted` says whether the generator's changes reach the new
173
+ // attempt. They do when nothing resets the tree, and when the reset target
174
+ // already contains the commit that landed them; re-running the generator there
175
+ // would apply them twice. They do not when the reset discards them, and
176
+ // keeping the marker then would skip the generator and record a success for a
177
+ // migration that never ran. The step kind is a plan fact, not an attempt's,
178
+ // and always survives. So does the dependency baseline: it tracks
179
+ // the last dependencies that were installed, so dropping it here would leave
180
+ // the retry with nothing to detect the previous attempt's package.json edits.
181
+ function rearm(step, keepGeneratorCompleted) {
182
+ return {
183
+ id: step.id,
184
+ roundIndex: step.roundIndex,
185
+ migrationId: step.migrationId,
186
+ status: 'pending',
187
+ attempt: step.attempt + 1,
188
+ dispenseCount: step.dispenseCount,
189
+ ...(step.hasGenerator !== undefined
190
+ ? { hasGenerator: step.hasGenerator }
191
+ : {}),
192
+ ...(step.depsHashAtDispense !== undefined
193
+ ? { depsHashAtDispense: step.depsHashAtDispense }
194
+ : {}),
195
+ ...(keepGeneratorCompleted && step.generatorCompleted
196
+ ? { generatorCompleted: true }
197
+ : {}),
198
+ };
199
+ }
200
+ // A guarded transition whose observation was made against an earlier attempt
201
+ // of the same step: the status recurred, so the observation says nothing about
202
+ // the attempt on disk now.
203
+ function staleAttempt(step, eventType, observedAttempt) {
204
+ return {
205
+ kind: 'error',
206
+ reason: `Cannot apply '${eventType}' to step '${step.id}': it was observed on attempt ${observedAttempt} and the step is now on attempt ${step.attempt}.`,
207
+ };
208
+ }
209
+ function illegal(step, eventType) {
210
+ return {
211
+ kind: 'error',
212
+ reason: `Cannot apply '${eventType}' to step '${step.id}' in status '${step.status}'.`,
213
+ };
214
+ }
215
+ function commit(state, index, updatedStep) {
216
+ const nextSteps = state.steps.slice();
217
+ nextSteps[index] = updatedStep;
218
+ return { kind: 'ok', state: { ...state, steps: nextSteps } };
219
+ }
220
+ /**
221
+ * Records that the run could not install the dependency changes a step left
222
+ * behind. Not a {@link StepEvent}: it annotates a step instead of moving it,
223
+ * and every status can carry it, since the orchestrator marks a step it has
224
+ * just settled while the worker marks one it is about to fail.
225
+ */
226
+ function markInstallFailed(state, stepId) {
227
+ return {
228
+ ...state,
229
+ steps: state.steps.map((s) => s.id === stepId ? { ...s, installFailed: true } : s),
230
+ };
231
+ }
232
+ // Step ids named by a 'failed' ledger entry with no later 'landed' entry
233
+ // covering them.
234
+ function uncoveredFailedStepIds(state) {
235
+ const uncovered = new Set();
236
+ const { commits } = state;
237
+ for (let i = 0; i < commits.length; i++) {
238
+ if (commits[i].kind !== 'failed')
239
+ continue;
240
+ for (const stepId of commits[i].stepIds) {
241
+ const covered = commits
242
+ .slice(i + 1)
243
+ .some((c) => c.kind === 'landed' && c.stepIds.includes(stepId));
244
+ if (!covered)
245
+ uncovered.add(stepId);
246
+ }
247
+ }
248
+ return [...uncovered];
249
+ }
250
+ /**
251
+ * A step has commit debt when a 'failed' ledger entry names it and no later
252
+ * 'landed' entry also names it (checkpoint entries neither create nor cover
253
+ * debt). There is no per-step commit object; debt is always derived from the
254
+ * ledger.
255
+ */
256
+ function hasPendingCommitDebt(state) {
257
+ return uncoveredFailedStepIds(state).length > 0;
258
+ }
259
+ // The 'landed' ledger entries naming the given step, in ledger order.
260
+ function coveringLandedEntries(state, stepId) {
261
+ return state.commits.filter((commit) => commit.kind === 'landed' && commit.stepIds.includes(stepId));
262
+ }
263
+ // The round with the highest index.
264
+ function latestRound(state) {
265
+ return state.rounds.reduce((newest, round) => (!newest || round.index > newest.index ? round : newest), undefined);
266
+ }
267
+ // '<package>:<name>' splits on the first ':', leaving names that contain a ':'
268
+ // intact; a bare id has no package.
269
+ function splitMigrationId(id) {
270
+ const colon = id.indexOf(':');
271
+ return colon === -1
272
+ ? { package: '', name: id }
273
+ : { package: id.slice(0, colon), name: id.slice(colon + 1) };
274
+ }
275
+ // Maps absorbed step ids to `{package, name}` for the commit body; an id with
276
+ // no matching step, or one whose migration id carries no package, can't be
277
+ // attributed there.
278
+ function stepsToPendingMigrations(state, stepIds) {
279
+ const pending = [];
280
+ for (const id of stepIds) {
281
+ const migrationId = state.steps.find((s) => s.id === id)?.migrationId;
282
+ if (!migrationId)
283
+ continue;
284
+ const { package: pkg, name } = splitMigrationId(migrationId);
285
+ if (!pkg)
286
+ continue;
287
+ pending.push({ package: pkg, name });
288
+ }
289
+ return pending;
290
+ }
291
+ /**
292
+ * Classifies a commit attempt into the ledger entry to record, or null when
293
+ * there is nothing to record ('no-changes' / 'disabled'). A landed entry
294
+ * covers the absorbed steps too: the commit's `git add -A` captured their
295
+ * diffs. A failed entry records only this step's debt.
296
+ */
297
+ function commitResultToLedgerEntry(result, stepId, absorbedStepIds) {
298
+ switch (result.status) {
299
+ case 'committed':
300
+ return {
301
+ kind: 'landed',
302
+ ...(result.sha ? { sha: result.sha } : {}),
303
+ stepIds: [stepId, ...absorbedStepIds],
304
+ };
305
+ case 'failed':
306
+ return { kind: 'failed', stepIds: [stepId] };
307
+ case 'no-changes':
308
+ case 'disabled':
309
+ return null;
310
+ default: {
311
+ const exhaustive = result;
312
+ return exhaustive;
313
+ }
314
+ }
315
+ }
@@ -1 +1,59 @@
1
+ import type { MigrateStep } from './run-state';
2
+ export declare function nowIso(): string;
3
+ /**
4
+ * Fingerprints the workspace dependencies so a step can persist what they
5
+ * looked like and a later actor can still tell whether the migration changed
6
+ * them. Hashed rather than stored verbatim to keep run.json small; the value
7
+ * is only ever compared for equality.
8
+ *
9
+ * `null` means the probe itself failed. Callers must not read that as
10
+ * "unchanged": the value is persisted and compared across processes, so a
11
+ * failed read on either end would otherwise silently skip an install that was
12
+ * needed.
13
+ */
14
+ export declare function depsHash(root: string): string | null;
15
+ /**
16
+ * Installs when the workspace dependencies differ from the step's recorded
17
+ * baseline, so a prompt applied by another actor, or a retry that only has the
18
+ * commit left to do, still installs what the changes need. Comparing against
19
+ * the persisted baseline is what makes that possible: by the time either runs,
20
+ * the edits are already on disk, so a snapshot taken here would see them as
21
+ * the starting point and never detect a change.
22
+ *
23
+ * The baseline is re-pointed at the current dependencies once the install
24
+ * lands, so the next actor to compare does not install the same change again.
25
+ *
26
+ * Every probe failure installs, on either end of the comparison. A step whose
27
+ * dispense-time probe failed has no baseline, which says the dependencies it
28
+ * started from are unknown rather than unchanged, and skipping there would
29
+ * strand the change with nothing left to detect it.
30
+ */
31
+ export declare function installDepsChangedSinceDispense(root: string, dir: string, step: MigrateStep, skipInstall: boolean, rerunCommand?: string): Promise<void>;
32
+ /**
33
+ * Records what an install that just landed means for the run: the installing
34
+ * step's dependency baseline moves to what is now on disk, and every step's
35
+ * install-failure mark clears.
36
+ *
37
+ * The baseline has to move, or the next actor to compare (the prompt fold, a
38
+ * retry, the step's own commit) reads the step's own edits as an unapplied
39
+ * change and pays a second full install. The marks clear because the package
40
+ * manager installs the whole workspace package.json, so this install also
41
+ * covers the dependency edits of every earlier step that failed to install its
42
+ * own.
43
+ *
44
+ * A failed probe leaves the baseline alone: an install that runs twice costs
45
+ * time, one that never runs leaves the workspace inconsistent with its
46
+ * package.json.
47
+ *
48
+ * Never throws. It runs inside callers that treat a throw as "the install
49
+ * failed", and the install has already succeeded by then. A run state this
50
+ * cannot read or write is a real problem, but it is reported by the next
51
+ * mandatory write rather than misattributed here, and the only cost of losing
52
+ * this one is a redundant install later.
53
+ */
54
+ export declare function recordInstallLanded(root: string, dir: string, stepId: string): void;
55
+ export declare function isPidAlive(pid: number): boolean;
56
+ export declare function summarizeError(e: unknown): string;
57
+ export declare function warnCommitFailed(name: string, cause?: unknown): void;
1
58
  export declare function pmExecPrefix(root: string): string;
59
+ export declare function pmInstallCommand(root: string): string;