@wp-typia/project-tools 0.16.11 → 0.16.13

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 (119) hide show
  1. package/README.md +9 -3
  2. package/dist/runtime/block-generator-service-core.d.ts +8 -0
  3. package/dist/runtime/block-generator-service-core.js +274 -0
  4. package/dist/runtime/block-generator-service-spec.d.ts +104 -0
  5. package/dist/runtime/block-generator-service-spec.js +139 -0
  6. package/dist/runtime/block-generator-service.d.ts +2 -110
  7. package/dist/runtime/block-generator-service.js +2 -389
  8. package/dist/runtime/built-in-block-artifact-documents.d.ts +3 -0
  9. package/dist/runtime/built-in-block-artifact-documents.js +2 -0
  10. package/dist/runtime/built-in-block-artifact-types.d.ts +51 -0
  11. package/dist/runtime/built-in-block-artifact-types.js +304 -0
  12. package/dist/runtime/built-in-block-artifacts.js +4 -803
  13. package/dist/runtime/built-in-block-attribute-emitters.d.ts +71 -0
  14. package/dist/runtime/built-in-block-attribute-emitters.js +176 -0
  15. package/dist/runtime/built-in-block-attribute-specs.d.ts +38 -0
  16. package/dist/runtime/built-in-block-attribute-specs.js +358 -0
  17. package/dist/runtime/built-in-block-code-templates/basic.d.ts +4 -0
  18. package/dist/runtime/built-in-block-code-templates/basic.js +249 -0
  19. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +4 -0
  20. package/dist/runtime/built-in-block-code-templates/compound-child.js +138 -0
  21. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +6 -0
  22. package/dist/runtime/built-in-block-code-templates/compound-parent.js +227 -0
  23. package/dist/runtime/built-in-block-code-templates/compound-persistence.d.ts +4 -0
  24. package/dist/runtime/built-in-block-code-templates/compound-persistence.js +478 -0
  25. package/dist/runtime/built-in-block-code-templates/compound.d.ts +3 -0
  26. package/dist/runtime/built-in-block-code-templates/compound.js +3 -0
  27. package/dist/runtime/built-in-block-code-templates/interactivity.d.ts +5 -0
  28. package/dist/runtime/built-in-block-code-templates/interactivity.js +547 -0
  29. package/dist/runtime/built-in-block-code-templates/persistence.d.ts +5 -0
  30. package/dist/runtime/built-in-block-code-templates/persistence.js +550 -0
  31. package/dist/runtime/built-in-block-code-templates/shared.d.ts +16 -0
  32. package/dist/runtime/built-in-block-code-templates/shared.js +53 -0
  33. package/dist/runtime/built-in-block-code-templates.d.ts +5 -32
  34. package/dist/runtime/built-in-block-code-templates.js +5 -2230
  35. package/dist/runtime/cli-add-block-config.d.ts +6 -0
  36. package/dist/runtime/cli-add-block-config.js +143 -0
  37. package/dist/runtime/cli-add-block-legacy-validator.d.ts +4 -0
  38. package/dist/runtime/cli-add-block-legacy-validator.js +168 -0
  39. package/dist/runtime/cli-add-block.js +3 -301
  40. package/dist/runtime/cli-add-workspace-assets.d.ts +38 -0
  41. package/dist/runtime/cli-add-workspace-assets.js +399 -0
  42. package/dist/runtime/cli-add-workspace.d.ts +2 -38
  43. package/dist/runtime/cli-add-workspace.js +5 -396
  44. package/dist/runtime/cli-diagnostics.js +76 -4
  45. package/dist/runtime/cli-doctor-environment.d.ts +12 -0
  46. package/dist/runtime/cli-doctor-environment.js +123 -0
  47. package/dist/runtime/cli-doctor-workspace.d.ts +18 -0
  48. package/dist/runtime/cli-doctor-workspace.js +308 -0
  49. package/dist/runtime/cli-doctor.d.ts +4 -2
  50. package/dist/runtime/cli-doctor.js +10 -405
  51. package/dist/runtime/cli-help.js +1 -1
  52. package/dist/runtime/cli-scaffold.d.ts +8 -1
  53. package/dist/runtime/cli-scaffold.js +47 -4
  54. package/dist/runtime/migration-command-surface.d.ts +67 -0
  55. package/dist/runtime/migration-command-surface.js +189 -0
  56. package/dist/runtime/migration-diff-rename.d.ts +13 -0
  57. package/dist/runtime/migration-diff-rename.js +192 -0
  58. package/dist/runtime/migration-diff-transform.d.ts +14 -0
  59. package/dist/runtime/migration-diff-transform.js +105 -0
  60. package/dist/runtime/migration-diff.js +12 -297
  61. package/dist/runtime/migration-generated-artifacts.d.ts +3 -0
  62. package/dist/runtime/migration-generated-artifacts.js +41 -0
  63. package/dist/runtime/migration-maintenance-fixtures.d.ts +23 -0
  64. package/dist/runtime/migration-maintenance-fixtures.js +126 -0
  65. package/dist/runtime/migration-maintenance-verify.d.ts +26 -0
  66. package/dist/runtime/migration-maintenance-verify.js +262 -0
  67. package/dist/runtime/migration-maintenance.d.ts +2 -0
  68. package/dist/runtime/migration-maintenance.js +2 -0
  69. package/dist/runtime/migration-planning.d.ts +23 -0
  70. package/dist/runtime/migration-planning.js +131 -0
  71. package/dist/runtime/migration-project-config-source.d.ts +6 -0
  72. package/dist/runtime/migration-project-config-source.js +424 -0
  73. package/dist/runtime/migration-project-layout-discovery.d.ts +61 -0
  74. package/dist/runtime/migration-project-layout-discovery.js +337 -0
  75. package/dist/runtime/migration-project-layout-paths.d.ts +135 -0
  76. package/dist/runtime/migration-project-layout-paths.js +288 -0
  77. package/dist/runtime/migration-project-layout.d.ts +3 -0
  78. package/dist/runtime/migration-project-layout.js +2 -0
  79. package/dist/runtime/migration-project-workspace.d.ts +47 -0
  80. package/dist/runtime/migration-project-workspace.js +212 -0
  81. package/dist/runtime/migration-project.d.ts +4 -94
  82. package/dist/runtime/migration-project.js +3 -1101
  83. package/dist/runtime/migration-render-diff-rule.d.ts +5 -0
  84. package/dist/runtime/migration-render-diff-rule.js +120 -0
  85. package/dist/runtime/migration-render-execution.d.ts +3 -0
  86. package/dist/runtime/migration-render-execution.js +428 -0
  87. package/dist/runtime/migration-render-generated.d.ts +27 -0
  88. package/dist/runtime/migration-render-generated.js +230 -0
  89. package/dist/runtime/migration-render-support.d.ts +3 -0
  90. package/dist/runtime/migration-render-support.js +16 -0
  91. package/dist/runtime/migration-render.d.ts +3 -33
  92. package/dist/runtime/migration-render.js +3 -789
  93. package/dist/runtime/migrations.d.ts +24 -121
  94. package/dist/runtime/migrations.js +12 -700
  95. package/dist/runtime/scaffold-apply-utils.d.ts +9 -0
  96. package/dist/runtime/scaffold-apply-utils.js +27 -4
  97. package/dist/runtime/scaffold-bootstrap.d.ts +45 -0
  98. package/dist/runtime/scaffold-bootstrap.js +185 -0
  99. package/dist/runtime/scaffold-onboarding.d.ts +12 -0
  100. package/dist/runtime/scaffold-onboarding.js +42 -5
  101. package/dist/runtime/scaffold-package-manager-files.d.ts +35 -0
  102. package/dist/runtime/scaffold-package-manager-files.js +79 -0
  103. package/dist/runtime/scaffold.d.ts +1 -12
  104. package/dist/runtime/scaffold.js +11 -394
  105. package/dist/runtime/template-source-contracts.d.ts +81 -0
  106. package/dist/runtime/template-source-contracts.js +1 -0
  107. package/dist/runtime/template-source-external.d.ts +21 -0
  108. package/dist/runtime/template-source-external.js +184 -0
  109. package/dist/runtime/template-source-locators.d.ts +4 -0
  110. package/dist/runtime/template-source-locators.js +72 -0
  111. package/dist/runtime/template-source-normalization.d.ts +7 -0
  112. package/dist/runtime/template-source-normalization.js +53 -0
  113. package/dist/runtime/template-source-remote.d.ts +23 -0
  114. package/dist/runtime/template-source-remote.js +336 -0
  115. package/dist/runtime/template-source-seeds.d.ts +12 -0
  116. package/dist/runtime/template-source-seeds.js +243 -0
  117. package/dist/runtime/template-source.d.ts +4 -86
  118. package/dist/runtime/template-source.js +9 -828
  119. package/package.json +4 -4
