@wp-typia/create 0.1.0

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 (95) hide show
  1. package/README.md +43 -0
  2. package/dist/cli.js +2492 -0
  3. package/dist/runtime/cli-core.js +222 -0
  4. package/dist/runtime/index.js +4 -0
  5. package/dist/runtime/migration-constants.js +14 -0
  6. package/dist/runtime/migration-diff.js +521 -0
  7. package/dist/runtime/migration-fixtures.js +89 -0
  8. package/dist/runtime/migration-manifest.js +129 -0
  9. package/dist/runtime/migration-project.js +167 -0
  10. package/dist/runtime/migration-render.js +267 -0
  11. package/dist/runtime/migration-types.js +1 -0
  12. package/dist/runtime/migration-utils.js +184 -0
  13. package/dist/runtime/migrations.js +232 -0
  14. package/dist/runtime/package-managers.js +135 -0
  15. package/dist/runtime/scaffold.js +334 -0
  16. package/dist/runtime/template-registry.js +75 -0
  17. package/package.json +65 -0
  18. package/templates/advanced/README.md.mustache +150 -0
  19. package/templates/advanced/block.json.mustache +43 -0
  20. package/templates/advanced/index.js +21 -0
  21. package/templates/advanced/package.json.mustache +47 -0
  22. package/templates/advanced/render.php.mustache +83 -0
  23. package/templates/advanced/scripts/lib/typia-metadata-core.ts +1413 -0
  24. package/templates/advanced/scripts/sync-types-to-block-json.ts.mustache +32 -0
  25. package/templates/advanced/src/admin/migration-dashboard.tsx.mustache +315 -0
  26. package/templates/advanced/src/components/ErrorBoundary.tsx.mustache +47 -0
  27. package/templates/advanced/src/deprecated.ts.mustache +2 -0
  28. package/templates/advanced/src/edit.tsx.mustache +97 -0
  29. package/templates/advanced/src/hooks/useDebounce.ts.mustache +20 -0
  30. package/templates/advanced/src/hooks/useLocalStorage.ts.mustache +31 -0
  31. package/templates/advanced/src/hooks.ts.mustache +56 -0
  32. package/templates/advanced/src/index.tsx.mustache +18 -0
  33. package/templates/advanced/src/migration-detector.ts.mustache +9 -0
  34. package/templates/advanced/src/migrations/config.ts.mustache +8 -0
  35. package/templates/advanced/src/migrations/examples/rename-transform-union/README.md.mustache +23 -0
  36. package/templates/advanced/src/migrations/examples/rename-transform-union/fixture.example.json.mustache +36 -0
  37. package/templates/advanced/src/migrations/examples/rename-transform-union/rule.example.ts.mustache +47 -0
  38. package/templates/advanced/src/migrations/fixtures/README.md.mustache +3 -0
  39. package/templates/advanced/src/migrations/generated/deprecated.ts.mustache +3 -0
  40. package/templates/advanced/src/migrations/generated/registry.ts.mustache +9 -0
  41. package/templates/advanced/src/migrations/generated/verify.ts.mustache +1 -0
  42. package/templates/advanced/src/migrations/helpers.ts.mustache +354 -0
  43. package/templates/advanced/src/migrations/index.ts.mustache +616 -0
  44. package/templates/advanced/src/migrations/rules/README.md.mustache +3 -0
  45. package/templates/advanced/src/migrations/versions/README.md.mustache +3 -0
  46. package/templates/advanced/src/save.tsx.mustache +12 -0
  47. package/templates/advanced/src/style.scss.mustache +84 -0
  48. package/templates/advanced/src/types.ts.mustache +46 -0
  49. package/templates/advanced/src/utils/classnames.ts.mustache +51 -0
  50. package/templates/advanced/src/utils/debounce.ts.mustache +37 -0
  51. package/templates/advanced/src/utils/index.ts.mustache +7 -0
  52. package/templates/advanced/src/utils/uuid.ts.mustache +17 -0
  53. package/templates/advanced/src/validators.ts.mustache +39 -0
  54. package/templates/advanced/src/view.ts.mustache +59 -0
  55. package/templates/advanced/tsconfig.json.mustache +20 -0
  56. package/templates/advanced/webpack.config.js.mustache +95 -0
  57. package/templates/basic/package.json.mustache +39 -0
  58. package/templates/basic/scripts/lib/typia-metadata-core.ts +1413 -0
  59. package/templates/basic/scripts/sync-types-to-block-json.ts +25 -0
  60. package/templates/basic/src/block.json +51 -0
  61. package/templates/basic/src/edit.tsx +85 -0
  62. package/templates/basic/src/hooks.ts +75 -0
  63. package/templates/basic/src/index.tsx +37 -0
  64. package/templates/basic/src/save.tsx +27 -0
  65. package/templates/basic/src/style.scss +42 -0
  66. package/templates/basic/src/types.ts +48 -0
  67. package/templates/basic/src/validators.ts +39 -0
  68. package/templates/basic/tsconfig.json +20 -0
  69. package/templates/basic/webpack.config.js +89 -0
  70. package/templates/full/package.json.mustache +40 -0
  71. package/templates/full/scripts/lib/typia-metadata-core.ts +1413 -0
  72. package/templates/full/scripts/sync-types-to-block-json.ts.mustache +32 -0
  73. package/templates/full/src/block.json.mustache +120 -0
  74. package/templates/full/src/edit.tsx.mustache +300 -0
  75. package/templates/full/src/editor.scss.mustache +251 -0
  76. package/templates/full/src/hooks.ts.mustache +141 -0
  77. package/templates/full/src/index.tsx.mustache +27 -0
  78. package/templates/full/src/save.tsx.mustache +39 -0
  79. package/templates/full/src/style.scss.mustache +224 -0
  80. package/templates/full/src/types.ts.mustache +35 -0
  81. package/templates/full/src/validators.ts.mustache +84 -0
  82. package/templates/full/tsconfig.json.mustache +20 -0
  83. package/templates/full/webpack.config.js.mustache +89 -0
  84. package/templates/interactivity/package.json.mustache +41 -0
  85. package/templates/interactivity/scripts/lib/typia-metadata-core.ts +1413 -0
  86. package/templates/interactivity/scripts/sync-types-to-block-json.ts.mustache +32 -0
  87. package/templates/interactivity/src/block.json.mustache +74 -0
  88. package/templates/interactivity/src/edit.tsx.mustache +206 -0
  89. package/templates/interactivity/src/index.tsx.mustache +20 -0
  90. package/templates/interactivity/src/interactivity.ts.mustache +183 -0
  91. package/templates/interactivity/src/save.tsx.mustache +87 -0
  92. package/templates/interactivity/src/style.scss.mustache +60 -0
  93. package/templates/interactivity/src/types.ts.mustache +30 -0
  94. package/templates/interactivity/tsconfig.json.mustache +20 -0
  95. package/templates/interactivity/webpack.config.js.mustache +89 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2492 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/runtime/cli-core.ts
