@wp-typia/project-tools 0.19.2 → 0.20.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 (77) hide show
  1. package/README.md +23 -0
  2. package/dist/runtime/ability-spec.d.ts +90 -0
  3. package/dist/runtime/ability-spec.js +51 -0
  4. package/dist/runtime/ai-artifacts.d.ts +39 -0
  5. package/dist/runtime/ai-artifacts.js +68 -0
  6. package/dist/runtime/ai-feature-artifacts.d.ts +85 -0
  7. package/dist/runtime/ai-feature-artifacts.js +139 -0
  8. package/dist/runtime/ai-feature-capability.d.ts +114 -0
  9. package/dist/runtime/ai-feature-capability.js +150 -0
  10. package/dist/runtime/block-generator-service-spec.js +6 -0
  11. package/dist/runtime/cli-add-shared.d.ts +32 -1
  12. package/dist/runtime/cli-add-shared.js +44 -0
  13. package/dist/runtime/cli-add-workspace-ability.d.ts +8 -0
  14. package/dist/runtime/cli-add-workspace-ability.js +810 -0
  15. package/dist/runtime/cli-add-workspace-ai-anchors.d.ts +22 -0
  16. package/dist/runtime/cli-add-workspace-ai-anchors.js +277 -0
  17. package/dist/runtime/cli-add-workspace-ai-source-emitters.d.ts +28 -0
  18. package/dist/runtime/cli-add-workspace-ai-source-emitters.js +346 -0
  19. package/dist/runtime/cli-add-workspace-ai.d.ts +14 -0
  20. package/dist/runtime/cli-add-workspace-ai.js +484 -0
  21. package/dist/runtime/cli-add-workspace.d.ts +10 -0
  22. package/dist/runtime/cli-add-workspace.js +10 -0
  23. package/dist/runtime/cli-add.d.ts +1 -1
  24. package/dist/runtime/cli-add.js +1 -1
  25. package/dist/runtime/cli-core.d.ts +3 -1
  26. package/dist/runtime/cli-core.js +3 -1
  27. package/dist/runtime/cli-diagnostics.d.ts +2 -0
  28. package/dist/runtime/cli-diagnostics.js +10 -1
  29. package/dist/runtime/cli-doctor-workspace.js +140 -1
  30. package/dist/runtime/cli-help.js +7 -0
  31. package/dist/runtime/cli-init.d.ts +49 -0
  32. package/dist/runtime/cli-init.js +354 -0
  33. package/dist/runtime/cli-scaffold.d.ts +1 -0
  34. package/dist/runtime/cli-scaffold.js +5 -1
  35. package/dist/runtime/cli-templates.js +36 -6
  36. package/dist/runtime/external-template-guards.d.ts +31 -0
  37. package/dist/runtime/external-template-guards.js +132 -0
  38. package/dist/runtime/index.d.ts +3 -1
  39. package/dist/runtime/index.js +2 -1
  40. package/dist/runtime/package-managers.d.ts +8 -0
  41. package/dist/runtime/package-managers.js +13 -0
  42. package/dist/runtime/package-versions.d.ts +8 -0
  43. package/dist/runtime/package-versions.js +84 -19
  44. package/dist/runtime/scaffold-compatibility.d.ts +65 -0
  45. package/dist/runtime/scaffold-compatibility.js +152 -0
  46. package/dist/runtime/scaffold-documents.js +19 -1
  47. package/dist/runtime/scaffold-onboarding.d.ts +4 -0
  48. package/dist/runtime/scaffold-onboarding.js +25 -1
  49. package/dist/runtime/scaffold-template-variable-groups.d.ts +2 -0
  50. package/dist/runtime/scaffold-template-variables.js +6 -0
  51. package/dist/runtime/scaffold.d.ts +3 -0
  52. package/dist/runtime/scaffold.js +2 -5
  53. package/dist/runtime/template-registry.d.ts +23 -1
  54. package/dist/runtime/template-registry.js +37 -3
  55. package/dist/runtime/template-source-external.js +9 -3
  56. package/dist/runtime/template-source-remote.js +5 -0
  57. package/dist/runtime/template-source-seeds.js +40 -6
  58. package/dist/runtime/typia-llm.d.ts +213 -0
  59. package/dist/runtime/typia-llm.js +348 -0
  60. package/dist/runtime/wordpress-ai.d.ts +122 -0
  61. package/dist/runtime/wordpress-ai.js +177 -0
  62. package/dist/runtime/workspace-inventory.d.ts +51 -4
  63. package/dist/runtime/workspace-inventory.js +157 -4
  64. package/package.json +17 -2
  65. package/templates/_shared/base/package.json.mustache +0 -1
  66. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +3 -3
  67. package/templates/_shared/compound/core/package.json.mustache +0 -1
  68. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +3 -3
  69. package/templates/_shared/compound/persistence/package.json.mustache +0 -1
  70. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +3 -3
  71. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +3 -3
  72. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +3 -3
  73. package/templates/_shared/persistence/core/package.json.mustache +0 -1
  74. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +3 -3
  75. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +3 -3
  76. package/templates/interactivity/package.json.mustache +0 -1
  77. package/templates/query-loop/{{slugKebabCase}}.php.mustache +3 -3