@@ -1,417 +1,22 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { execFileSync } from "node:child_process";
5
- import { access, constants as fsConstants, rm, writeFile } from "node:fs/promises";
6
- import { parseScaffoldBlockMetadata } from "@wp-typia/block-runtime/blocks";
7
- import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, } from "./template-builtins.js";
8
- import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_SET, } from "./hooked-blocks.js";
9
- import { isBuiltInTemplateId, listTemplates } from "./template-registry.js";
10
- import { readWorkspaceInventory } from "./workspace-inventory.js";
11
- import { getInvalidWorkspaceProjectReason, parseWorkspacePackageJson, WORKSPACE_TEMPLATE_PACKAGE, tryResolveWorkspaceProject, } from "./workspace-project.js";
12
1
  import { createCliCommandError, formatDoctorCheckLine, formatDoctorSummaryLine, getDoctorFailureDetailLines, } from "./cli-diagnostics.js";
13
- const WORKSPACE_COLLECTION_IMPORT_LINE = "import '../../collection';";
14
- const WORKSPACE_COLLECTION_IMPORT_PATTERN = /^\s*import\s+["']\.\.\/\.\.\/collection["']\s*;?\s*$/m;
15
- const WORKSPACE_BINDING_SERVER_GLOB = "/src/bindings/*/server.php";
16
- const WORKSPACE_BINDING_EDITOR_SCRIPT = "build/bindings/index.js";
17
- const WORKSPACE_BINDING_EDITOR_ASSET = "build/bindings/index.asset.php";
18
- const WORKSPACE_GENERATED_BLOCK_ARTIFACTS = [
19
- "block.json",
20
- "typia.manifest.json",
21
- "typia.schema.json",
22
- "typia-validator.php",
23
- "typia.openapi.json",
24
- ];
25
- function readCommandVersion(command, args = ["--version"]) {
26
- try {
27
- return execFileSync(command, args, {
28
- encoding: "utf8",
29
- stdio: ["ignore", "pipe", "ignore"],
30
- }).trim();
31
- }
32
- catch {
33
- return null;
34
- }
35
- }
36
- function compareMajorVersion(actualVersion, minimumMajor) {
37
- const parsed = Number.parseInt(actualVersion.replace(/^v/, "").split(".")[0] ?? "", 10);
38
- return Number.isFinite(parsed) && parsed >= minimumMajor;
39
- }
40
- async function checkWritableDirectory(directory) {
41
- try {
42
- await access(directory, fsConstants.W_OK);
43
- return true;
44
- }
45
- catch {
46
- return false;
47
- }
48
- }
49
- async function checkTempDirectory() {
50
- const tempFile = path.join(os.tmpdir(), `wp-typia-${Date.now()}.tmp`);
51
- try {
52
- await writeFile(tempFile, "ok", "utf8");
53
- await rm(tempFile, { force: true });
54
- return true;
55
- }
56
- catch {
57
- return false;
58
- }
59
- }
60
- function createDoctorCheck(label, status, detail) {
61
- return { detail, label, status };
62
- }
63
- function getWorkspaceBootstrapRelativePath(packageName) {
64
- const packageBaseName = packageName.split("/").pop() ?? packageName;
65
- return `${packageBaseName}.php`;
66
- }
67
- function checkExistingFiles(projectDir, label, filePaths) {
68
- const missing = filePaths
69
- .filter((filePath) => typeof filePath === "string")
70
- .filter((filePath) => !fs.existsSync(path.join(projectDir, filePath)));
71
- return createDoctorCheck(label, missing.length === 0 ? "pass" : "fail", missing.length === 0 ? "All referenced files exist" : `Missing: ${missing.join(", ")}`);
72
- }
73
- function checkWorkspacePackageMetadata(workspace, packageJson) {
74
- const issues = [];
75
- const packageName = packageJson.name;
76
- const bootstrapRelativePath = getWorkspaceBootstrapRelativePath(typeof packageName === "string" && packageName.length > 0 ? packageName : workspace.packageName);
77
- const wpTypia = packageJson.wpTypia;
78
- if (typeof packageName !== "string" || packageName.length === 0) {
79
- issues.push("package.json must define a string name for workspace bootstrap resolution");
80
- }
81
- if (wpTypia?.projectType !== "workspace") {
82
- issues.push('wpTypia.projectType must be "workspace"');
83
- }
84
- if (wpTypia?.templatePackage !== WORKSPACE_TEMPLATE_PACKAGE) {
85
- issues.push(`wpTypia.templatePackage must be "${WORKSPACE_TEMPLATE_PACKAGE}"`);
86
- }
87
- if (wpTypia?.namespace !== workspace.workspace.namespace) {
88
- issues.push(`wpTypia.namespace must equal "${workspace.workspace.namespace}"`);
89
- }
90
- if (wpTypia?.textDomain !== workspace.workspace.textDomain) {
91
- issues.push(`wpTypia.textDomain must equal "${workspace.workspace.textDomain}"`);
92
- }
93
- if (wpTypia?.phpPrefix !== workspace.workspace.phpPrefix) {
94
- issues.push(`wpTypia.phpPrefix must equal "${workspace.workspace.phpPrefix}"`);
95
- }
96
- if (!fs.existsSync(path.join(workspace.projectDir, bootstrapRelativePath))) {
97
- issues.push(`Missing bootstrap file ${bootstrapRelativePath}`);
98
- }
99
- return createDoctorCheck("Workspace package metadata", issues.length === 0 ? "pass" : "fail", issues.length === 0
100
- ? `package.json metadata aligns with ${workspace.packageName} and ${bootstrapRelativePath}`
101
- : issues.join("; "));
102
- }
103
- function getWorkspaceBlockRequiredFiles(block) {
104
- const blockDir = path.join("src", "blocks", block.slug);
105
- return Array.from(new Set([
106
- block.typesFile,
107
- block.apiTypesFile,
108
- block.openApiFile,
109
- path.join(blockDir, "index.tsx"),
110
- ...WORKSPACE_GENERATED_BLOCK_ARTIFACTS.map((fileName) => path.join(blockDir, fileName)),
111
- ].filter((filePath) => typeof filePath === "string")));
112
- }
113
- function checkWorkspaceBlockMetadata(projectDir, workspace, block) {
114
- const blockJsonRelativePath = path.join("src", "blocks", block.slug, "block.json");
115
- const blockJsonPath = path.join(projectDir, blockJsonRelativePath);
116
- if (!fs.existsSync(blockJsonPath)) {
117
- return createDoctorCheck(`Block metadata ${block.slug}`, "fail", `Missing ${blockJsonRelativePath}`);
118
- }
119
- let blockJson;
120
- try {
121
- blockJson = parseScaffoldBlockMetadata(JSON.parse(fs.readFileSync(blockJsonPath, "utf8")));
122
- }
123
- catch (error) {
124
- return createDoctorCheck(`Block metadata ${block.slug}`, "fail", error instanceof Error ? error.message : String(error));
125
- }
126
- const expectedName = `${workspace.workspace.namespace}/${block.slug}`;
127
- const issues = [];
128
- if (blockJson.name !== expectedName) {
129
- issues.push(`block.json name must equal "${expectedName}"`);
130
- }
131
- if (blockJson.textdomain !== workspace.workspace.textDomain) {
132
- issues.push(`block.json textdomain must equal "${workspace.workspace.textDomain}"`);
133
- }
134
- return createDoctorCheck(`Block metadata ${block.slug}`, issues.length === 0 ? "pass" : "fail", issues.length === 0
135
- ? `block.json matches ${expectedName} and ${workspace.workspace.textDomain}`
136
- : issues.join("; "));
137
- }
138
- function checkWorkspaceBlockHooks(projectDir, blockSlug) {
139
- const blockJsonRelativePath = path.join("src", "blocks", blockSlug, "block.json");
140
- const blockJsonPath = path.join(projectDir, blockJsonRelativePath);
141
- if (!fs.existsSync(blockJsonPath)) {
142
- return createDoctorCheck(`Block hooks ${blockSlug}`, "fail", `Missing ${blockJsonRelativePath}`);
143
- }
144
- let blockJson;
145
- try {
146
- blockJson = parseScaffoldBlockMetadata(JSON.parse(fs.readFileSync(blockJsonPath, "utf8")));
147
- }
148
- catch (error) {
149
- return createDoctorCheck(`Block hooks ${blockSlug}`, "fail", error instanceof Error ? error.message : String(error));
150
- }
151
- const blockHooks = blockJson.blockHooks;
152
- if (blockHooks === undefined) {
153
- return createDoctorCheck(`Block hooks ${blockSlug}`, "pass", "No blockHooks metadata configured");
154
- }
155
- if (!blockHooks || typeof blockHooks !== "object" || Array.isArray(blockHooks)) {
156
- return createDoctorCheck(`Block hooks ${blockSlug}`, "fail", `${blockJsonRelativePath} must define blockHooks as an object when present.`);
157
- }
158
- const blockName = typeof blockJson.name === "string" && blockJson.name.trim().length > 0
159
- ? blockJson.name.trim()
160
- : null;
161
- const invalidEntries = Object.entries(blockHooks).filter(([anchor, position]) => (blockName !== null && anchor.trim() === blockName) ||
162
- anchor.trim().length === 0 ||
163
- anchor !== anchor.trim() ||
164
- !HOOKED_BLOCK_ANCHOR_PATTERN.test(anchor) ||
165
- typeof position !== "string" ||
166
- !HOOKED_BLOCK_POSITION_SET.has(position));
167
- return createDoctorCheck(`Block hooks ${blockSlug}`, invalidEntries.length === 0 ? "pass" : "fail", invalidEntries.length === 0
168
- ? `blockHooks metadata is valid${Object.keys(blockHooks).length > 0 ? ` (${Object.keys(blockHooks).join(", ")})` : ""}`
169
- : `Invalid blockHooks entries: ${invalidEntries
170
- .map(([anchor, position]) => `${anchor || "<empty>"} => ${String(position)}`)
171
- .join(", ")}`);
172
- }
173
- function checkWorkspaceBlockCollectionImport(projectDir, blockSlug) {
174
- const entryRelativePath = path.join("src", "blocks", blockSlug, "index.tsx");
175
- const entryPath = path.join(projectDir, entryRelativePath);
176
- if (!fs.existsSync(entryPath)) {
177
- return createDoctorCheck(`Block collection ${blockSlug}`, "fail", `Missing ${entryRelativePath}`);
178
- }
179
- const source = fs.readFileSync(entryPath, "utf8");
180
- const hasCollectionImport = WORKSPACE_COLLECTION_IMPORT_PATTERN.test(source);
181
- return createDoctorCheck(`Block collection ${blockSlug}`, hasCollectionImport ? "pass" : "fail", hasCollectionImport
182
- ? "Shared block collection import is present"
183
- : `Missing a shared collection import like ${WORKSPACE_COLLECTION_IMPORT_LINE}`);
184
- }
185
- function checkWorkspacePatternBootstrap(projectDir, packageName) {
186
- const packageBaseName = packageName.split("/").pop() ?? packageName;
187
- const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
188
- if (!fs.existsSync(bootstrapPath)) {
189
- return createDoctorCheck("Pattern bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
190
- }
191
- const source = fs.readFileSync(bootstrapPath, "utf8");
192
- const hasCategoryAnchor = source.includes("register_block_pattern_category");
193
- const hasPatternGlob = source.includes("/src/patterns/*.php");
194
- return createDoctorCheck("Pattern bootstrap", hasCategoryAnchor && hasPatternGlob ? "pass" : "fail", hasCategoryAnchor && hasPatternGlob
195
- ? "Pattern category and loader hooks are present"
196
- : "Missing pattern category registration or src/patterns loader hook");
197
- }
198
- function checkWorkspaceBindingBootstrap(projectDir, packageName) {
199
- const packageBaseName = packageName.split("/").pop() ?? packageName;
200
- const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
201
- if (!fs.existsSync(bootstrapPath)) {
202
- return createDoctorCheck("Binding bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
203
- }
204
- const source = fs.readFileSync(bootstrapPath, "utf8");
205
- const hasServerGlob = source.includes(WORKSPACE_BINDING_SERVER_GLOB);
206
- const hasEditorEnqueueHook = source.includes("enqueue_block_editor_assets");
207
- const hasEditorScript = source.includes(WORKSPACE_BINDING_EDITOR_SCRIPT);
208
- const hasEditorAsset = source.includes(WORKSPACE_BINDING_EDITOR_ASSET);
209
- return createDoctorCheck("Binding bootstrap", hasServerGlob && hasEditorEnqueueHook && hasEditorScript && hasEditorAsset ? "pass" : "fail", hasServerGlob && hasEditorEnqueueHook && hasEditorScript && hasEditorAsset
210
- ? "Binding source PHP and editor bootstrap hooks are present"
211
- : "Missing binding source PHP require glob or editor enqueue hook");
212
- }
213
- function checkWorkspaceBindingSourcesIndex(projectDir, bindingSources) {
214
- const indexRelativePath = [path.join("src", "bindings", "index.ts"), path.join("src", "bindings", "index.js")].find((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
215
- if (!indexRelativePath) {
216
- return createDoctorCheck("Binding sources index", "fail", "Missing src/bindings/index.ts or src/bindings/index.js");
217
- }
218
- const indexPath = path.join(projectDir, indexRelativePath);
219
- const source = fs.readFileSync(indexPath, "utf8");
220
- const missingImports = bindingSources.filter((bindingSource) => !source.includes(`./${bindingSource.slug}/editor`));
221
- return createDoctorCheck("Binding sources index", missingImports.length === 0 ? "pass" : "fail", missingImports.length === 0
222
- ? "Binding source editor registrations are aggregated"
223
- : `Missing editor imports for: ${missingImports.map((entry) => entry.slug).join(", ")}`);
224
- }
225
- function checkVariationEntrypoint(projectDir, blockSlug) {
226
- const entryPath = path.join(projectDir, "src", "blocks", blockSlug, "index.tsx");
227
- if (!fs.existsSync(entryPath)) {
228
- return createDoctorCheck(`Variation entrypoint ${blockSlug}`, "fail", `Missing ${path.relative(projectDir, entryPath)}`);
229
- }
230
- const source = fs.readFileSync(entryPath, "utf8");
231
- const hasImport = source.includes("./variations");
232
- const hasCall = source.includes("registerWorkspaceVariations()");
233
- return createDoctorCheck(`Variation entrypoint ${blockSlug}`, hasImport && hasCall ? "pass" : "fail", hasImport && hasCall
234
- ? "Variations registration hook is present"
235
- : "Missing ./variations import or registerWorkspaceVariations() call");
236
- }
237
- function checkMigrationWorkspaceHint(workspace, packageJson) {
238
- const hasMigrationScript = typeof packageJson.scripts?.["migration:doctor"] === "string";
239
- const migrationConfigRelativePath = path.join("src", "migrations", "config.ts");
240
- const hasMigrationConfig = fs.existsSync(path.join(workspace.projectDir, migrationConfigRelativePath));
241
- if (!hasMigrationScript && !hasMigrationConfig) {
242
- return null;
243
- }
244
- return createDoctorCheck("Migration workspace", hasMigrationConfig ? "pass" : "fail", hasMigrationConfig
245
- ? "Run `wp-typia migrate doctor --all` for migration target, snapshot, fixture, and generated artifact checks"
246
- : `Missing ${migrationConfigRelativePath} for the configured migration workspace`);
247
- }
2
+ import { getEnvironmentDoctorChecks } from "./cli-doctor-environment.js";
3
+ import { getWorkspaceDoctorChecks } from "./cli-doctor-workspace.js";
248
4
  /**
249
5
  * Collect all runtime doctor checks for the current environment.
250
6
  *
251
- * The returned array includes command availability checks, directory
252
- * writability checks, and built-in template asset checks in display order.
7
+ * The returned array concatenates environment checks (command availability,
8
+ * directory writability, and built-in template assets) followed by
9
+ * workspace checks (package metadata, inventory, blocks, variations,
10
+ * patterns, bindings, and optional migration hints) in display order.
253
11
  *
254
12
  * @param cwd Working directory to validate for writability.
255
13
  * @returns Ordered doctor check rows ready for CLI rendering.
256
14
  */
257
15
  export async function getDoctorChecks(cwd) {
258
- const checks = [];
259
- const bunVersion = readCommandVersion("bun");
260
- const nodeVersion = readCommandVersion("node");
261
- const gitVersion = readCommandVersion("git");
262
- const cwdWritable = await checkWritableDirectory(cwd);
263
- const tempWritable = await checkTempDirectory();
264
- checks.push({
265
- status: bunVersion && compareMajorVersion(bunVersion, 1) ? "pass" : "fail",
266
- label: "Bun",
267
- detail: bunVersion ? `Detected ${bunVersion}` : "Not available",
268
- });
269
- checks.push({
270
- status: nodeVersion && compareMajorVersion(nodeVersion, 20) ? "pass" : "fail",
271
- label: "Node",
272
- detail: nodeVersion ? `Detected ${nodeVersion}` : "Not available",
273
- });
274
- checks.push({
275
- status: gitVersion ? "pass" : "fail",
276
- label: "git",
277
- detail: gitVersion ?? "Not available",
278
- });
279
- checks.push({
280
- status: cwdWritable ? "pass" : "fail",
281
- label: "Current directory",
282
- detail: cwdWritable ? "Writable" : "Not writable",
283
- });
284
- checks.push({
285
- status: tempWritable ? "pass" : "fail",
286
- label: "Temp directory",
287
- detail: tempWritable ? "Writable" : "Not writable",
288
- });
289
- for (const template of listTemplates()) {
290
- if (!isBuiltInTemplateId(template.id)) {
291
- const templateDirExists = fs.existsSync(template.templateDir);
292
- const hasAssets = templateDirExists &&
293
- fs.existsSync(path.join(template.templateDir, "package.json.mustache"));
294
- checks.push({
295
- status: !templateDirExists || hasAssets ? "pass" : "fail",
296
- label: `Template ${template.id}`,
297
- detail: !templateDirExists
298
- ? "External template metadata only; local overlay package is not installed."
299
- : hasAssets
300
- ? template.templateDir
301
- : "Missing core template assets",
302
- });
303
- continue;
304
- }
305
- const builtInTemplateId = template.id;
306
- const layerDirs = builtInTemplateId === "persistence"
307
- ? Array.from(new Set([
308
- ...getBuiltInTemplateLayerDirs(builtInTemplateId, { persistencePolicy: "authenticated" }),
309
- ...getBuiltInTemplateLayerDirs(builtInTemplateId, { persistencePolicy: "public" }),
310
- ]))
311
- : builtInTemplateId === "compound"
312
- ? Array.from(new Set([
313
- ...getBuiltInTemplateLayerDirs(builtInTemplateId),
314
- ...getBuiltInTemplateLayerDirs(builtInTemplateId, {
315
- persistenceEnabled: true,
316
- persistencePolicy: "authenticated",
317
- }),
318
- ...getBuiltInTemplateLayerDirs(builtInTemplateId, {
319
- persistenceEnabled: true,
320
- persistencePolicy: "public",
321
- }),
322
- ]))
323
- : getBuiltInTemplateLayerDirs(builtInTemplateId);
324
- const missingRequiredLayer = layerDirs.some((layerDir) => !fs.existsSync(layerDir) &&
325
- !isOmittableBuiltInTemplateLayerDir(builtInTemplateId, layerDir));
326
- const existingLayerDirs = layerDirs.filter((layerDir) => fs.existsSync(layerDir));
327
- const hasAssets = !missingRequiredLayer &&
328
- existingLayerDirs.some((layerDir) => fs.existsSync(path.join(layerDir, "package.json.mustache"))) &&
329
- existingLayerDirs.some((layerDir) => fs.existsSync(path.join(layerDir, "src")));
330
- checks.push({
331
- status: hasAssets ? "pass" : "fail",
332
- label: `Template ${template.id}`,
333
- detail: hasAssets
334
- ? existingLayerDirs.join(" + ")
335
- : "Missing core template assets",
336
- });
337
- }
338
- let workspace = null;
339
- let invalidWorkspaceReason = null;
340
- try {
341
- invalidWorkspaceReason = getInvalidWorkspaceProjectReason(cwd);
342
- workspace = tryResolveWorkspaceProject(cwd);
343
- }
344
- catch (error) {
345
- checks.push(createDoctorCheck("Workspace package metadata", "fail", error instanceof Error ? error.message : String(error)));
346
- return checks;
347
- }
348
- if (!workspace) {
349
- if (invalidWorkspaceReason) {
350
- checks.push(createDoctorCheck("Workspace package metadata", "fail", invalidWorkspaceReason));
351
- }
352
- return checks;
353
- }
354
- checks.push(createDoctorCheck("Workspace marker", "pass", `Official workspace detected for ${workspace.workspace.namespace}`));
355
- let workspacePackageJson;
356
- try {
357
- workspacePackageJson = parseWorkspacePackageJson(workspace.projectDir);
358
- }
359
- catch (error) {
360
- checks.push(createDoctorCheck("Workspace package metadata", "fail", error instanceof Error ? error.message : String(error)));
361
- return checks;
362
- }
363
- checks.push(checkWorkspacePackageMetadata(workspace, workspacePackageJson));
364
- try {
365
- const inventory = readWorkspaceInventory(workspace.projectDir);
366
- 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)`));
367
- for (const block of inventory.blocks) {
368
- checks.push(checkExistingFiles(workspace.projectDir, `Block ${block.slug}`, [
369
- ...getWorkspaceBlockRequiredFiles(block),
370
- ]));
371
- checks.push(checkWorkspaceBlockMetadata(workspace.projectDir, workspace, block));
372
- checks.push(checkWorkspaceBlockHooks(workspace.projectDir, block.slug));
373
- checks.push(checkWorkspaceBlockCollectionImport(workspace.projectDir, block.slug));
374
- }
375
- const registeredBlockSlugs = new Set(inventory.blocks.map((block) => block.slug));
376
- const variationTargetBlocks = new Set();
377
- for (const variation of inventory.variations) {
378
- if (!registeredBlockSlugs.has(variation.block)) {
379
- checks.push(createDoctorCheck(`Variation ${variation.block}/${variation.slug}`, "fail", `Variation references unknown block "${variation.block}"`));
380
- continue;
381
- }
382
- variationTargetBlocks.add(variation.block);
383
- checks.push(checkExistingFiles(workspace.projectDir, `Variation ${variation.block}/${variation.slug}`, [variation.file]));
384
- }
385
- for (const blockSlug of variationTargetBlocks) {
386
- checks.push(checkVariationEntrypoint(workspace.projectDir, blockSlug));
387
- }
388
- const shouldCheckPatternBootstrap = inventory.patterns.length > 0 ||
389
- fs.existsSync(path.join(workspace.projectDir, "src", "patterns"));
390
- if (shouldCheckPatternBootstrap) {
391
- checks.push(checkWorkspacePatternBootstrap(workspace.projectDir, workspace.packageName));
392
- }
393
- for (const pattern of inventory.patterns) {
394
- checks.push(checkExistingFiles(workspace.projectDir, `Pattern ${pattern.slug}`, [pattern.file]));
395
- }
396
- if (inventory.bindingSources.length > 0) {
397
- checks.push(checkWorkspaceBindingBootstrap(workspace.projectDir, workspace.packageName));
398
- checks.push(checkWorkspaceBindingSourcesIndex(workspace.projectDir, inventory.bindingSources));
399
- }
400
- for (const bindingSource of inventory.bindingSources) {
401
- checks.push(checkExistingFiles(workspace.projectDir, `Binding source ${bindingSource.slug}`, [
402
- bindingSource.serverFile,
403
- bindingSource.editorFile,
404
- ]));
405
- }
406
- const migrationWorkspaceCheck = checkMigrationWorkspaceHint(workspace, workspacePackageJson);
407
- if (migrationWorkspaceCheck) {
408
- checks.push(migrationWorkspaceCheck);
409
- }
410
- }
411
- catch (error) {
412
- checks.push(createDoctorCheck("Workspace inventory", "fail", error instanceof Error ? error.message : String(error)));
413
- }
414
- return checks;
16
+ return [
17
+ ...(await getEnvironmentDoctorChecks(cwd)),
18
+ ...getWorkspaceDoctorChecks(cwd),
19
+ ];
415
20
  }
416
21
  /**
417
22
  * Run doctor checks, render each line, and fail when any check does not pass.
@@ -39,7 +39,7 @@ Notes:
39
39
  \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
40
40
  \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
41
41
  \`add hooked-block\` patches an existing workspace block's \`block.json\` \`blockHooks\` metadata.
42
- \`wp-typia doctor\` checks environment readiness plus workspace inventory and source-tree drift.
42
+ \`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.
43
43
  \`wp-typia migrate doctor --all\` checks migration target alignment, snapshots, fixtures, and generated migration artifacts.
44
44
  \`migrate\` is the canonical migration command; \`migrations\` is no longer supported.`;
45
45
  }
@@ -78,7 +78,14 @@ export declare function runScaffoldFlow({ projectInput, cwd, templateId, dataSto
78
78
  projectDir: string;
79
79
  projectInput: string;
80
80
  packageManager: PackageManagerId;
81
- result: import("./scaffold.js").ScaffoldProjectResult;
82
81
  nextSteps: string[];
82
+ result: {
83
+ warnings: string[];
84
+ packageManager: PackageManagerId;
85
+ projectDir: string;
86
+ selectedVariant: string | null;
87
+ templateId: string;
88
+ variables: import("./scaffold.js").ScaffoldTemplateVariables;
89
+ };
83
90
  }>;
84
91
  export {};
@@ -6,6 +6,29 @@ import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
6
6
  import { getOptionalOnboardingNote, getOptionalOnboardingSteps, } from "./scaffold-onboarding.js";
7
7
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
8
8
  import { resolveOptionalInteractiveExternalLayerId, } from "./external-layer-selection.js";
9
+ function validateCreateProjectInput(projectInput) {
10
+ const normalizedProjectInput = projectInput.trim();
11
+ if (normalizedProjectInput.length === 0) {
12
+ throw new Error("Project directory is required. Usage: wp-typia create <project-dir> (or wp-typia <project-dir> when <project-dir> is the only positional argument).");
13
+ }
14
+ const normalizedProjectPath = path.normalize(normalizedProjectInput).replace(/[\\/]+$/u, "") ||
15
+ path.normalize(normalizedProjectInput);
16
+ if (normalizedProjectPath === "." || normalizedProjectPath === "..") {
17
+ throw new Error("`wp-typia create` requires a new project directory. Use an explicit child directory instead of `.` or `..`.");
18
+ }
19
+ }
20
+ function collectProjectDirectoryWarnings(projectDir) {
21
+ const warnings = [];
22
+ const projectName = path.basename(projectDir);
23
+ if (/\s/u.test(projectName)) {
24
+ warnings.push(`Project directory "${projectName}" contains spaces. The generated next-step commands will be quoted, but a simple kebab-case directory name is usually easier to use with shells and downstream tooling.`);
25
+ }
26
+ const shellSensitiveCharacters = Array.from(new Set(projectName.match(/[^A-Za-z0-9._ -]/gu) ?? []));
27
+ if (shellSensitiveCharacters.length > 0) {
28
+ warnings.push(`Project directory "${projectName}" contains shell-sensitive characters (${shellSensitiveCharacters.join(", ")}). Prefer letters, numbers, ".", "_" and "-" when possible.`);
29
+ }
30
+ return warnings;
31
+ }
9
32
  function templateUsesPersistenceSettings(templateId, options) {
10
33
  if (templateId === "persistence") {
11
34
  return true;
@@ -15,6 +38,19 @@ function templateUsesPersistenceSettings(templateId, options) {
15
38
  }
16
39
  return Boolean(options.dataStorageMode || options.persistencePolicy);
17
40
  }
41
+ function templateSupportsPersistenceFlags(templateId) {
42
+ return templateId === "persistence" || templateId === "compound";
43
+ }
44
+ function validateCreateFlagContract(options) {
45
+ const { dataStorageMode, persistencePolicy, templateId, variant } = options;
46
+ if ((dataStorageMode || persistencePolicy) &&
47
+ !templateSupportsPersistenceFlags(templateId)) {
48
+ throw new Error("`--data-storage` and `--persistence-policy` are supported only for `wp-typia create --template persistence` or `--template compound`.");
49
+ }
50
+ if (variant && isBuiltInTemplateId(templateId)) {
51
+ throw new Error(`--variant is only supported for official external template configs. Received variant "${variant}" for built-in template "${templateId}".`);
52
+ }
53
+ }
18
54
  function parseSelectableValue(label, value, isValue, allowedValues) {
19
55
  if (isValue(value)) {
20
56
  return value;
@@ -107,15 +143,19 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
107
143
  externalLayerSource.trim().length > 0
108
144
  ? externalLayerSource.trim()
109
145
  : undefined;
110
- if (!projectInput) {
111
- throw new Error("Project directory is required. Usage: wp-typia create <project-dir> (or wp-typia <project-dir> when <project-dir> is the only positional argument).");
112
- }
146
+ validateCreateProjectInput(projectInput);
113
147
  const resolvedTemplateId = await resolveTemplateId({
114
148
  templateId,
115
149
  yes,
116
150
  isInteractive,
117
151
  selectTemplate,
118
152
  });
153
+ validateCreateFlagContract({
154
+ dataStorageMode,
155
+ persistencePolicy,
156
+ templateId: resolvedTemplateId,
157
+ variant,
158
+ });
119
159
  const resolvedExternalLayerSelection = isBuiltInTemplateId(resolvedTemplateId) && isInteractive
120
160
  ? await resolveOptionalInteractiveExternalLayerId({
121
161
  callerCwd: cwd,
@@ -237,7 +277,6 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
237
277
  projectDir,
238
278
  projectInput,
239
279
  packageManager: resolvedPackageManager,
240
- result,
241
280
  nextSteps: getNextSteps({
242
281
  projectInput,
243
282
  projectDir,
@@ -245,6 +284,10 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
245
284
  noInstall,
246
285
  templateId: resolvedTemplateId,
247
286
  }),
287
+ result: {
288
+ ...result,
289
+ warnings: [...result.warnings, ...collectProjectDirectoryWarnings(projectDir)],
290
+ },
248
291
  };
249
292
  }
250
293
  finally {
@@ -0,0 +1,67 @@
1
+ import type { ReadlinePrompt } from './cli-prompt.js';
2
+ import type { ParsedMigrationArgs, RenderLine } from './migration-types.js';
3
+ export type CommandRenderOptions = {
4
+ prompt?: ReadlinePrompt;
5
+ renderLine?: RenderLine;
6
+ };
7
+ export type DiffLikeOptions = {
8
+ fromMigrationVersion?: string;
9
+ renderLine?: RenderLine;
10
+ toMigrationVersion?: string;
11
+ };
12
+ export type VerifyOptions = {
13
+ all?: boolean;
14
+ fromMigrationVersion?: string;
15
+ renderLine?: RenderLine;
16
+ };
17
+ export type FixturesOptions = {
18
+ all?: boolean;
19
+ confirmOverwrite?: ((message: string) => boolean) | undefined;
20
+ force?: boolean;
21
+ fromMigrationVersion?: string;
22
+ isInteractive?: boolean;
23
+ renderLine?: RenderLine;
24
+ toMigrationVersion?: string;
25
+ };
26
+ export type FuzzOptions = {
27
+ all?: boolean;
28
+ fromMigrationVersion?: string;
29
+ iterations?: number;
30
+ renderLine?: RenderLine;
31
+ seed?: number;
32
+ };
33
+ export type WizardOptions = CommandRenderOptions & {
34
+ isInteractive?: boolean;
35
+ };
36
+ /**
37
+ * Returns the formatted help text for migration CLI commands and flags.
38
+ *
39
+ * @returns Multi-line usage text for the `wp-typia migrate` command surface.
40
+ */
41
+ export declare function formatMigrationHelpText(): string;
42
+ /**
43
+ * Parses migration CLI arguments into a structured command payload.
44
+ *
45
+ * @param argv Command-line arguments that follow the `migrate` subcommand.
46
+ * @returns Parsed migration command and normalized flags for runtime dispatch.
47
+ * @throws Error When no arguments are provided, an unknown flag is encountered, or legacy semver flags are used.
48
+ */
49
+ export declare function parseMigrationArgs(argv: string[]): ParsedMigrationArgs;
50
+ /**
51
+ * Parse an optional positive integer flag value.
52
+ *
53
+ * @param value Raw CLI flag value, or `undefined` when the flag was omitted.
54
+ * @param label Human-readable flag label used in validation error messages.
55
+ * @returns The parsed integer when provided, otherwise `undefined`.
56
+ * @throws Error When the value is not a base-10 integer greater than zero.
57
+ */
58
+ export declare function parsePositiveInteger(value: string | undefined, label: string): number | undefined;
59
+ /**
60
+ * Parse an optional non-negative integer flag value.
61
+ *
62
+ * @param value Raw CLI flag value, or `undefined` when the flag was omitted.
63
+ * @param label Human-readable flag label used in validation error messages.
64
+ * @returns The parsed integer when provided, otherwise `undefined`.
65
+ * @throws Error When the value is not a base-10 integer greater than or equal to zero.
66
+ */
67
+ export declare function parseNonNegativeInteger(value: string | undefined, label: string): number | undefined;