@wp-typia/project-tools 0.16.14 → 0.18.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 (79) hide show
  1. package/dist/runtime/block-generator-service-core.d.ts +1 -1
  2. package/dist/runtime/block-generator-service-core.js +2 -1
  3. package/dist/runtime/block-generator-service-spec.d.ts +8 -1
  4. package/dist/runtime/block-generator-service-spec.js +27 -0
  5. package/dist/runtime/built-in-block-artifacts.js +1 -0
  6. package/dist/runtime/built-in-block-code-artifacts.js +14 -1
  7. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +2 -2
  8. package/dist/runtime/built-in-block-code-templates/compound-child.js +30 -2
  9. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +1 -1
  10. package/dist/runtime/built-in-block-code-templates/compound-parent.js +139 -19
  11. package/dist/runtime/built-in-block-code-templates/query-loop.d.ts +1 -0
  12. package/dist/runtime/built-in-block-code-templates/query-loop.js +70 -0
  13. package/dist/runtime/built-in-block-code-templates.d.ts +1 -0
  14. package/dist/runtime/built-in-block-code-templates.js +1 -0
  15. package/dist/runtime/built-in-block-non-ts-artifacts.js +2 -0
  16. package/dist/runtime/cli-add-block.d.ts +2 -1
  17. package/dist/runtime/cli-add-block.js +19 -1
  18. package/dist/runtime/cli-add-shared.d.ts +55 -1
  19. package/dist/runtime/cli-add-shared.js +101 -2
  20. package/dist/runtime/cli-add-workspace-assets.d.ts +21 -1
  21. package/dist/runtime/cli-add-workspace-assets.js +417 -1
  22. package/dist/runtime/cli-add-workspace-rest.d.ts +14 -0
  23. package/dist/runtime/cli-add-workspace-rest.js +1060 -0
  24. package/dist/runtime/cli-add-workspace.d.ts +10 -1
  25. package/dist/runtime/cli-add-workspace.js +10 -1
  26. package/dist/runtime/cli-add.d.ts +3 -3
  27. package/dist/runtime/cli-add.js +2 -2
  28. package/dist/runtime/cli-core.d.ts +3 -1
  29. package/dist/runtime/cli-core.js +2 -1
  30. package/dist/runtime/cli-doctor-workspace.js +135 -1
  31. package/dist/runtime/cli-help.js +10 -5
  32. package/dist/runtime/cli-scaffold.d.ts +11 -2
  33. package/dist/runtime/cli-scaffold.js +137 -36
  34. package/dist/runtime/cli-templates.d.ts +4 -4
  35. package/dist/runtime/cli-templates.js +79 -39
  36. package/dist/runtime/index.d.ts +4 -3
  37. package/dist/runtime/index.js +3 -2
  38. package/dist/runtime/local-dev-presets.js +27 -12
  39. package/dist/runtime/rest-resource-artifacts.d.ts +35 -0
  40. package/dist/runtime/rest-resource-artifacts.js +158 -0
  41. package/dist/runtime/scaffold-answer-resolution.d.ts +1 -1
  42. package/dist/runtime/scaffold-answer-resolution.js +94 -3
  43. package/dist/runtime/scaffold-apply-utils.d.ts +4 -3
  44. package/dist/runtime/scaffold-apply-utils.js +34 -17
  45. package/dist/runtime/scaffold-bootstrap.d.ts +15 -0
  46. package/dist/runtime/scaffold-bootstrap.js +29 -7
  47. package/dist/runtime/scaffold-documents.js +9 -9
  48. package/dist/runtime/scaffold-onboarding.js +17 -1
  49. package/dist/runtime/scaffold-package-manager-files.js +6 -1
  50. package/dist/runtime/scaffold-template-variables.js +6 -0
  51. package/dist/runtime/scaffold.d.ts +15 -1
  52. package/dist/runtime/scaffold.js +50 -8
  53. package/dist/runtime/template-defaults.d.ts +7 -0
  54. package/dist/runtime/template-defaults.js +4 -0
  55. package/dist/runtime/template-registry.d.ts +1 -1
  56. package/dist/runtime/template-registry.js +14 -1
  57. package/dist/runtime/template-render.d.ts +5 -2
  58. package/dist/runtime/template-render.js +9 -3
  59. package/dist/runtime/template-source-contracts.d.ts +11 -0
  60. package/dist/runtime/template-source-external.d.ts +1 -1
  61. package/dist/runtime/template-source-external.js +45 -13
  62. package/dist/runtime/template-source-normalization.d.ts +1 -1
  63. package/dist/runtime/template-source-normalization.js +5 -1
  64. package/dist/runtime/template-source-remote.d.ts +5 -0
  65. package/dist/runtime/template-source-remote.js +33 -0
  66. package/dist/runtime/template-source.js +30 -1
  67. package/dist/runtime/workspace-inventory.d.ts +43 -1
  68. package/dist/runtime/workspace-inventory.js +132 -1
  69. package/dist/runtime/workspace-project.d.ts +1 -1
  70. package/dist/runtime/workspace-project.js +2 -2
  71. package/package.json +3 -3
  72. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +428 -49
  73. package/templates/query-loop/inc/query-runtime.php.mustache +85 -0
  74. package/templates/query-loop/package.json.mustache +32 -0
  75. package/templates/query-loop/src/patterns/grid.php.mustache +49 -0
  76. package/templates/query-loop/src/patterns/list.php.mustache +48 -0
  77. package/templates/query-loop/src/query-extension.ts.mustache +41 -0
  78. package/templates/query-loop/webpack.config.js.mustache +16 -0
  79. package/templates/query-loop/{{slugKebabCase}}.php.mustache +84 -0