@@ -11,6 +11,10 @@ const WORKSPACE_BINDING_SERVER_GLOB = "/src/bindings/*/server.php";
11
11
  const WORKSPACE_BINDING_EDITOR_SCRIPT = "build/bindings/index.js";
12
12
  const WORKSPACE_BINDING_EDITOR_ASSET = "build/bindings/index.asset.php";
13
13
  const WORKSPACE_REST_RESOURCE_GLOB = "/inc/rest/*.php";
14
+ const WORKSPACE_ABILITY_GLOB = "/inc/abilities/*.php";
15
+ const WORKSPACE_ABILITY_EDITOR_SCRIPT = "build/abilities/index.js";
16
+ const WORKSPACE_ABILITY_EDITOR_ASSET = "build/abilities/index.asset.php";
17
+ const WORKSPACE_AI_FEATURE_GLOB = "/inc/ai-features/*.php";
14
18
  const WORKSPACE_EDITOR_PLUGIN_EDITOR_SCRIPT = "build/editor-plugins/index.js";
15
19
  const WORKSPACE_EDITOR_PLUGIN_EDITOR_ASSET = "build/editor-plugins/index.asset.php";
16
20
  const WORKSPACE_EDITOR_PLUGIN_EDITOR_STYLE = "build/editor-plugins/style-index.css";
@@ -249,6 +253,126 @@ function checkWorkspaceRestResourceBootstrap(projectDir, packageName, phpPrefix)
249
253
  ? "REST resource PHP loader hook is present"
250
254
  : "Missing REST resource PHP require glob or init hook");
251
255
  }