4
+ import fs3 from "node:fs";
5
+ import os from "node:os";
6
+ import path3 from "node:path";
7
+ import readline from "node:readline";
8
+ import { access, constants as fsConstants, rm, writeFile } from "node:fs/promises";
9
+ import { execFileSync } from "node:child_process";
10
+
11
+ // src/runtime/scaffold.ts
12
+ import fs2 from "node:fs";
13
+ import { promises as fsp } from "node:fs";
14
+ import path2 from "node:path";
15
+ import { execSync } from "node:child_process";
16
+
17
+ // src/runtime/package-managers.ts
18
+ var PACKAGE_MANAGER_DATA = [
19
+ {
20
+ id: "bun",
21
+ label: "Bun",
22
+ packageManagerField: "bun@1.3.10",
23
+ installCommand: "bun install",
24
+ frozenInstallCommand: "bun install --frozen-lockfile"
25
+ },
26
+ {
27
+ id: "npm",
28
+ label: "npm",
29
+ packageManagerField: "npm@11.6.1",
30
+ installCommand: "npm install",
31
+ frozenInstallCommand: "npm ci"
32
+ },
33
+ {
34
+ id: "pnpm",
35
+ label: "pnpm",
36
+ packageManagerField: "pnpm@8.3.1",
37
+ installCommand: "pnpm install",
38
+ frozenInstallCommand: "pnpm install --frozen-lockfile"
39
+ },
40
+ {
41
+ id: "yarn",
42
+ label: "Yarn",
43
+ packageManagerField: "yarn@3.2.4",
44
+ installCommand: "yarn install",
45
+ frozenInstallCommand: "yarn install --frozen-lockfile"
46
+ }
47
+ ];
48
+ var PACKAGE_MANAGER_IDS = PACKAGE_MANAGER_DATA.map((manager) => manager.id);
49
+ var PACKAGE_MANAGERS = Object.freeze(Object.fromEntries(PACKAGE_MANAGER_DATA.map((manager) => [manager.id, manager])));
50
+ var DEV_INSTALL_FLAGS = {
51
+ bun: "add -d",
52
+ npm: "install --save-dev",
53
+ pnpm: "add -D",
54
+ yarn: "add -D"
55
+ };
56
+ var STOP_CHARS = new Set([`
57
+ `, "\r", "`", '"', "'", ")", "]", "}", "!", ",", "."]);
58
+ function getPackageManager(id) {
59
+ const manager = PACKAGE_MANAGERS[id];
60
+ if (!manager) {
61
+ throw new Error(`Unknown package manager "${id}". Expected one of: ${PACKAGE_MANAGER_IDS.join(", ")}`);
62
+ }
63
+ return manager;
64
+ }
65
+ function getPackageManagerSelectOptions() {
66
+ return PACKAGE_MANAGER_DATA.map((manager) => ({
67
+ label: manager.label,
68
+ value: manager.id,
69
+ hint: manager.installCommand
70
+ }));
71
+ }
72
+ function formatRunScript(packageManagerId, scriptName, extraArgs = "") {
73
+ const args = extraArgs.trim();
74
+ if (packageManagerId === "bun") {
75
+ return args ? `bun run ${scriptName} ${args}` : `bun run ${scriptName}`;
76
+ }
77
+ if (packageManagerId === "npm") {
78
+ return args ? `npm run ${scriptName} -- ${args}` : `npm run ${scriptName}`;
79
+ }
80
+ if (packageManagerId === "pnpm") {
81
+ return args ? `pnpm run ${scriptName} -- ${args}` : `pnpm run ${scriptName}`;
82
+ }
83
+ return args ? `yarn run ${scriptName} ${args}` : `yarn run ${scriptName}`;
84
+ }
85
+ function formatInstallCommand(packageManagerId) {
86
+ return getPackageManager(packageManagerId).installCommand;
87
+ }
88
+ function consumeCommandArguments(content, startIndex) {
89
+ let cursor = startIndex;
90
+ let args = "";
91
+ while (cursor < content.length) {
92
+ const current = content[cursor];
93
+ if (STOP_CHARS.has(current) || content.startsWith("&&", cursor) || content.startsWith("||", cursor) || current === ";") {
94
+ break;
95
+ }
96
+ args += current;
97
+ cursor += 1;
98
+ }
99
+ return {
100
+ args: args.trim(),
101
+ cursor
102
+ };
103
+ }
104
+ function replaceBunRunCommands(content, packageManagerId) {
105
+ const marker = "bun run ";
106
+ let result = "";
107
+ let cursor = 0;
108
+ while (cursor < content.length) {
109
+ const index = content.indexOf(marker, cursor);
110
+ if (index === -1) {
111
+ result += content.slice(cursor);
112
+ break;
113
+ }
114
+ if (index > 0 && /[A-Za-z0-9_-]/.test(content[index - 1])) {
115
+ result += content.slice(cursor, index + marker.length);
116
+ cursor = index + marker.length;
117
+ continue;
118
+ }
119
+ result += content.slice(cursor, index);
120
+ const scriptNameStart = index + marker.length;
121
+ const scriptNameMatch = /^[A-Za-z0-9:_-]+/.exec(content.slice(scriptNameStart));
122
+ if (!scriptNameMatch) {
123
+ result += marker;
124
+ cursor = scriptNameStart;
125
+ continue;
126
+ }
127
+ const scriptName = scriptNameMatch[0];
128
+ const argsStart = scriptNameStart + scriptName.length;
129
+ const { args, cursor: nextCursor } = consumeCommandArguments(content, argsStart);
130
+ result += formatRunScript(packageManagerId, scriptName, args);
131
+ cursor = nextCursor;
132
+ }
133
+ return result;
134
+ }
135
+ function replaceDevDependencyInstalls(content, packageManagerId) {
136
+ return content.replace(/\bbun add -d ([^\s&|;`"'()]+)\b/g, (_, packageName) => {
137
+ if (packageManagerId === "bun") {
138
+ return `bun add -d ${packageName}`;
139
+ }
140
+ return `${packageManagerId} ${DEV_INSTALL_FLAGS[packageManagerId]} ${packageName}`;
141
+ });
142
+ }
143
+ function transformPackageManagerText(content, packageManagerId) {
144
+ const manager = getPackageManager(packageManagerId);
145
+ return replaceDevDependencyInstalls(replaceBunRunCommands(content.replace(/\bbun install --frozen-lockfile\b/g, manager.frozenInstallCommand).replace(/\bbun install\b/g, manager.installCommand), packageManagerId), packageManagerId).replace(/\s*&&\s*/g, " && ").replace(/\s*\|\|\s*/g, " || ");
146
+ }
147
+
148
+ // src/runtime/template-registry.ts
149
+ import fs from "node:fs";
150
+ import path from "node:path";
151
+ import { fileURLToPath } from "node:url";
152
+ var __dirname2 = path.dirname(fileURLToPath(import.meta.url));
153
+ function resolvePackageRoot(startDir) {
154
+ let currentDir = startDir;
155
+ while (true) {
156
+ const packageJsonPath = path.join(currentDir, "package.json");
157
+ if (fs.existsSync(packageJsonPath)) {
158
+ try {
159
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
160
+ if (packageJson.name === "@wp-typia/create") {
161
+ return currentDir;
162
+ }
163
+ } catch {}
164
+ }
165
+ const parentDir = path.dirname(currentDir);
166
+ if (parentDir === currentDir) {
167
+ throw new Error("Unable to resolve the @wp-typia/create package root.");
168
+ }
169
+ currentDir = parentDir;
170
+ }
171
+ }
172
+ var TEMPLATE_ROOT = path.join(resolvePackageRoot(__dirname2), "templates");
173
+ var TEMPLATE_REGISTRY = Object.freeze([
174
+ {
175
+ id: "basic",
176
+ description: "A lightweight WordPress block with Typia validation",
177
+ defaultCategory: "text",
178
+ features: ["Type-safe attributes", "Runtime validation", "Minimal setup"],
179
+ templateDir: path.join(TEMPLATE_ROOT, "basic")
180
+ },
181
+ {
182
+ id: "full",
183
+ description: "A full-featured WordPress block with Typia validation and utilities",
184
+ defaultCategory: "widgets",
185
+ features: ["Advanced controls", "Custom hooks", "Style options"],
186
+ templateDir: path.join(TEMPLATE_ROOT, "full")
187
+ },
188
+ {
189
+ id: "interactivity",
190
+ description: "An interactive WordPress block with Typia validation and Interactivity API",
191
+ defaultCategory: "widgets",
192
+ features: ["Interactivity API", "Client-side state", "Event handling"],
193
+ templateDir: path.join(TEMPLATE_ROOT, "interactivity")
194
+ },
195
+ {
196
+ id: "advanced",
197
+ description: "An advanced WordPress block with Typia validation and migration tooling",
198
+ defaultCategory: "widgets",
199
+ features: ["Migration system", "Version tracking", "Admin dashboard"],
200
+ templateDir: path.join(TEMPLATE_ROOT, "advanced")
201
+ }
202
+ ]);
203
+ var TEMPLATE_IDS = TEMPLATE_REGISTRY.map((template) => template.id);
204
+ function listTemplates() {
205
+ return TEMPLATE_REGISTRY;
206
+ }
207
+ function getTemplateById(templateId) {
208
+ const template = TEMPLATE_REGISTRY.find((entry) => entry.id === templateId);
209
+ if (!template) {
210
+ throw new Error(`Unknown template "${templateId}". Expected one of: ${TEMPLATE_IDS.join(", ")}`);
211
+ }
212
+ return template;
213
+ }
214
+ function getTemplateSelectOptions() {
215
+ return TEMPLATE_REGISTRY.map((template) => ({
216
+ label: template.id,
217
+ value: template.id,
218
+ hint: template.features.join(", ")
219
+ }));
220
+ }
221
+
222
+ // src/runtime/scaffold.ts
223
+ var BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
224
+ var LOCKFILES = {
225
+ bun: ["bun.lock", "bun.lockb"],
226
+ npm: ["package-lock.json"],
227
+ pnpm: ["pnpm-lock.yaml"],
228
+ yarn: ["yarn.lock"]
229
+ };
230
+ function toKebabCase(input) {
231
+ return input.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").toLowerCase();
232
+ }
233
+ function toSnakeCase(input) {
234
+ return toKebabCase(input).replace(/-/g, "_");
235
+ }
236
+ function toPascalCase(input) {
237
+ return toKebabCase(input).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
238
+ }
239
+ function toTitle(input) {
240
+ return toKebabCase(input).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
241
+ }
242
+ function validateBlockSlug(input) {
243
+ return BLOCK_SLUG_PATTERN.test(input) || "Use lowercase letters, numbers, and hyphens only";
244
+ }
245
+ function detectAuthor() {
246
+ try {
247
+ return execSync("git config user.name", {
248
+ encoding: "utf8",
249
+ stdio: ["ignore", "pipe", "ignore"]
250
+ }).trim() || "Your Name";
251
+ } catch {
252
+ return "Your Name";
253
+ }
254
+ }
255
+ function getDefaultAnswers(projectName, templateId) {
256
+ const template = getTemplateById(templateId);
257
+ const slugDefault = toKebabCase(projectName || "my-wp-typia-block");
258
+ return {
259
+ author: detectAuthor(),
260
+ description: template.description,
261
+ namespace: "create-block",
262
+ slug: slugDefault,
263
+ title: toTitle(slugDefault)
264
+ };
265
+ }
266
+ async function resolveTemplateId({
267
+ templateId,
268
+ yes = false,
269
+ isInteractive = false,
270
+ selectTemplate
271
+ }) {
272
+ if (templateId) {
273
+ return getTemplateById(templateId).id;
274
+ }
275
+ if (yes) {
276
+ return "basic";
277
+ }
278
+ if (!isInteractive || !selectTemplate) {
279
+ throw new Error(`Template is required in non-interactive mode. Use --template <${TEMPLATE_IDS.join("|")}>.`);
280
+ }
281
+ return selectTemplate();
282
+ }
283
+ async function resolvePackageManagerId({
284
+ packageManager,
285
+ yes = false,
286
+ isInteractive = false,
287
+ selectPackageManager
288
+ }) {
289
+ if (packageManager) {
290
+ return getPackageManager(packageManager).id;
291
+ }
292
+ if (yes) {
293
+ throw new Error(`Package manager is required when using --yes. Use --package-manager <${PACKAGE_MANAGER_IDS.join("|")}>.`);
294
+ }
295
+ if (!isInteractive || !selectPackageManager) {
296
+ throw new Error(`Package manager is required in non-interactive mode. Use --package-manager <${PACKAGE_MANAGER_IDS.join("|")}>.`);
297
+ }
298
+ return selectPackageManager();
299
+ }
300
+ async function collectScaffoldAnswers({
301
+ projectName,
302
+ templateId,
303
+ yes = false,
304
+ promptText
305
+ }) {
306
+ const defaults = getDefaultAnswers(projectName, templateId);
307
+ if (yes) {
308
+ return defaults;
309
+ }
310
+ if (!promptText) {
311
+ throw new Error("Interactive answers require a promptText callback.");
312
+ }
313
+ const slug = toKebabCase(await promptText("Block slug", defaults.slug, validateBlockSlug));
314
+ return {
315
+ author: await promptText("Author", defaults.author),
316
+ description: await promptText("Description", defaults.description),
317
+ namespace: await promptText("Namespace", defaults.namespace),
318
+ slug,
319
+ title: await promptText("Block title", toTitle(slug))
320
+ };
321
+ }
322
+ function getTemplateVariables(templateId, answers) {
323
+ const template = getTemplateById(templateId);
324
+ const slug = toKebabCase(answers.slug);
325
+ const slugSnakeCase = toSnakeCase(slug);
326
+ const pascalCase = toPascalCase(slug);
327
+ const title = answers.title.trim();
328
+ const namespace = answers.namespace.trim();
329
+ const description = answers.description.trim();
330
+ return {
331
+ author: answers.author.trim(),
332
+ category: template.defaultCategory,
333
+ cssClassName: `wp-block-${slug}`,
334
+ dashCase: slug,
335
+ dashicon: "smiley",
336
+ description,
337
+ keyword: slug.replace(/-/g, " "),
338
+ namespace,
339
+ needsMigration: "{{needsMigration}}",
340
+ pascalCase,
341
+ slug,
342
+ slugCamelCase: pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1),
343
+ slugKebabCase: slug,
344
+ slugSnakeCase,
345
+ textDomain: slugSnakeCase,
346
+ textdomain: slugSnakeCase,
347
+ title,
348
+ titleCase: pascalCase
349
+ };
350
+ }
351
+ function replaceVariables(content, variables) {
352
+ return content.replace(/\{\{([^}]+)\}\}/g, (match, rawKey) => {
353
+ const key = rawKey.trim();
354
+ return Object.prototype.hasOwnProperty.call(variables, key) ? variables[key] : match;
355
+ });
356
+ }
357
+ async function ensureDirectory(targetDir, allowExisting = false) {
358
+ if (!fs2.existsSync(targetDir)) {
359
+ await fsp.mkdir(targetDir, { recursive: true });
360
+ return;
361
+ }
362
+ if (allowExisting) {
363
+ return;
364
+ }
365
+ const entries = await fsp.readdir(targetDir);
366
+ if (entries.length > 0) {
367
+ throw new Error(`Target directory is not empty: ${targetDir}`);
368
+ }
369
+ }
370
+ async function copyTemplateDir(sourceDir, targetDir, variables) {
371
+ const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
372
+ for (const entry of entries) {
373
+ const sourcePath = path2.join(sourceDir, entry.name);
374
+ const destinationName = entry.name.endsWith(".mustache") ? entry.name.slice(0, -".mustache".length) : entry.name;
375
+ const destinationPath = path2.join(targetDir, destinationName);
376
+ if (entry.isDirectory()) {
377
+ await fsp.mkdir(destinationPath, { recursive: true });
378
+ await copyTemplateDir(sourcePath, destinationPath, variables);
379
+ continue;
380
+ }
381
+ const content = await fsp.readFile(sourcePath, "utf8");
382
+ await fsp.writeFile(destinationPath, replaceVariables(content, variables), "utf8");
383
+ }
384
+ }
385
+ function buildReadme(templateId, variables, packageManager) {
386
+ return `# ${variables.title}
387
+
388
+ ${variables.description}
389
+
390
+ ## Template
391
+
392
+ ${templateId}
393
+
394
+ ## Development
395
+
396
+ \`\`\`bash
397
+ ${formatInstallCommand(packageManager)}
398
+ ${formatRunScript(packageManager, "start")}
399
+ \`\`\`
400
+
401
+ ## Build
402
+
403
+ \`\`\`bash
404
+ ${formatRunScript(packageManager, "build")}
405
+ \`\`\`
406
+
407
+ ## Type Sync
408
+
409
+ \`\`\`bash
410
+ ${formatRunScript(packageManager, "sync-types")}
411
+ \`\`\`
412
+
413
+ \`src/types.ts\` remains the source of truth for \`block.json\` and \`typia.manifest.json\`.
414
+ `;
415
+ }
416
+ function buildGitignore() {
417
+ return `# Dependencies
418
+ node_modules/
419
+ .yarn/
420
+ .pnp.*
421
+
422
+ # Build
423
+ build/
424
+ dist/
425
+
426
+ # Editor
427
+ .vscode/
428
+ .idea/
429
+
430
+ # OS
431
+ .DS_Store
432
+ Thumbs.db
433
+
434
+ # WordPress
435
+ *.log
436
+ .wp-env/
437
+ `;
438
+ }
439
+ async function normalizePackageManagerFiles(targetDir, packageManagerId) {
440
+ const yarnRcPath = path2.join(targetDir, ".yarnrc.yml");
441
+ if (packageManagerId === "yarn") {
442
+ await fsp.writeFile(yarnRcPath, `nodeLinker: node-modules
443
+ `, "utf8");
444
+ return;
445
+ }
446
+ if (fs2.existsSync(yarnRcPath)) {
447
+ await fsp.rm(yarnRcPath, { force: true });
448
+ }
449
+ }
450
+ async function normalizePackageJson(targetDir, packageManagerId) {
451
+ const packageJsonPath = path2.join(targetDir, "package.json");
452
+ if (!fs2.existsSync(packageJsonPath)) {
453
+ return;
454
+ }
455
+ const packageManager = getPackageManager(packageManagerId);
456
+ const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
457
+ packageJson.packageManager = packageManager.packageManagerField;
458
+ if (packageJson.scripts) {
459
+ for (const [key, value] of Object.entries(packageJson.scripts)) {
460
+ if (typeof value === "string") {
461
+ packageJson.scripts[key] = transformPackageManagerText(value, packageManagerId);
462
+ }
463
+ }
464
+ }
465
+ await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
466
+ `, "utf8");
467
+ }
468
+ async function removeUnexpectedLockfiles(targetDir, packageManagerId) {
469
+ const keep = new Set(LOCKFILES[packageManagerId] ?? []);
470
+ const allLockfiles = Object.values(LOCKFILES).flat();
471
+ await Promise.all(allLockfiles.map(async (filename) => {
472
+ if (keep.has(filename)) {
473
+ return;
474
+ }
475
+ const filePath = path2.join(targetDir, filename);
476
+ if (fs2.existsSync(filePath)) {
477
+ await fsp.rm(filePath, { force: true });
478
+ }
479
+ }));
480
+ }
481
+ async function replaceTextRecursively(targetDir, packageManagerId) {
482
+ const textExtensions = new Set([
483
+ ".css",
484
+ ".js",
485
+ ".json",
486
+ ".jsx",
487
+ ".md",
488
+ ".php",
489
+ ".scss",
490
+ ".ts",
491
+ ".tsx",
492
+ ".txt"
493
+ ]);
494
+ async function visit(currentPath) {
495
+ const stats = await fsp.stat(currentPath);
496
+ if (stats.isDirectory()) {
497
+ const entries = await fsp.readdir(currentPath);
498
+ for (const entry of entries) {
499
+ await visit(path2.join(currentPath, entry));
500
+ }
501
+ return;
502
+ }
503
+ if (path2.basename(currentPath) === "package.json" || !textExtensions.has(path2.extname(currentPath))) {
504
+ return;
505
+ }
506
+ const content = await fsp.readFile(currentPath, "utf8");
507
+ const nextContent = transformPackageManagerText(content, packageManagerId).replace(/yourusername\/wp-typia-boilerplate/g, "imjlk/wp-typia").replace(/yourusername\/wp-typia/g, "imjlk/wp-typia");
508
+ if (nextContent !== content) {
509
+ await fsp.writeFile(currentPath, nextContent, "utf8");
510
+ }
511
+ }
512
+ await visit(targetDir);
513
+ }
514
+ async function defaultInstallDependencies({
515
+ projectDir,
516
+ packageManager
517
+ }) {
518
+ execSync(formatInstallCommand(packageManager), {
519
+ cwd: projectDir,
520
+ stdio: "inherit"
521
+ });
522
+ }
523
+ async function scaffoldProject({
524
+ projectDir,
525
+ templateId,
526
+ answers,
527
+ packageManager,
528
+ allowExistingDir = false,
529
+ noInstall = false,
530
+ installDependencies = undefined
531
+ }) {
532
+ const template = getTemplateById(templateId);
533
+ const resolvedPackageManager = getPackageManager(packageManager).id;
534
+ await ensureDirectory(projectDir, allowExistingDir);
535
+ const variables = getTemplateVariables(template.id, answers);
536
+ await copyTemplateDir(template.templateDir, projectDir, variables);
537
+ const readmePath = path2.join(projectDir, "README.md");
538
+ if (!fs2.existsSync(readmePath)) {
539
+ await fsp.writeFile(readmePath, buildReadme(template.id, variables, resolvedPackageManager), "utf8");
540
+ }
541
+ await fsp.writeFile(path2.join(projectDir, ".gitignore"), buildGitignore(), "utf8");
542
+ await normalizePackageJson(projectDir, resolvedPackageManager);
543
+ await normalizePackageManagerFiles(projectDir, resolvedPackageManager);
544
+ await removeUnexpectedLockfiles(projectDir, resolvedPackageManager);
545
+ await replaceTextRecursively(projectDir, resolvedPackageManager);
546
+ if (!noInstall) {
547
+ const installer = installDependencies ?? defaultInstallDependencies;
548
+ await installer({
549
+ projectDir,
550
+ packageManager: resolvedPackageManager
551
+ });
552
+ }
553
+ return {
554
+ projectDir,
555
+ templateId: template.id,
556
+ packageManager: resolvedPackageManager,
557
+ variables
558
+ };
559
+ }
560
+
561
+ // src/runtime/cli-core.ts
562
+ function createReadlinePrompt() {
563
+ const rl = readline.createInterface({
564
+ input: process.stdin,
565
+ output: process.stdout
566
+ });
567
+ return {
568
+ async text(message, defaultValue, validate) {
569
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
570
+ const answer = await new Promise((resolve) => {
571
+ rl.question(`${message}${suffix}: `, resolve);
572
+ });
573
+ const value = String(answer).trim() || defaultValue;
574
+ if (validate) {
575
+ const result = validate(value);
576
+ if (result !== true) {
577
+ console.error(`❌ ${typeof result === "string" ? result : "Invalid input"}`);
578
+ return this.text(message, defaultValue, validate);
579
+ }
580
+ }
581
+ return value;
582
+ },
583
+ async select(message, options, defaultValue = 1) {
584
+ console.log(message);
585
+ options.forEach((option, index) => {
586
+ const hint = option.hint ? ` - ${option.hint}` : "";
587
+ console.log(` ${index + 1}. ${option.label}${hint}`);
588
+ });
589
+ const answer = await this.text("Choice", String(defaultValue));
590
+ const numericChoice = Number(answer);
591
+ if (!Number.isNaN(numericChoice) && options[numericChoice - 1]) {
592
+ return options[numericChoice - 1].value;
593
+ }
594
+ const directChoice = options.find((option) => option.value === answer);
595
+ if (directChoice) {
596
+ return directChoice.value;
597
+ }
598
+ console.error(`❌ Invalid selection: ${answer}`);
599
+ return this.select(message, options, defaultValue);
600
+ },
601
+ close() {
602
+ rl.close();
603
+ }
604
+ };
605
+ }
606
+ function formatHelpText() {
607
+ return `Usage:
608
+ wp-typia <project-dir> [--template <id>] [--yes] [--no-install] [--package-manager <id>]
609
+ wp-typia templates list
610
+ wp-typia templates inspect <id>
611
+ wp-typia migrations <init|snapshot|diff|scaffold|verify> [...]
612
+ wp-typia doctor
613
+
614
+ Templates: ${TEMPLATE_IDS.join(", ")}
615
+ Package managers: ${PACKAGE_MANAGER_IDS.join(", ")}`;
616
+ }
617
+ function formatTemplateSummary(template) {
618
+ return `${template.id.padEnd(14)} ${template.description}`;
619
+ }
620
+ function formatTemplateFeatures(template) {
621
+ return ` ${template.features.join(" • ")}`;
622
+ }
623
+ function formatTemplateDetails(template) {
624
+ return [
625
+ template.id,
626
+ template.description,
627
+ `Category: ${template.defaultCategory}`,
628
+ `Path: ${template.templateDir}`,
629
+ `Features: ${template.features.join(", ")}`
630
+ ].join(`
631
+ `);
632
+ }
633
+ function readCommandVersion(command, args = ["--version"]) {
634
+ try {
635
+ return execFileSync(command, args, {
636
+ encoding: "utf8",
637
+ stdio: ["ignore", "pipe", "ignore"]
638
+ }).trim();
639
+ } catch {
640
+ return null;
641
+ }
642
+ }
643
+ function compareMajorVersion(actualVersion, minimumMajor) {
644
+ const parsed = Number.parseInt(actualVersion.replace(/^v/, "").split(".")[0] ?? "", 10);
645
+ return Number.isFinite(parsed) && parsed >= minimumMajor;
646
+ }
647
+ async function checkWritableDirectory(directory) {
648
+ try {
649
+ await access(directory, fsConstants.W_OK);
650
+ return true;
651
+ } catch {
652
+ return false;
653
+ }
654
+ }
655
+ async function checkTempDirectory() {
656
+ const tempFile = path3.join(os.tmpdir(), `wp-typia-${Date.now()}.tmp`);
657
+ try {
658
+ await writeFile(tempFile, "ok", "utf8");
659
+ await rm(tempFile, { force: true });
660
+ return true;
661
+ } catch {
662
+ return false;
663
+ }
664
+ }
665
+ async function getDoctorChecks(cwd) {
666
+ const checks = [];
667
+ const bunVersion = readCommandVersion("bun");
668
+ const nodeVersion = readCommandVersion("node");
669
+ const gitVersion = readCommandVersion("git");
670
+ const cwdWritable = await checkWritableDirectory(cwd);
671
+ const tempWritable = await checkTempDirectory();
672
+ checks.push({
673
+ status: bunVersion && compareMajorVersion(bunVersion, 1) ? "pass" : "fail",
674
+ label: "Bun",
675
+ detail: bunVersion ? `Detected ${bunVersion}` : "Not available"
676
+ });
677
+ checks.push({
678
+ status: nodeVersion && compareMajorVersion(nodeVersion, 16) ? "pass" : "fail",
679
+ label: "Node",
680
+ detail: nodeVersion ? `Detected ${nodeVersion}` : "Not available"
681
+ });
682
+ checks.push({
683
+ status: gitVersion ? "pass" : "fail",
684
+ label: "git",
685
+ detail: gitVersion ?? "Not available"
686
+ });
687
+ checks.push({
688
+ status: cwdWritable ? "pass" : "fail",
689
+ label: "Current directory",
690
+ detail: cwdWritable ? "Writable" : "Not writable"
691
+ });
692
+ checks.push({
693
+ status: tempWritable ? "pass" : "fail",
694
+ label: "Temp directory",
695
+ detail: tempWritable ? "Writable" : "Not writable"
696
+ });
697
+ for (const template of listTemplates()) {
698
+ const hasAssets = fs3.existsSync(template.templateDir) && fs3.existsSync(path3.join(template.templateDir, "package.json.mustache"));
699
+ checks.push({
700
+ status: hasAssets ? "pass" : "fail",
701
+ label: `Template ${template.id}`,
702
+ detail: hasAssets ? template.templateDir : "Missing core template assets"
703
+ });
704
+ }
705
+ return checks;
706
+ }
707
+ async function runDoctor(cwd, { renderLine = (check) => console.log(`${check.status.toUpperCase()} ${check.label}: ${check.detail}`) } = {}) {
708
+ const checks = await getDoctorChecks(cwd);
709
+ for (const check of checks) {
710
+ renderLine(check);
711
+ }
712
+ if (checks.some((check) => check.status === "fail")) {
713
+ throw new Error("Doctor found one or more failing checks.");
714
+ }
715
+ return checks;
716
+ }
717
+ function getNextSteps({
718
+ projectInput,
719
+ projectDir,
720
+ packageManager,
721
+ noInstall
722
+ }) {
723
+ const steps = [`cd ${path3.isAbsolute(projectInput) ? projectDir : projectInput}`];
724
+ if (noInstall) {
725
+ steps.push(formatInstallCommand(packageManager));
726
+ }
727
+ steps.push(formatRunScript(packageManager, "start"));
728
+ return steps;
729
+ }
730
+ async function runScaffoldFlow({
731
+ projectInput,
732
+ cwd = process.cwd(),
733
+ templateId,
734
+ packageManager,
735
+ yes = false,
736
+ noInstall = false,
737
+ isInteractive = false,
738
+ allowExistingDir = false,
739
+ selectTemplate,
740
+ selectPackageManager,
741
+ promptText,
742
+ installDependencies = undefined
743
+ }) {
744
+ if (!projectInput) {
745
+ throw new Error("Project directory is required. Usage: wp-typia <project-dir>");
746
+ }
747
+ const resolvedTemplateId = await resolveTemplateId({
748
+ templateId,
749
+ yes,
750
+ isInteractive,
751
+ selectTemplate
752
+ });
753
+ const resolvedPackageManager = await resolvePackageManagerId({
754
+ packageManager,
755
+ yes,
756
+ isInteractive,
757
+ selectPackageManager
758
+ });
759
+ const projectDir = path3.resolve(cwd, projectInput);
760
+ const projectName = path3.basename(projectDir);
761
+ const answers = await collectScaffoldAnswers({
762
+ projectName,
763
+ templateId: resolvedTemplateId,
764
+ yes,
765
+ promptText
766
+ });
767
+ const result = await scaffoldProject({
768
+ answers,
769
+ allowExistingDir,
770
+ installDependencies,
771
+ noInstall,
772
+ packageManager: resolvedPackageManager,
773
+ projectDir,
774
+ templateId: resolvedTemplateId
775
+ });
776
+ return {
777
+ projectDir,
778
+ projectInput,
779
+ packageManager: resolvedPackageManager,
780
+ result,
781
+ nextSteps: getNextSteps({
782
+ projectInput,
783
+ projectDir,
784
+ packageManager: resolvedPackageManager,
785
+ noInstall
786
+ })
787
+ };
788
+ }
789
+
790
+ // src/runtime/migrations.ts
791
+ import fs9 from "node:fs";
792
+ import path10 from "node:path";
793
+ import { execSync as execSync3 } from "node:child_process";
794
+
795
+ // src/runtime/migration-constants.ts
796
+ import path4 from "node:path";
797
+ var ROOT_BLOCK_JSON = "block.json";
798
+ var ROOT_MANIFEST = "typia.manifest.json";
799
+ var ROOT_PHP_MIGRATION_REGISTRY = "typia-migration-registry.php";
800
+ var ROOT_SAVE_FILE = path4.join("src", "save.tsx");
801
+ var ROOT_TYPES_FILE = path4.join("src", "types.ts");
802
+ var MIGRATIONS_DIR = path4.join("src", "migrations");
803
+ var CONFIG_FILE = path4.join(MIGRATIONS_DIR, "config.ts");
804
+ var GENERATED_DIR = path4.join(MIGRATIONS_DIR, "generated");
805
+ var FIXTURES_DIR = path4.join(MIGRATIONS_DIR, "fixtures");
806
+ var RULES_DIR = path4.join(MIGRATIONS_DIR, "rules");
807
+ var SNAPSHOT_DIR = path4.join(MIGRATIONS_DIR, "versions");
808
+ var SUPPORTED_PROJECT_FILES = ["package.json", ROOT_BLOCK_JSON, ROOT_SAVE_FILE, ROOT_TYPES_FILE];
809
+ var MIGRATION_TODO_PREFIX = "TODO MIGRATION:";
810
+
811
+ // src/runtime/migration-diff.ts
812
+ import fs5 from "node:fs";
813
+ import path6 from "node:path";
814
+
815
+ // src/runtime/migration-manifest.ts
816
+ function flattenManifestLeafAttributes(attributes) {
817
+ return Object.entries(attributes).flatMap(([key, attribute]) => flattenManifestAttribute(attribute, key, key, {
818
+ rootPath: key,
819
+ unionBranch: null,
820
+ unionDiscriminator: null,
821
+ unionRoot: null
822
+ }));
823
+ }
824
+ function flattenManifestAttribute(attribute, currentPath, sourcePath, context) {
825
+ if (!attribute) {
826
+ return [];
827
+ }
828
+ if (attribute.ts.kind === "object") {
829
+ const properties = Object.entries(attribute.ts.properties ?? {});
830
+ if (properties.length === 0) {
831
+ return [{ ...context, attribute, currentPath, sourcePath }];
832
+ }
833
+ return properties.flatMap(([key, property]) => flattenManifestAttribute(property, `${currentPath}.${key}`, `${sourcePath}.${key}`, {
834
+ ...context
835
+ }));
836
+ }
837
+ if (attribute.ts.kind === "union" && attribute.ts.union) {
838
+ const unionMetadata = attribute.ts.union;
839
+ return Object.entries(attribute.ts.union.branches ?? {}).flatMap(([branchKey, branchAttribute]) => Object.entries(branchAttribute.ts.properties ?? {}).filter(([key]) => key !== unionMetadata.discriminator).flatMap(([key, property]) => flattenManifestAttribute(property, `${currentPath}.${branchKey}.${key}`, `${sourcePath}.${key}`, {
840
+ rootPath: context.rootPath,
841
+ unionBranch: branchKey,
842
+ unionDiscriminator: unionMetadata.discriminator,
843
+ unionRoot: currentPath
844
+ })));
845
+ }
846
+ return [{ ...context, attribute, currentPath, sourcePath }];
847
+ }
848
+ function getAttributeByCurrentPath(attributes, currentPath) {
849
+ const segments = String(currentPath).split(".");
850
+ const rootKey = segments.shift();
851
+ if (!rootKey) {
852
+ return null;
853
+ }
854
+ let attribute = attributes[rootKey] ?? null;
855
+ while (attribute && segments.length > 0) {
856
+ if (attribute.ts.kind === "union" && attribute.ts.union) {
857
+ const branchKey = segments.shift();
858
+ if (!branchKey || !(branchKey in attribute.ts.union.branches)) {
859
+ return null;
860
+ }
861
+ attribute = attribute.ts.union.branches[branchKey];
862
+ continue;
863
+ }
864
+ if (attribute.ts.kind === "object" && attribute.ts.properties) {
865
+ const propertyKey = segments.shift();
866
+ if (!propertyKey || !(propertyKey in attribute.ts.properties)) {
867
+ return null;
868
+ }
869
+ attribute = attribute.ts.properties[propertyKey];
870
+ continue;
871
+ }
872
+ return null;
873
+ }
874
+ return attribute;
875
+ }
876
+ function hasManifestDefault(attribute) {
877
+ return attribute?.typia?.hasDefault === true;
878
+ }
879
+ function getManifestDefaultValue(attribute) {
880
+ return attribute?.typia?.defaultValue ?? null;
881
+ }
882
+ function summarizeManifest(manifest) {
883
+ return {
884
+ attributes: Object.fromEntries(Object.entries(manifest.attributes ?? {}).map(([name, attribute]) => [
885
+ name,
886
+ {
887
+ constraints: attribute.typia?.constraints ?? {},
888
+ defaultValue: attribute.typia?.defaultValue ?? null,
889
+ hasDefault: attribute.typia?.hasDefault ?? false,
890
+ enum: attribute.wp?.enum ?? null,
891
+ kind: attribute.ts?.kind ?? null,
892
+ required: attribute.ts?.required ?? false,
893
+ union: attribute.ts?.union ?? null
894
+ }
895
+ ])),
896
+ manifestVersion: manifest.manifestVersion ?? null,
897
+ sourceType: manifest.sourceType ?? null
898
+ };
899
+ }
900
+ function summarizeUnionBranches(manifestSummary) {
901
+ if (!manifestSummary?.attributes) {
902
+ return [];
903
+ }
904
+ return Object.entries(manifestSummary.attributes).filter(([, attribute]) => attribute.kind === "union" && attribute.union).map(([field, attribute]) => ({
905
+ branches: Object.keys(attribute.union?.branches ?? {}),
906
+ discriminator: attribute.union?.discriminator ?? null,
907
+ field
908
+ }));
909
+ }
910
+ function defaultValueForManifestAttribute(attribute) {
911
+ if (attribute.typia?.hasDefault) {
912
+ return attribute.typia.defaultValue;
913
+ }
914
+ if (attribute.wp?.enum && attribute.wp.enum.length > 0) {
915
+ return attribute.wp.enum[0] ?? null;
916
+ }
917
+ switch (attribute.ts.kind) {
918
+ case "string":
919
+ return "";
920
+ case "number":
921
+ return 0;
922
+ case "boolean":
923
+ return false;
924
+ case "array":
925
+ return [];
926
+ case "object": {
927
+ const result = {};
928
+ for (const [key, property] of Object.entries(attribute.ts.properties ?? {})) {
929
+ result[key] = defaultValueForManifestAttribute(property);
930
+ }
931
+ return result;
932
+ }
933
+ case "union": {
934
+ const firstBranch = Object.values(attribute.ts.union?.branches ?? {})[0];
935
+ return firstBranch ? defaultValueForManifestAttribute(firstBranch) : null;
936
+ }
937
+ default:
938
+ return null;
939
+ }
940
+ }
941
+
942
+ // src/runtime/migration-utils.ts
943
+ import fs4 from "node:fs";
944
+ import path5 from "node:path";
945
+ import { execSync as execSync2 } from "node:child_process";
946
+ function cloneJsonValue(value) {
947
+ return JSON.parse(JSON.stringify(value));
948
+ }
949
+ function getValueAtPath(input, pathLabel) {
950
+ return String(pathLabel).split(".").reduce((value, segment) => value && typeof value === "object" ? value[segment] : undefined, input);
951
+ }
952
+ function setValueAtPath(input, pathLabel, value) {
953
+ const segments = String(pathLabel).split(".");
954
+ let target = input;
955
+ while (segments.length > 1) {
956
+ const segment = segments.shift();
957
+ if (!segment) {
958
+ continue;
959
+ }
960
+ if (!target[segment] || typeof target[segment] !== "object" || Array.isArray(target[segment])) {
961
+ target[segment] = {};
962
+ }
963
+ target = target[segment];
964
+ }
965
+ target[segments[0]] = value;
966
+ }
967
+ function deleteValueAtPath(input, pathLabel) {
968
+ const segments = String(pathLabel).split(".");
969
+ let target = input;
970
+ while (segments.length > 1) {
971
+ const segment = segments.shift();
972
+ if (!segment || !target[segment] || typeof target[segment] !== "object") {
973
+ return;
974
+ }
975
+ target = target[segment];
976
+ }
977
+ delete target[segments[0]];
978
+ }
979
+ function createFixtureScalarValue(pathLabel) {
980
+ const normalized = String(pathLabel).toLowerCase();
981
+ if (normalized.includes("id")) {
982
+ return "00000000-0000-4000-8000-000000000000";
983
+ }
984
+ if (normalized.includes("count") || normalized.includes("number")) {
985
+ return 1;
986
+ }
987
+ if (normalized.includes("visible") || normalized.startsWith("is")) {
988
+ return true;
989
+ }
990
+ return `legacy:${pathLabel}`;
991
+ }
992
+ function createTransformFixtureValue(attribute, pathLabel) {
993
+ switch (attribute?.ts?.kind) {
994
+ case "number":
995
+ return "42";
996
+ case "boolean":
997
+ return "1";
998
+ case "union":
999
+ return { kind: "unknown" };
1000
+ default:
1001
+ return createFixtureScalarValue(pathLabel);
1002
+ }
1003
+ }
1004
+ function readJson(filePath) {
1005
+ return JSON.parse(fs4.readFileSync(filePath, "utf8"));
1006
+ }
1007
+ function renderPhpValue(value, indentLevel) {
1008
+ const indent = "\t".repeat(indentLevel);
1009
+ const nestedIndent = "\t".repeat(indentLevel + 1);
1010
+ if (value === null) {
1011
+ return "null";
1012
+ }
1013
+ if (typeof value === "string") {
1014
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
1015
+ }
1016
+ if (typeof value === "number" || typeof value === "boolean") {
1017
+ return String(value);
1018
+ }
1019
+ if (Array.isArray(value)) {
1020
+ if (value.length === 0) {
1021
+ return "[]";
1022
+ }
1023
+ const items = value.map((item) => `${nestedIndent}${renderPhpValue(item, indentLevel + 1)}`);
1024
+ return `[
1025
+ ${items.join(`,
1026
+ `)}
1027
+ ${indent}]`;
1028
+ }
1029
+ if (typeof value === "object") {
1030
+ const entries = Object.entries(value);
1031
+ if (entries.length === 0) {
1032
+ return "[]";
1033
+ }
1034
+ const items = entries.map(([key, item]) => `${nestedIndent}'${String(key).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}' => ${renderPhpValue(item, indentLevel + 1)}`);
1035
+ return `[
1036
+ ${items.join(`,
1037
+ `)}
1038
+ ${indent}]`;
1039
+ }
1040
+ throw new Error(`Unable to encode PHP migration registry value for ${String(value)}`);
1041
+ }
1042
+ function copyFile(sourcePath, targetPath) {
1043
+ fs4.mkdirSync(path5.dirname(targetPath), { recursive: true });
1044
+ fs4.copyFileSync(sourcePath, targetPath);
1045
+ }
1046
+ function sanitizeSaveSnapshotSource(source) {
1047
+ return source.replace(/^import\s+\{[^}]+\}\s+from\s+['"]\.\/types['"];?\n?/gm, "").replace(/^interface\s+SaveProps\s*\{[\s\S]*?\}\n?/m, "").replace(/: SaveProps/g, ": { attributes: any }").replace(/attributes:\s*[A-Za-z0-9_<>{}\[\]|&,\s]+;/g, "attributes: any;").replace(/\(\{\s*attributes\s*\}:\s*\{\s*attributes:\s*any\s*\}\)/g, "({ attributes }: { attributes: any })");
1048
+ }
1049
+ function sanitizeSnapshotBlockJson(blockJson) {
1050
+ const snapshot = { ...blockJson };
1051
+ for (const key of [
1052
+ "editorScript",
1053
+ "script",
1054
+ "scriptModule",
1055
+ "viewScript",
1056
+ "viewScriptModule",
1057
+ "style",
1058
+ "editorStyle",
1059
+ "render"
1060
+ ]) {
1061
+ delete snapshot[key];
1062
+ }
1063
+ return snapshot;
1064
+ }
1065
+ function runProjectScriptIfPresent(projectDir, scriptName) {
1066
+ const packageJson = readJson(path5.join(projectDir, "package.json"));
1067
+ if (!packageJson.scripts?.[scriptName]) {
1068
+ return;
1069
+ }
1070
+ const packageManagerId = detectPackageManagerId(projectDir);
1071
+ execSync2(formatRunScript(packageManagerId, scriptName), {
1072
+ cwd: projectDir,
1073
+ stdio: "inherit"
1074
+ });
1075
+ }
1076
+ function detectPackageManagerId(projectDir) {
1077
+ const packageJson = readJson(path5.join(projectDir, "package.json"));
1078
+ const field = String(packageJson.packageManager ?? "");
1079
+ if (field.startsWith("bun@"))
1080
+ return "bun";
1081
+ if (field.startsWith("npm@"))
1082
+ return "npm";
1083
+ if (field.startsWith("pnpm@"))
1084
+ return "pnpm";
1085
+ if (field.startsWith("yarn@"))
1086
+ return "yarn";
1087
+ return "bun";
1088
+ }
1089
+ function getLocalTsxBinary(projectDir) {
1090
+ const filename = process.platform === "win32" ? "tsx.cmd" : "tsx";
1091
+ const binaryPath = path5.join(projectDir, "node_modules", ".bin", filename);
1092
+ if (!fs4.existsSync(binaryPath)) {
1093
+ throw new Error("Local tsx binary was not found. Install project dependencies before running migration verification.");
1094
+ }
1095
+ return binaryPath;
1096
+ }
1097
+ function resolveTargetVersion(currentVersion, value) {
1098
+ return value === "current" ? currentVersion : value;
1099
+ }
1100
+ function assertSemver(value, label) {
1101
+ if (!/^\d+\.\d+\.\d+$/.test(value)) {
1102
+ throw new Error(`Invalid ${label}: ${value}. Expected x.y.z`);
1103
+ }
1104
+ }
1105
+ function compareSemver(left, right) {
1106
+ const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
1107
+ const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
1108
+ for (let index = 0;index < Math.max(leftParts.length, rightParts.length); index += 1) {
1109
+ const delta = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
1110
+ if (delta !== 0) {
1111
+ return delta;
1112
+ }
1113
+ }
1114
+ return 0;
1115
+ }
1116
+ function escapeForCode(value) {
1117
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1118
+ }
1119
+ function renderObjectKey(key) {
1120
+ return JSON.stringify(String(key));
1121
+ }
1122
+ function isNumber(value) {
1123
+ return typeof value === "number" && Number.isFinite(value);
1124
+ }
1125
+
1126
+ // src/runtime/migration-diff.ts
1127
+ function createMigrationDiff(state, fromVersion, toVersion) {
1128
+ const snapshotManifestPath = path6.join(state.projectDir, SNAPSHOT_DIR, fromVersion, ROOT_MANIFEST);
1129
+ if (!fs5.existsSync(snapshotManifestPath)) {
1130
+ throw new Error(`Snapshot manifest for ${fromVersion} does not exist. Run \`migrations snapshot --version ${fromVersion}\` first.`);
1131
+ }
1132
+ const targetManifest = toVersion === state.config.currentVersion ? state.currentManifest : readJson(path6.join(state.projectDir, SNAPSHOT_DIR, toVersion, ROOT_MANIFEST));
1133
+ const oldManifest = readJson(snapshotManifestPath);
1134
+ const oldAttributes = oldManifest.attributes ?? {};
1135
+ const newAttributes = targetManifest.attributes ?? {};
1136
+ const oldLeafAttributes = flattenManifestLeafAttributes(oldAttributes);
1137
+ const newLeafAttributes = flattenManifestLeafAttributes(newAttributes);
1138
+ const autoItems = [];
1139
+ const manualItems = [];
1140
+ const addedKeys = [];
1141
+ const removedKeys = [];
1142
+ for (const [key, newAttribute] of Object.entries(newAttributes)) {
1143
+ const oldAttribute = oldAttributes[key];
1144
+ if (!oldAttribute) {
1145
+ addedKeys.push(key);
1146
+ if (newAttribute.ts.required && !hasManifestDefault(newAttribute)) {
1147
+ manualItems.push({
1148
+ detail: "required field has no default in current schema",
1149
+ kind: "required-addition",
1150
+ path: key,
1151
+ status: "manual"
1152
+ });
1153
+ } else {
1154
+ autoItems.push({
1155
+ detail: hasManifestDefault(newAttribute) ? `default ${JSON.stringify(getManifestDefaultValue(newAttribute))}` : "optional addition",
1156
+ kind: hasManifestDefault(newAttribute) ? "add-default" : "add-optional",
1157
+ path: key,
1158
+ status: "auto"
1159
+ });
1160
+ }
1161
+ continue;
1162
+ }
1163
+ const outcome = compareManifestAttribute(oldAttribute, newAttribute, key);
1164
+ if (outcome.status === "manual") {
1165
+ manualItems.push(outcome);
1166
+ } else {
1167
+ autoItems.push(outcome);
1168
+ }
1169
+ }
1170
+ for (const key of Object.keys(oldAttributes)) {
1171
+ if (!(key in newAttributes)) {
1172
+ removedKeys.push(key);
1173
+ autoItems.push({
1174
+ detail: "field removed from current schema",
1175
+ kind: "drop",
1176
+ path: key,
1177
+ status: "auto"
1178
+ });
1179
+ }
1180
+ }
1181
+ const renameCandidates = createRenameCandidates(oldAttributes, newAttributes, removedKeys, addedKeys, oldLeafAttributes, newLeafAttributes);
1182
+ const activeRenameCandidates = renameCandidates.filter((candidate) => candidate.autoApply);
1183
+ for (const candidate of activeRenameCandidates) {
1184
+ removeOutcomeByPath(autoItems, candidate.legacyPath, "drop");
1185
+ removeOutcomeByPath(autoItems, candidate.currentPath, "add-default");
1186
+ removeOutcomeByPath(autoItems, candidate.currentPath, "add-optional");
1187
+ removeOutcomeByPath(manualItems, candidate.currentPath, "required-addition");
1188
+ removeOutcomesByPath(manualItems, candidate.currentPath);
1189
+ autoItems.push({
1190
+ detail: `legacy field ${candidate.legacyPath}`,
1191
+ kind: "rename",
1192
+ path: candidate.currentPath,
1193
+ status: "auto"
1194
+ });
1195
+ }
1196
+ const transformSuggestions = createTransformSuggestions({
1197
+ addedKeys,
1198
+ manualItems,
1199
+ newAttributes,
1200
+ newLeafAttributes,
1201
+ oldAttributes,
1202
+ oldLeafAttributes,
1203
+ renameCandidates,
1204
+ removedKeys
1205
+ });
1206
+ return {
1207
+ currentTypeName: targetManifest.sourceType ?? state.currentManifest.sourceType,
1208
+ fromVersion,
1209
+ summary: {
1210
+ auto: autoItems.length,
1211
+ autoItems,
1212
+ manual: manualItems.length,
1213
+ manualItems,
1214
+ renameCandidates,
1215
+ transformSuggestions
1216
+ },
1217
+ toVersion
1218
+ };
1219
+ }
1220
+ function removeOutcomeByPath(items, pathLabel, kind) {
1221
+ const index = items.findIndex((item) => item.path === pathLabel && item.kind === kind);
1222
+ if (index >= 0) {
1223
+ items.splice(index, 1);
1224
+ }
1225
+ }
1226
+ function removeOutcomesByPath(items, pathLabel) {
1227
+ for (let index = items.length - 1;index >= 0; index -= 1) {
1228
+ if (items[index]?.path === pathLabel) {
1229
+ items.splice(index, 1);
1230
+ }
1231
+ }
1232
+ }
1233
+ function compareManifestAttribute(oldAttribute, newAttribute, attributePath) {
1234
+ if (oldAttribute.ts.kind !== newAttribute.ts.kind) {
1235
+ return manualOutcome(attributePath, "type-change", `${oldAttribute.ts.kind} -> ${newAttribute.ts.kind}`);
1236
+ }
1237
+ if (oldAttribute.ts.kind === "union") {
1238
+ return compareUnionAttribute(oldAttribute, newAttribute, attributePath);
1239
+ }
1240
+ if (oldAttribute.ts.kind === "object") {
1241
+ return compareObjectAttribute(oldAttribute, newAttribute, attributePath);
1242
+ }
1243
+ if (oldAttribute.ts.kind === "array") {
1244
+ if (!oldAttribute.ts.items || !newAttribute.ts.items) {
1245
+ return autoOutcome(attributePath, "copy", "array shape unchanged");
1246
+ }
1247
+ const nested = compareManifestAttribute(oldAttribute.ts.items, newAttribute.ts.items, `${attributePath}[]`);
1248
+ return nested.status === "manual" ? nested : autoOutcome(attributePath, "hydrate", "array items can be normalized");
1249
+ }
1250
+ if (hasStricterConstraints(oldAttribute, newAttribute)) {
1251
+ return manualOutcome(attributePath, "stricter-constraints", describeConstraintChange(oldAttribute, newAttribute));
1252
+ }
1253
+ return autoOutcome(attributePath, "copy", "compatible primitive field");
1254
+ }
1255
+ function compareObjectAttribute(oldAttribute, newAttribute, attributePath) {
1256
+ const oldProperties = oldAttribute.ts.properties ?? {};
1257
+ const newProperties = newAttribute.ts.properties ?? {};
1258
+ for (const [key, nextProperty] of Object.entries(newProperties)) {
1259
+ const previousProperty = oldProperties[key];
1260
+ if (!previousProperty) {
1261
+ if (nextProperty.ts.required && !hasManifestDefault(nextProperty)) {
1262
+ return manualOutcome(`${attributePath}.${key}`, "object-change", "required field has no default in current schema");
1263
+ }
1264
+ continue;
1265
+ }
1266
+ const nested = compareManifestAttribute(previousProperty, nextProperty, `${attributePath}.${key}`);
1267
+ if (nested.status === "manual") {
1268
+ return nested;
1269
+ }
1270
+ }
1271
+ return autoOutcome(attributePath, "hydrate", "object can be normalized with current manifest defaults");
1272
+ }
1273
+ function compareUnionAttribute(oldAttribute, newAttribute, attributePath) {
1274
+ const oldUnion = oldAttribute.ts.union;
1275
+ const newUnion = newAttribute.ts.union;
1276
+ if (!oldUnion || !newUnion) {
1277
+ return manualOutcome(attributePath, "union-change", "missing union metadata");
1278
+ }
1279
+ if (oldUnion.discriminator !== newUnion.discriminator) {
1280
+ return manualOutcome(attributePath, "union-discriminator-change", `${oldUnion.discriminator} -> ${newUnion.discriminator}`);
1281
+ }
1282
+ const oldBranchKeys = Object.keys(oldUnion.branches);
1283
+ const newBranchKeys = Object.keys(newUnion.branches);
1284
+ for (const branchKey of oldBranchKeys) {
1285
+ if (!(branchKey in newUnion.branches)) {
1286
+ return manualOutcome(attributePath, "union-branch-removal", `branch ${branchKey} was removed`);
1287
+ }
1288
+ const nested = compareManifestAttribute(oldUnion.branches[branchKey], newUnion.branches[branchKey], `${attributePath}.${branchKey}`);
1289
+ if (nested.status === "manual") {
1290
+ return nested;
1291
+ }
1292
+ }
1293
+ const addedBranches = newBranchKeys.filter((branchKey) => !(branchKey in oldUnion.branches));
1294
+ if (addedBranches.length > 0) {
1295
+ return autoOutcome(attributePath, "union-branch-addition", `branches added: ${addedBranches.join(", ")}`);
1296
+ }
1297
+ return autoOutcome(attributePath, "copy", "compatible discriminated union");
1298
+ }
1299
+ function hasStricterConstraints(oldAttribute, newAttribute) {
1300
+ const oldConstraints = oldAttribute.typia.constraints;
1301
+ const nextConstraints = newAttribute.typia.constraints;
1302
+ const oldEnum = oldAttribute.wp.enum ?? null;
1303
+ const nextEnum = newAttribute.wp.enum ?? null;
1304
+ if (nextEnum && (!oldEnum || !oldEnum.every((value) => nextEnum.includes(value)))) {
1305
+ return true;
1306
+ }
1307
+ if (isNumber(nextConstraints.minLength) && (!isNumber(oldConstraints.minLength) || nextConstraints.minLength > oldConstraints.minLength)) {
1308
+ return true;
1309
+ }
1310
+ if (isNumber(nextConstraints.maxLength) && (!isNumber(oldConstraints.maxLength) || nextConstraints.maxLength < oldConstraints.maxLength)) {
1311
+ return true;
1312
+ }
1313
+ if (isNumber(nextConstraints.minimum) && (!isNumber(oldConstraints.minimum) || nextConstraints.minimum > oldConstraints.minimum)) {
1314
+ return true;
1315
+ }
1316
+ if (isNumber(nextConstraints.maximum) && (!isNumber(oldConstraints.maximum) || nextConstraints.maximum < oldConstraints.maximum)) {
1317
+ return true;
1318
+ }
1319
+ if (nextConstraints.pattern && nextConstraints.pattern !== oldConstraints.pattern) {
1320
+ return true;
1321
+ }
1322
+ if (nextConstraints.format && nextConstraints.format !== oldConstraints.format) {
1323
+ return true;
1324
+ }
1325
+ if (nextConstraints.typeTag && nextConstraints.typeTag !== oldConstraints.typeTag) {
1326
+ return true;
1327
+ }
1328
+ return false;
1329
+ }
1330
+ function createRenameCandidates(oldAttributes, newAttributes, removedKeys, addedKeys, oldLeafAttributes, newLeafAttributes) {
1331
+ const assessments = [];
1332
+ for (const currentPath of addedKeys) {
1333
+ const nextAttribute = newAttributes[currentPath];
1334
+ if (!nextAttribute)
1335
+ continue;
1336
+ for (const legacyPath of removedKeys) {
1337
+ const previous = oldAttributes[legacyPath];
1338
+ if (!previous)
1339
+ continue;
1340
+ const candidate = assessRenameCandidate(previous, nextAttribute, legacyPath, currentPath);
1341
+ if (candidate) {
1342
+ assessments.push(candidate);
1343
+ }
1344
+ }
1345
+ }
1346
+ const oldLeafMap = new Map(oldLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
1347
+ const newLeafMap = new Map(newLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
1348
+ const removedLeafDescriptors = oldLeafAttributes.filter((descriptor) => !newLeafMap.has(descriptor.currentPath));
1349
+ const addedLeafDescriptors = newLeafAttributes.filter((descriptor) => !oldLeafMap.has(descriptor.currentPath));
1350
+ for (const nextDescriptor of addedLeafDescriptors) {
1351
+ if (!nextDescriptor.currentPath.includes(".")) {
1352
+ continue;
1353
+ }
1354
+ for (const previousDescriptor of removedLeafDescriptors) {
1355
+ if (!previousDescriptor.currentPath.includes(".")) {
1356
+ continue;
1357
+ }
1358
+ const candidate = assessRenameCandidate(previousDescriptor.attribute, nextDescriptor.attribute, previousDescriptor.currentPath, nextDescriptor.currentPath);
1359
+ if (candidate) {
1360
+ assessments.push(candidate);
1361
+ }
1362
+ }
1363
+ }
1364
+ return assessments.map((candidate) => {
1365
+ const currentMatches = assessments.filter((item) => item.currentPath === candidate.currentPath).sort((left, right) => right.score - left.score);
1366
+ const legacyMatches = assessments.filter((item) => item.legacyPath === candidate.legacyPath).sort((left, right) => right.score - left.score);
1367
+ const currentLeader = currentMatches[0];
1368
+ const legacyLeader = legacyMatches[0];
1369
+ const currentHasTie = currentMatches.length > 1 && Math.abs((currentMatches[1]?.score ?? 0) - currentLeader.score) < 0.05;
1370
+ const legacyHasTie = legacyMatches.length > 1 && Math.abs((legacyMatches[1]?.score ?? 0) - legacyLeader.score) < 0.05;
1371
+ return {
1372
+ ...candidate,
1373
+ autoApply: currentLeader.legacyPath === candidate.legacyPath && legacyLeader.currentPath === candidate.currentPath && !currentHasTie && !legacyHasTie && candidate.score >= 0.6
1374
+ };
1375
+ }).filter((candidate, index, list) => {
1376
+ const firstMatch = list.findIndex((item) => item.currentPath === candidate.currentPath && item.legacyPath === candidate.legacyPath);
1377
+ return firstMatch === index;
1378
+ }).sort((left, right) => right.score - left.score);
1379
+ }
1380
+ function createTransformSuggestions({
1381
+ oldAttributes,
1382
+ newAttributes,
1383
+ addedKeys,
1384
+ removedKeys,
1385
+ manualItems,
1386
+ renameCandidates,
1387
+ oldLeafAttributes,
1388
+ newLeafAttributes
1389
+ }) {
1390
+ const suggestions = [];
1391
+ const activeRenameTargets = new Set(renameCandidates.filter((candidate) => candidate.autoApply).map((candidate) => candidate.currentPath));
1392
+ const oldLeafMap = new Map(oldLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
1393
+ const newLeafMap = new Map(newLeafAttributes.map((descriptor) => [descriptor.currentPath, descriptor]));
1394
+ for (const currentPath of [
1395
+ ...new Set([
1396
+ ...Object.keys(newAttributes),
1397
+ ...manualItems.map((item) => item.path),
1398
+ ...newLeafAttributes.map((item) => item.currentPath)
1399
+ ])
1400
+ ]) {
1401
+ if (activeRenameTargets.has(currentPath)) {
1402
+ continue;
1403
+ }
1404
+ const manualItem = manualItems.find((item) => item.path === currentPath || item.path.startsWith(`${currentPath}.`));
1405
+ const currentAttribute = newLeafMap.get(currentPath)?.attribute ?? getAttributeByCurrentPath(newAttributes, currentPath) ?? null;
1406
+ if (!manualItem || !currentAttribute) {
1407
+ continue;
1408
+ }
1409
+ const exactLegacy = oldLeafMap.get(currentPath)?.attribute ?? getAttributeByCurrentPath(oldAttributes, currentPath) ?? null;
1410
+ if (exactLegacy && exactLegacy.ts.kind !== currentAttribute.ts.kind) {
1411
+ suggestions.push({
1412
+ bodyLines: buildTransformBodyLines(currentAttribute, currentPath),
1413
+ attribute: currentAttribute,
1414
+ currentPath,
1415
+ legacyPath: currentPath,
1416
+ reason: `semantic coercion suggested for ${manualItem.kind}`
1417
+ });
1418
+ continue;
1419
+ }
1420
+ const bestRenameCandidate = renameCandidates.find((candidate) => candidate.currentPath === currentPath);
1421
+ if (bestRenameCandidate && !bestRenameCandidate.autoApply) {
1422
+ suggestions.push({
1423
+ bodyLines: buildTransformBodyLines(currentAttribute, bestRenameCandidate.legacyPath),
1424
+ attribute: currentAttribute,
1425
+ currentPath,
1426
+ legacyPath: bestRenameCandidate.legacyPath,
1427
+ reason: `review coercion from ${bestRenameCandidate.legacyPath}`
1428
+ });
1429
+ continue;
1430
+ }
1431
+ const addedCurrent = addedKeys.includes(currentPath) || newLeafMap.has(currentPath) && !oldLeafMap.has(currentPath);
1432
+ if (!addedCurrent) {
1433
+ continue;
1434
+ }
1435
+ const compatibleLegacyPath = [
1436
+ ...removedKeys,
1437
+ ...oldLeafAttributes.filter((descriptor) => !newLeafMap.has(descriptor.currentPath)).map((descriptor) => descriptor.currentPath)
1438
+ ].find((legacyPath) => passesNameSimilarityRule(legacyPath, currentPath));
1439
+ if (compatibleLegacyPath) {
1440
+ suggestions.push({
1441
+ bodyLines: buildTransformBodyLines(currentAttribute, compatibleLegacyPath),
1442
+ attribute: currentAttribute,
1443
+ currentPath,
1444
+ legacyPath: compatibleLegacyPath,
1445
+ reason: `review coercion from ${compatibleLegacyPath}`
1446
+ });
1447
+ }
1448
+ }
1449
+ return suggestions;
1450
+ }
1451
+ function isRenameCandidateShapeCompatible(oldAttribute, newAttribute) {
1452
+ if (!oldAttribute || !newAttribute || oldAttribute.ts.kind !== newAttribute.ts.kind) {
1453
+ return false;
1454
+ }
1455
+ if (["string", "number", "boolean"].includes(oldAttribute.ts.kind)) {
1456
+ return hasRenameCompatibleConstraints(oldAttribute, newAttribute);
1457
+ }
1458
+ if (oldAttribute.ts.kind === "union") {
1459
+ return compareUnionAttribute(oldAttribute, newAttribute, "$rename").status === "auto";
1460
+ }
1461
+ return false;
1462
+ }
1463
+ function assessRenameCandidate(oldAttribute, newAttribute, legacyPath, currentPath) {
1464
+ if (!isRenameCandidateShapeCompatible(oldAttribute, newAttribute)) {
1465
+ return null;
1466
+ }
1467
+ const baseScore = scoreRenameSimilarity(legacyPath, currentPath);
1468
+ const score = getParentPath(legacyPath) === getParentPath(currentPath) ? Math.max(baseScore, 0.75) : baseScore;
1469
+ return {
1470
+ autoApply: false,
1471
+ currentPath,
1472
+ legacyPath,
1473
+ reason: describeRenameReason(oldAttribute, legacyPath, currentPath, score),
1474
+ score
1475
+ };
1476
+ }
1477
+ function hasRenameCompatibleConstraints(oldAttribute, newAttribute) {
1478
+ const oldEnum = oldAttribute.wp.enum ?? null;
1479
+ const nextEnum = newAttribute.wp.enum ?? null;
1480
+ if (oldEnum && nextEnum) {
1481
+ const oldIsSubset = oldEnum.every((value) => nextEnum.includes(value));
1482
+ if (!oldIsSubset) {
1483
+ return false;
1484
+ }
1485
+ } else if (oldEnum && !nextEnum) {
1486
+ return false;
1487
+ }
1488
+ const oldConstraints = oldAttribute.typia.constraints ?? {};
1489
+ const nextConstraints = newAttribute.typia.constraints ?? {};
1490
+ return [
1491
+ compareMinimumBound(oldConstraints.minLength, nextConstraints.minLength),
1492
+ compareMaximumBound(oldConstraints.maxLength, nextConstraints.maxLength),
1493
+ compareMinimumBound(oldConstraints.minimum, nextConstraints.minimum),
1494
+ compareMaximumBound(oldConstraints.maximum, nextConstraints.maximum),
1495
+ comparePatternBound(oldConstraints.pattern, nextConstraints.pattern),
1496
+ comparePatternBound(oldConstraints.format, nextConstraints.format),
1497
+ comparePatternBound(oldConstraints.typeTag, nextConstraints.typeTag)
1498
+ ].every(Boolean);
1499
+ }
1500
+ function compareMinimumBound(oldValue, nextValue) {
1501
+ if (nextValue === null || nextValue === undefined)
1502
+ return true;
1503
+ if (oldValue === null || oldValue === undefined)
1504
+ return true;
1505
+ return Number(oldValue) <= Number(nextValue);
1506
+ }
1507
+ function compareMaximumBound(oldValue, nextValue) {
1508
+ if (nextValue === null || nextValue === undefined)
1509
+ return true;
1510
+ if (oldValue === null || oldValue === undefined)
1511
+ return true;
1512
+ return Number(oldValue) >= Number(nextValue);
1513
+ }
1514
+ function comparePatternBound(oldValue, nextValue) {
1515
+ return oldValue === nextValue || oldValue === null || oldValue === undefined;
1516
+ }
1517
+ function scoreRenameSimilarity(legacyPath, currentPath) {
1518
+ const legacy = normalizeFieldName(legacyPath);
1519
+ const current = normalizeFieldName(currentPath);
1520
+ if (legacy === current)
1521
+ return 1;
1522
+ if (shareAliasGroup(legacy, current))
1523
+ return 0.9;
1524
+ const legacyTokens = tokenizeFieldName(legacy);
1525
+ const currentTokens = tokenizeFieldName(current);
1526
+ const overlap = legacyTokens.filter((token) => currentTokens.includes(token));
1527
+ const jaccard = overlap.length / new Set([...legacyTokens, ...currentTokens]).size;
1528
+ if (legacy.includes(current) || current.includes(legacy)) {
1529
+ return Math.max(jaccard, 0.7);
1530
+ }
1531
+ if (legacyTokens.length > 0 && currentTokens.length > 0 && legacyTokens[legacyTokens.length - 1] === currentTokens[currentTokens.length - 1]) {
1532
+ return Math.max(jaccard, 0.6);
1533
+ }
1534
+ return jaccard;
1535
+ }
1536
+ function passesNameSimilarityRule(legacyPath, currentPath) {
1537
+ return scoreRenameSimilarity(legacyPath, currentPath) >= 0.6;
1538
+ }
1539
+ function normalizeFieldName(name) {
1540
+ return String(name).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9]+/g, " ").trim().toLowerCase().replace(/\s+/g, "");
1541
+ }
1542
+ function tokenizeFieldName(name) {
1543
+ return String(name).replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
1544
+ }
1545
+ function getParentPath(pathLabel) {
1546
+ const segments = String(pathLabel).split(".");
1547
+ return segments.length <= 1 ? "" : segments.slice(0, -1).join(".");
1548
+ }
1549
+ function shareAliasGroup(left, right) {
1550
+ const aliasGroups = [
1551
+ ["content", "headline", "body", "text", "copy", "message"],
1552
+ ["id", "uniqueid", "uuid"],
1553
+ ["visible", "isvisible", "show", "shown", "enabled"],
1554
+ ["align", "alignment", "textalign"],
1555
+ ["count", "clickcount", "counter"],
1556
+ ["url", "href", "link"]
1557
+ ];
1558
+ return aliasGroups.some((group) => group.includes(left) && group.includes(right));
1559
+ }
1560
+ function describeRenameReason(attribute, legacyPath, currentPath, score) {
1561
+ if (attribute.ts.kind === "union") {
1562
+ return `compatible discriminated union (${legacyPath} → ${currentPath})`;
1563
+ }
1564
+ if (score >= 0.9)
1565
+ return "high-confidence compatible field";
1566
+ if (score >= 0.6)
1567
+ return "name-similar compatible field";
1568
+ return "compatible field requiring review";
1569
+ }
1570
+ function buildTransformBodyLines(attribute, legacyPath) {
1571
+ switch (attribute.ts.kind) {
1572
+ case "string":
1573
+ return [`// return typeof legacyValue === "string" ? legacyValue : String(legacyValue ?? "");`];
1574
+ case "number":
1575
+ return [
1576
+ `// const numericValue = typeof legacyValue === "number" ? legacyValue : Number(legacyValue ?? 0);`,
1577
+ `// return Number.isNaN(numericValue) ? undefined : numericValue;`
1578
+ ];
1579
+ case "boolean":
1580
+ return [`// return typeof legacyValue === "boolean" ? legacyValue : Boolean(legacyValue);`];
1581
+ case "union":
1582
+ return [
1583
+ `// const legacyObject = typeof legacyValue === "object" && legacyValue !== null ? legacyValue : {};`,
1584
+ `// return legacyObject; // adjust discriminator / branch fields before verify`
1585
+ ];
1586
+ default:
1587
+ return [`// return legacyValue; // customize migration from ${legacyPath}`];
1588
+ }
1589
+ }
1590
+ function describeConstraintChange(oldAttribute, newAttribute) {
1591
+ const details = [];
1592
+ const oldConstraints = oldAttribute.typia.constraints;
1593
+ const nextConstraints = newAttribute.typia.constraints;
1594
+ if (newAttribute.wp.enum && JSON.stringify(newAttribute.wp.enum) !== JSON.stringify(oldAttribute.wp.enum)) {
1595
+ details.push("enum changed");
1596
+ }
1597
+ for (const key of ["minLength", "maxLength", "minimum", "maximum", "pattern", "format", "typeTag"]) {
1598
+ if (oldConstraints[key] !== nextConstraints[key]) {
1599
+ details.push(`${key}: ${oldConstraints[key]} -> ${nextConstraints[key]}`);
1600
+ }
1601
+ }
1602
+ return details.join(", ");
1603
+ }
1604
+ function autoOutcome(pathLabel, kind, detail) {
1605
+ return { detail, kind, path: pathLabel, status: "auto" };
1606
+ }
1607
+ function manualOutcome(pathLabel, kind, detail) {
1608
+ return { detail, kind, path: pathLabel, status: "manual" };
1609
+ }
1610
+
1611
+ // src/runtime/migration-fixtures.ts
1612
+ import fs6 from "node:fs";
1613
+ import path7 from "node:path";
1614
+ function ensureEdgeFixtureFile(projectDir, fromVersion, toVersion, diff) {
1615
+ const fixturePath = path7.join(projectDir, FIXTURES_DIR, `${fromVersion}-to-${toVersion}.json`);
1616
+ if (fs6.existsSync(fixturePath)) {
1617
+ return;
1618
+ }
1619
+ const manifest = readJson(path7.join(projectDir, SNAPSHOT_DIR, fromVersion, ROOT_MANIFEST));
1620
+ const attributes = {};
1621
+ for (const [key, attribute] of Object.entries(manifest.attributes ?? {})) {
1622
+ attributes[key] = defaultValueForManifestAttribute(attribute) ?? null;
1623
+ }
1624
+ const cases = [
1625
+ {
1626
+ input: attributes,
1627
+ name: "default"
1628
+ },
1629
+ ...createRenameFixtureCases(attributes, diff.summary.renameCandidates),
1630
+ ...createTransformFixtureCases(attributes, diff.summary.transformSuggestions),
1631
+ ...createUnionFixtureCases(attributes, manifest.attributes ?? {}, diff.summary.renameCandidates)
1632
+ ];
1633
+ const fixtureDocument = {
1634
+ cases,
1635
+ fromVersion,
1636
+ toVersion
1637
+ };
1638
+ fs6.writeFileSync(fixturePath, `${JSON.stringify(fixtureDocument, null, "\t")}
1639
+ `, "utf8");
1640
+ }
1641
+ function createRenameFixtureCases(baseAttributes, renameCandidates) {
1642
+ return renameCandidates.filter((candidate) => candidate.autoApply).map((candidate) => {
1643
+ const nextInput = cloneJsonValue(baseAttributes);
1644
+ const legacyValue = getValueAtPath(nextInput, candidate.legacyPath);
1645
+ deleteValueAtPath(nextInput, candidate.currentPath);
1646
+ if (legacyValue === undefined) {
1647
+ setValueAtPath(nextInput, candidate.legacyPath, createFixtureScalarValue(candidate.currentPath));
1648
+ }
1649
+ return {
1650
+ input: nextInput,
1651
+ name: `rename:${candidate.legacyPath}->${candidate.currentPath}`
1652
+ };
1653
+ });
1654
+ }
1655
+ function createTransformFixtureCases(baseAttributes, transformSuggestions) {
1656
+ return transformSuggestions.map((suggestion) => {
1657
+ const nextInput = cloneJsonValue(baseAttributes);
1658
+ const legacyPath = suggestion.legacyPath ?? suggestion.currentPath;
1659
+ setValueAtPath(nextInput, legacyPath, createTransformFixtureValue(suggestion.attribute, suggestion.currentPath));
1660
+ return {
1661
+ input: nextInput,
1662
+ name: `transform:${legacyPath}->${suggestion.currentPath}`
1663
+ };
1664
+ });
1665
+ }
1666
+ function createUnionFixtureCases(baseAttributes, manifestAttributes, renameCandidates) {
1667
+ const cases = [];
1668
+ for (const [key, attribute] of Object.entries(manifestAttributes)) {
1669
+ if (attribute.ts.kind !== "union" || !attribute.ts.union) {
1670
+ continue;
1671
+ }
1672
+ for (const [branchKey, branch] of Object.entries(attribute.ts.union.branches ?? {})) {
1673
+ const nextInput = cloneJsonValue(baseAttributes);
1674
+ const legacyPath = renameCandidates.find((candidate) => candidate.autoApply && candidate.currentPath === key)?.legacyPath ?? key;
1675
+ setValueAtPath(nextInput, legacyPath, createUnionBranchFixtureValue(attribute.ts.union.discriminator, branchKey, branch));
1676
+ cases.push({
1677
+ input: nextInput,
1678
+ name: `union:${key}:${branchKey}`
1679
+ });
1680
+ }
1681
+ }
1682
+ return cases;
1683
+ }
1684
+ function createUnionBranchFixtureValue(discriminator, branchKey, branchAttribute) {
1685
+ const branchValue = defaultValueForManifestAttribute(branchAttribute);
1686
+ if (typeof branchValue === "object" && branchValue !== null && !Array.isArray(branchValue)) {
1687
+ return {
1688
+ ...branchValue,
1689
+ [discriminator]: branchKey
1690
+ };
1691
+ }
1692
+ return {
1693
+ [discriminator]: branchKey
1694
+ };
1695
+ }
1696
+
1697
+ // src/runtime/migration-project.ts
1698
+ import fs7 from "node:fs";
1699
+ import path8 from "node:path";
1700
+ function ensureAdvancedMigrationProject(projectDir) {
1701
+ const missing = SUPPORTED_PROJECT_FILES.filter((relativePath) => !fs7.existsSync(path8.join(projectDir, relativePath)));
1702
+ if (missing.length > 0) {
1703
+ throw new Error(`This directory is not a supported advanced migration project. Missing: ${missing.join(", ")}`);
1704
+ }
1705
+ }
1706
+ function getProjectPaths(projectDir) {
1707
+ return {
1708
+ configFile: path8.join(projectDir, CONFIG_FILE),
1709
+ fixturesDir: path8.join(projectDir, FIXTURES_DIR),
1710
+ generatedDir: path8.join(projectDir, GENERATED_DIR),
1711
+ rulesDir: path8.join(projectDir, RULES_DIR),
1712
+ snapshotDir: path8.join(projectDir, SNAPSHOT_DIR)
1713
+ };
1714
+ }
1715
+ function ensureMigrationDirectories(projectDir) {
1716
+ const paths = getProjectPaths(projectDir);
1717
+ fs7.mkdirSync(paths.fixturesDir, { recursive: true });
1718
+ fs7.mkdirSync(paths.generatedDir, { recursive: true });
1719
+ fs7.mkdirSync(paths.rulesDir, { recursive: true });
1720
+ fs7.mkdirSync(paths.snapshotDir, { recursive: true });
1721
+ }
1722
+ function writeInitialMigrationScaffold(projectDir, currentVersion) {
1723
+ const paths = getProjectPaths(projectDir);
1724
+ const readmeFiles = [
1725
+ [path8.join(paths.snapshotDir, "README.md"), `# Version Snapshots
1726
+
1727
+ Snapshots for ${currentVersion} and future versions live here.
1728
+ `],
1729
+ [path8.join(paths.rulesDir, "README.md"), `# Migration Rules
1730
+
1731
+ Scaffold direct legacy-to-current migration rules in this directory.
1732
+ `],
1733
+ [path8.join(paths.fixturesDir, "README.md"), `# Migration Fixtures
1734
+
1735
+ Generated fixtures are used by verify to assert migrations.
1736
+ `]
1737
+ ];
1738
+ for (const [targetPath, content] of readmeFiles) {
1739
+ if (!fs7.existsSync(targetPath)) {
1740
+ fs7.writeFileSync(targetPath, content, "utf8");
1741
+ }
1742
+ }
1743
+ }
1744
+ function loadMigrationProject(projectDir, { allowMissingConfig = false } = {}) {
1745
+ ensureAdvancedMigrationProject(projectDir);
1746
+ if (!fs7.existsSync(path8.join(projectDir, ROOT_MANIFEST))) {
1747
+ runProjectScriptIfPresent(projectDir, "sync-types");
1748
+ }
1749
+ const paths = getProjectPaths(projectDir);
1750
+ const config = allowMissingConfig && !fs7.existsSync(paths.configFile) ? {
1751
+ blockName: readProjectBlockName(projectDir),
1752
+ currentVersion: "0.0.0",
1753
+ snapshotDir: SNAPSHOT_DIR.replace(/\\/g, "/"),
1754
+ supportedVersions: []
1755
+ } : parseMigrationConfig(fs7.readFileSync(paths.configFile, "utf8"));
1756
+ return {
1757
+ config,
1758
+ currentBlockJson: readJson(path8.join(projectDir, ROOT_BLOCK_JSON)),
1759
+ currentManifest: readJson(path8.join(projectDir, ROOT_MANIFEST)),
1760
+ paths,
1761
+ projectDir
1762
+ };
1763
+ }
1764
+ function discoverMigrationEntries(state) {
1765
+ const entries = [];
1766
+ const currentVersion = state.config.currentVersion;
1767
+ for (const version of state.config.supportedVersions) {
1768
+ if (version === currentVersion) {
1769
+ continue;
1770
+ }
1771
+ const snapshotRoot = path8.join(state.projectDir, SNAPSHOT_DIR, version);
1772
+ const manifestPath = path8.join(snapshotRoot, ROOT_MANIFEST);
1773
+ const blockJsonPath = path8.join(snapshotRoot, ROOT_BLOCK_JSON);
1774
+ const savePath = path8.join(snapshotRoot, "save.tsx");
1775
+ const rulePath = getRuleFilePath(state.paths, version, currentVersion);
1776
+ if (fs7.existsSync(manifestPath) && fs7.existsSync(blockJsonPath) && fs7.existsSync(savePath) && fs7.existsSync(rulePath)) {
1777
+ entries.push({
1778
+ blockJsonImport: `../versions/${version}/block.json`,
1779
+ fixtureImport: `../fixtures/${version}-to-${currentVersion}.json`,
1780
+ fromVersion: version,
1781
+ manifestImport: `../versions/${version}/typia.manifest.json`,
1782
+ ruleImport: `../rules/${version}-to-${currentVersion}`,
1783
+ rulePath,
1784
+ saveImport: `../versions/${version}/save`,
1785
+ toVersion: currentVersion
1786
+ });
1787
+ }
1788
+ }
1789
+ return entries.sort((left, right) => compareSemver(right.fromVersion, left.fromVersion));
1790
+ }
1791
+ function parseMigrationConfig(source) {
1792
+ const blockName = matchConfigValue(source, "blockName");
1793
+ const currentVersion = matchConfigValue(source, "currentVersion");
1794
+ const snapshotDir = matchConfigValue(source, "snapshotDir");
1795
+ const supportedVersionsMatch = source.match(/supportedVersions:\s*\[([\s\S]*?)\]/);
1796
+ if (!blockName || !currentVersion || !snapshotDir || !supportedVersionsMatch) {
1797
+ throw new Error("Unable to parse migration config. Regenerate with `wp-typia migrations init`.");
1798
+ }
1799
+ const supportedVersions = supportedVersionsMatch[1].split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean).sort(compareSemver);
1800
+ return {
1801
+ blockName,
1802
+ currentVersion,
1803
+ snapshotDir,
1804
+ supportedVersions
1805
+ };
1806
+ }
1807
+ function matchConfigValue(source, key) {
1808
+ return source.match(new RegExp(`${key}:\\s*"([^"]+)"`))?.[1] ?? null;
1809
+ }
1810
+ function writeMigrationConfig(projectDir, config) {
1811
+ const paths = getProjectPaths(projectDir);
1812
+ fs7.mkdirSync(path8.dirname(paths.configFile), { recursive: true });
1813
+ fs7.writeFileSync(paths.configFile, `export const migrationConfig = {
1814
+ blockName: "${config.blockName}",
1815
+ currentVersion: "${config.currentVersion}",
1816
+ supportedVersions: [${config.supportedVersions.map((version) => `"${version}"`).join(", ")}],
1817
+ snapshotDir: "${config.snapshotDir}",
1818
+ } as const;
1819
+
1820
+ export default migrationConfig;
1821
+ `, "utf8");
1822
+ }
1823
+ function readProjectBlockName(projectDir) {
1824
+ const blockJson = readJson(path8.join(projectDir, ROOT_BLOCK_JSON));
1825
+ const blockName = blockJson?.name;
1826
+ if (typeof blockName !== "string" || blockName.length === 0) {
1827
+ throw new Error("Unable to resolve block name from block.json");
1828
+ }
1829
+ return blockName;
1830
+ }
1831
+ function assertRuleHasNoTodos(projectDir, fromVersion, toVersion) {
1832
+ const rulePath = getRuleFilePath(getProjectPaths(projectDir), fromVersion, toVersion);
1833
+ if (!fs7.existsSync(rulePath)) {
1834
+ throw new Error(`Missing migration rule: ${path8.relative(projectDir, rulePath)}`);
1835
+ }
1836
+ const source = fs7.readFileSync(rulePath, "utf8");
1837
+ if (source.includes(MIGRATION_TODO_PREFIX)) {
1838
+ throw new Error(`Migration rule still contains ${MIGRATION_TODO_PREFIX} markers: ${path8.relative(projectDir, rulePath)}`);
1839
+ }
1840
+ }
1841
+ function getRuleFilePath(paths, fromVersion, toVersion) {
1842
+ return path8.join(paths.rulesDir, `${fromVersion}-to-${toVersion}.ts`);
1843
+ }
1844
+ function readRuleMetadata(rulePath) {
1845
+ const source = fs7.readFileSync(rulePath, "utf8");
1846
+ const unresolvedBlock = source.match(/export const unresolved = \[([\s\S]*?)\] as const;/);
1847
+ const renameMapBlock = source.match(/export const renameMap: RenameMap = \{([\s\S]*?)\};/);
1848
+ const transformsBlock = source.match(/export const transforms: TransformMap = \{([\s\S]*?)\};/);
1849
+ const unresolved = unresolvedBlock ? [...unresolvedBlock[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]) : [];
1850
+ const renameMap = renameMapBlock ? [...renameMapBlock[1].matchAll(/^\s*"([^"]+)":\s*"([^"]+)"/gm)].map((match) => ({
1851
+ currentPath: match[1],
1852
+ legacyPath: match[2]
1853
+ })) : [];
1854
+ const transforms = transformsBlock ? [...transformsBlock[1].matchAll(/^\s*"([^"]+)":\s*\(/gm)].map((match) => match[1]) : [];
1855
+ return { renameMap, transforms, unresolved };
1856
+ }
1857
+
1858
+ // src/runtime/migration-render.ts
1859
+ import fs8 from "node:fs";
1860
+ import path9 from "node:path";
1861
+ function formatDiffReport(diff) {
1862
+ const lines = [
1863
+ `Migration diff: ${diff.fromVersion} -> ${diff.toVersion}`,
1864
+ `Current type: ${diff.currentTypeName}`,
1865
+ `Safe changes: ${diff.summary.auto}`,
1866
+ `Manual changes: ${diff.summary.manual}`
1867
+ ];
1868
+ if (diff.summary.autoItems.length > 0) {
1869
+ lines.push("", "Safe changes:");
1870
+ for (const item of diff.summary.autoItems) {
1871
+ lines.push(` - ${item.path}: ${item.kind}${item.detail ? ` (${item.detail})` : ""}`);
1872
+ }
1873
+ }
1874
+ if (diff.summary.manualItems.length > 0) {
1875
+ lines.push("", "Manual review required:");
1876
+ for (const item of diff.summary.manualItems) {
1877
+ lines.push(` - ${item.path}: ${item.kind}${item.detail ? ` (${item.detail})` : ""}`);
1878
+ }
1879
+ }
1880
+ if (diff.summary.renameCandidates.length > 0) {
1881
+ const autoApplied = diff.summary.renameCandidates.filter((item) => item.autoApply);
1882
+ const suggested = diff.summary.renameCandidates.filter((item) => !item.autoApply);
1883
+ if (autoApplied.length > 0) {
1884
+ lines.push("", "Auto-applied renames:");
1885
+ for (const item of autoApplied) {
1886
+ lines.push(` - ${item.currentPath} <- ${item.legacyPath} (${item.reason}, score ${item.score.toFixed(2)})`);
1887
+ }
1888
+ }
1889
+ if (suggested.length > 0) {
1890
+ lines.push("", "Suggested renames:");
1891
+ for (const item of suggested) {
1892
+ lines.push(` - ${item.currentPath} <- ${item.legacyPath} (${item.reason}, score ${item.score.toFixed(2)})`);
1893
+ }
1894
+ }
1895
+ }
1896
+ if (diff.summary.transformSuggestions.length > 0) {
1897
+ lines.push("", "Suggested transforms:");
1898
+ for (const item of diff.summary.transformSuggestions) {
1899
+ lines.push(` - ${item.currentPath}${item.legacyPath ? ` <- ${item.legacyPath}` : ""} (${item.reason})`);
1900
+ }
1901
+ }
1902
+ return lines.join(`
1903
+ `);
1904
+ }
1905
+ function renderMigrationRuleFile({
1906
+ currentAttributes,
1907
+ currentTypeName,
1908
+ diff,
1909
+ fromVersion,
1910
+ targetVersion
1911
+ }) {
1912
+ const activeRenameCandidates = diff.summary.renameCandidates.filter((candidate) => candidate.autoApply);
1913
+ const suggestedRenameCandidates = diff.summary.renameCandidates.filter((candidate) => !candidate.autoApply);
1914
+ const lines = [];
1915
+ lines.push(`import type { ${currentTypeName} } from "../../types";`);
1916
+ lines.push(`import currentManifest from "../../../${ROOT_MANIFEST}";`);
1917
+ lines.push(`import {`);
1918
+ lines.push(` type RenameMap,`);
1919
+ lines.push(` type TransformMap,`);
1920
+ lines.push(` resolveMigrationAttribute,`);
1921
+ lines.push(`} from "../helpers";`);
1922
+ lines.push("");
1923
+ lines.push(`export const fromVersion = "${fromVersion}" as const;`);
1924
+ lines.push(`export const toVersion = "${targetVersion}" as const;`);
1925
+ lines.push("");
1926
+ lines.push("export const renameMap: RenameMap = {");
1927
+ for (const candidate of activeRenameCandidates) {
1928
+ lines.push(` ${renderObjectKey(candidate.currentPath)}: "${escapeForCode(candidate.legacyPath)}",`);
1929
+ }
1930
+ for (const candidate of suggestedRenameCandidates) {
1931
+ lines.push(` // ${renderObjectKey(candidate.currentPath)}: "${escapeForCode(candidate.legacyPath)}",`);
1932
+ }
1933
+ lines.push("};");
1934
+ lines.push("");
1935
+ lines.push("export const transforms: TransformMap = {");
1936
+ for (const suggestion of diff.summary.transformSuggestions) {
1937
+ lines.push(` // ${renderObjectKey(suggestion.currentPath)}: (legacyValue, legacyInput) => {`);
1938
+ for (const bodyLine of suggestion.bodyLines) {
1939
+ lines.push(` ${bodyLine}`);
1940
+ }
1941
+ lines.push(` // },`);
1942
+ }
1943
+ lines.push("};");
1944
+ lines.push("");
1945
+ lines.push("export const unresolved = [");
1946
+ for (const item of diff.summary.manualItems) {
1947
+ lines.push(` "${item.path}: ${item.kind}${item.detail ? ` (${escapeForCode(item.detail)})` : ""}",`);
1948
+ }
1949
+ for (const candidate of suggestedRenameCandidates) {
1950
+ lines.push(` "${candidate.currentPath}: rename candidate from ${candidate.legacyPath}",`);
1951
+ }
1952
+ for (const suggestion of diff.summary.transformSuggestions) {
1953
+ lines.push(` "${suggestion.currentPath}: transform suggested from ${suggestion.legacyPath ?? suggestion.currentPath}",`);
1954
+ }
1955
+ lines.push("] as const;");
1956
+ lines.push("");
1957
+ lines.push(`export function migrate(input: Record<string, unknown>): ${currentTypeName} {`);
1958
+ lines.push(` return {`);
1959
+ for (const key of Object.keys(currentAttributes)) {
1960
+ for (const manualItem of diff.summary.manualItems.filter((item) => item.path === key || item.path.startsWith(`${key}.`))) {
1961
+ lines.push(` // ${MIGRATION_TODO_PREFIX} ${manualItem.path}: ${manualItem.kind}${manualItem.detail ? ` (${manualItem.detail})` : ""}`);
1962
+ }
1963
+ for (const renameCandidate of suggestedRenameCandidates.filter((item) => item.currentPath === key || item.currentPath.startsWith(`${key}.`))) {
1964
+ lines.push(` // ${MIGRATION_TODO_PREFIX} consider renameMap[${JSON.stringify(renameCandidate.currentPath)}] = "${renameCandidate.legacyPath}"`);
1965
+ }
1966
+ for (const suggestion of diff.summary.transformSuggestions.filter((item) => item.currentPath === key || item.currentPath.startsWith(`${key}.`))) {
1967
+ lines.push(` // ${MIGRATION_TODO_PREFIX} review transforms[${JSON.stringify(suggestion.currentPath)}]`);
1968
+ }
1969
+ lines.push(` ${key}: resolveMigrationAttribute(currentManifest.attributes.${key}, "${key}", "${key}", input, renameMap, transforms),`);
1970
+ }
1971
+ lines.push(` } as ${currentTypeName};`);
1972
+ lines.push("}");
1973
+ lines.push("");
1974
+ return `${lines.join(`
1975
+ `)}
1976
+ `;
1977
+ }
1978
+ function renderMigrationRegistryFile(state, entries) {
1979
+ const imports = [`import currentManifest from "../../../${ROOT_MANIFEST}";`];
1980
+ const body = [];
1981
+ entries.forEach((entry, index) => {
1982
+ imports.push(`import manifest_${index} from "${entry.manifestImport}";`);
1983
+ imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
1984
+ body.push(` {`);
1985
+ body.push(` fromVersion: "${entry.fromVersion}",`);
1986
+ body.push(` manifest: manifest_${index},`);
1987
+ body.push(` rule: rule_${index},`);
1988
+ body.push(` },`);
1989
+ });
1990
+ return `${imports.join(`
1991
+ `)}
1992
+
1993
+ export const migrationRegistry = {
1994
+ currentVersion: "${state.config.currentVersion}",
1995
+ currentManifest,
1996
+ entries: [
1997
+ ${body.join(`
1998
+ `)}
1999
+ ],
2000
+ } as const;
2001
+
2002
+ export default migrationRegistry;
2003
+ `;
2004
+ }
2005
+ function renderGeneratedDeprecatedFile(entries) {
2006
+ if (entries.length === 0) {
2007
+ return `import type { BlockConfiguration } from "@wordpress/blocks";
2008
+
2009
+ export const deprecated: NonNullable<BlockConfiguration["deprecated"]> = [];
2010
+ `;
2011
+ }
2012
+ const imports = [`import type { BlockConfiguration } from "@wordpress/blocks";`];
2013
+ const definitions = [];
2014
+ const arrayEntries = [];
2015
+ entries.forEach((entry, index) => {
2016
+ imports.push(`import block_${index} from "${entry.blockJsonImport}";`);
2017
+ imports.push(`import save_${index} from "${entry.saveImport}";`);
2018
+ imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
2019
+ definitions.push(`const deprecated_${index}: NonNullable<BlockConfiguration["deprecated"]>[number] = {`);
2020
+ definitions.push(` attributes: (block_${index}.attributes ?? {}) as Record<string, unknown>,`);
2021
+ definitions.push(` save: save_${index} as BlockConfiguration["save"],`);
2022
+ definitions.push(` migrate(attributes: Record<string, unknown>) {`);
2023
+ definitions.push(` return rule_${index}.migrate(attributes);`);
2024
+ definitions.push(` },`);
2025
+ definitions.push(`};`);
2026
+ arrayEntries.push(`deprecated_${index}`);
2027
+ });
2028
+ return `${imports.join(`
2029
+ `)}
2030
+
2031
+ ${definitions.join(`
2032
+
2033
+ `)}
2034
+
2035
+ export const deprecated: NonNullable<BlockConfiguration["deprecated"]> = [${arrayEntries.join(", ")}];
2036
+ `;
2037
+ }
2038
+ function renderPhpMigrationRegistryFile(state, entries) {
2039
+ const snapshots = Object.fromEntries(state.config.supportedVersions.map((version) => {
2040
+ const snapshotRoot = path9.join(state.projectDir, SNAPSHOT_DIR, version);
2041
+ const manifestPath = path9.join(snapshotRoot, ROOT_MANIFEST);
2042
+ const blockJsonPath = path9.join(snapshotRoot, ROOT_BLOCK_JSON);
2043
+ const savePath = path9.join(snapshotRoot, "save.tsx");
2044
+ return [
2045
+ version,
2046
+ {
2047
+ blockJson: fs8.existsSync(blockJsonPath) ? {
2048
+ attributeNames: Object.keys(readJson(blockJsonPath).attributes ?? {}),
2049
+ name: readJson(blockJsonPath).name ?? null
2050
+ } : null,
2051
+ hasSaveSnapshot: fs8.existsSync(savePath),
2052
+ manifest: fs8.existsSync(manifestPath) ? summarizeManifest(readJson(manifestPath)) : null
2053
+ }
2054
+ ];
2055
+ }));
2056
+ const edgeSummaries = entries.map((entry) => {
2057
+ const ruleMetadata = readRuleMetadata(entry.rulePath);
2058
+ const snapshotManifest = snapshots[entry.fromVersion]?.manifest ?? null;
2059
+ return {
2060
+ autoAppliedRenameCount: ruleMetadata.renameMap.length,
2061
+ autoAppliedRenames: ruleMetadata.renameMap,
2062
+ fromVersion: entry.fromVersion,
2063
+ nestedPathRenames: ruleMetadata.renameMap.filter((item) => item.currentPath.includes(".")),
2064
+ ruleFile: path9.relative(state.projectDir, entry.rulePath).replace(/\\/g, "/"),
2065
+ toVersion: entry.toVersion,
2066
+ transformKeys: ruleMetadata.transforms,
2067
+ unionBranches: snapshotManifest ? summarizeUnionBranches(snapshotManifest) : [],
2068
+ unresolved: ruleMetadata.unresolved
2069
+ };
2070
+ });
2071
+ return `<?php
2072
+ declare(strict_types=1);
2073
+
2074
+ /**
2075
+ * Generated from advanced migration snapshots. Do not edit manually.
2076
+ */
2077
+ return ${renderPhpValue({
2078
+ blockName: state.config.blockName,
2079
+ currentManifest: summarizeManifest(state.currentManifest),
2080
+ currentVersion: state.config.currentVersion,
2081
+ edges: edgeSummaries,
2082
+ legacyVersions: state.config.supportedVersions.filter((version) => version !== state.config.currentVersion),
2083
+ snapshotDir: state.config.snapshotDir,
2084
+ snapshots,
2085
+ supportedVersions: state.config.supportedVersions
2086
+ }, 0)};
2087
+ `;
2088
+ }
2089
+ function renderVerifyFile(state, entries) {
2090
+ const imports = [
2091
+ `import { validators } from "../../validators";`,
2092
+ `import { deprecated } from "./deprecated";`
2093
+ ];
2094
+ const checks = [];
2095
+ entries.forEach((entry, index) => {
2096
+ imports.push(`import fixture_${index} from "${entry.fixtureImport}";`);
2097
+ imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
2098
+ checks.push(` if (selectedVersions.length === 0 || selectedVersions.includes("${entry.fromVersion}")) {`);
2099
+ checks.push(` if (rule_${index}.unresolved.length > 0) {`);
2100
+ checks.push(` throw new Error("Unresolved migration TODOs remain for ${entry.fromVersion} -> ${entry.toVersion}: " + rule_${index}.unresolved.join(", "));`);
2101
+ checks.push(` }`);
2102
+ checks.push(` const cases_${index} = Array.isArray(fixture_${index}.cases) ? fixture_${index}.cases : [];`);
2103
+ checks.push(` for (const fixtureCase of cases_${index}) {`);
2104
+ checks.push(` const migrated_${index} = rule_${index}.migrate(fixtureCase.input ?? {});`);
2105
+ checks.push(` const validation_${index} = validators.validate(migrated_${index});`);
2106
+ checks.push(` if (!validation_${index}.success) {`);
2107
+ checks.push(` throw new Error("Current validator rejected migrated fixture for ${entry.fromVersion} case " + String(fixtureCase.name ?? "unknown") + ": " + JSON.stringify(validation_${index}.errors));`);
2108
+ checks.push(` }`);
2109
+ checks.push(` }`);
2110
+ checks.push(` console.log("Verified ${entry.fromVersion} -> ${entry.toVersion} (" + cases_${index}.length + " case(s))");`);
2111
+ checks.push(` }`);
2112
+ });
2113
+ return `${imports.join(`
2114
+ `)}
2115
+
2116
+ const args = process.argv.slice(2);
2117
+ const selectedVersions =
2118
+ args[0] === "--all"
2119
+ ? []
2120
+ : args[0] === "--from" && args[1]
2121
+ ? [args[1]]
2122
+ : [];
2123
+
2124
+ if (deprecated.length !== ${entries.length}) {
2125
+ throw new Error("Generated deprecated entries are out of sync with migration registry.");
2126
+ }
2127
+
2128
+ ${checks.join(`
2129
+ `)}
2130
+
2131
+ console.log("Migration verification passed for ${state.config.blockName}");
2132
+ `;
2133
+ }
2134
+
2135
+ // src/runtime/migrations.ts
2136
+ function formatMigrationHelpText() {
2137
+ return `Usage:
2138
+ wp-typia migrations init --current-version <semver>
2139
+ wp-typia migrations snapshot --version <semver>
2140
+ wp-typia migrations diff --from <semver> [--to current]
2141
+ wp-typia migrations scaffold --from <semver> [--to current]
2142
+ wp-typia migrations verify [--from <semver>|--all]`;
2143
+ }
2144
+ function parseMigrationArgs(argv) {
2145
+ const parsed = {
2146
+ command: undefined,
2147
+ flags: {
2148
+ all: false,
2149
+ currentVersion: undefined,
2150
+ from: undefined,
2151
+ to: "current",
2152
+ version: undefined
2153
+ }
2154
+ };
2155
+ if (argv.length === 0) {
2156
+ throw new Error(formatMigrationHelpText());
2157
+ }
2158
+ parsed.command = argv[0];
2159
+ for (let index = 1;index < argv.length; index += 1) {
2160
+ const arg = argv[index];
2161
+ const next = argv[index + 1];
2162
+ if (arg === "--")
2163
+ continue;
2164
+ if (arg === "--all") {
2165
+ parsed.flags.all = true;
2166
+ continue;
2167
+ }
2168
+ if (arg === "--current-version") {
2169
+ parsed.flags.currentVersion = next;
2170
+ index += 1;
2171
+ continue;
2172
+ }
2173
+ if (arg.startsWith("--current-version=")) {
2174
+ parsed.flags.currentVersion = arg.split("=", 2)[1];
2175
+ continue;
2176
+ }
2177
+ if (arg === "--from") {
2178
+ parsed.flags.from = next;
2179
+ index += 1;
2180
+ continue;
2181
+ }
2182
+ if (arg.startsWith("--from=")) {
2183
+ parsed.flags.from = arg.split("=", 2)[1];
2184
+ continue;
2185
+ }
2186
+ if (arg === "--to") {
2187
+ parsed.flags.to = next;
2188
+ index += 1;
2189
+ continue;
2190
+ }
2191
+ if (arg.startsWith("--to=")) {
2192
+ parsed.flags.to = arg.split("=", 2)[1];
2193
+ continue;
2194
+ }
2195
+ if (arg === "--version") {
2196
+ parsed.flags.version = next;
2197
+ index += 1;
2198
+ continue;
2199
+ }
2200
+ if (arg.startsWith("--version=")) {
2201
+ parsed.flags.version = arg.split("=", 2)[1];
2202
+ continue;
2203
+ }
2204
+ throw new Error(`Unknown migrations flag: ${arg}`);
2205
+ }
2206
+ return parsed;
2207
+ }
2208
+ function runMigrationCommand(command, cwd, { renderLine = console.log } = {}) {
2209
+ switch (command.command) {
2210
+ case "init":
2211
+ if (!command.flags.currentVersion) {
2212
+ throw new Error("`migrations init` requires --current-version <semver>.");
2213
+ }
2214
+ return initProjectMigrations(cwd, command.flags.currentVersion, { renderLine });
2215
+ case "snapshot":
2216
+ if (!command.flags.version) {
2217
+ throw new Error("`migrations snapshot` requires --version <semver>.");
2218
+ }
2219
+ return snapshotProjectVersion(cwd, command.flags.version, { renderLine });
2220
+ case "diff":
2221
+ if (!command.flags.from) {
2222
+ throw new Error("`migrations diff` requires --from <semver>.");
2223
+ }
2224
+ return diffProjectMigrations(cwd, {
2225
+ fromVersion: command.flags.from,
2226
+ renderLine,
2227
+ toVersion: command.flags.to ?? "current"
2228
+ });
2229
+ case "scaffold":
2230
+ if (!command.flags.from) {
2231
+ throw new Error("`migrations scaffold` requires --from <semver>.");
2232
+ }
2233
+ return scaffoldProjectMigrations(cwd, {
2234
+ fromVersion: command.flags.from,
2235
+ renderLine,
2236
+ toVersion: command.flags.to ?? "current"
2237
+ });
2238
+ case "verify":
2239
+ return verifyProjectMigrations(cwd, {
2240
+ all: command.flags.all,
2241
+ fromVersion: command.flags.from,
2242
+ renderLine
2243
+ });
2244
+ default:
2245
+ throw new Error(formatMigrationHelpText());
2246
+ }
2247
+ }
2248
+ function initProjectMigrations(projectDir, currentVersion, { renderLine = console.log } = {}) {
2249
+ ensureAdvancedMigrationProject(projectDir);
2250
+ assertSemver(currentVersion, "current version");
2251
+ const blockName = readProjectBlockName(projectDir);
2252
+ ensureMigrationDirectories(projectDir);
2253
+ writeMigrationConfig(projectDir, {
2254
+ blockName,
2255
+ currentVersion,
2256
+ snapshotDir: SNAPSHOT_DIR.replace(/\\/g, "/"),
2257
+ supportedVersions: [currentVersion]
2258
+ });
2259
+ writeInitialMigrationScaffold(projectDir, currentVersion);
2260
+ snapshotProjectVersion(projectDir, currentVersion, { renderLine, skipConfigUpdate: true });
2261
+ regenerateGeneratedArtifacts(projectDir);
2262
+ renderLine(`Initialized migrations for ${blockName} at version ${currentVersion}`);
2263
+ return loadMigrationProject(projectDir);
2264
+ }
2265
+ function snapshotProjectVersion(projectDir, version, {
2266
+ renderLine = console.log,
2267
+ skipConfigUpdate = false
2268
+ } = {}) {
2269
+ ensureAdvancedMigrationProject(projectDir);
2270
+ assertSemver(version, "snapshot version");
2271
+ runProjectScriptIfPresent(projectDir, "sync-types");
2272
+ const state = loadMigrationProject(projectDir, { allowMissingConfig: skipConfigUpdate });
2273
+ const snapshotRoot = path10.join(projectDir, SNAPSHOT_DIR, version);
2274
+ fs9.mkdirSync(snapshotRoot, { recursive: true });
2275
+ fs9.writeFileSync(path10.join(snapshotRoot, ROOT_BLOCK_JSON), `${JSON.stringify(sanitizeSnapshotBlockJson(readJson(path10.join(projectDir, ROOT_BLOCK_JSON))), null, "\t")}
2276
+ `, "utf8");
2277
+ copyFile(path10.join(projectDir, ROOT_MANIFEST), path10.join(snapshotRoot, ROOT_MANIFEST));
2278
+ fs9.writeFileSync(path10.join(snapshotRoot, "save.tsx"), sanitizeSaveSnapshotSource(fs9.readFileSync(path10.join(projectDir, ROOT_SAVE_FILE), "utf8")), "utf8");
2279
+ if (!skipConfigUpdate) {
2280
+ const nextSupported = [...new Set([...state.config.supportedVersions, version])].sort(compareSemver);
2281
+ writeMigrationConfig(projectDir, {
2282
+ ...state.config,
2283
+ currentVersion: version,
2284
+ supportedVersions: nextSupported
2285
+ });
2286
+ }
2287
+ regenerateGeneratedArtifacts(projectDir);
2288
+ renderLine(`Snapshot stored for ${version}`);
2289
+ return loadMigrationProject(projectDir);
2290
+ }
2291
+ function diffProjectMigrations(projectDir, { fromVersion, toVersion = "current", renderLine = console.log } = {}) {
2292
+ if (!fromVersion) {
2293
+ throw new Error("`migrations diff` requires --from <semver>.");
2294
+ }
2295
+ const state = loadMigrationProject(projectDir);
2296
+ const targetVersion = resolveTargetVersion(state.config.currentVersion, toVersion);
2297
+ const diff = createMigrationDiff(state, fromVersion, targetVersion);
2298
+ renderLine(formatDiffReport(diff));
2299
+ return diff;
2300
+ }
2301
+ function scaffoldProjectMigrations(projectDir, { fromVersion, toVersion = "current", renderLine = console.log } = {}) {
2302
+ if (!fromVersion) {
2303
+ throw new Error("`migrations scaffold` requires --from <semver>.");
2304
+ }
2305
+ ensureMigrationDirectories(projectDir);
2306
+ const state = loadMigrationProject(projectDir);
2307
+ const targetVersion = resolveTargetVersion(state.config.currentVersion, toVersion);
2308
+ const diff = createMigrationDiff(state, fromVersion, targetVersion);
2309
+ const paths = getProjectPaths(projectDir);
2310
+ const rulePath = getRuleFilePath(paths, fromVersion, targetVersion);
2311
+ if (!fs9.existsSync(rulePath)) {
2312
+ fs9.writeFileSync(rulePath, renderMigrationRuleFile({
2313
+ currentAttributes: state.currentManifest.attributes ?? {},
2314
+ currentTypeName: state.currentManifest.sourceType,
2315
+ diff,
2316
+ fromVersion,
2317
+ targetVersion
2318
+ }), "utf8");
2319
+ }
2320
+ ensureEdgeFixtureFile(projectDir, fromVersion, targetVersion, diff);
2321
+ regenerateGeneratedArtifacts(projectDir);
2322
+ renderLine(formatDiffReport(diff));
2323
+ renderLine(`Scaffolded ${path10.relative(projectDir, rulePath)}`);
2324
+ return { diff, rulePath };
2325
+ }
2326
+ function verifyProjectMigrations(projectDir, { all = false, fromVersion, renderLine = console.log } = {}) {
2327
+ const state = loadMigrationProject(projectDir);
2328
+ const verifyScriptPath = path10.join(projectDir, GENERATED_DIR, "verify.ts");
2329
+ if (!fs9.existsSync(verifyScriptPath)) {
2330
+ throw new Error("Generated verify script is missing. Run `wp-typia migrations scaffold --from <semver>` first.");
2331
+ }
2332
+ const targetVersions = all ? state.config.supportedVersions.filter((version) => version !== state.config.currentVersion) : fromVersion ? [fromVersion] : state.config.supportedVersions.filter((version) => version !== state.config.currentVersion);
2333
+ if (targetVersions.length === 0) {
2334
+ renderLine("No legacy versions configured for verification.");
2335
+ return { verifiedVersions: [] };
2336
+ }
2337
+ for (const version of targetVersions) {
2338
+ assertRuleHasNoTodos(projectDir, version, state.config.currentVersion);
2339
+ }
2340
+ const tsxBinary = getLocalTsxBinary(projectDir);
2341
+ const filteredArgs = all ? ["--all"] : fromVersion ? ["--from", fromVersion] : [];
2342
+ execSync3(`"${tsxBinary}" "${verifyScriptPath}" ${filteredArgs.join(" ")}`.trim(), {
2343
+ cwd: projectDir,
2344
+ stdio: "inherit"
2345
+ });
2346
+ renderLine(`Verified migrations for ${targetVersions.join(", ")}`);
2347
+ return { verifiedVersions: targetVersions };
2348
+ }
2349
+ function regenerateGeneratedArtifacts(projectDir) {
2350
+ const state = loadMigrationProject(projectDir);
2351
+ const entries = discoverMigrationEntries(state);
2352
+ fs9.mkdirSync(state.paths.generatedDir, { recursive: true });
2353
+ fs9.writeFileSync(path10.join(state.paths.generatedDir, "registry.ts"), renderMigrationRegistryFile(state, entries), "utf8");
2354
+ fs9.writeFileSync(path10.join(state.paths.generatedDir, "deprecated.ts"), renderGeneratedDeprecatedFile(entries), "utf8");
2355
+ fs9.writeFileSync(path10.join(state.paths.generatedDir, "verify.ts"), renderVerifyFile(state, entries), "utf8");
2356
+ fs9.writeFileSync(path10.join(projectDir, ROOT_PHP_MIGRATION_REGISTRY), renderPhpMigrationRegistryFile(state, entries), "utf8");
2357
+ }
2358
+
2359
+ // src/cli.ts
2360
+ function parseArgs(argv) {
2361
+ const parsed = {
2362
+ help: false,
2363
+ noInstall: false,
2364
+ packageManager: undefined,
2365
+ positionals: [],
2366
+ template: undefined,
2367
+ yes: false
2368
+ };
2369
+ for (let index = 0;index < argv.length; index += 1) {
2370
+ const arg = argv[index];
2371
+ if (!arg.startsWith("-")) {
2372
+ parsed.positionals.push(arg);
2373
+ continue;
2374
+ }
2375
+ if (arg === "--help" || arg === "-h") {
2376
+ parsed.help = true;
2377
+ continue;
2378
+ }
2379
+ if (arg === "--yes" || arg === "-y") {
2380
+ parsed.yes = true;
2381
+ continue;
2382
+ }
2383
+ if (arg === "--no-install") {
2384
+ parsed.noInstall = true;
2385
+ continue;
2386
+ }
2387
+ if (arg === "--template" || arg === "-t") {
2388
+ parsed.template = argv[index + 1];
2389
+ index += 1;
2390
+ continue;
2391
+ }
2392
+ if (arg.startsWith("--template=")) {
2393
+ parsed.template = arg.split("=", 2)[1];
2394
+ continue;
2395
+ }
2396
+ if (arg === "--package-manager" || arg === "-p") {
2397
+ parsed.packageManager = argv[index + 1];
2398
+ index += 1;
2399
+ continue;
2400
+ }
2401
+ if (arg.startsWith("--package-manager=")) {
2402
+ parsed.packageManager = arg.split("=", 2)[1];
2403
+ continue;
2404
+ }
2405
+ throw new Error(`Unknown flag: ${arg}`);
2406
+ }
2407
+ return parsed;
2408
+ }
2409
+ function printDoctorLine({
2410
+ status,
2411
+ label,
2412
+ detail
2413
+ }) {
2414
+ const prefix = status === "pass" ? "PASS" : status === "fail" ? "FAIL" : "WARN";
2415
+ console.log(`${prefix} ${label}: ${detail}`);
2416
+ }
2417
+ async function runScaffold(parsed, cwd) {
2418
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
2419
+ const prompt = createReadlinePrompt();
2420
+ try {
2421
+ const flow = await runScaffoldFlow({
2422
+ cwd,
2423
+ isInteractive,
2424
+ noInstall: parsed.noInstall,
2425
+ packageManager: parsed.packageManager,
2426
+ projectInput: parsed.positionals[0],
2427
+ promptText: (message, defaultValue, validate) => prompt.text(message, defaultValue, validate),
2428
+ selectPackageManager: () => prompt.select("Choose a package manager", getPackageManagerSelectOptions(), 1),
2429
+ selectTemplate: () => prompt.select("Select a template", getTemplateSelectOptions(), 1),
2430
+ templateId: parsed.template,
2431
+ yes: parsed.yes
2432
+ });
2433
+ console.log(`
2434
+ ✅ Created ${flow.result.variables.title} in ${flow.projectDir}`);
2435
+ console.log("Next steps:");
2436
+ for (const step of flow.nextSteps) {
2437
+ console.log(` ${step}`);
2438
+ }
2439
+ } finally {
2440
+ prompt.close();
2441
+ }
2442
+ }
2443
+ async function main(argv = process.argv.slice(2), cwd = process.cwd()) {
2444
+ if (argv[0] === "migrations") {
2445
+ const migrationCommand = parseMigrationArgs(argv.slice(1));
2446
+ if (!migrationCommand.command) {
2447
+ console.log(formatMigrationHelpText());
2448
+ return;
2449
+ }
2450
+ await runMigrationCommand(migrationCommand, cwd, { renderLine: console.log });
2451
+ return;
2452
+ }
2453
+ const parsed = parseArgs(argv);
2454
+ if (parsed.help) {
2455
+ console.log(formatHelpText());
2456
+ return;
2457
+ }
2458
+ const [first, second] = parsed.positionals;
2459
+ if (first === "templates") {
2460
+ if (second === "list") {
2461
+ for (const template of listTemplates()) {
2462
+ console.log(formatTemplateSummary(template));
2463
+ console.log(formatTemplateFeatures(template));
2464
+ }
2465
+ return;
2466
+ }
2467
+ if (second === "inspect") {
2468
+ const templateId = parsed.positionals[2];
2469
+ if (!templateId) {
2470
+ throw new Error(`Template ID is required. Use one of: ${listTemplates().map((template) => template.id).join(", ")}`);
2471
+ }
2472
+ console.log(formatTemplateDetails(getTemplateById(templateId)));
2473
+ return;
2474
+ }
2475
+ throw new Error("Usage: wp-typia templates <list|inspect>");
2476
+ }
2477
+ if (first === "doctor") {
2478
+ await runDoctor(cwd, { renderLine: printDoctorLine });
2479
+ return;
2480
+ }
2481
+ if (parsed.yes && !parsed.packageManager) {
2482
+ throw new Error(`Package manager is required when using --yes. Use --package-manager <${PACKAGE_MANAGER_IDS.join("|")}>.`);
2483
+ }
2484
+ await runScaffold(parsed, cwd);
2485
+ }
2486
+ main().catch((error) => {
2487
+ console.error("❌ wp-typia failed:", error instanceof Error ? error.message : error);
2488
+ process.exit(1);
2489
+ });
2490
+ export {
2491
+ main
2492
+ };