@@ -1,6 +1,15 @@
1
1
  import type { HookedBlockPositionId } from "./hooked-blocks.js";
2
2
  import { type RunAddHookedBlockCommandOptions, type RunAddVariationCommandOptions } from "./cli-add-shared.js";
3
- export { runAddBindingSourceCommand, runAddPatternCommand, } from "./cli-add-workspace-assets.js";
3
+ /**
4
+ * Re-export focused workspace asset scaffold commands from the companion
5
+ * `cli-add-workspace-assets` module.
6
+ */
7
+ export { runAddEditorPluginCommand, runAddBindingSourceCommand, runAddPatternCommand, } from "./cli-add-workspace-assets.js";
8
+ /**
9
+ * Re-export the plugin-level REST resource scaffold workflow from the focused
10
+ * rest-resource runtime helper module.
11
+ */
12
+ export { runAddRestResourceCommand } from "./cli-add-workspace-rest.js";
4
13
  /**
5
14
  * Add one variation entry to an existing workspace block.
6
15
  *
@@ -119,7 +119,16 @@ async function writeVariationRegistry(projectDir, blockSlug, variationSlug) {
119
119
  const nextVariationSlugs = Array.from(new Set([...existingVariationSlugs, variationSlug])).sort();
120
120
  await fsp.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
121
121
  }
122
- export { runAddBindingSourceCommand, runAddPatternCommand, } from "./cli-add-workspace-assets.js";
122
+ /**
123
+ * Re-export focused workspace asset scaffold commands from the companion
124
+ * `cli-add-workspace-assets` module.
125
+ */
126
+ export { runAddEditorPluginCommand, runAddBindingSourceCommand, runAddPatternCommand, } from "./cli-add-workspace-assets.js";
127
+ /**
128
+ * Re-export the plugin-level REST resource scaffold workflow from the focused
129
+ * rest-resource runtime helper module.
130
+ */
131
+ export { runAddRestResourceCommand } from "./cli-add-workspace-rest.js";
123
132
  /**
124
133
  * Add one variation entry to an existing workspace block.
125
134
  *
@@ -7,8 +7,8 @@
7
7
  * - `cli-add-block` for built-in block scaffolding
8
8
  * - `cli-add-workspace` for workspace mutation commands
9
9
  */
10
- export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, formatAddHelpText, } from "./cli-add-shared.js";
11
- export type { AddBlockTemplateId, AddKindId, } from "./cli-add-shared.js";
10
+ export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, } from "./cli-add-shared.js";
11
+ export type { AddBlockTemplateId, AddKindId, EditorPluginSlotId, } from "./cli-add-shared.js";
12
12
  export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
13
- export { runAddBindingSourceCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
13
+ export { runAddBindingSourceCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
14
14
  export { getWorkspaceBlockSelectOptions } from "./workspace-inventory.js";
@@ -7,7 +7,7 @@
7
7
  * - `cli-add-block` for built-in block scaffolding
8
8
  * - `cli-add-workspace` for workspace mutation commands
9
9
  */
10
- export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, formatAddHelpText, } from "./cli-add-shared.js";
10
+ export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, } from "./cli-add-shared.js";
11
11
  export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
