@wp-typia/project-tools 0.11.1

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 (187) hide show
  1. package/README.md +32 -0
  2. package/dist/runtime/cli-add.d.ts +38 -0
  3. package/dist/runtime/cli-add.js +561 -0
  4. package/dist/runtime/cli-core.d.ts +25 -0
  5. package/dist/runtime/cli-core.js +25 -0
  6. package/dist/runtime/cli-doctor.d.ts +34 -0
  7. package/dist/runtime/cli-doctor.js +131 -0
  8. package/dist/runtime/cli-help.d.ts +9 -0
  9. package/dist/runtime/cli-help.js +37 -0
  10. package/dist/runtime/cli-prompt.d.ts +21 -0
  11. package/dist/runtime/cli-prompt.js +53 -0
  12. package/dist/runtime/cli-scaffold.d.ts +79 -0
  13. package/dist/runtime/cli-scaffold.js +206 -0
  14. package/dist/runtime/cli-templates.d.ts +30 -0
  15. package/dist/runtime/cli-templates.js +61 -0
  16. package/dist/runtime/index.d.ts +9 -0
  17. package/dist/runtime/index.js +7 -0
  18. package/dist/runtime/json-utils.d.ts +10 -0
  19. package/dist/runtime/json-utils.js +12 -0
  20. package/dist/runtime/local-dev-presets.d.ts +26 -0
  21. package/dist/runtime/local-dev-presets.js +132 -0
  22. package/dist/runtime/metadata-analysis.d.ts +11 -0
  23. package/dist/runtime/metadata-analysis.js +285 -0
  24. package/dist/runtime/metadata-model.d.ts +84 -0
  25. package/dist/runtime/metadata-model.js +59 -0
  26. package/dist/runtime/metadata-parser.d.ts +53 -0
  27. package/dist/runtime/metadata-parser.js +794 -0
  28. package/dist/runtime/metadata-php-render.d.ts +29 -0
  29. package/dist/runtime/metadata-php-render.js +549 -0
  30. package/dist/runtime/metadata-projection.d.ts +7 -0
  31. package/dist/runtime/metadata-projection.js +233 -0
  32. package/dist/runtime/migration-constants.d.ts +15 -0
  33. package/dist/runtime/migration-constants.js +16 -0
  34. package/dist/runtime/migration-diff.d.ts +2 -0
  35. package/dist/runtime/migration-diff.js +537 -0
  36. package/dist/runtime/migration-fixtures.d.ts +8 -0
  37. package/dist/runtime/migration-fixtures.js +94 -0
  38. package/dist/runtime/migration-fuzz-plan.d.ts +2 -0
  39. package/dist/runtime/migration-fuzz-plan.js +50 -0
  40. package/dist/runtime/migration-manifest.d.ts +19 -0
  41. package/dist/runtime/migration-manifest.js +129 -0
  42. package/dist/runtime/migration-project.d.ts +94 -0
  43. package/dist/runtime/migration-project.js +1101 -0
  44. package/dist/runtime/migration-render.d.ts +11 -0
  45. package/dist/runtime/migration-render.js +741 -0
  46. package/dist/runtime/migration-risk.d.ts +4 -0
  47. package/dist/runtime/migration-risk.js +52 -0
  48. package/dist/runtime/migration-types.d.ts +249 -0
  49. package/dist/runtime/migration-types.js +1 -0
  50. package/dist/runtime/migration-ui-capability.d.ts +17 -0
  51. package/dist/runtime/migration-ui-capability.js +190 -0
  52. package/dist/runtime/migration-utils.d.ts +69 -0
  53. package/dist/runtime/migration-utils.js +246 -0
  54. package/dist/runtime/migrations.d.ts +249 -0
  55. package/dist/runtime/migrations.js +1061 -0
  56. package/dist/runtime/object-utils.d.ts +12 -0
  57. package/dist/runtime/object-utils.js +14 -0
  58. package/dist/runtime/package-managers.d.ts +28 -0
  59. package/dist/runtime/package-managers.js +156 -0
  60. package/dist/runtime/package-versions.d.ts +10 -0
  61. package/dist/runtime/package-versions.js +68 -0
  62. package/dist/runtime/scaffold-onboarding.d.ts +32 -0
  63. package/dist/runtime/scaffold-onboarding.js +99 -0
  64. package/dist/runtime/scaffold.d.ts +146 -0
  65. package/dist/runtime/scaffold.js +612 -0
  66. package/dist/runtime/schema-core.d.ts +267 -0
  67. package/dist/runtime/schema-core.js +597 -0
  68. package/dist/runtime/starter-manifests.d.ts +25 -0
  69. package/dist/runtime/starter-manifests.js +383 -0
  70. package/dist/runtime/string-case.d.ts +36 -0
  71. package/dist/runtime/string-case.js +69 -0
  72. package/dist/runtime/template-builtins.d.ts +38 -0
  73. package/dist/runtime/template-builtins.js +72 -0
  74. package/dist/runtime/template-defaults.d.ts +75 -0
  75. package/dist/runtime/template-defaults.js +65 -0
  76. package/dist/runtime/template-registry.d.ts +36 -0
  77. package/dist/runtime/template-registry.js +94 -0
  78. package/dist/runtime/template-render.d.ts +24 -0
  79. package/dist/runtime/template-render.js +113 -0
  80. package/dist/runtime/template-source.d.ts +71 -0
  81. package/dist/runtime/template-source.js +821 -0
  82. package/dist/runtime/typia-tags.d.ts +1 -0
  83. package/dist/runtime/typia-tags.js +1 -0
  84. package/package.json +79 -0
  85. package/templates/_shared/base/languages/.gitkeep +1 -0
  86. package/templates/_shared/base/package.json.mustache +41 -0
  87. package/templates/_shared/base/scripts/sync-types-to-block-json.ts.mustache +118 -0
  88. package/templates/_shared/base/src/hooks.ts.mustache +19 -0
  89. package/templates/_shared/base/src/validator-toolkit.ts.mustache +31 -0
  90. package/templates/_shared/base/tsconfig.json.mustache +21 -0
  91. package/templates/_shared/base/webpack.config.js.mustache +99 -0
  92. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +53 -0
  93. package/templates/_shared/compound/core/package.json.mustache +45 -0
  94. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +559 -0
  95. package/templates/_shared/compound/core/scripts/block-config.ts.mustache +13 -0
  96. package/templates/_shared/compound/core/scripts/sync-types-to-block-json.ts.mustache +53 -0
  97. package/templates/_shared/compound/core/webpack.config.js.mustache +141 -0
  98. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +51 -0
  99. package/templates/_shared/compound/persistence/package.json.mustache +50 -0
  100. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +59 -0
  101. package/templates/_shared/compound/persistence/scripts/sync-rest-contracts.ts.mustache +101 -0
  102. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +21 -0
  103. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-validators.ts.mustache +32 -0
  104. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +68 -0
  105. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +52 -0
  106. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +192 -0
  107. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +123 -0
  108. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  109. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +132 -0
  110. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +158 -0
  111. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +3 -0
  112. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +56 -0
  113. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +32 -0
  114. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +294 -0
  115. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +312 -0
  116. package/templates/_shared/migration-ui/common/src/admin/migration-dashboard.tsx +394 -0
  117. package/templates/_shared/migration-ui/common/src/migration-detector.ts +9 -0
  118. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +490 -0
  119. package/templates/_shared/migration-ui/common/src/migrations/index.ts +886 -0
  120. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +290 -0
  121. package/templates/_shared/persistence/core/package.json.mustache +46 -0
  122. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +113 -0
  123. package/templates/_shared/persistence/core/scripts/sync-types-to-block-json.ts.mustache +125 -0
  124. package/templates/_shared/persistence/core/src/api-types.ts.mustache +21 -0
  125. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +32 -0
  126. package/templates/_shared/persistence/core/src/api.ts.mustache +68 -0
  127. package/templates/_shared/persistence/core/src/data.ts.mustache +192 -0
  128. package/templates/_shared/persistence/core/src/index.tsx.mustache +25 -0
  129. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +134 -0
  130. package/templates/_shared/persistence/core/src/save.tsx.mustache +5 -0
  131. package/templates/_shared/persistence/core/src/validators.ts.mustache +32 -0
  132. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +336 -0
  133. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +308 -0
  134. package/templates/_shared/presets/test-preset/.wp-env.test.json.mustache +16 -0
  135. package/templates/_shared/presets/test-preset/playwright.config.ts.mustache +22 -0
  136. package/templates/_shared/presets/test-preset/scripts/wait-for-wp-env.mjs.mustache +102 -0
  137. package/templates/_shared/presets/test-preset/scripts/wp-env-utils.cjs.mustache +32 -0
  138. package/templates/_shared/presets/test-preset/tests/e2e/smoke.spec.ts.mustache +34 -0
  139. package/templates/_shared/presets/wp-env/.wp-env.json.mustache +16 -0
  140. package/templates/_shared/rest-helpers/auth/inc/rest-auth.php.mustache +37 -0
  141. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +314 -0
  142. package/templates/_shared/rest-helpers/shared/inc/rest-shared.php.mustache +58 -0
  143. package/templates/_shared/workspace/persistence-auth/inc/rest-auth.php.mustache +36 -0
  144. package/templates/_shared/workspace/persistence-auth/inc/rest-shared.php.mustache +55 -0
  145. package/templates/_shared/workspace/persistence-auth/server.php.mustache +237 -0
  146. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +273 -0
  147. package/templates/_shared/workspace/persistence-public/inc/rest-shared.php.mustache +55 -0
  148. package/templates/_shared/workspace/persistence-public/server.php.mustache +252 -0
  149. package/templates/basic/src/block.json.mustache +51 -0
  150. package/templates/basic/src/edit.tsx.mustache +128 -0
  151. package/templates/basic/src/editor.scss.mustache +8 -0
  152. package/templates/basic/src/hooks.ts.mustache +18 -0
  153. package/templates/basic/src/index.tsx.mustache +45 -0
  154. package/templates/basic/src/save.tsx.mustache +30 -0
  155. package/templates/basic/src/style.scss.mustache +40 -0
  156. package/templates/basic/src/types.ts.mustache +56 -0
  157. package/templates/basic/src/validators.ts.mustache +26 -0
  158. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +37 -0
  159. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +25 -0
  160. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +93 -0
  161. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  162. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +25 -0
  163. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +32 -0
  164. package/templates/compound/src/blocks/{{slugKebabCase}}/style.scss.mustache +31 -0
  165. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -0
  166. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +17 -0
  167. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +35 -0
  168. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +50 -0
  169. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +11 -0
  170. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +25 -0
  171. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +24 -0
  172. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +12 -0
  173. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +17 -0
  174. package/templates/interactivity/package.json.mustache +42 -0
  175. package/templates/interactivity/src/block.json.mustache +73 -0
  176. package/templates/interactivity/src/edit.tsx.mustache +270 -0
  177. package/templates/interactivity/src/index.tsx.mustache +32 -0
  178. package/templates/interactivity/src/interactivity.ts.mustache +152 -0
  179. package/templates/interactivity/src/save.tsx.mustache +101 -0
  180. package/templates/interactivity/src/style.scss.mustache +60 -0
  181. package/templates/interactivity/src/types.ts.mustache +32 -0
  182. package/templates/interactivity/src/validators.ts.mustache +36 -0
  183. package/templates/persistence/src/block.json.mustache +52 -0
  184. package/templates/persistence/src/edit.tsx.mustache +165 -0
  185. package/templates/persistence/src/render.php.mustache +126 -0
  186. package/templates/persistence/src/style.scss.mustache +46 -0
  187. package/templates/persistence/src/types.ts.mustache +55 -0