256
+ function getWorkspaceAbilityRequiredFiles(ability) {
257
+ return Array.from(new Set([
258
+ ability.clientFile,
259
+ ability.configFile,
260
+ ability.dataFile,
261
+ ability.inputSchemaFile,
262
+ ability.outputSchemaFile,
263
+ ability.phpFile,
264
+ ability.typesFile,
265
+ ]));
266
+ }
267
+ function checkWorkspaceAbilityConfig(projectDir, ability) {
268
+ const configPath = path.join(projectDir, ability.configFile);
269
+ if (!fs.existsSync(configPath)) {
270
+ return createDoctorCheck(`Ability config ${ability.slug}`, "fail", `Missing ${ability.configFile}`);
271
+ }
272
+ try {
273
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
274
+ const abilityId = typeof config.abilityId === "string" ? config.abilityId.trim() : "";
275
+ const categorySlug = typeof config.category?.slug === "string"
276
+ ? config.category.slug.trim()
277
+ : "";
278
+ const hasValidAbilityId = /^[a-z0-9-]+\/[a-z0-9-]+$/u.test(abilityId);
279
+ const hasValidCategorySlug = /^[a-z0-9-]+$/u.test(categorySlug);
280
+ return createDoctorCheck(`Ability config ${ability.slug}`, hasValidAbilityId && hasValidCategorySlug ? "pass" : "fail", hasValidAbilityId && hasValidCategorySlug
281
+ ? `Ability id ${abilityId} in category ${categorySlug} is valid`
282
+ : "Ability config must define a valid abilityId (`namespace/ability-name`) and category.slug.");
283
+ }
284
+ catch (error) {
285
+ return createDoctorCheck(`Ability config ${ability.slug}`, "fail", error instanceof Error ? error.message : String(error));
286
+ }
287
+ }
288
+ function checkWorkspaceAbilityBootstrap(projectDir, packageName, phpPrefix) {
289
+ const packageBaseName = packageName.split("/").pop() ?? packageName;
290
+ const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
291
+ if (!fs.existsSync(bootstrapPath)) {
292
+ return createDoctorCheck("Ability bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
293
+ }
294
+ const source = fs.readFileSync(bootstrapPath, "utf8");
295
+ const loadFunctionName = `${phpPrefix}_load_workflow_abilities`;
296
+ const enqueueFunctionName = `${phpPrefix}_enqueue_workflow_abilities`;
297
+ const loadHook = `add_action( 'plugins_loaded', '${loadFunctionName}' );`;
298
+ const adminEnqueueHook = `add_action( 'admin_enqueue_scripts', '${enqueueFunctionName}' );`;
299
+ const editorEnqueueHook = `add_action( 'enqueue_block_editor_assets', '${enqueueFunctionName}' );`;
300
+ const hasLoaderHook = source.includes(loadHook);
301
+ const hasAdminEnqueueHook = source.includes(adminEnqueueHook);
302
+ const hasEditorEnqueueHook = source.includes(editorEnqueueHook);
303
+ const hasServerGlob = source.includes(WORKSPACE_ABILITY_GLOB);
304
+ const hasEditorScript = source.includes(WORKSPACE_ABILITY_EDITOR_SCRIPT);
305
+ const hasEditorAsset = source.includes(WORKSPACE_ABILITY_EDITOR_ASSET);
306
+ return createDoctorCheck("Ability bootstrap", hasLoaderHook &&
307
+ hasAdminEnqueueHook &&
308
+ hasEditorEnqueueHook &&
309
+ hasServerGlob &&
310
+ hasEditorScript &&
311
+ hasEditorAsset
312
+ ? "pass"
313
+ : "fail", hasLoaderHook &&
314
+ hasAdminEnqueueHook &&
315
+ hasEditorEnqueueHook &&
316
+ hasServerGlob &&
317
+ hasEditorScript &&
318
+ hasEditorAsset
319
+ ? "Ability loader and admin/editor client bootstrap hooks are present"
320
+ : "Missing ability loader hook or build/abilities script asset references");
321
+ }
322
+ function checkWorkspaceAbilityIndex(projectDir, abilities) {
323
+ const indexRelativePath = [
324
+ path.join("src", "abilities", "index.ts"),
325
+ path.join("src", "abilities", "index.js"),
326
+ ].find((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
327
+ if (!indexRelativePath) {
328
+ return createDoctorCheck("Abilities index", "fail", "Missing src/abilities/index.ts or src/abilities/index.js");
329
+ }
330
+ const indexPath = path.join(projectDir, indexRelativePath);
331
+ const source = fs.readFileSync(indexPath, "utf8");
332
+ const missingExports = abilities.filter((ability) => {
333
+ const exportPattern = new RegExp(`^\\s*export\\s+(?:\\*\\s+from|\\{[^}]+\\}\\s+from)\\s+['"\`]\\./${escapeRegex(ability.slug)}\\/client['"\`]`, "mu");
334
+ return !exportPattern.test(source);
335
+ });
336
+ return createDoctorCheck("Abilities index", missingExports.length === 0 ? "pass" : "fail", missingExports.length === 0
337
+ ? "Ability client helpers are aggregated"
338
+ : `Missing ability exports for: ${missingExports.map((entry) => entry.slug).join(", ")}`);
339
+ }
340
+ function getWorkspaceAiFeatureRequiredFiles(aiFeature) {
341
+ return Array.from(new Set([
342
+ aiFeature.aiSchemaFile,
343
+ aiFeature.apiFile,
344
+ path.join(path.dirname(aiFeature.typesFile), "api-schemas", "feature-request.schema.json"),
345
+ path.join(path.dirname(aiFeature.typesFile), "api-schemas", "feature-response.schema.json"),
346
+ path.join(path.dirname(aiFeature.typesFile), "api-schemas", "feature-result.schema.json"),
347
+ aiFeature.clientFile,
348
+ aiFeature.dataFile,
349
+ aiFeature.openApiFile,
350
+ aiFeature.phpFile,
351
+ aiFeature.typesFile,
352
+ aiFeature.validatorsFile,
353
+ ]));
354
+ }
355
+ function checkWorkspaceAiFeatureConfig(aiFeature) {
356
+ const hasNamespace = REST_RESOURCE_NAMESPACE_PATTERN.test(aiFeature.namespace);
357
+ return createDoctorCheck(`AI feature config ${aiFeature.slug}`, hasNamespace ? "pass" : "fail", hasNamespace
358
+ ? `AI feature namespace ${aiFeature.namespace} is valid`
359
+ : "AI feature namespace is invalid");
360
+ }
361
+ function checkWorkspaceAiFeatureBootstrap(projectDir, packageName, phpPrefix) {
362
+ const packageBaseName = packageName.split("/").pop() ?? packageName;
363
+ const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
364
+ if (!fs.existsSync(bootstrapPath)) {
365
+ return createDoctorCheck("AI feature bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
366
+ }
367
+ const source = fs.readFileSync(bootstrapPath, "utf8");
368
+ const registerFunctionName = `${phpPrefix}_register_ai_features`;
369
+ const registerHook = `add_action( 'init', '${registerFunctionName}', 20 );`;
370
+ const hasServerGlob = source.includes(WORKSPACE_AI_FEATURE_GLOB);
371
+ const hasRegisterHook = source.includes(registerHook);
372
+ return createDoctorCheck("AI feature bootstrap", hasServerGlob && hasRegisterHook ? "pass" : "fail", hasServerGlob && hasRegisterHook
373
+ ? "AI feature PHP loader hook is present"
374
+ : "Missing AI feature PHP require glob or init hook");
375
+ }
252
376
  function getWorkspaceEditorPluginRequiredFiles(editorPlugin) {
253
377
  const editorPluginDir = path.join("src", "editor-plugins", editorPlugin.slug);
254
378
  return Array.from(new Set([
@@ -377,7 +501,7 @@ export function getWorkspaceDoctorChecks(cwd) {
377
501
  checks.push(checkWorkspacePackageMetadata(workspace, workspacePackageJson));
378
502
  try {
379
503
  const inventory = readWorkspaceInventory(workspace.projectDir);
380
- checks.push(createDoctorCheck("Workspace inventory", "pass", `${inventory.blocks.length} block(s), ${inventory.variations.length} variation(s), ${inventory.patterns.length} pattern(s), ${inventory.bindingSources.length} binding source(s), ${inventory.restResources.length} REST resource(s), ${inventory.editorPlugins.length} editor plugin(s)`));
504
+ checks.push(createDoctorCheck("Workspace inventory", "pass", `${inventory.blocks.length} block(s), ${inventory.variations.length} variation(s), ${inventory.patterns.length} pattern(s), ${inventory.bindingSources.length} binding source(s), ${inventory.restResources.length} REST resource(s), ${inventory.abilities.length} ability scaffold(s), ${inventory.aiFeatures.length} AI feature(s), ${inventory.editorPlugins.length} editor plugin(s)`));
381
505
  for (const block of inventory.blocks) {
382
506
  checks.push(checkExistingFiles(workspace.projectDir, `Block ${block.slug}`, getWorkspaceBlockRequiredFiles(block)));
383
507
  checks.push(checkWorkspaceBlockMetadata(workspace.projectDir, workspace, block));
@@ -422,6 +546,21 @@ export function getWorkspaceDoctorChecks(cwd) {
422
546
  checks.push(checkWorkspaceRestResourceConfig(restResource));
423
547
  checks.push(checkExistingFiles(workspace.projectDir, `REST resource ${restResource.slug}`, getWorkspaceRestResourceRequiredFiles(restResource)));
424
548
  }
549
+ if (inventory.abilities.length > 0) {
550
+ checks.push(checkWorkspaceAbilityBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
551
+ checks.push(checkWorkspaceAbilityIndex(workspace.projectDir, inventory.abilities));
552
+ }
553
+ for (const ability of inventory.abilities) {
554
+ checks.push(checkWorkspaceAbilityConfig(workspace.projectDir, ability));
555
+ checks.push(checkExistingFiles(workspace.projectDir, `Ability ${ability.slug}`, getWorkspaceAbilityRequiredFiles(ability)));
556
+ }
557
+ if (inventory.aiFeatures.length > 0) {
558
+ checks.push(checkWorkspaceAiFeatureBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
559
+ }
560
+ for (const aiFeature of inventory.aiFeatures) {
561
+ checks.push(checkWorkspaceAiFeatureConfig(aiFeature));
562
+ checks.push(checkExistingFiles(workspace.projectDir, `AI feature ${aiFeature.slug}`, getWorkspaceAiFeatureRequiredFiles(aiFeature)));
563
+ }
425
564
  if (inventory.editorPlugins.length > 0) {
426
565
  checks.push(checkWorkspaceEditorPluginBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
427
566
  checks.push(checkWorkspaceEditorPluginIndex(workspace.projectDir, inventory.editorPlugins));
@@ -17,11 +17,14 @@ export function formatHelpText() {
17
17
  wp-typia create <project-dir> [--template persistence] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--alternate-render-targets <email,mjml,plain-text>] [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-migration-ui] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
18
18
  wp-typia create <project-dir> [--template compound] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--inner-blocks-preset <freeform|ordered|horizontal|locked-structure>] [--alternate-render-targets <email,mjml,plain-text>] [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-migration-ui] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
19
19
  wp-typia <project-dir> [create flags...]
20
+ wp-typia init [project-dir]
20
21
  wp-typia add block <name> --template <basic|interactivity|persistence|compound> [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--inner-blocks-preset <freeform|ordered|horizontal|locked-structure>] [--alternate-render-targets <email,mjml,plain-text>] [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>]
21
22
  wp-typia add variation <name> --block <block-slug>
22
23
  wp-typia add pattern <name>
23
24
  wp-typia add binding-source <name>
24
25
  wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <method[,method...]>]
26
+ wp-typia add ability <name>
27
+ wp-typia add ai-feature <name> [--namespace <vendor/v1>]
25
28
  wp-typia add editor-plugin <name> [--slot <PluginSidebar>]
26
29
  wp-typia add hooked-block <block-slug> --anchor <anchor-block-name> --position <before|after|firstChild|lastChild>
27
30
  wp-typia migrate <init|snapshot|diff|scaffold|verify|doctor|fixtures|fuzz> [...]
@@ -37,15 +40,19 @@ Package managers: ${PACKAGE_MANAGER_IDS.join(", ")}
37
40
  Notes:
38
41
  \`wp-typia create\` is the canonical scaffold command.
39
42
  \`wp-typia <project-dir>\` remains a backward-compatible alias to \`create\` when \`<project-dir>\` is the only positional argument.
43
+ \`wp-typia init\` is a preview-only retrofit planner for existing projects. It does not write files yet.
40
44
  Use \`--template workspace\` as shorthand for \`@wp-typia/create-workspace-template\`, the official empty workspace scaffold behind \`wp-typia add ...\`.
41
45
  \`query-loop\` is create-only. Use \`wp-typia create <project-dir> --template query-loop\`; \`wp-typia add block\` accepts only basic, interactivity, persistence, and compound families.
42
46
  \`add variation\` uses an existing workspace block from \`scripts/block-config.ts\`.
43
47
  \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
44
48
  \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
45
49
  \`add rest-resource\` scaffolds plugin-level TypeScript REST contracts under \`src/rest/\` and PHP route glue under \`inc/rest/\`.
50
+ \`add ability\` scaffolds typed workflow abilities under \`src/abilities/\` and server registration under \`inc/abilities/\`.
51
+ \`add ai-feature\` scaffolds server-owned AI feature endpoints under \`src/ai-features/\` and PHP route glue under \`inc/ai-features/\`.
46
52
  \`add editor-plugin\` scaffolds a document-level editor extension under \`src/editor-plugins/\`.
47
53
  \`add hooked-block\` patches an existing workspace block's \`block.json\` \`blockHooks\` metadata.
48
54
  \`wp-typia doctor\` always checks environment readiness and reports when it only ran environment-level diagnostics; official workspace roots also get inventory and source-tree drift checks.
55
+ \`wp-typia init\` previews the minimum sync/doctor/migration adoption layer for supported existing layouts before a future write mode exists.
49
56
  \`wp-typia migrate doctor --all\` checks migration target alignment, snapshots, fixtures, and generated migration artifacts.
50
57
  \`migrate\` is the canonical migration command; \`migrations\` is no longer supported.`;
51
58
  }
@@ -0,0 +1,49 @@
1
+ import { type PackageManagerId } from "./package-managers.js";
2
+ type InitPlanAction = "add" | "update";
3
+ type InitPlanStatus = "already-initialized" | "preview";
4
+ type InitPlanLayoutKind = "generated-project" | "multi-block" | "official-workspace" | "single-block" | "unsupported";
5
+ interface InitDependencyChange {
6
+ action: InitPlanAction;
7
+ currentValue?: string;
8
+ name: string;
9
+ requiredValue: string;
10
+ }
11
+ interface InitScriptChange {
12
+ action: InitPlanAction;
13
+ currentValue?: string;
14
+ name: string;
15
+ requiredValue: string;
16
+ }
17
+ interface InitPackageManagerFieldChange {
18
+ action: InitPlanAction;
19
+ currentValue?: string;
20
+ requiredValue: string;
21
+ }
22
+ interface InitFilePlan {
23
+ path: string;
24
+ purpose: string;
25
+ }
26
+ export interface RetrofitInitPlan {
27
+ commandMode: "preview-only";
28
+ detectedLayout: {
29
+ blockNames: string[];
30
+ description: string;
31
+ kind: InitPlanLayoutKind;
32
+ };
33
+ generatedArtifacts: string[];
34
+ nextSteps: string[];
35
+ notes: string[];
36
+ packageChanges: {
37
+ addDevDependencies: InitDependencyChange[];
38
+ packageManagerField?: InitPackageManagerFieldChange;
39
+ scripts: InitScriptChange[];
40
+ };
41
+ plannedFiles: InitFilePlan[];
42
+ packageManager: PackageManagerId;
43
+ projectDir: string;
44
+ projectName: string;
45
+ status: InitPlanStatus;
46
+ summary: string;
47
+ }
48
+ export declare function getInitPlan(projectDir: string): RetrofitInitPlan;
49
+ export {};
@@ -0,0 +1,354 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { discoverMigrationInitLayout } from "./migration-project.js";
4
+ import { formatAddDevDependenciesCommand, formatPackageExecCommand, formatRunScript, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
5
+ import { getPackageVersions } from "./package-versions.js";
6
+ import { parseWorkspacePackageManagerId, tryResolveWorkspaceProject, } from "./workspace-project.js";
7
+ const SUPPORTED_RETROFIT_LAYOUT_NOTE = "Supported retrofit layouts currently mirror the migration bootstrap detector: `src/block.json` + `src/types.ts` + `src/save.tsx`, legacy root `block.json` + `src/types.ts` + `src/save.tsx`, or multi-block `src/blocks/*/block.json` workspaces.";
8
+ const BASE_RETROFIT_SCRIPTS = {
9
+ sync: "tsx scripts/sync-project.ts",
10
+ "sync-types": "tsx scripts/sync-types-to-block-json.ts",
11
+ typecheck: "bun run sync --check && tsc --noEmit",
12
+ };
13
+ const BASE_RETROFIT_DEV_DEPENDENCIES = [
14
+ "@typia/unplugin",
15
+ "@wp-typia/block-runtime",
16
+ "@wp-typia/block-types",
17
+ "tsx",
18
+ "typescript",
19
+ "typia",
20
+ ];
21
+ function normalizeRelativePath(value) {
22
+ return value.replace(/\\/gu, "/");
23
+ }
24
+ function readProjectPackageJson(projectDir) {
25
+ const packageJsonPath = path.join(projectDir, "package.json");
26
+ if (!fs.existsSync(packageJsonPath)) {
27
+ return null;
28
+ }
29
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
30
+ }
31
+ function inferInitPackageManager(projectDir, packageJson) {
32
+ if (packageJson?.packageManager) {
33
+ return parseWorkspacePackageManagerId(packageJson.packageManager);
34
+ }
35
+ if (fs.existsSync(path.join(projectDir, "bun.lock")) ||
36
+ fs.existsSync(path.join(projectDir, "bun.lockb"))) {
37
+ return "bun";
38
+ }
39
+ if (fs.existsSync(path.join(projectDir, "pnpm-lock.yaml"))) {
40
+ return "pnpm";
41
+ }
42
+ if (fs.existsSync(path.join(projectDir, "yarn.lock")) ||
43
+ fs.existsSync(path.join(projectDir, ".yarnrc.yml"))) {
44
+ return "yarn";
45
+ }
46
+ return "npm";
47
+ }
48
+ function getWpTypiaCliSpecifier() {
49
+ const versions = getPackageVersions();
50
+ return versions.wpTypiaPackageExactVersion === "0.0.0"
51
+ ? "wp-typia"
52
+ : `wp-typia@${versions.wpTypiaPackageExactVersion}`;
53
+ }
54
+ function buildRequiredDevDependencyMap() {
55
+ const versions = getPackageVersions();
56
+ return {
57
+ "@typia/unplugin": "^12.0.1",
58
+ "@wp-typia/block-runtime": versions.blockRuntimePackageVersion,
59
+ "@wp-typia/block-types": versions.blockTypesPackageVersion,
60
+ tsx: "^4.20.5",
61
+ typescript: "^5.9.2",
62
+ typia: "^12.0.1",
63
+ };
64
+ }
65
+ function getExistingDependencyVersion(packageJson, name) {
66
+ return packageJson?.devDependencies?.[name] ?? packageJson?.dependencies?.[name];
67
+ }
68
+ function buildDependencyChanges(packageJson) {
69
+ const requiredDependencies = buildRequiredDevDependencyMap();
70
+ return BASE_RETROFIT_DEV_DEPENDENCIES.flatMap((name) => {
71
+ const requiredValue = requiredDependencies[name];
72
+ const currentValue = getExistingDependencyVersion(packageJson, name);
73
+ if (currentValue === requiredValue) {
74
+ return [];
75
+ }
76
+ return [
77
+ {
78
+ action: currentValue ? "update" : "add",
79
+ ...(currentValue ? { currentValue } : {}),
80
+ name,
81
+ requiredValue,
82
+ },
83
+ ];
84
+ });
85
+ }
86
+ function buildScriptChanges(packageJson, packageManager) {
87
+ const scripts = packageJson?.scripts ?? {};
88
+ return Object.entries(BASE_RETROFIT_SCRIPTS).flatMap(([name, commandSource]) => {
89
+ const requiredValue = transformPackageManagerText(commandSource, packageManager);
90
+ const currentValue = scripts[name];
91
+ if (currentValue === requiredValue) {
92
+ return [];
93
+ }
94
+ return [
95
+ {
96
+ action: typeof currentValue === "string" ? "update" : "add",
97
+ ...(typeof currentValue === "string" ? { currentValue } : {}),
98
+ name,
99
+ requiredValue,
100
+ },
101
+ ];
102
+ });
103
+ }
104
+ function buildPackageManagerFieldChange(packageJson, packageManager) {
105
+ if (packageManager === "npm") {
106
+ return undefined;
107
+ }
108
+ const requiredValue = getPackageManager(packageManager).packageManagerField;
109
+ const currentValue = packageJson?.packageManager;
110
+ if (currentValue === requiredValue) {
111
+ return undefined;
112
+ }
113
+ return {
114
+ action: typeof currentValue === "string" ? "update" : "add",
115
+ ...(typeof currentValue === "string" ? { currentValue } : {}),
116
+ requiredValue,
117
+ };
118
+ }
119
+ function buildGeneratedArtifactPaths(blockJsonFile, manifestFile) {
120
+ const manifestDir = path.dirname(manifestFile);
121
+ const artifactPaths = [
122
+ blockJsonFile,
123
+ manifestFile,
124
+ path.join(manifestDir, "typia.schema.json"),
125
+ path.join(manifestDir, "typia-validator.php"),
126
+ path.join(manifestDir, "typia.openapi.json"),
127
+ ];
128
+ return Array.from(new Set(artifactPaths.map((filePath) => normalizeRelativePath(filePath))));
129
+ }
130
+ function buildLayoutDetails(projectDir) {
131
+ try {
132
+ const discoveredLayout = discoverMigrationInitLayout(projectDir);
133
+ if (discoveredLayout.mode === "multi") {
134
+ return {
135
+ blockNames: discoveredLayout.blocks.map((block) => block.blockName),
136
+ description: `Detected a supported multi-block retrofit candidate (${discoveredLayout.blocks.length} targets).`,
137
+ generatedArtifacts: discoveredLayout.blocks.flatMap((block) => buildGeneratedArtifactPaths(block.blockJsonFile, block.manifestFile)),
138
+ kind: "multi-block",
139
+ notes: [
140
+ "Migration bootstrap can stay optional. Add it later with `wp-typia migrate init --current-migration-version v1` once the typed sync surface is in place.",
141
+ ],
142
+ };
143
+ }
144
+ return {
145
+ blockNames: [discoveredLayout.block.blockName],
146
+ description: "Detected a supported single-block retrofit candidate.",
147
+ generatedArtifacts: buildGeneratedArtifactPaths(discoveredLayout.block.blockJsonFile, discoveredLayout.block.manifestFile),
148
+ kind: "single-block",
149
+ notes: discoveredLayout.block.blockJsonFile === "block.json"
150
+ ? [
151
+ "Legacy root `block.json` layouts are still supported for retrofit planning, but newer scaffolds keep generated block metadata under `src/`.",
152
+ ]
153
+ : [],
154
+ };
155
+ }
156
+ catch (error) {
157
+ const message = error instanceof Error ? error.message : String(error);
158
+ return {
159
+ blockNames: [],
160
+ description: "No supported retrofit layout was auto-detected yet.",
161
+ generatedArtifacts: [],
162
+ kind: "unsupported",
163
+ notes: [message, SUPPORTED_RETROFIT_LAYOUT_NOTE],
164
+ };
165
+ }
166
+ }
167
+ function hasExistingWpTypiaProjectSurface(packageJson) {
168
+ const scripts = packageJson?.scripts ?? {};
169
+ const hasSyncSurface = typeof scripts.sync === "string" || typeof scripts["sync-types"] === "string";
170
+ const hasRuntimeDeps = typeof getExistingDependencyVersion(packageJson, "@wp-typia/block-runtime") ===
171
+ "string" &&
172
+ typeof getExistingDependencyVersion(packageJson, "@wp-typia/block-types") ===
173
+ "string";
174
+ return hasSyncSurface && hasRuntimeDeps;
175
+ }
176
+ function buildPlannedFiles(layoutKind) {
177
+ const plannedFiles = [
178
+ {
179
+ path: "scripts/sync-types-to-block-json.ts",
180
+ purpose: "Generate block.json and Typia metadata artifacts from the current TypeScript source of truth.",
181
+ },
182
+ {
183
+ path: "scripts/sync-project.ts",
184
+ purpose: "Provide one shared sync entrypoint that can grow into sync-rest or workspace-aware refresh steps later.",
185
+ },
186
+ ];
187
+ if (layoutKind === "unsupported") {
188
+ plannedFiles.unshift({
189
+ path: "package.json",
190
+ purpose: "Add the minimum wp-typia devDependencies and scripts once the project matches a supported retrofit layout.",
191
+ });
192
+ }
193
+ return plannedFiles;
194
+ }
195
+ function buildChangeSummary(changes) {
196
+ const lines = [];
197
+ for (const dependencyChange of changes.packageChanges.addDevDependencies) {
198
+ lines.push(`devDependency ${dependencyChange.action} ${dependencyChange.name} -> ${dependencyChange.requiredValue}`);
199
+ }
200
+ if (changes.packageChanges.packageManagerField) {
201
+ lines.push(`packageManager ${changes.packageChanges.packageManagerField.action} -> ${changes.packageChanges.packageManagerField.requiredValue}`);
202
+ }
203
+ for (const scriptChange of changes.packageChanges.scripts) {
204
+ lines.push(`script ${scriptChange.action} ${scriptChange.name} -> ${scriptChange.requiredValue}`);
205
+ }
206
+ for (const filePlan of changes.plannedFiles) {
207
+ lines.push(`file add ${filePlan.path} (${filePlan.purpose})`);
208
+ }
209
+ for (const artifactPath of changes.generatedArtifacts) {
210
+ lines.push(`generated artifact ${artifactPath}`);
211
+ }
212
+ return lines;
213
+ }
214
+ function buildNextSteps(options) {
215
+ const cliSpecifier = getWpTypiaCliSpecifier();
216
+ const syncTypesRun = formatRunScript(options.packageManager, "sync-types");
217
+ const syncRun = formatRunScript(options.packageManager, "sync");
218
+ const doctorRun = formatPackageExecCommand(options.packageManager, cliSpecifier, "doctor");
219
+ const migrationInitRun = formatPackageExecCommand(options.packageManager, cliSpecifier, "migrate init --current-migration-version v1");
220
+ const dependencyInstallCommand = formatAddDevDependenciesCommand(options.packageManager, buildRequiredDevDependencyMapEntries());
221
+ if (options.layoutKind === "unsupported") {
222
+ return [
223
+ "Align the project to one of the supported retrofit layouts listed below, then rerun `wp-typia init`.",
224
+ dependencyInstallCommand,
225
+ syncTypesRun,
226
+ doctorRun,
227
+ ];
228
+ }
229
+ const steps = [
230
+ ...(options.changeSummaryLines.length > 0
231
+ ? [
232
+ "Apply the planned package.json changes and helper files listed below.",
233
+ dependencyInstallCommand,
234
+ ]
235
+ : []),
236
+ syncRun,
237
+ doctorRun,
238
+ `Optional migration bootstrap: ${migrationInitRun}`,
239
+ ];
240
+ return steps;
241
+ }
242
+ function buildRequiredDevDependencyMapEntries() {
243
+ return Object.entries(buildRequiredDevDependencyMap()).map(([name, version]) => `${name}@${version.replace(/^workspace:/u, "")}`);
244
+ }
245
+ export function getInitPlan(projectDir) {
246
+ const resolvedProjectDir = path.resolve(projectDir);
247
+ const packageJson = readProjectPackageJson(resolvedProjectDir);
248
+ const packageManager = inferInitPackageManager(resolvedProjectDir, packageJson);
249
+ const workspace = tryResolveWorkspaceProject(resolvedProjectDir);
250
+ if (workspace) {
251
+ return {
252
+ commandMode: "preview-only",
253
+ detectedLayout: {
254
+ blockNames: [],
255
+ description: "Already an official wp-typia workspace.",
256
+ kind: "official-workspace",
257
+ },
258
+ generatedArtifacts: [],
259
+ nextSteps: [
260
+ formatPackageExecCommand(packageManager, getWpTypiaCliSpecifier(), "doctor"),
261
+ "Use `wp-typia add ...` to extend this workspace instead of rerunning init.",
262
+ ],
263
+ notes: [
264
+ "The official workspace template already owns inventory, doctor, and add-command workflows.",
265
+ ],
266
+ packageChanges: {
267
+ addDevDependencies: [],
268
+ scripts: [],
269
+ },
270
+ plannedFiles: [],
271
+ packageManager,
272
+ projectDir: workspace.projectDir,
273
+ projectName: workspace.packageName,
274
+ status: "already-initialized",
275
+ summary: "This directory is already an official wp-typia workspace. No retrofit bootstrap is needed.",
276
+ };
277
+ }
278
+ const projectName = typeof packageJson?.name === "string" && packageJson.name.length > 0
279
+ ? packageJson.name
280
+ : path.basename(resolvedProjectDir);
281
+ const layout = buildLayoutDetails(resolvedProjectDir);
282
+ const dependencyChanges = buildDependencyChanges(packageJson);
283
+ const scriptChanges = buildScriptChanges(packageJson, packageManager);
284
+ const packageManagerFieldChange = buildPackageManagerFieldChange(packageJson, packageManager);
285
+ const rawPlannedFiles = layout.kind === "generated-project" || layout.kind === "official-workspace"
286
+ ? []
287
+ : buildPlannedFiles(layout.kind);
288
+ const hasExistingSurface = hasExistingWpTypiaProjectSurface(packageJson);
289
+ const status = hasExistingSurface &&
290
+ dependencyChanges.length === 0 &&
291
+ scriptChanges.length === 0 &&
292
+ packageManagerFieldChange === undefined
293
+ ? "already-initialized"
294
+ : "preview";
295
+ const plannedFiles = status === "already-initialized" ? [] : rawPlannedFiles;
296
+ const detectedLayout = status === "already-initialized" && hasExistingSurface
297
+ ? {
298
+ blockNames: layout.blockNames,
299
+ description: layout.kind === "unsupported"
300
+ ? "Already exposes the minimum wp-typia sync surface."
301
+ : `Already exposes the minimum wp-typia sync surface for ${layout.kind === "multi-block" ? "a multi-block project" : "a single-block project"}.`,
302
+ kind: "generated-project",
303
+ }
304
+ : {
305
+ blockNames: layout.blockNames,
306
+ description: layout.description,
307
+ kind: layout.kind,
308
+ };
309
+ const plan = {
310
+ commandMode: "preview-only",
311
+ detectedLayout,
312
+ generatedArtifacts: status === "already-initialized" && detectedLayout.kind === "generated-project"
313
+ ? []
314
+ : layout.generatedArtifacts,
315
+ nextSteps: buildNextSteps({
316
+ changeSummaryLines: buildChangeSummary({
317
+ generatedArtifacts: status === "already-initialized" &&
318
+ detectedLayout.kind === "generated-project"
319
+ ? []
320
+ : layout.generatedArtifacts,
321
+ packageChanges: {
322
+ addDevDependencies: dependencyChanges,
323
+ ...(packageManagerFieldChange
324
+ ? { packageManagerField: packageManagerFieldChange }
325
+ : {}),
326
+ scripts: scriptChanges,
327
+ },
328
+ plannedFiles,
329
+ }),
330
+ layoutKind: detectedLayout.kind,
331
+ packageManager,
332
+ }),
333
+ notes: Array.from(new Set([
334
+ "Preview only: `wp-typia init` does not write files yet.",
335
+ ...layout.notes,
336
+ ])),
337
+ packageChanges: {
338
+ addDevDependencies: dependencyChanges,
339
+ ...(packageManagerFieldChange
340
+ ? { packageManagerField: packageManagerFieldChange }
341
+ : {}),
342
+ scripts: scriptChanges,
343
+ },
344
+ plannedFiles,
345
+ packageManager,
346
+ projectDir: resolvedProjectDir,
347
+ projectName,
348
+ status,
349
+ summary: status === "already-initialized"
350
+ ? "This project already exposes the minimum wp-typia retrofit surface."
351
+ : "This command previews the minimum wp-typia adoption layer for the current project without rewriting it into a full scaffold.",
352
+ };
353
+ return plan;
354
+ }
@@ -18,6 +18,7 @@ interface GetOptionalOnboardingOptions {
18
18
  }
19
19
  interface OptionalOnboardingGuidance {
20
20
  note: string;
21
+ shortNote: string;
21
22
  steps: string[];
22
23
  }
23
24
  export interface ScaffoldDryRunPlan {
@@ -7,7 +7,7 @@ import { parseCompoundInnerBlocksPreset } from "./compound-inner-blocks.js";
7
7
  import { formatInstallCommand, formatRunScript, } from "./package-managers.js";
8
8
  import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
9
9
  import { createManagedTempRoot } from "./temp-roots.js";
10
- import { getOptionalOnboardingNote, getOptionalOnboardingSteps, } from "./scaffold-onboarding.js";
10
+ import { getOptionalOnboardingNote, getOptionalOnboardingShortNote, getOptionalOnboardingSteps, } from "./scaffold-onboarding.js";
11
11
  import { formatNonEmptyTargetDirectoryError } from "./scaffold-bootstrap.js";
12
12
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
13
13
  import { resolveOptionalInteractiveExternalLayerId, } from "./external-layer-selection.js";
@@ -254,6 +254,10 @@ export function getOptionalOnboarding({ availableScripts, packageManager, templa
254
254
  availableScripts,
255
255
  compoundPersistenceEnabled,
256
256
  }),
257
+ shortNote: getOptionalOnboardingShortNote(packageManager, templateId, {
258
+ availableScripts,
259
+ compoundPersistenceEnabled,
260
+ }),
257
261
  steps: getOptionalOnboardingSteps(packageManager, templateId, {
258
262
  availableScripts,
259
263
  compoundPersistenceEnabled,