12
- export { runAddBindingSourceCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
12
+ export { runAddBindingSourceCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
13
13
  export { getWorkspaceBlockSelectOptions } from "./workspace-inventory.js";
@@ -11,6 +11,7 @@
11
11
  * and `HOOKED_BLOCK_POSITION_IDS`,
12
12
  * `getWorkspaceBlockSelectOptions`, and `seedWorkspaceMigrationProject` for
13
13
  * explicit `wp-typia add` flows,
14
+ * `runAddRestResourceCommand` for plugin-level REST resource scaffolds,
14
15
  * `getDoctorChecks`, `runDoctor`, and `DoctorCheck` for diagnostics,
15
16
  * `createCliCommandError` and `formatCliDiagnosticError` for shared
16
17
  * non-interactive failure rendering,
@@ -25,8 +26,9 @@
25
26
  export { getDoctorChecks, runDoctor, type DoctorCheck } from "./cli-doctor.js";
26
27
  export { createCliCommandError, CliDiagnosticError, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
27
28
  export type { CliDiagnosticMessage } from "./cli-diagnostics.js";
28
- export { formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
29
+ export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
29
30
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
31
+ export type { EditorPluginSlotId } from "./cli-add.js";
30
32
  export type { HookedBlockPositionId } from "./hooked-blocks.js";
31
33
  export { formatHelpText } from "./cli-help.js";
32
34
  export { getNextSteps, getOptionalOnboarding, runScaffoldFlow, } from "./cli-scaffold.js";
@@ -11,6 +11,7 @@
11
11
  * and `HOOKED_BLOCK_POSITION_IDS`,
12
12
  * `getWorkspaceBlockSelectOptions`, and `seedWorkspaceMigrationProject` for
13
13
  * explicit `wp-typia add` flows,
14
+ * `runAddRestResourceCommand` for plugin-level REST resource scaffolds,
14
15
  * `getDoctorChecks`, `runDoctor`, and `DoctorCheck` for diagnostics,
15
16
  * `createCliCommandError` and `formatCliDiagnosticError` for shared
16
17
  * non-interactive failure rendering,
@@ -24,7 +25,7 @@
24
25
  */
25
26
  export { getDoctorChecks, runDoctor } from "./cli-doctor.js";
26
27
  export { createCliCommandError, CliDiagnosticError, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
27
- export { formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
28
+ export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
28
29
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
29
30
  export { formatHelpText } from "./cli-help.js";
30
31
  export { getNextSteps, getOptionalOnboarding, runScaffoldFlow, } from "./cli-scaffold.js";
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parseScaffoldBlockMetadata } from "@wp-typia/block-runtime/blocks";
4
+ import { EDITOR_PLUGIN_SLOT_IDS, REST_RESOURCE_METHOD_IDS, REST_RESOURCE_NAMESPACE_PATTERN, } from "./cli-add-shared.js";
4
5
  import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_SET, } from "./hooked-blocks.js";
5
6
  import { readWorkspaceInventory } from "./workspace-inventory.js";
6
7
  import { getInvalidWorkspaceProjectReason, parseWorkspacePackageJson, WORKSPACE_TEMPLATE_PACKAGE, tryResolveWorkspaceProject, } from "./workspace-project.js";
@@ -9,6 +10,10 @@ const WORKSPACE_COLLECTION_IMPORT_PATTERN = /^\s*import\s+["']\.\.\/\.\.\/collec
9
10
  const WORKSPACE_BINDING_SERVER_GLOB = "/src/bindings/*/server.php";
10
11
  const WORKSPACE_BINDING_EDITOR_SCRIPT = "build/bindings/index.js";
11
12
  const WORKSPACE_BINDING_EDITOR_ASSET = "build/bindings/index.asset.php";
13
+ const WORKSPACE_REST_RESOURCE_GLOB = "/inc/rest/*.php";
14
+ const WORKSPACE_EDITOR_PLUGIN_EDITOR_SCRIPT = "build/editor-plugins/index.js";
15
+ const WORKSPACE_EDITOR_PLUGIN_EDITOR_ASSET = "build/editor-plugins/index.asset.php";
16
+ const WORKSPACE_EDITOR_PLUGIN_EDITOR_STYLE = "build/editor-plugins/style-index.css";
12
17
  const WORKSPACE_GENERATED_BLOCK_ARTIFACTS = [
13
18
  "block.json",
14
19
  "typia.manifest.json",
@@ -22,6 +27,9 @@ function createDoctorCheck(label, status, detail) {
22
27
  function createDoctorScopeCheck(status, detail) {
23
28
  return createDoctorCheck("Doctor scope", status, detail);
24
29
  }
30
+ function escapeRegex(value) {
31
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
32
+ }
25
33
  function getWorkspaceBootstrapRelativePath(packageName) {
26
34
  const packageBaseName = packageName.split("/").pop() ?? packageName;
27
35
  return `${packageBaseName}.php`;
@@ -184,6 +192,117 @@ function checkWorkspaceBindingSourcesIndex(projectDir, bindingSources) {
184
192
  ? "Binding source editor registrations are aggregated"
185
193
  : `Missing editor imports for: ${missingImports.map((entry) => entry.slug).join(", ")}`);
186
194
  }
195
+ function getWorkspaceRestResourceRequiredFiles(restResource) {
196
+ const schemaNames = new Set();
197
+ if (restResource.methods.includes("list")) {
198
+ schemaNames.add("list-query");
199
+ schemaNames.add("list-response");
200
+ }
201
+ if (restResource.methods.includes("read")) {
202
+ schemaNames.add("read-query");
203
+ schemaNames.add("read-response");
204
+ }
205
+ if (restResource.methods.includes("create")) {
206
+ schemaNames.add("create-request");
207
+ schemaNames.add("create-response");
208
+ }
209
+ if (restResource.methods.includes("update")) {
210
+ schemaNames.add("update-query");
211
+ schemaNames.add("update-request");
212
+ schemaNames.add("update-response");
213
+ }
214
+ if (restResource.methods.includes("delete")) {
215
+ schemaNames.add("delete-query");
216
+ schemaNames.add("delete-response");
217
+ }
218
+ return Array.from(new Set([
219
+ restResource.apiFile,
220
+ ...Array.from(schemaNames, (schemaName) => path.join(path.dirname(restResource.typesFile), "api-schemas", `${schemaName}.schema.json`)),
221
+ restResource.clientFile,
222
+ restResource.dataFile,
223
+ restResource.openApiFile,
224
+ restResource.phpFile,
225
+ restResource.typesFile,
226
+ restResource.validatorsFile,
227
+ ]));
228
+ }
229
+ function checkWorkspaceRestResourceConfig(restResource) {
230
+ const hasNamespace = REST_RESOURCE_NAMESPACE_PATTERN.test(restResource.namespace);
231
+ const hasMethods = restResource.methods.length > 0 &&
232
+ restResource.methods.every((method) => REST_RESOURCE_METHOD_IDS.includes(method));
233
+ return createDoctorCheck(`REST resource config ${restResource.slug}`, hasNamespace && hasMethods ? "pass" : "fail", hasNamespace && hasMethods
234
+ ? `REST resource namespace ${restResource.namespace} with methods ${restResource.methods.join(", ")}`
235
+ : "REST resource namespace or methods are invalid");
236
+ }
237
+ function checkWorkspaceRestResourceBootstrap(projectDir, packageName, phpPrefix) {
238
+ const packageBaseName = packageName.split("/").pop() ?? packageName;
239
+ const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
240
+ if (!fs.existsSync(bootstrapPath)) {
241
+ return createDoctorCheck("REST resource bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
242
+ }
243
+ const source = fs.readFileSync(bootstrapPath, "utf8");
244
+ const registerFunctionName = `${phpPrefix}_register_rest_resources`;
245
+ const registerHook = `add_action( 'init', '${registerFunctionName}', 20 );`;
246
+ const hasServerGlob = source.includes(WORKSPACE_REST_RESOURCE_GLOB);
247
+ const hasRegisterHook = source.includes(registerHook);
248
+ return createDoctorCheck("REST resource bootstrap", hasServerGlob && hasRegisterHook ? "pass" : "fail", hasServerGlob && hasRegisterHook
249
+ ? "REST resource PHP loader hook is present"
250
+ : "Missing REST resource PHP require glob or init hook");
251
+ }
252
+ function getWorkspaceEditorPluginRequiredFiles(editorPlugin) {
253
+ const editorPluginDir = path.join("src", "editor-plugins", editorPlugin.slug);
254
+ return Array.from(new Set([
255
+ editorPlugin.file,
256
+ path.join(editorPluginDir, "Sidebar.tsx"),
257
+ path.join(editorPluginDir, "data.ts"),
258
+ path.join(editorPluginDir, "types.ts"),
259
+ path.join(editorPluginDir, "style.scss"),
260
+ ]));
261
+ }
262
+ function checkWorkspaceEditorPluginConfig(editorPlugin) {
263
+ const validSlots = new Set(EDITOR_PLUGIN_SLOT_IDS);
264
+ const isValidSlot = validSlots.has(editorPlugin.slot);
265
+ return createDoctorCheck(`Editor plugin config ${editorPlugin.slug}`, isValidSlot ? "pass" : "fail", isValidSlot
266
+ ? `Editor plugin slot ${editorPlugin.slot} is supported`
267
+ : `Unsupported editor plugin slot "${editorPlugin.slot}". Expected one of: ${EDITOR_PLUGIN_SLOT_IDS.join(", ")}`);
268
+ }
269
+ function checkWorkspaceEditorPluginBootstrap(projectDir, packageName, phpPrefix) {
270
+ const packageBaseName = packageName.split("/").pop() ?? packageName;
271
+ const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
272
+ if (!fs.existsSync(bootstrapPath)) {
273
+ return createDoctorCheck("Editor plugin bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
274
+ }
275
+ const source = fs.readFileSync(bootstrapPath, "utf8");
276
+ const enqueueFunctionName = `${phpPrefix}_enqueue_editor_plugins_editor`;
277
+ const enqueueHook = `add_action( 'enqueue_block_editor_assets', '${enqueueFunctionName}' );`;
278
+ const hasEditorEnqueueHook = source.includes(enqueueHook);
279
+ const hasEditorScript = source.includes(WORKSPACE_EDITOR_PLUGIN_EDITOR_SCRIPT);
280
+ const hasEditorAsset = source.includes(WORKSPACE_EDITOR_PLUGIN_EDITOR_ASSET);
281
+ const hasEditorStyle = source.includes(WORKSPACE_EDITOR_PLUGIN_EDITOR_STYLE);
282
+ return createDoctorCheck("Editor plugin bootstrap", hasEditorEnqueueHook && hasEditorScript && hasEditorAsset && hasEditorStyle ? "pass" : "fail", hasEditorEnqueueHook && hasEditorScript && hasEditorAsset && hasEditorStyle
283
+ ? "Editor plugin enqueue hook is present"
284
+ : "Missing editor plugin enqueue hook or build/editor-plugins script/style asset references");
285
+ }
286
+ function checkWorkspaceEditorPluginIndex(projectDir, editorPlugins) {
287
+ const indexRelativePath = [
288
+ path.join("src", "editor-plugins", "index.ts"),
289
+ path.join("src", "editor-plugins", "index.js"),
290
+ ].find((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
291
+ if (!indexRelativePath) {
292
+ return createDoctorCheck("Editor plugins index", "fail", "Missing src/editor-plugins/index.ts or src/editor-plugins/index.js");
293
+ }
294
+ const indexPath = path.join(projectDir, indexRelativePath);
295
+ const source = fs.readFileSync(indexPath, "utf8");
296
+ const missingImports = editorPlugins.filter((editorPlugin) => {
297
+ const importPattern = new RegExp(`['"\`]\\./${escapeRegex(editorPlugin.slug)}(?:/[^'"\`]*)?['"\`]`, "u");
298
+ return !importPattern.test(source);
299
+ });
300
+ return createDoctorCheck("Editor plugins index", missingImports.length === 0 ? "pass" : "fail", missingImports.length === 0
301
+ ? "Editor plugin registrations are aggregated"
302
+ : `Missing editor plugin imports for: ${missingImports
303
+ .map((entry) => entry.slug)
304
+ .join(", ")}`);
305
+ }
187
306
  function checkVariationEntrypoint(projectDir, blockSlug) {
188
307
  const entryPath = path.join(projectDir, "src", "blocks", blockSlug, "index.tsx");
189
308
  if (!fs.existsSync(entryPath)) {
@@ -258,7 +377,7 @@ export function getWorkspaceDoctorChecks(cwd) {
258
377
  checks.push(checkWorkspacePackageMetadata(workspace, workspacePackageJson));
259
378
  try {
260
379
  const inventory = readWorkspaceInventory(workspace.projectDir);
261
- 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)`));
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)`));
262
381
  for (const block of inventory.blocks) {
263
382
  checks.push(checkExistingFiles(workspace.projectDir, `Block ${block.slug}`, getWorkspaceBlockRequiredFiles(block)));
264
383
  checks.push(checkWorkspaceBlockMetadata(workspace.projectDir, workspace, block));
@@ -296,6 +415,21 @@ export function getWorkspaceDoctorChecks(cwd) {
296
415
  bindingSource.editorFile,
297
416
  ]));
298
417
  }
418
+ if (inventory.restResources.length > 0) {
419
+ checks.push(checkWorkspaceRestResourceBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
420
+ }
421
+ for (const restResource of inventory.restResources) {
422
+ checks.push(checkWorkspaceRestResourceConfig(restResource));
423
+ checks.push(checkExistingFiles(workspace.projectDir, `REST resource ${restResource.slug}`, getWorkspaceRestResourceRequiredFiles(restResource)));
424
+ }
425
+ if (inventory.editorPlugins.length > 0) {
426
+ checks.push(checkWorkspaceEditorPluginBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
427
+ checks.push(checkWorkspaceEditorPluginIndex(workspace.projectDir, inventory.editorPlugins));
428
+ }
429
+ for (const editorPlugin of inventory.editorPlugins) {
430
+ checks.push(checkExistingFiles(workspace.projectDir, `Editor plugin ${editorPlugin.slug}`, getWorkspaceEditorPluginRequiredFiles(editorPlugin)));
431
+ checks.push(checkWorkspaceEditorPluginConfig(editorPlugin));
432
+ }
299
433
  const migrationWorkspaceCheck = checkMigrationWorkspaceHint(workspace, workspacePackageJson);
300
434
  if (migrationWorkspaceCheck) {
301
435
  checks.push(migrationWorkspaceCheck);
@@ -10,16 +10,19 @@ import { TEMPLATE_IDS } from "./template-registry.js";
10
10
  */
11
11
  export function formatHelpText() {
12
12
  return `Usage:
13
- wp-typia create <project-dir> [--template <basic|interactivity>] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-migration-ui] [--with-wp-env] [--with-test-preset] [--yes] [--no-install] [--package-manager <id>]
14
- wp-typia create <project-dir> [--template <./path|github:owner/repo/path[#ref]>] [--variant <name>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-wp-env] [--with-test-preset] [--yes] [--no-install] [--package-manager <id>]
15
- wp-typia create <project-dir> [--template <npm-package>] [--variant <name>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-wp-env] [--with-test-preset] [--yes] [--no-install] [--package-manager <id>]
16
- wp-typia create <project-dir> [--template persistence] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--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] [--no-install] [--package-manager <id>]
17
- wp-typia create <project-dir> [--template compound] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--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] [--no-install] [--package-manager <id>]
13
+ wp-typia create <project-dir> [--template <basic|interactivity>] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--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>]
14
+ wp-typia create <project-dir> [--template query-loop] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--query-post-type <post-type>] [--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>]
15
+ wp-typia create <project-dir> [--template <./path|github:owner/repo/path[#ref]>] [--variant <name>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
16
+ wp-typia create <project-dir> [--template <npm-package>] [--variant <name>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
17
+ wp-typia create <project-dir> [--template persistence] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--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
+ wp-typia create <project-dir> [--template compound] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--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
19
  wp-typia <project-dir> [create flags...]
19
20
  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>] [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>]
20
21
  wp-typia add variation <name> --block <block-slug>
21
22
  wp-typia add pattern <name>
22
23
  wp-typia add binding-source <name>
24
+ wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <method[,method...]>]
25
+ wp-typia add editor-plugin <name> [--slot <PluginSidebar>]
23
26
  wp-typia add hooked-block <block-slug> --anchor <anchor-block-name> --position <before|after|firstChild|lastChild>
24
27
  wp-typia migrate <init|snapshot|diff|scaffold|verify|doctor|fixtures|fuzz> [...]
25
28
  wp-typia templates list
@@ -38,6 +41,8 @@ Notes:
38
41
  \`add variation\` uses an existing workspace block from \`scripts/block-config.ts\`.
39
42
  \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
40
43
  \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
44
+ \`add rest-resource\` scaffolds plugin-level TypeScript REST contracts under \`src/rest/\` and PHP route glue under \`inc/rest/\`.
45
+ \`add editor-plugin\` scaffolds a document-level editor extension under \`src/editor-plugins/\`.
41
46
  \`add hooked-block\` patches an existing workspace block's \`block.json\` \`blockHooks\` metadata.
42
47
  \`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
48
  \`wp-typia migrate doctor --all\` checks migration target alignment, snapshots, fixtures, and generated migration artifacts.
@@ -1,5 +1,5 @@
1
1
  import { collectScaffoldAnswers, scaffoldProject } from "./scaffold.js";
2
- import type { DataStorageMode, PersistencePolicy } from "./scaffold.js";
2
+ import type { DataStorageMode, PersistencePolicy, ScaffoldProgressEvent } from "./scaffold.js";
3
3
  import type { PackageManagerId } from "./package-managers.js";
4
4
  import { type ExternalLayerSelectionOption } from "./external-layer-selection.js";
5
5
  import type { TemplateDefinition } from "./template-registry.js";
@@ -20,20 +20,27 @@ interface OptionalOnboardingGuidance {
20
20
  note: string;
21
21
  steps: string[];
22
22
  }
23
+ export interface ScaffoldDryRunPlan {
24
+ dependencyInstall: "skipped-by-flag" | "would-install";
25
+ files: string[];
26
+ }
23
27
  interface RunScaffoldFlowOptions {
24
28
  allowExistingDir?: boolean;
25
29
  cwd?: string;
26
30
  dataStorageMode?: string;
31
+ dryRun?: boolean;
27
32
  externalLayerId?: string;
28
33
  externalLayerSource?: string;
29
34
  installDependencies?: Parameters<typeof scaffoldProject>[0]["installDependencies"];
30
35
  isInteractive?: boolean;
31
36
  namespace?: string;
32
37
  noInstall?: boolean;
38
+ onProgress?: ((event: ScaffoldProgressEvent) => void | Promise<void>) | undefined;
33
39
  packageManager?: string;
34
40
  phpPrefix?: string;
35
41
  projectInput: string;
36
42
  promptText?: Parameters<typeof collectScaffoldAnswers>[0]["promptText"];
43
+ queryPostType?: string;
37
44
  selectDataStorage?: () => Promise<DataStorageMode>;
38
45
  selectExternalLayerId?: (options: ExternalLayerSelectionOption[]) => Promise<string>;
39
46
  selectPackageManager?: () => Promise<PackageManagerId>;
@@ -73,8 +80,10 @@ export declare function getOptionalOnboarding({ availableScripts, packageManager
73
80
  * project.
74
81
  * @returns The scaffold result together with next-step guidance.
75
82
  */
76
- export declare function runScaffoldFlow({ projectInput, cwd, templateId, dataStorageMode, externalLayerId, externalLayerSource, persistencePolicy, packageManager, namespace, textDomain, phpPrefix, yes, noInstall, isInteractive, allowExistingDir, selectTemplate, selectDataStorage, selectExternalLayerId, selectPersistencePolicy, selectPackageManager, promptText, installDependencies, variant, selectWithTestPreset, selectWithWpEnv, selectWithMigrationUi, withMigrationUi, withTestPreset, withWpEnv, }: RunScaffoldFlowOptions): Promise<{
83
+ export declare function runScaffoldFlow({ projectInput, cwd, templateId, dataStorageMode, dryRun, externalLayerId, externalLayerSource, persistencePolicy, packageManager, namespace, textDomain, phpPrefix, queryPostType, yes, noInstall, onProgress, isInteractive, allowExistingDir, selectTemplate, selectDataStorage, selectExternalLayerId, selectPersistencePolicy, selectPackageManager, promptText, installDependencies, variant, selectWithTestPreset, selectWithWpEnv, selectWithMigrationUi, withMigrationUi, withTestPreset, withWpEnv, }: RunScaffoldFlowOptions): Promise<{
84
+ dryRun: boolean;
77
85
  optionalOnboarding: OptionalOnboardingGuidance;
86
+ plan: ScaffoldDryRunPlan | undefined;
78
87
  projectDir: string;
79
88
  projectInput: string;
80
89
  packageManager: PackageManagerId;
@@ -1,11 +1,79 @@
1
1
  import fs from "node:fs";
2
+ import { promises as fsp } from "node:fs";
3
+ import os from "node:os";
2
4
  import path from "node:path";
3
5
  import { collectScaffoldAnswers, DATA_STORAGE_MODES, PERSISTENCE_POLICIES, isDataStorageMode, isPersistencePolicy, resolvePackageManagerId, resolveTemplateId, scaffoldProject, } from "./scaffold.js";
4
6
  import { formatInstallCommand, formatRunScript, } from "./package-managers.js";
5
7
  import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
6
8
  import { getOptionalOnboardingNote, getOptionalOnboardingSteps, } from "./scaffold-onboarding.js";
9
+ import { formatNonEmptyTargetDirectoryError } from "./scaffold-bootstrap.js";
7
10
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
8
11
  import { resolveOptionalInteractiveExternalLayerId, } from "./external-layer-selection.js";
12
+ async function listRelativeProjectFiles(rootDir) {
13
+ const relativeFiles = [];
14
+ async function visit(currentDir) {
15
+ const entries = await fsp.readdir(currentDir, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ const absolutePath = path.join(currentDir, entry.name);
18
+ if (entry.isDirectory()) {
19
+ await visit(absolutePath);
20
+ continue;
21
+ }
22
+ relativeFiles.push(path
23
+ .relative(rootDir, absolutePath)
24
+ .replace(path.sep === "\\" ? /\\/gu : /\//gu, "/"));
25
+ }
26
+ }
27
+ await visit(rootDir);
28
+ return relativeFiles.sort((left, right) => left.localeCompare(right));
29
+ }
30
+ async function assertDryRunTargetDirectoryReady(projectDir, allowExistingDir) {
31
+ if (!fs.existsSync(projectDir) || allowExistingDir) {
32
+ return;
33
+ }
34
+ const entries = await fsp.readdir(projectDir);
35
+ if (entries.length > 0) {
36
+ throw new Error(formatNonEmptyTargetDirectoryError(projectDir));
37
+ }
38
+ }
39
+ async function buildScaffoldDryRunPlan({ allowExistingDir, answers, cwd, dataStorageMode, externalLayerId, externalLayerSource, externalLayerSourceLabel, installDependencies, noInstall, onProgress, packageManager, persistencePolicy, projectDir, templateId, variant, withMigrationUi, withTestPreset, withWpEnv, }) {
40
+ await assertDryRunTargetDirectoryReady(projectDir, allowExistingDir);
41
+ const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-scaffold-plan-"));
42
+ const previewProjectDir = path.join(tempRoot, "preview-project");
43
+ try {
44
+ const result = await scaffoldProject({
45
+ allowExistingDir: false,
46
+ answers,
47
+ cwd,
48
+ dataStorageMode,
49
+ externalLayerId,
50
+ externalLayerSource,
51
+ externalLayerSourceLabel,
52
+ installDependencies,
53
+ noInstall: true,
54
+ onProgress,
55
+ packageManager,
56
+ persistencePolicy,
57
+ projectDir: previewProjectDir,
58
+ templateId,
59
+ variant,
60
+ withMigrationUi,
61
+ withTestPreset,
62
+ withWpEnv,
63
+ });
64
+ const files = await listRelativeProjectFiles(previewProjectDir);
65
+ return {
66
+ plan: {
67
+ dependencyInstall: noInstall ? "skipped-by-flag" : "would-install",
68
+ files,
69
+ },
70
+ result,
71
+ };
72
+ }
73
+ finally {
74
+ await fsp.rm(tempRoot, { force: true, recursive: true });
75
+ }
76
+ }
9
77
  function validateCreateProjectInput(projectInput) {
10
78
  const normalizedProjectInput = projectInput.trim();
11
79
  if (normalizedProjectInput.length === 0) {
@@ -135,7 +203,7 @@ export function getOptionalOnboarding({ availableScripts, packageManager, templa
135
203
  * project.
136
204
  * @returns The scaffold result together with next-step guidance.
137
205
  */
138
- export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templateId, dataStorageMode, externalLayerId, externalLayerSource, persistencePolicy, packageManager, namespace, textDomain, phpPrefix, yes = false, noInstall = false, isInteractive = false, allowExistingDir = false, selectTemplate, selectDataStorage, selectExternalLayerId, selectPersistencePolicy, selectPackageManager, promptText, installDependencies = undefined, variant, selectWithTestPreset, selectWithWpEnv, selectWithMigrationUi, withMigrationUi, withTestPreset, withWpEnv, }) {
206
+ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templateId, dataStorageMode, dryRun = false, externalLayerId, externalLayerSource, persistencePolicy, packageManager, namespace, textDomain, phpPrefix, queryPostType, yes = false, noInstall = false, onProgress, isInteractive = false, allowExistingDir = false, selectTemplate, selectDataStorage, selectExternalLayerId, selectPersistencePolicy, selectPackageManager, promptText, installDependencies = undefined, variant, selectWithTestPreset, selectWithWpEnv, selectWithMigrationUi, withMigrationUi, withTestPreset, withWpEnv, }) {
139
207
  const normalizedExternalLayerId = typeof externalLayerId === "string" && externalLayerId.trim().length > 0
140
208
  ? externalLayerId.trim()
141
209
  : undefined;
@@ -228,52 +296,82 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
228
296
  persistencePolicy: resolvedPersistencePolicy,
229
297
  phpPrefix,
230
298
  projectName,
299
+ queryPostType,
231
300
  templateId: resolvedTemplateId,
232
301
  textDomain,
233
302
  yes,
234
303
  promptText,
235
304
  });
236
- const result = await scaffoldProject({
237
- answers,
238
- allowExistingDir,
239
- cwd,
240
- dataStorageMode: resolvedDataStorage,
241
- externalLayerId: resolvedExternalLayerSelection.externalLayerId,
242
- externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
243
- externalLayerSourceLabel: normalizedExternalLayerSource,
244
- installDependencies,
245
- noInstall,
246
- packageManager: resolvedPackageManager,
247
- persistencePolicy: resolvedPersistencePolicy,
248
- projectDir,
249
- templateId: resolvedTemplateId,
250
- variant,
251
- withMigrationUi: resolvedWithMigrationUi,
252
- withTestPreset: resolvedWithTestPreset,
253
- withWpEnv: resolvedWithWpEnv,
254
- });
305
+ const resolvedResult = dryRun
306
+ ? await buildScaffoldDryRunPlan({
307
+ allowExistingDir,
308
+ answers,
309
+ cwd,
310
+ dataStorageMode: resolvedDataStorage,
311
+ externalLayerId: resolvedExternalLayerSelection.externalLayerId,
312
+ externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
313
+ externalLayerSourceLabel: normalizedExternalLayerSource,
314
+ installDependencies,
315
+ noInstall,
316
+ onProgress,
317
+ packageManager: resolvedPackageManager,
318
+ persistencePolicy: resolvedPersistencePolicy,
319
+ projectDir,
320
+ templateId: resolvedTemplateId,
321
+ variant,
322
+ withMigrationUi: resolvedWithMigrationUi,
323
+ withTestPreset: resolvedWithTestPreset,
324
+ withWpEnv: resolvedWithWpEnv,
325
+ })
326
+ : {
327
+ plan: undefined,
328
+ result: await scaffoldProject({
329
+ answers,
330
+ allowExistingDir,
331
+ cwd,
332
+ dataStorageMode: resolvedDataStorage,
333
+ externalLayerId: resolvedExternalLayerSelection.externalLayerId,
334
+ externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
335
+ externalLayerSourceLabel: normalizedExternalLayerSource,
336
+ installDependencies,
337
+ noInstall,
338
+ onProgress,
339
+ packageManager: resolvedPackageManager,
340
+ persistencePolicy: resolvedPersistencePolicy,
341
+ projectDir,
342
+ templateId: resolvedTemplateId,
343
+ variant,
344
+ withMigrationUi: resolvedWithMigrationUi,
345
+ withTestPreset: resolvedWithTestPreset,
346
+ withWpEnv: resolvedWithWpEnv,
347
+ }),
348
+ };
255
349
  let availableScripts;
256
- try {
257
- const parsedPackageJson = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
258
- const scripts = parsedPackageJson.scripts &&
259
- typeof parsedPackageJson.scripts === "object" &&
260
- !Array.isArray(parsedPackageJson.scripts)
261
- ? parsedPackageJson.scripts
262
- : {};
263
- availableScripts = Object.entries(scripts)
264
- .filter(([, value]) => typeof value === "string")
265
- .map(([scriptName]) => scriptName);
266
- }
267
- catch {
268
- availableScripts = undefined;
350
+ if (!dryRun) {
351
+ try {
352
+ const parsedPackageJson = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
353
+ const scripts = parsedPackageJson.scripts &&
354
+ typeof parsedPackageJson.scripts === "object" &&
355
+ !Array.isArray(parsedPackageJson.scripts)
356
+ ? parsedPackageJson.scripts
357
+ : {};
358
+ availableScripts = Object.entries(scripts)
359
+ .filter(([, value]) => typeof value === "string")
360
+ .map(([scriptName]) => scriptName);
361
+ }
362
+ catch {
363
+ availableScripts = undefined;
364
+ }
269
365
  }
270
366
  return {
367
+ dryRun,
271
368
  optionalOnboarding: getOptionalOnboarding({
272
369
  availableScripts,
273
370
  packageManager: resolvedPackageManager,
274
371
  templateId: resolvedTemplateId,
275
- compoundPersistenceEnabled: result.variables.compoundPersistenceEnabled === "true",
372
+ compoundPersistenceEnabled: resolvedResult.result.variables.compoundPersistenceEnabled === "true",
276
373
  }),
374
+ plan: resolvedResult.plan,
277
375
  projectDir,
278
376
  projectInput,
279
377
  packageManager: resolvedPackageManager,
@@ -285,8 +383,11 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
285
383
  templateId: resolvedTemplateId,
286
384
  }),
287
385
  result: {
288
- ...result,
289
- warnings: [...result.warnings, ...collectProjectDirectoryWarnings(projectDir)],
386
+ ...resolvedResult.result,
387
+ warnings: [
388
+ ...resolvedResult.result.warnings,
389
+ ...collectProjectDirectoryWarnings(projectDir),
390
+ ],
290
391
  },
291
392
  };
292
393
  }
@@ -8,18 +8,18 @@ import type { TemplateDefinition } from "./template-registry.js";
8
8
  */
9
9
  export declare function formatTemplateSummary(template: TemplateDefinition): string;
10
10
  /**
11
- * Format the feature hint line shown under a template summary.
11
+ * Format the feature and capability hint lines shown under a template summary.
12
12
  *
13
13
  * @param template Template metadata including the `features` list.
14
- * @returns Indented feature text for CLI list output.
14
+ * @returns Indented feature and capability text for CLI list output.
15
15
  */
16
16
  export declare function formatTemplateFeatures(template: TemplateDefinition): string;
17
17
  /**
18
18
  * Format the detailed template description for `templates inspect`.
19
19
  *
20
20
  * This expands special layer combinations for the `persistence` and `compound`
21
- * templates and returns a multi-line block including category, overlay path,
22
- * resolved layers, and feature labels.
21
+ * templates and returns a multi-line block centered on human-facing identity,
22
+ * capabilities, and logical layer composition.
23
23
  *
24
24
  * @param template Template metadata including `id`, `defaultCategory`,
25
25
  * `templateDir`, and `features`.