@@ -0,0 +1,1061 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import { createReadlinePrompt } from "./cli-prompt.js";
5
+ import { formatRunScript } from "./package-managers.js";
6
+ import { ROOT_PHP_MIGRATION_REGISTRY, SNAPSHOT_DIR, } from "./migration-constants.js";
7
+ import { createMigrationDiff } from "./migration-diff.js";
8
+ import { createMigrationFuzzPlan } from "./migration-fuzz-plan.js";
9
+ import { createEdgeFixtureDocument, ensureEdgeFixtureFile } from "./migration-fixtures.js";
10
+ import { assertRuleHasNoTodos, assertNoLegacySemverMigrationWorkspace, discoverMigrationInitLayout, discoverMigrationEntries, ensureAdvancedMigrationProject, ensureMigrationDirectories, getFixtureFilePath, getAvailableSnapshotVersionsForBlock, getGeneratedDirForBlock, getProjectPaths, getRuleFilePath, getSnapshotBlockJsonPath, getSnapshotManifestPath, getSnapshotRoot, getSnapshotSavePath, loadMigrationProject, readRuleMetadata, writeInitialMigrationScaffold, writeMigrationConfig, } from "./migration-project.js";
11
+ import { formatDiffReport, renderFuzzFile, renderGeneratedDeprecatedFile, renderGeneratedMigrationIndexFile, renderMigrationRegistryFile, renderMigrationRuleFile, renderPhpMigrationRegistryFile, renderVerifyFile, } from "./migration-render.js";
12
+ import { createMigrationRiskSummary, formatMigrationRiskSummary } from "./migration-risk.js";
13
+ import { assertMigrationVersionLabel, compareMigrationVersionLabels, copyFile, detectPackageManagerId, formatLegacyMigrationWorkspaceResetGuidance, getLocalTsxBinary, isInteractiveTerminal, readJson, resolveTargetMigrationVersion, runProjectScriptIfPresent, sanitizeSaveSnapshotSource, sanitizeSnapshotBlockJson, } from "./migration-utils.js";
14
+ /**
15
+ * Returns the formatted help text for migration CLI commands and flags.
16
+ *
17
+ * @returns Multi-line usage text for the `wp-typia migrate` command surface.
18
+ */
19
+ export function formatMigrationHelpText() {
20
+ return `Usage:
21
+ wp-typia migrate init --current-migration-version <label>
22
+ wp-typia migrate snapshot --migration-version <label>
23
+ wp-typia migrate plan --from-migration-version <label> [--to-migration-version current]
24
+ wp-typia migrate wizard
25
+ wp-typia migrate diff --from-migration-version <label> [--to-migration-version current]
26
+ wp-typia migrate scaffold --from-migration-version <label> [--to-migration-version current]
27
+ wp-typia migrate verify [--from-migration-version <label>|--all]
28
+ wp-typia migrate doctor [--from-migration-version <label>|--all]
29
+ wp-typia migrate fixtures [--from-migration-version <label>|--all] [--to-migration-version current] [--force]
30
+ wp-typia migrate fuzz [--from-migration-version <label>|--all] [--iterations <n>] [--seed <n>]
31
+
32
+ Notes:
33
+ \`migrate init\` auto-detects supported single-block and \`src/blocks/*\` multi-block layouts.
34
+ Migration versions use strict schema labels like \`v1\`, \`v2\`, and \`v3\`.
35
+ \`migrate wizard\` is TTY-only and helps you choose one legacy migration version to preview.
36
+ \`migrate plan\` and \`migrate wizard\` are read-only previews; they do not scaffold rules or fixtures.
37
+ --all runs across every configured legacy migration version and every configured block target.
38
+ Existing fixture files are preserved and reported as skipped unless you pass \`--force\`.
39
+ Use \`migrate fixtures --force\` as the explicit refresh path for generated fixture files.
40
+ In TTY usage, \`migrate fixtures --force\` asks before overwriting existing fixture files.
41
+ In non-interactive usage, \`migrate fixtures --force\` overwrites immediately for script compatibility.`;
42
+ }
43
+ /**
44
+ * Parses migration CLI arguments into a structured command payload.
45
+ *
46
+ * @param argv Command-line arguments that follow the `migrate` subcommand.
47
+ * @returns Parsed migration command and normalized flags for runtime dispatch.
48
+ * @throws Error When no arguments are provided, an unknown flag is encountered, or legacy semver flags are used.
49
+ */
50
+ export function parseMigrationArgs(argv) {
51
+ const parsed = {
52
+ command: undefined,
53
+ flags: {
54
+ all: false,
55
+ currentMigrationVersion: undefined,
56
+ force: false,
57
+ fromMigrationVersion: undefined,
58
+ iterations: undefined,
59
+ migrationVersion: undefined,
60
+ seed: undefined,
61
+ toMigrationVersion: "current",
62
+ },
63
+ };
64
+ if (argv.length === 0) {
65
+ throw new Error(formatMigrationHelpText());
66
+ }
67
+ parsed.command = argv[0];
68
+ for (let index = 1; index < argv.length; index += 1) {
69
+ const arg = argv[index];
70
+ const next = argv[index + 1];
71
+ if (arg === "--")
72
+ continue;
73
+ if (arg === "--all") {
74
+ parsed.flags.all = true;
75
+ continue;
76
+ }
77
+ if (arg === "--force") {
78
+ parsed.flags.force = true;
79
+ continue;
80
+ }
81
+ if (arg === "--current-migration-version") {
82
+ parsed.flags.currentMigrationVersion = next;
83
+ index += 1;
84
+ continue;
85
+ }
86
+ if (arg.startsWith("--current-migration-version=")) {
87
+ parsed.flags.currentMigrationVersion = arg.split("=", 2)[1];
88
+ continue;
89
+ }
90
+ if (arg === "--from-migration-version") {
91
+ parsed.flags.fromMigrationVersion = next;
92
+ index += 1;
93
+ continue;
94
+ }
95
+ if (arg.startsWith("--from-migration-version=")) {
96
+ parsed.flags.fromMigrationVersion = arg.split("=", 2)[1];
97
+ continue;
98
+ }
99
+ if (arg === "--iterations") {
100
+ parsed.flags.iterations = next;
101
+ index += 1;
102
+ continue;
103
+ }
104
+ if (arg.startsWith("--iterations=")) {
105
+ parsed.flags.iterations = arg.split("=", 2)[1];
106
+ continue;
107
+ }
108
+ if (arg === "--seed") {
109
+ parsed.flags.seed = next;
110
+ index += 1;
111
+ continue;
112
+ }
113
+ if (arg.startsWith("--seed=")) {
114
+ parsed.flags.seed = arg.split("=", 2)[1];
115
+ continue;
116
+ }
117
+ if (arg === "--to-migration-version") {
118
+ parsed.flags.toMigrationVersion = next;
119
+ index += 1;
120
+ continue;
121
+ }
122
+ if (arg.startsWith("--to-migration-version=")) {
123
+ parsed.flags.toMigrationVersion = arg.split("=", 2)[1];
124
+ continue;
125
+ }
126
+ if (arg === "--migration-version") {
127
+ parsed.flags.migrationVersion = next;
128
+ index += 1;
129
+ continue;
130
+ }
131
+ if (arg.startsWith("--migration-version=")) {
132
+ parsed.flags.migrationVersion = arg.split("=", 2)[1];
133
+ continue;
134
+ }
135
+ if (arg === "--current-version" ||
136
+ arg.startsWith("--current-version=") ||
137
+ arg === "--version" ||
138
+ arg.startsWith("--version=") ||
139
+ arg === "--from" ||
140
+ arg.startsWith("--from=") ||
141
+ arg === "--to" ||
142
+ arg.startsWith("--to=")) {
143
+ throwLegacyMigrationFlagError(arg);
144
+ }
145
+ throw new Error(`Unknown migration flag: ${arg}`);
146
+ }
147
+ return parsed;
148
+ }
149
+ export { formatDiffReport };
150
+ /**
151
+ * Dispatch a parsed migration command to the matching runtime workflow.
152
+ *
153
+ * Most commands execute synchronously and preserve direct throw semantics for
154
+ * existing callers. The interactive `wizard` command returns a promise because
155
+ * it waits for prompt selection before running the shared read-only planner.
156
+ *
157
+ * @param command Parsed migration command and flags.
158
+ * @param cwd Project directory to operate on.
159
+ * @param options Optional prompt/render hooks for testable and interactive execution.
160
+ * @returns The command result, or a promise when the selected command is interactive.
161
+ */
162
+ export function runMigrationCommand(command, cwd, { prompt, renderLine = console.log } = {}) {
163
+ switch (command.command) {
164
+ case "init":
165
+ if (!command.flags.currentMigrationVersion) {
166
+ throw new Error("`migrate init` requires --current-migration-version <label>.");
167
+ }
168
+ return initProjectMigrations(cwd, command.flags.currentMigrationVersion, { renderLine });
169
+ case "snapshot":
170
+ if (!command.flags.migrationVersion) {
171
+ throw new Error("`migrate snapshot` requires --migration-version <label>.");
172
+ }
173
+ return snapshotProjectVersion(cwd, command.flags.migrationVersion, { renderLine });
174
+ case "plan":
175
+ if (!command.flags.fromMigrationVersion) {
176
+ throw new Error("`migrate plan` requires --from-migration-version <label>.");
177
+ }
178
+ return planProjectMigrations(cwd, {
179
+ fromMigrationVersion: command.flags.fromMigrationVersion,
180
+ renderLine,
181
+ toMigrationVersion: command.flags.toMigrationVersion ?? "current",
182
+ });
183
+ case "wizard":
184
+ return wizardProjectMigrations(cwd, {
185
+ prompt,
186
+ renderLine,
187
+ });
188
+ case "diff":
189
+ if (!command.flags.fromMigrationVersion) {
190
+ throw new Error("`migrate diff` requires --from-migration-version <label>.");
191
+ }
192
+ return diffProjectMigrations(cwd, {
193
+ fromMigrationVersion: command.flags.fromMigrationVersion,
194
+ renderLine,
195
+ toMigrationVersion: command.flags.toMigrationVersion ?? "current",
196
+ });
197
+ case "scaffold":
198
+ if (!command.flags.fromMigrationVersion) {
199
+ throw new Error("`migrate scaffold` requires --from-migration-version <label>.");
200
+ }
201
+ return scaffoldProjectMigrations(cwd, {
202
+ fromMigrationVersion: command.flags.fromMigrationVersion,
203
+ renderLine,
204
+ toMigrationVersion: command.flags.toMigrationVersion ?? "current",
205
+ });
206
+ case "verify":
207
+ return verifyProjectMigrations(cwd, {
208
+ all: command.flags.all,
209
+ fromMigrationVersion: command.flags.fromMigrationVersion,
210
+ renderLine,
211
+ });
212
+ case "doctor":
213
+ return doctorProjectMigrations(cwd, {
214
+ all: command.flags.all,
215
+ fromMigrationVersion: command.flags.fromMigrationVersion,
216
+ renderLine,
217
+ });
218
+ case "fixtures":
219
+ return fixturesProjectMigrations(cwd, {
220
+ all: command.flags.all,
221
+ force: command.flags.force,
222
+ fromMigrationVersion: command.flags.fromMigrationVersion,
223
+ renderLine,
224
+ toMigrationVersion: command.flags.toMigrationVersion ?? "current",
225
+ });
226
+ case "fuzz":
227
+ return fuzzProjectMigrations(cwd, {
228
+ all: command.flags.all,
229
+ fromMigrationVersion: command.flags.fromMigrationVersion,
230
+ iterations: parsePositiveInteger(command.flags.iterations, "iterations") ?? 25,
231
+ renderLine,
232
+ seed: parseNonNegativeInteger(command.flags.seed, "seed") ?? undefined,
233
+ });
234
+ default:
235
+ throw new Error(formatMigrationHelpText());
236
+ }
237
+ }
238
+ /**
239
+ * Preview one migration edge without scaffolding rules, fixtures, or generated files.
240
+ *
241
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
242
+ * @param options Selected source/target versions plus optional line rendering overrides.
243
+ * @returns A structured summary of the selected edge, included/skipped block targets, and next steps.
244
+ */
245
+ export function planProjectMigrations(projectDir, { fromMigrationVersion, renderLine = console.log, toMigrationVersion = "current" } = {}) {
246
+ if (!fromMigrationVersion) {
247
+ throw new Error("`migrate plan` requires --from-migration-version <label>.");
248
+ }
249
+ const state = loadMigrationProject(projectDir, { allowSyncTypes: false });
250
+ const availableLegacyVersions = listPreviewableLegacyVersions(state).sort(compareMigrationVersionLabels).reverse();
251
+ const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
252
+ assertDistinctMigrationEdge("plan", fromMigrationVersion, targetMigrationVersion);
253
+ resolveLegacyVersions(state, {
254
+ fromMigrationVersion,
255
+ availableVersions: availableLegacyVersions,
256
+ });
257
+ const includedBlocks = state.blocks.filter((block) => hasSnapshotForVersion(state, block, fromMigrationVersion));
258
+ if (includedBlocks.length === 0) {
259
+ throw new Error(createMissingProjectSnapshotMessage(state, fromMigrationVersion));
260
+ }
261
+ const skippedBlocks = state.blocks
262
+ .filter((block) => !hasSnapshotForVersion(state, block, fromMigrationVersion))
263
+ .map((block) => block.blockName);
264
+ const summaries = includedBlocks.map((block) => {
265
+ const diff = createMigrationDiff(state, block, fromMigrationVersion, targetMigrationVersion);
266
+ return {
267
+ blockName: block.blockName,
268
+ diff,
269
+ riskSummary: createMigrationRiskSummary(diff),
270
+ };
271
+ });
272
+ const nextSteps = createMigrationPlanNextSteps(fromMigrationVersion, targetMigrationVersion, state.config.currentMigrationVersion);
273
+ renderLine(`Current migration version: ${state.config.currentMigrationVersion}`);
274
+ renderLine(`Available legacy migration versions: ${availableLegacyVersions.length > 0 ? availableLegacyVersions.join(", ") : "None configured"}`);
275
+ renderLine(`Selected migration edge: ${fromMigrationVersion} -> ${targetMigrationVersion}`);
276
+ renderLine(`Included block targets: ${includedBlocks.map((block) => block.blockName).join(", ")}`);
277
+ renderLine(`Skipped block targets: ${skippedBlocks.length > 0 ? skippedBlocks.join(", ") : "None"}`);
278
+ for (const summary of summaries) {
279
+ renderLine(`Block: ${summary.blockName}`);
280
+ renderLine(formatDiffReport(summary.diff, { includeRiskSummary: false }));
281
+ renderLine(`Risk summary: ${formatMigrationRiskSummary(summary.riskSummary)}`);
282
+ }
283
+ renderLine("Next steps:");
284
+ for (const command of nextSteps) {
285
+ renderLine(` ${command}`);
286
+ }
287
+ renderLine(`Optional after editing rules: ${formatEdgeCommand("fixtures", fromMigrationVersion, targetMigrationVersion, state.config.currentMigrationVersion)} --force`);
288
+ return {
289
+ availableLegacyVersions,
290
+ currentMigrationVersion: state.config.currentMigrationVersion,
291
+ fromMigrationVersion,
292
+ includedBlocks: includedBlocks.map((block) => block.blockName),
293
+ nextSteps,
294
+ skippedBlocks,
295
+ summaries,
296
+ targetMigrationVersion,
297
+ };
298
+ }
299
+ /**
300
+ * Interactively choose one legacy version to preview, then run the same read-only planner.
301
+ *
302
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
303
+ * @param options Interactive prompt and rendering settings. Throws when no TTY is available.
304
+ * @returns The planned migration summary, or `{ cancelled: true }` when the user exits the wizard.
305
+ */
306
+ export async function wizardProjectMigrations(projectDir, { isInteractive = isInteractiveTerminal(), prompt, renderLine = console.log, } = {}) {
307
+ if (!isInteractive) {
308
+ throw new Error("`migrate wizard` requires an interactive terminal. " +
309
+ "Use `wp-typia migrate plan --from-migration-version <label>` for a read-only preview or run the direct migration commands with explicit flags.");
310
+ }
311
+ const state = loadMigrationProject(projectDir, { allowSyncTypes: false });
312
+ const availableLegacyVersions = listPreviewableLegacyVersions(state).sort(compareMigrationVersionLabels).reverse();
313
+ if (availableLegacyVersions.length === 0) {
314
+ throw new Error("No legacy migration versions are configured yet. " +
315
+ "Capture an older schema release with `wp-typia migrate snapshot --migration-version <label>` first, then rerun `wp-typia migrate wizard`.");
316
+ }
317
+ const activePrompt = prompt ?? createReadlinePrompt();
318
+ const createdPrompt = !prompt;
319
+ try {
320
+ const selectedVersion = await activePrompt.select("Choose a legacy version to preview", [
321
+ ...availableLegacyVersions.map((version) => ({
322
+ hint: `Preview ${version} -> ${state.config.currentMigrationVersion}`,
323
+ label: version,
324
+ value: version,
325
+ })),
326
+ {
327
+ hint: "Exit without previewing a migration edge",
328
+ label: "Cancel",
329
+ value: "cancel",
330
+ },
331
+ ], 1);
332
+ if (selectedVersion === "cancel") {
333
+ renderLine("Cancelled migration planning.");
334
+ return { cancelled: true };
335
+ }
336
+ return planProjectMigrations(projectDir, {
337
+ fromMigrationVersion: selectedVersion,
338
+ renderLine,
339
+ });
340
+ }
341
+ finally {
342
+ if (createdPrompt) {
343
+ activePrompt.close();
344
+ }
345
+ }
346
+ }
347
+ /**
348
+ * Initializes migration scaffolding for a detected single-block or multi-block project layout.
349
+ *
350
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
351
+ * @param currentMigrationVersion Initial migration version label to seed, such as `v1`.
352
+ * @param options Console rendering options used to report retrofit detection and initialization output.
353
+ * @returns The loaded migration project state after the config, snapshots, and generated files are written.
354
+ * @throws Error When the project layout is unsupported or the migration version label is invalid.
355
+ */
356
+ export function initProjectMigrations(projectDir, currentMigrationVersion, { renderLine = console.log } = {}) {
357
+ assertMigrationVersionLabel(currentMigrationVersion, "current migration version");
358
+ assertNoLegacySemverMigrationWorkspace(projectDir);
359
+ const discoveredLayout = discoverMigrationInitLayout(projectDir);
360
+ const configuredBlocks = discoveredLayout.mode === "multi" ? discoveredLayout.blocks : undefined;
361
+ ensureAdvancedMigrationProject(projectDir, configuredBlocks);
362
+ ensureMigrationDirectories(projectDir, configuredBlocks);
363
+ writeMigrationConfig(projectDir, {
364
+ blockName: discoveredLayout.mode === "single"
365
+ ? discoveredLayout.block.blockName
366
+ : undefined,
367
+ blocks: configuredBlocks,
368
+ currentMigrationVersion,
369
+ snapshotDir: SNAPSHOT_DIR.replace(/\\/g, "/"),
370
+ supportedMigrationVersions: [currentMigrationVersion],
371
+ });
372
+ writeInitialMigrationScaffold(projectDir, currentMigrationVersion, configuredBlocks);
373
+ snapshotProjectVersion(projectDir, currentMigrationVersion, { renderLine, skipConfigUpdate: true });
374
+ regenerateGeneratedArtifacts(projectDir);
375
+ if (discoveredLayout.mode === "multi") {
376
+ renderLine(`Detected multi-block migration retrofit (${discoveredLayout.blocks.length} targets): ${discoveredLayout.blocks.map((block) => block.blockName).join(", ")}`);
377
+ }
378
+ else {
379
+ renderLine(`Detected single-block migration retrofit: ${discoveredLayout.block.blockName}`);
380
+ }
381
+ renderLine("Wrote src/migrations/config.ts");
382
+ renderLine(`Initialized migrations for ${discoveredLayout.mode === "multi"
383
+ ? discoveredLayout.blocks.map((block) => block.blockName).join(", ")
384
+ : discoveredLayout.block.blockName} at migration version ${currentMigrationVersion}`);
385
+ return loadMigrationProject(projectDir);
386
+ }
387
+ /**
388
+ * Captures the current project state as a named migration snapshot and refreshes generated artifacts.
389
+ *
390
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
391
+ * @param migrationVersion Migration version label to snapshot, such as `v2`.
392
+ * @param options Console rendering options and snapshot side-effect flags.
393
+ * @returns The loaded migration project state after the snapshot files and registry outputs are refreshed.
394
+ * @throws Error When the label is invalid, the project is not migration-capable, or `sync-types` fails.
395
+ */
396
+ export function snapshotProjectVersion(projectDir, migrationVersion, { renderLine = console.log, skipConfigUpdate = false, skipSyncTypes = false, } = {}) {
397
+ ensureAdvancedMigrationProject(projectDir);
398
+ assertMigrationVersionLabel(migrationVersion, "migration version");
399
+ if (!skipSyncTypes) {
400
+ try {
401
+ runProjectScriptIfPresent(projectDir, "sync-types");
402
+ }
403
+ catch (error) {
404
+ const syncTypesCommand = formatRunScript(detectPackageManagerId(projectDir), "sync-types");
405
+ const reason = error instanceof Error ? error.message : String(error);
406
+ throw new Error(`Could not capture migration snapshot ${migrationVersion} because \`${syncTypesCommand}\` failed first. ` +
407
+ `Install project dependencies if needed, rerun \`${syncTypesCommand}\` in the project root to inspect the underlying error, ` +
408
+ `then retry \`wp-typia migrate snapshot --migration-version ${migrationVersion}\`.\n` +
409
+ `Original error: ${reason}`);
410
+ }
411
+ }
412
+ const state = loadMigrationProject(projectDir, { allowMissingConfig: skipConfigUpdate });
413
+ for (const block of state.blocks) {
414
+ const snapshotRoot = getSnapshotRoot(projectDir, block, migrationVersion);
415
+ fs.mkdirSync(snapshotRoot, { recursive: true });
416
+ fs.writeFileSync(getSnapshotBlockJsonPath(projectDir, block, migrationVersion), `${JSON.stringify(sanitizeSnapshotBlockJson(readJson(path.join(projectDir, block.blockJsonFile))), null, "\t")}\n`, "utf8");
417
+ copyFile(path.join(projectDir, block.manifestFile), getSnapshotManifestPath(projectDir, block, migrationVersion));
418
+ fs.writeFileSync(getSnapshotSavePath(projectDir, block, migrationVersion), sanitizeSaveSnapshotSource(fs.readFileSync(path.join(projectDir, block.saveFile), "utf8")), "utf8");
419
+ }
420
+ if (!skipConfigUpdate) {
421
+ const nextSupported = [
422
+ ...new Set([...state.config.supportedMigrationVersions, migrationVersion]),
423
+ ].sort(compareMigrationVersionLabels);
424
+ writeMigrationConfig(projectDir, {
425
+ ...state.config,
426
+ currentMigrationVersion: migrationVersion,
427
+ supportedMigrationVersions: nextSupported,
428
+ });
429
+ }
430
+ regenerateGeneratedArtifacts(projectDir);
431
+ renderLine(`Snapshot stored for migration version ${migrationVersion}`);
432
+ return loadMigrationProject(projectDir);
433
+ }
434
+ /**
435
+ * Computes and renders migration diffs for a selected legacy-to-target edge.
436
+ *
437
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
438
+ * @param options Selected source and target migration versions plus optional line rendering overrides.
439
+ * @returns A single diff for single-block workspaces, or an array of per-block diffs for multi-block workspaces.
440
+ * @throws Error When `fromMigrationVersion` is missing or no eligible snapshots exist for the selected edge.
441
+ */
442
+ export function diffProjectMigrations(projectDir, { fromMigrationVersion, toMigrationVersion = "current", renderLine = console.log, } = {}) {
443
+ if (!fromMigrationVersion) {
444
+ throw new Error("`migrate diff` requires --from-migration-version <label>.");
445
+ }
446
+ const state = loadMigrationProject(projectDir);
447
+ const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
448
+ assertDistinctMigrationEdge("diff", fromMigrationVersion, targetMigrationVersion);
449
+ const diffs = state.blocks
450
+ .filter((block) => hasSnapshotForVersion(state, block, fromMigrationVersion))
451
+ .map((block) => ({
452
+ block,
453
+ diff: createMigrationDiff(state, block, fromMigrationVersion, targetMigrationVersion),
454
+ }));
455
+ if (diffs.length === 0) {
456
+ throw new Error(createMissingProjectSnapshotMessage(state, fromMigrationVersion));
457
+ }
458
+ for (const { block, diff } of diffs) {
459
+ renderLine(`Block: ${block.blockName}`);
460
+ renderLine(formatDiffReport(diff));
461
+ }
462
+ return diffs.length === 1 ? diffs[0].diff : diffs;
463
+ }
464
+ /**
465
+ * Scaffolds migration rule and fixture files for a selected legacy-to-target edge.
466
+ *
467
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
468
+ * @param options Selected source and target migration versions plus optional line rendering overrides.
469
+ * @returns A single scaffold result for single-block workspaces, or a grouped result for multi-block workspaces.
470
+ * @throws Error When `fromMigrationVersion` is missing or no eligible snapshots exist for the selected edge.
471
+ */
472
+ export function scaffoldProjectMigrations(projectDir, { fromMigrationVersion, toMigrationVersion = "current", renderLine = console.log, } = {}) {
473
+ if (!fromMigrationVersion) {
474
+ throw new Error("`migrate scaffold` requires --from-migration-version <label>.");
475
+ }
476
+ ensureMigrationDirectories(projectDir);
477
+ const state = loadMigrationProject(projectDir);
478
+ const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
479
+ assertDistinctMigrationEdge("scaffold", fromMigrationVersion, targetMigrationVersion);
480
+ const paths = getProjectPaths(projectDir);
481
+ const scaffolded = [];
482
+ let eligibleBlocks = 0;
483
+ for (const block of state.blocks) {
484
+ if (!hasSnapshotForVersion(state, block, fromMigrationVersion)) {
485
+ renderLine(`Skipped ${block.blockName}: no snapshot for ${fromMigrationVersion}`);
486
+ continue;
487
+ }
488
+ eligibleBlocks += 1;
489
+ const diff = createMigrationDiff(state, block, fromMigrationVersion, targetMigrationVersion);
490
+ const rulePath = getRuleFilePath(paths, block, fromMigrationVersion, targetMigrationVersion);
491
+ if (!fs.existsSync(rulePath)) {
492
+ fs.mkdirSync(path.dirname(rulePath), { recursive: true });
493
+ fs.writeFileSync(rulePath, renderMigrationRuleFile({
494
+ block,
495
+ currentAttributes: block.currentManifest.attributes ?? {},
496
+ currentTypeName: block.currentManifest.sourceType,
497
+ diff,
498
+ fromVersion: fromMigrationVersion,
499
+ projectDir,
500
+ rulePath,
501
+ targetVersion: targetMigrationVersion,
502
+ }), "utf8");
503
+ }
504
+ ensureEdgeFixtureFile(projectDir, block, fromMigrationVersion, targetMigrationVersion, diff);
505
+ scaffolded.push({ blockName: block.blockName, diff, rulePath });
506
+ }
507
+ regenerateGeneratedArtifacts(projectDir);
508
+ for (const entry of scaffolded) {
509
+ renderLine(`Block: ${entry.blockName}`);
510
+ renderLine(formatDiffReport(entry.diff));
511
+ renderLine(`Scaffolded ${path.relative(projectDir, entry.rulePath)}`);
512
+ }
513
+ if (eligibleBlocks === 0) {
514
+ throw new Error(createMissingProjectSnapshotMessage(state, fromMigrationVersion));
515
+ }
516
+ return scaffolded.length === 1 ? scaffolded[0] : { scaffolded };
517
+ }
518
+ /**
519
+ * Run deterministic migration verification against generated fixtures.
520
+ *
521
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
522
+ * @param options Verification scope and console rendering options.
523
+ * @returns Verified legacy versions.
524
+ */
525
+ export function verifyProjectMigrations(projectDir, { all = false, fromMigrationVersion, renderLine = console.log } = {}) {
526
+ const state = loadMigrationProject(projectDir);
527
+ const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
528
+ const blockEntries = getSelectedEntriesByBlock(state, targetVersions, "verify");
529
+ const legacySingleBlock = isLegacySingleBlockProject(state);
530
+ if (targetVersions.length === 0) {
531
+ renderLine("No legacy migration versions configured for verification.");
532
+ return { verifiedVersions: [] };
533
+ }
534
+ const tsxBinary = getLocalTsxBinary(projectDir);
535
+ for (const [blockKey, entries] of Object.entries(blockEntries)) {
536
+ const block = state.blocks.find((entry) => entry.key === blockKey);
537
+ if (!block || entries.length === 0) {
538
+ continue;
539
+ }
540
+ for (const entry of entries) {
541
+ assertRuleHasNoTodos(projectDir, block, entry.fromVersion, state.config.currentMigrationVersion);
542
+ }
543
+ const verifyScriptPath = path.join(getGeneratedDirForBlock(state.paths, block), "verify.ts");
544
+ if (!fs.existsSync(verifyScriptPath)) {
545
+ const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
546
+ throw new Error(`Generated verify script is missing for ${block.blockName} (${selectedVersionsForBlock.join(", ")}). ` +
547
+ `Run \`${formatScaffoldCommand(selectedVersionsForBlock)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
548
+ }
549
+ const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
550
+ const filteredArgs = all
551
+ ? ["--all"]
552
+ : ["--from-migration-version", selectedVersionsForBlock[0]];
553
+ execFileSync(tsxBinary, [verifyScriptPath, ...filteredArgs], {
554
+ cwd: projectDir,
555
+ shell: process.platform === "win32",
556
+ stdio: "inherit",
557
+ });
558
+ renderLine(legacySingleBlock
559
+ ? `Verified migrations for ${selectedVersionsForBlock.join(", ")}`
560
+ : `Verified ${block.blockName} migrations for ${selectedVersionsForBlock.join(", ")}`);
561
+ }
562
+ return { verifiedVersions: targetVersions };
563
+ }
564
+ /**
565
+ * Validate the migration workspace without mutating files.
566
+ *
567
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
568
+ * @param options Doctor scope and console rendering options.
569
+ * @returns Structured doctor check results for the selected legacy versions.
570
+ */
571
+ export function doctorProjectMigrations(projectDir, { all = false, fromMigrationVersion, renderLine = console.log } = {}) {
572
+ const checks = [];
573
+ const recordCheck = (status, label, detail) => {
574
+ checks.push({ detail, label, status });
575
+ renderLine(`${status === "pass" ? "PASS" : "FAIL"} ${label}: ${detail}`);
576
+ };
577
+ let state;
578
+ try {
579
+ state = loadMigrationProject(projectDir);
580
+ const legacySingleBlock = isLegacySingleBlockProject(state);
581
+ recordCheck("pass", "Migration config", legacySingleBlock
582
+ ? `Loaded ${state.blocks[0]?.blockName} @ ${state.config.currentMigrationVersion}`
583
+ : `Loaded ${state.blocks.length} block target(s) @ ${state.config.currentMigrationVersion}`);
584
+ }
585
+ catch (error) {
586
+ recordCheck("fail", "Migration config", error instanceof Error ? error.message : String(error));
587
+ throw new Error("Migration doctor failed.");
588
+ }
589
+ const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
590
+ const legacySingleBlock = isLegacySingleBlockProject(state);
591
+ const snapshotVersions = new Set(targetVersions.length > 0
592
+ ? [state.config.currentMigrationVersion, ...targetVersions]
593
+ : state.config.supportedMigrationVersions);
594
+ for (const version of snapshotVersions) {
595
+ for (const block of state.blocks) {
596
+ const snapshotRoot = getSnapshotRoot(projectDir, block, version);
597
+ const blockJsonPath = getSnapshotBlockJsonPath(projectDir, block, version);
598
+ const manifestPath = getSnapshotManifestPath(projectDir, block, version);
599
+ const savePath = getSnapshotSavePath(projectDir, block, version);
600
+ const hasSnapshot = fs.existsSync(snapshotRoot);
601
+ const snapshotIsOptional = !hasSnapshot && isSnapshotOptionalForBlockVersion(state, block, version);
602
+ recordCheck(hasSnapshot || snapshotIsOptional ? "pass" : "fail", legacySingleBlock ? `Snapshot ${version}` : `Snapshot ${block.blockName} @ ${version}`, hasSnapshot
603
+ ? path.relative(projectDir, snapshotRoot)
604
+ : `Not present for this version`);
605
+ if (!hasSnapshot) {
606
+ continue;
607
+ }
608
+ for (const targetPath of [blockJsonPath, manifestPath, savePath]) {
609
+ recordCheck(fs.existsSync(targetPath) ? "pass" : "fail", legacySingleBlock
610
+ ? `Snapshot file ${version}`
611
+ : `Snapshot file ${block.blockName} @ ${version}`, fs.existsSync(targetPath)
612
+ ? path.relative(projectDir, targetPath)
613
+ : `Missing ${path.relative(projectDir, targetPath)}`);
614
+ }
615
+ }
616
+ }
617
+ try {
618
+ const generatedEntries = collectGeneratedMigrationEntries(state);
619
+ const expectedGeneratedFiles = new Map();
620
+ for (const block of state.blocks) {
621
+ const blockGeneratedEntries = generatedEntries.filter(({ entry }) => entry.block.key === block.key);
622
+ const entries = blockGeneratedEntries.map(({ entry }) => entry);
623
+ const generatedDir = getGeneratedDirForBlock(state.paths, block);
624
+ expectedGeneratedFiles.set(path.join(generatedDir, "registry.ts"), renderMigrationRegistryFile(state, block.key, blockGeneratedEntries));
625
+ expectedGeneratedFiles.set(path.join(generatedDir, "deprecated.ts"), renderGeneratedDeprecatedFile(entries));
626
+ expectedGeneratedFiles.set(path.join(generatedDir, "verify.ts"), renderVerifyFile(state, block.key, entries));
627
+ expectedGeneratedFiles.set(path.join(generatedDir, "fuzz.ts"), renderFuzzFile(state, block.key, blockGeneratedEntries));
628
+ }
629
+ expectedGeneratedFiles.set(path.join(state.paths.generatedDir, "index.ts"), renderGeneratedMigrationIndexFile(state, generatedEntries.map(({ entry }) => entry)));
630
+ expectedGeneratedFiles.set(path.join(projectDir, ROOT_PHP_MIGRATION_REGISTRY), renderPhpMigrationRegistryFile(state, generatedEntries.map(({ entry }) => entry)));
631
+ for (const [filePath, expectedSource] of expectedGeneratedFiles) {
632
+ const inSync = fs.existsSync(filePath) && fs.readFileSync(filePath, "utf8") === expectedSource;
633
+ recordCheck(inSync ? "pass" : "fail", `Generated ${path.relative(projectDir, filePath)}`, inSync
634
+ ? "In sync"
635
+ : `Run \`wp-typia migrate scaffold --from-migration-version <label>\` or regenerate artifacts`);
636
+ }
637
+ }
638
+ catch (error) {
639
+ recordCheck("fail", "Generated artifacts", error instanceof Error ? error.message : String(error));
640
+ }
641
+ for (const version of targetVersions) {
642
+ for (const block of state.blocks) {
643
+ if (!hasSnapshotForVersion(state, block, version)) {
644
+ recordCheck("pass", `Snapshot coverage ${block.blockName} @ ${version}`, "Target not present for this version");
645
+ continue;
646
+ }
647
+ const rulePath = getRuleFilePath(state.paths, block, version, state.config.currentMigrationVersion);
648
+ const fixturePath = getFixtureFilePath(state.paths, block, version, state.config.currentMigrationVersion);
649
+ recordCheck(fs.existsSync(rulePath) ? "pass" : "fail", legacySingleBlock ? `Rule ${version}` : `Rule ${block.blockName} @ ${version}`, fs.existsSync(rulePath)
650
+ ? path.relative(projectDir, rulePath)
651
+ : `Missing ${path.relative(projectDir, rulePath)}`);
652
+ recordCheck(fs.existsSync(fixturePath) ? "pass" : "fail", legacySingleBlock ? `Fixture ${version}` : `Fixture ${block.blockName} @ ${version}`, fs.existsSync(fixturePath)
653
+ ? path.relative(projectDir, fixturePath)
654
+ : `Missing ${path.relative(projectDir, fixturePath)}`);
655
+ if (!fs.existsSync(rulePath) || !fs.existsSync(fixturePath)) {
656
+ continue;
657
+ }
658
+ try {
659
+ assertRuleHasNoTodos(projectDir, block, version, state.config.currentMigrationVersion);
660
+ recordCheck("pass", legacySingleBlock
661
+ ? `Rule TODOs ${version}`
662
+ : `Rule TODOs ${block.blockName} @ ${version}`, "No TODO MIGRATION markers remain");
663
+ }
664
+ catch (error) {
665
+ recordCheck("fail", legacySingleBlock
666
+ ? `Rule TODOs ${version}`
667
+ : `Rule TODOs ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
668
+ }
669
+ try {
670
+ const ruleMetadata = readRuleMetadata(rulePath);
671
+ recordCheck(ruleMetadata.unresolved.length === 0 ? "pass" : "fail", legacySingleBlock
672
+ ? `Rule unresolved ${version}`
673
+ : `Rule unresolved ${block.blockName} @ ${version}`, ruleMetadata.unresolved.length === 0
674
+ ? "No unresolved entries remain"
675
+ : ruleMetadata.unresolved.join(", "));
676
+ }
677
+ catch (error) {
678
+ recordCheck("fail", legacySingleBlock
679
+ ? `Rule unresolved ${version}`
680
+ : `Rule unresolved ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
681
+ }
682
+ try {
683
+ const fixtureDocument = readJson(fixturePath);
684
+ recordCheck(Array.isArray(fixtureDocument.cases) && fixtureDocument.cases.length > 0 ? "pass" : "fail", legacySingleBlock
685
+ ? `Fixture parse ${version}`
686
+ : `Fixture parse ${block.blockName} @ ${version}`, Array.isArray(fixtureDocument.cases) && fixtureDocument.cases.length > 0
687
+ ? `${fixtureDocument.cases.length} case(s)`
688
+ : "Fixture document has no cases");
689
+ const diff = createMigrationDiff(state, block, version, state.config.currentMigrationVersion);
690
+ const expectedFixture = createEdgeFixtureDocument(projectDir, block, version, state.config.currentMigrationVersion, diff);
691
+ const actualCaseNames = new Set((fixtureDocument.cases ?? []).map((fixtureCase) => fixtureCase.name));
692
+ const missingCases = expectedFixture.cases
693
+ .map((fixtureCase) => fixtureCase.name)
694
+ .filter((name) => !actualCaseNames.has(name));
695
+ recordCheck(missingCases.length === 0 ? "pass" : "fail", legacySingleBlock
696
+ ? `Fixture coverage ${version}`
697
+ : `Fixture coverage ${block.blockName} @ ${version}`, missingCases.length === 0 ? "All expected fixture cases are present" : `Missing ${missingCases.join(", ")}`);
698
+ recordCheck("pass", legacySingleBlock
699
+ ? `Risk summary ${version}`
700
+ : `Risk summary ${block.blockName} @ ${version}`, formatMigrationRiskSummary(createMigrationRiskSummary(diff)));
701
+ }
702
+ catch (error) {
703
+ recordCheck("fail", legacySingleBlock
704
+ ? `Fixture parse ${version}`
705
+ : `Fixture parse ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
706
+ }
707
+ }
708
+ }
709
+ const failedChecks = checks.filter((check) => check.status === "fail");
710
+ renderLine(`${failedChecks.length === 0 ? "PASS" : "FAIL"} Migration doctor summary: ${checks.length - failedChecks.length}/${checks.length} checks passed`);
711
+ if (failedChecks.length > 0) {
712
+ throw new Error("Migration doctor failed.");
713
+ }
714
+ return {
715
+ checkedVersions: targetVersions,
716
+ checks,
717
+ };
718
+ }
719
+ /**
720
+ * Generate or refresh migration fixtures for one or more legacy edges.
721
+ *
722
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
723
+ * @param options Fixture generation scope and refresh options.
724
+ * @returns Generated and skipped legacy versions.
725
+ */
726
+ export function fixturesProjectMigrations(projectDir, { all = false, confirmOverwrite, force = false, fromMigrationVersion, isInteractive = isInteractiveTerminal(), renderLine = console.log, toMigrationVersion = "current", } = {}) {
727
+ ensureMigrationDirectories(projectDir);
728
+ const state = loadMigrationProject(projectDir);
729
+ const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
730
+ const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
731
+ if (targetVersions.length === 0) {
732
+ renderLine("No legacy migration versions configured for fixture generation.");
733
+ return { generatedVersions: [], skippedVersions: [] };
734
+ }
735
+ const generatedVersions = [];
736
+ const skippedVersions = [];
737
+ const fixtureTargets = collectFixtureTargets(state, targetVersions, targetMigrationVersion);
738
+ if (force) {
739
+ const overwriteTargets = fixtureTargets.filter(({ fixturePath }) => fs.existsSync(fixturePath));
740
+ if (isInteractive && overwriteTargets.length > 0) {
741
+ const confirmed = confirmOverwrite?.(`About to overwrite ${overwriteTargets.length} existing migration fixture file(s). Continue?`) ?? promptForConfirmation(`About to overwrite ${overwriteTargets.length} existing migration fixture file(s). Continue?`);
742
+ if (!confirmed) {
743
+ renderLine(`Cancelled fixture refresh. Kept ${overwriteTargets.length} existing fixture file(s).`);
744
+ return {
745
+ generatedVersions,
746
+ skippedVersions: overwriteTargets.map(({ scopedLabel }) => scopedLabel),
747
+ };
748
+ }
749
+ }
750
+ }
751
+ for (const { block, fixturePath, scopedLabel, version } of fixtureTargets) {
752
+ const existed = fs.existsSync(fixturePath);
753
+ const diff = createMigrationDiff(state, block, version, targetMigrationVersion);
754
+ const result = ensureEdgeFixtureFile(projectDir, block, version, targetMigrationVersion, diff, { force });
755
+ if (result.written) {
756
+ generatedVersions.push(scopedLabel);
757
+ renderLine(`${existed ? "Refreshed" : "Generated"} fixture ${path.relative(projectDir, fixturePath)}`);
758
+ }
759
+ else {
760
+ skippedVersions.push(scopedLabel);
761
+ renderLine(`Preserved existing fixture ${path.relative(projectDir, fixturePath)} (use --force to refresh)`);
762
+ }
763
+ }
764
+ return {
765
+ generatedVersions,
766
+ skippedVersions,
767
+ };
768
+ }
769
+ /**
770
+ * Run seeded migration fuzz verification against generated fuzz artifacts.
771
+ *
772
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
773
+ * @param options Fuzz scope, iteration count, seed, and console rendering options.
774
+ * @returns Fuzzed legacy versions and the effective seed.
775
+ */
776
+ export function fuzzProjectMigrations(projectDir, { all = false, fromMigrationVersion, iterations = 25, renderLine = console.log, seed, } = {}) {
777
+ const state = loadMigrationProject(projectDir);
778
+ const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
779
+ const blockEntries = getSelectedEntriesByBlock(state, targetVersions, "fuzz");
780
+ const legacySingleBlock = isLegacySingleBlockProject(state);
781
+ if (targetVersions.length === 0) {
782
+ renderLine("No legacy migration versions configured for fuzzing.");
783
+ return { fuzzedVersions: [] };
784
+ }
785
+ const tsxBinary = getLocalTsxBinary(projectDir);
786
+ for (const [blockKey, entries] of Object.entries(blockEntries)) {
787
+ const block = state.blocks.find((entry) => entry.key === blockKey);
788
+ if (!block || entries.length === 0) {
789
+ continue;
790
+ }
791
+ for (const entry of entries) {
792
+ assertRuleHasNoTodos(projectDir, block, entry.fromVersion, state.config.currentMigrationVersion);
793
+ }
794
+ const fuzzScriptPath = path.join(getGeneratedDirForBlock(state.paths, block), "fuzz.ts");
795
+ if (!fs.existsSync(fuzzScriptPath)) {
796
+ const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
797
+ throw new Error(`Generated fuzz script is missing for ${block.blockName} (${selectedVersionsForBlock.join(", ")}). ` +
798
+ `Run \`${formatScaffoldCommand(selectedVersionsForBlock)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
799
+ }
800
+ const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
801
+ const args = [
802
+ fuzzScriptPath,
803
+ ...(all ? ["--all"] : ["--from-migration-version", selectedVersionsForBlock[0]]),
804
+ "--iterations",
805
+ String(iterations),
806
+ ...(seed === undefined ? [] : ["--seed", String(seed)]),
807
+ ];
808
+ execFileSync(tsxBinary, args, {
809
+ cwd: projectDir,
810
+ shell: process.platform === "win32",
811
+ stdio: "inherit",
812
+ });
813
+ renderLine(legacySingleBlock
814
+ ? `Fuzzed migrations for ${selectedVersionsForBlock.join(", ")}`
815
+ : `Fuzzed ${block.blockName} migrations for ${selectedVersionsForBlock.join(", ")}`);
816
+ }
817
+ return { fuzzedVersions: targetVersions, seed };
818
+ }
819
+ function parsePositiveInteger(value, label) {
820
+ if (!value) {
821
+ return undefined;
822
+ }
823
+ if (!/^\d+$/.test(value)) {
824
+ throw new Error(`Invalid ${label}: ${value}. Expected a positive integer.`);
825
+ }
826
+ const parsed = Number.parseInt(value, 10);
827
+ if (!Number.isInteger(parsed) || parsed <= 0) {
828
+ throw new Error(`Invalid ${label}: ${value}. Expected a positive integer.`);
829
+ }
830
+ return parsed;
831
+ }
832
+ function parseNonNegativeInteger(value, label) {
833
+ if (!value) {
834
+ return undefined;
835
+ }
836
+ if (!/^\d+$/.test(value)) {
837
+ throw new Error(`Invalid ${label}: ${value}. Expected a non-negative integer.`);
838
+ }
839
+ const parsed = Number.parseInt(value, 10);
840
+ if (!Number.isInteger(parsed) || parsed < 0) {
841
+ throw new Error(`Invalid ${label}: ${value}. Expected a non-negative integer.`);
842
+ }
843
+ return parsed;
844
+ }
845
+ function resolveLegacyVersions(state, { all = false, availableVersions, fromMigrationVersion, }) {
846
+ const configuredLegacyVersions = listConfiguredLegacyVersions(state);
847
+ const legacyVersions = availableVersions ?? configuredLegacyVersions;
848
+ if (fromMigrationVersion) {
849
+ if (!legacyVersions.includes(fromMigrationVersion)) {
850
+ throw new Error(legacyVersions.length === 0
851
+ ? availableVersions && configuredLegacyVersions.length > 0
852
+ ? `Unsupported migration version: ${fromMigrationVersion}. No previewable legacy migration versions are available yet because none currently have snapshot coverage. ` +
853
+ `Restore or recapture the missing snapshots first.`
854
+ : `Unsupported migration version: ${fromMigrationVersion}. No legacy migration versions are configured yet. ` +
855
+ `Capture an older schema release with \`wp-typia migrate snapshot --migration-version <label>\` first.`
856
+ : `Unsupported migration version: ${fromMigrationVersion}. Available legacy migration versions: ${legacyVersions.join(", ")}.`);
857
+ }
858
+ return [fromMigrationVersion];
859
+ }
860
+ if (all) {
861
+ return legacyVersions;
862
+ }
863
+ return legacyVersions.slice(0, 1);
864
+ }
865
+ function listConfiguredLegacyVersions(state) {
866
+ return state.config.supportedMigrationVersions
867
+ .filter((version) => version !== state.config.currentMigrationVersion)
868
+ .sort(compareMigrationVersionLabels);
869
+ }
870
+ function listPreviewableLegacyVersions(state) {
871
+ return [...new Set(state.blocks.flatMap((block) => getAvailableSnapshotVersionsForBlock(state.projectDir, state.config.supportedMigrationVersions, block)))]
872
+ .filter((version) => version !== state.config.currentMigrationVersion)
873
+ .sort(compareMigrationVersionLabels);
874
+ }
875
+ function collectGeneratedMigrationEntries(state) {
876
+ return discoverMigrationEntries(state).map((entry) => {
877
+ const block = state.blocks.find((target) => target.key === entry.block.key);
878
+ if (!block) {
879
+ throw new Error(`Unknown migration block target: ${entry.block.key}`);
880
+ }
881
+ const diff = createMigrationDiff(state, entry.block, entry.fromVersion, entry.toVersion);
882
+ const legacyManifest = readJson(getSnapshotManifestPath(state.projectDir, entry.block, entry.fromVersion));
883
+ return {
884
+ diff,
885
+ entry,
886
+ fuzzPlan: createMigrationFuzzPlan(legacyManifest, block.currentManifest, diff),
887
+ riskSummary: createMigrationRiskSummary(diff),
888
+ };
889
+ });
890
+ }
891
+ function regenerateGeneratedArtifacts(projectDir) {
892
+ const state = loadMigrationProject(projectDir);
893
+ const generatedEntries = collectGeneratedMigrationEntries(state);
894
+ for (const block of state.blocks) {
895
+ const blockGeneratedEntries = generatedEntries.filter(({ entry }) => entry.block.key === block.key);
896
+ const entries = blockGeneratedEntries.map(({ entry }) => entry);
897
+ const generatedDir = getGeneratedDirForBlock(state.paths, block);
898
+ fs.mkdirSync(generatedDir, { recursive: true });
899
+ fs.writeFileSync(path.join(generatedDir, "registry.ts"), renderMigrationRegistryFile(state, block.key, blockGeneratedEntries), "utf8");
900
+ fs.writeFileSync(path.join(generatedDir, "deprecated.ts"), renderGeneratedDeprecatedFile(entries), "utf8");
901
+ fs.writeFileSync(path.join(generatedDir, "verify.ts"), renderVerifyFile(state, block.key, entries), "utf8");
902
+ fs.writeFileSync(path.join(generatedDir, "fuzz.ts"), renderFuzzFile(state, block.key, blockGeneratedEntries), "utf8");
903
+ }
904
+ fs.writeFileSync(path.join(state.paths.generatedDir, "index.ts"), renderGeneratedMigrationIndexFile(state, generatedEntries.map(({ entry }) => entry)), "utf8");
905
+ fs.writeFileSync(path.join(projectDir, ROOT_PHP_MIGRATION_REGISTRY), renderPhpMigrationRegistryFile(state, generatedEntries.map(({ entry }) => entry)), "utf8");
906
+ }
907
+ /**
908
+ * Initialize migration scaffolding for one or more block targets.
909
+ *
910
+ * Writes the migration config, creates the initial scaffold files, snapshots
911
+ * the current project state, and regenerates generated migration artifacts.
912
+ *
913
+ * @param projectDir Absolute or relative project directory containing the migration workspace.
914
+ * @param currentMigrationVersion Initial migration version label to seed into the migration config.
915
+ * @param blocks Block targets to register for migration-aware scaffolding.
916
+ * @param options Console rendering options for initialization output.
917
+ * @returns The loaded migration project state after initialization completes.
918
+ */
919
+ export function seedProjectMigrations(projectDir, currentMigrationVersion, blocks, { renderLine = console.log } = {}) {
920
+ ensureAdvancedMigrationProject(projectDir, blocks);
921
+ assertMigrationVersionLabel(currentMigrationVersion, "current migration version");
922
+ ensureMigrationDirectories(projectDir, blocks);
923
+ writeMigrationConfig(projectDir, {
924
+ blocks,
925
+ currentMigrationVersion,
926
+ snapshotDir: SNAPSHOT_DIR.replace(/\\/g, "/"),
927
+ supportedMigrationVersions: [currentMigrationVersion],
928
+ });
929
+ writeInitialMigrationScaffold(projectDir, currentMigrationVersion, blocks);
930
+ snapshotProjectVersion(projectDir, currentMigrationVersion, {
931
+ renderLine,
932
+ skipConfigUpdate: true,
933
+ skipSyncTypes: true,
934
+ });
935
+ regenerateGeneratedArtifacts(projectDir);
936
+ renderLine(`Initialized migrations for ${blocks.map((block) => block.blockName).join(", ")} at migration version ${currentMigrationVersion}`);
937
+ return loadMigrationProject(projectDir);
938
+ }
939
+ function hasSnapshotForVersion(state, block, version) {
940
+ return fs.existsSync(getSnapshotManifestPath(state.projectDir, block, version));
941
+ }
942
+ function getSelectedEntriesByBlock(state, targetVersions, command) {
943
+ const discoveredEntries = discoverMigrationEntries(state);
944
+ const discoveredEntryKeys = new Set(discoveredEntries.map((entry) => `${entry.block.key}:${entry.fromVersion}`));
945
+ const missingEntries = targetVersions.flatMap((version) => state.blocks
946
+ .filter((block) => hasSnapshotForVersion(state, block, version))
947
+ .filter((block) => !discoveredEntryKeys.has(`${block.key}:${version}`))
948
+ .map((block) => ({ block, version })));
949
+ if (missingEntries.length > 0) {
950
+ const missingLabels = missingEntries
951
+ .map(({ block, version }) => `${block.blockName} @ ${version}`)
952
+ .join(", ");
953
+ const missingVersions = [...new Set(missingEntries.map(({ version }) => version))].sort(compareMigrationVersionLabels);
954
+ throw new Error(`Missing migration ${command} inputs for ${missingLabels}. ` +
955
+ `Run \`${formatScaffoldCommand(missingVersions)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
956
+ }
957
+ return groupEntriesByBlock(discoveredEntries.filter((entry) => targetVersions.includes(entry.fromVersion)));
958
+ }
959
+ function isSnapshotOptionalForBlockVersion(state, block, version) {
960
+ if (block.layout !== "multi") {
961
+ return false;
962
+ }
963
+ const introducedVersions = [...new Set(state.config.supportedMigrationVersions)]
964
+ .filter((candidateVersion) => hasSnapshotForVersion(state, block, candidateVersion))
965
+ .sort(compareMigrationVersionLabels);
966
+ const firstIntroducedVersion = introducedVersions[0];
967
+ if (!firstIntroducedVersion) {
968
+ return false;
969
+ }
970
+ return compareMigrationVersionLabels(version, firstIntroducedVersion) < 0;
971
+ }
972
+ function groupEntriesByBlock(entries) {
973
+ return entries.reduce((accumulator, entry) => {
974
+ if (!accumulator[entry.block.key]) {
975
+ accumulator[entry.block.key] = [];
976
+ }
977
+ accumulator[entry.block.key].push(entry);
978
+ return accumulator;
979
+ }, {});
980
+ }
981
+ function isLegacySingleBlockProject(state) {
982
+ return state.blocks.length === 1 && state.blocks[0]?.layout === "legacy";
983
+ }
984
+ function assertDistinctMigrationEdge(command, fromVersion, toVersion) {
985
+ if (fromVersion === toVersion) {
986
+ throw new Error(`\`migrate ${command}\` requires different source and target migration versions, but both resolved to ${fromVersion}. ` +
987
+ `Choose an older snapshot with \`--from-migration-version <label>\` or capture a newer schema release with \`wp-typia migrate snapshot --migration-version <label>\` first.`);
988
+ }
989
+ }
990
+ function createMigrationPlanNextSteps(fromVersion, targetVersion, currentVersion) {
991
+ if (targetVersion !== currentVersion) {
992
+ return [
993
+ formatEdgeCommand("scaffold", fromVersion, targetVersion, currentVersion),
994
+ ];
995
+ }
996
+ return [
997
+ formatEdgeCommand("scaffold", fromVersion, targetVersion, currentVersion),
998
+ `wp-typia migrate doctor --from-migration-version ${fromVersion}`,
999
+ `wp-typia migrate verify --from-migration-version ${fromVersion}`,
1000
+ `wp-typia migrate fuzz --from-migration-version ${fromVersion}`,
1001
+ ];
1002
+ }
1003
+ function formatEdgeCommand(command, fromVersion, targetVersion, currentVersion) {
1004
+ return targetVersion === currentVersion
1005
+ ? `wp-typia migrate ${command} --from-migration-version ${fromVersion}`
1006
+ : `wp-typia migrate ${command} --from-migration-version ${fromVersion} --to-migration-version ${targetVersion}`;
1007
+ }
1008
+ function createMissingProjectSnapshotMessage(state, fromVersion) {
1009
+ const snapshotVersions = [...new Set(state.blocks.flatMap((block) => getAvailableSnapshotVersionsForBlock(state.projectDir, state.config.supportedMigrationVersions, block)))].sort(compareMigrationVersionLabels);
1010
+ return snapshotVersions.length === 0
1011
+ ? `No migration block targets have a snapshot for ${fromVersion}. No snapshots exist yet in this project. ` +
1012
+ `Run \`wp-typia migrate snapshot --migration-version ${fromVersion}\` first if you want to preserve that schema state.`
1013
+ : `No migration block targets have a snapshot for ${fromVersion}. ` +
1014
+ `Available snapshot versions in this project: ${snapshotVersions.join(", ")}. ` +
1015
+ `Run \`wp-typia migrate snapshot --migration-version ${fromVersion}\` first if you want to preserve that schema state.`;
1016
+ }
1017
+ function formatScaffoldCommand(versions) {
1018
+ const uniqueVersions = [...new Set(versions)].sort(compareMigrationVersionLabels);
1019
+ return uniqueVersions.length === 1
1020
+ ? `wp-typia migrate scaffold --from-migration-version ${uniqueVersions[0]}`
1021
+ : "wp-typia migrate scaffold --from-migration-version <label>";
1022
+ }
1023
+ function throwLegacyMigrationFlagError(flag) {
1024
+ const replacement = flag.startsWith("--current-version")
1025
+ ? "--current-migration-version"
1026
+ : flag.startsWith("--version")
1027
+ ? "--migration-version"
1028
+ : flag.startsWith("--from")
1029
+ ? "--from-migration-version"
1030
+ : "--to-migration-version";
1031
+ throw new Error(`Legacy migration flag \`${flag}\` is no longer supported. Use \`${replacement}\` with schema labels like \`v1\` and \`v2\` instead. ` +
1032
+ formatLegacyMigrationWorkspaceResetGuidance());
1033
+ }
1034
+ function promptForConfirmation(message) {
1035
+ process.stdout.write(`${message} [y/N]: `);
1036
+ const buffer = Buffer.alloc(1);
1037
+ let answer = "";
1038
+ while (true) {
1039
+ const bytesRead = fs.readSync(process.stdin.fd, buffer, 0, 1, null);
1040
+ if (bytesRead === 0) {
1041
+ break;
1042
+ }
1043
+ const char = buffer.toString("utf8", 0, bytesRead);
1044
+ if (char === "\n" || char === "\r") {
1045
+ break;
1046
+ }
1047
+ answer += char;
1048
+ }
1049
+ const normalized = answer.trim().toLowerCase();
1050
+ return normalized === "y" || normalized === "yes";
1051
+ }
1052
+ function collectFixtureTargets(state, targetVersions, targetVersion) {
1053
+ return targetVersions.flatMap((version) => state.blocks
1054
+ .filter((block) => hasSnapshotForVersion(state, block, version))
1055
+ .map((block) => ({
1056
+ block,
1057
+ fixturePath: getFixtureFilePath(state.paths, block, version, targetVersion),
1058
+ scopedLabel: `${block.key}@${version}`,
1059
+ version,
1060
+ })));
1061
+ }