@wp-typia/project-tools 0.17.0 → 0.19.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 (76) hide show
  1. package/dist/runtime/alternate-render-targets.d.ts +5 -0
  2. package/dist/runtime/alternate-render-targets.js +29 -0
  3. package/dist/runtime/block-generator-service-core.d.ts +2 -2
  4. package/dist/runtime/block-generator-service-core.js +13 -8
  5. package/dist/runtime/block-generator-service-spec.d.ts +10 -2
  6. package/dist/runtime/block-generator-service-spec.js +43 -1
  7. package/dist/runtime/built-in-block-artifacts.js +1 -0
  8. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +2 -2
  9. package/dist/runtime/built-in-block-code-templates/compound-child.js +35 -2
  10. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +2 -2
  11. package/dist/runtime/built-in-block-code-templates/compound-parent.js +204 -27
  12. package/dist/runtime/built-in-block-code-templates/compound-persistence.d.ts +1 -1
  13. package/dist/runtime/built-in-block-code-templates/compound-persistence.js +11 -8
  14. package/dist/runtime/built-in-block-non-ts-artifacts.js +505 -2
  15. package/dist/runtime/cli-add-block.d.ts +6 -2
  16. package/dist/runtime/cli-add-block.js +71 -24
  17. package/dist/runtime/cli-add-shared.d.ts +58 -2
  18. package/dist/runtime/cli-add-shared.js +111 -12
  19. package/dist/runtime/cli-add-workspace-assets.d.ts +21 -1
  20. package/dist/runtime/cli-add-workspace-assets.js +417 -1
  21. package/dist/runtime/cli-add-workspace-rest.d.ts +14 -0
  22. package/dist/runtime/cli-add-workspace-rest.js +1060 -0
  23. package/dist/runtime/cli-add-workspace.d.ts +10 -1
  24. package/dist/runtime/cli-add-workspace.js +10 -1
  25. package/dist/runtime/cli-add.d.ts +3 -3
  26. package/dist/runtime/cli-add.js +2 -2
  27. package/dist/runtime/cli-core.d.ts +5 -1
  28. package/dist/runtime/cli-core.js +3 -1
  29. package/dist/runtime/cli-doctor-workspace.js +135 -1
  30. package/dist/runtime/cli-help.js +12 -7
  31. package/dist/runtime/cli-scaffold.d.ts +12 -2
  32. package/dist/runtime/cli-scaffold.js +222 -46
  33. package/dist/runtime/cli-templates.d.ts +4 -4
  34. package/dist/runtime/cli-templates.js +104 -39
  35. package/dist/runtime/cli-validation.d.ts +66 -0
  36. package/dist/runtime/cli-validation.js +92 -0
  37. package/dist/runtime/compound-inner-blocks.d.ts +78 -0
  38. package/dist/runtime/compound-inner-blocks.js +88 -0
  39. package/dist/runtime/index.d.ts +6 -3
  40. package/dist/runtime/index.js +4 -2
  41. package/dist/runtime/local-dev-presets.js +7 -2
  42. package/dist/runtime/migration-command-surface.js +2 -0
  43. package/dist/runtime/package-versions.d.ts +1 -0
  44. package/dist/runtime/package-versions.js +12 -0
  45. package/dist/runtime/rest-resource-artifacts.d.ts +35 -0
  46. package/dist/runtime/rest-resource-artifacts.js +158 -0
  47. package/dist/runtime/scaffold-answer-resolution.js +78 -8
  48. package/dist/runtime/scaffold-apply-utils.d.ts +4 -3
  49. package/dist/runtime/scaffold-apply-utils.js +34 -17
  50. package/dist/runtime/scaffold-bootstrap.d.ts +15 -0
  51. package/dist/runtime/scaffold-bootstrap.js +29 -7
  52. package/dist/runtime/scaffold-documents.js +24 -3
  53. package/dist/runtime/scaffold-identifiers.d.ts +17 -0
  54. package/dist/runtime/scaffold-identifiers.js +22 -0
  55. package/dist/runtime/scaffold-onboarding.js +25 -13
  56. package/dist/runtime/scaffold-package-manager-files.js +6 -1
  57. package/dist/runtime/scaffold-template-variables.js +22 -0
  58. package/dist/runtime/scaffold.d.ts +22 -1
  59. package/dist/runtime/scaffold.js +56 -11
  60. package/dist/runtime/template-render.d.ts +5 -2
  61. package/dist/runtime/template-render.js +9 -3
  62. package/dist/runtime/template-source-contracts.d.ts +11 -0
  63. package/dist/runtime/template-source-external.d.ts +1 -1
  64. package/dist/runtime/template-source-external.js +45 -13
  65. package/dist/runtime/template-source-normalization.d.ts +1 -1
  66. package/dist/runtime/template-source-normalization.js +5 -1
  67. package/dist/runtime/template-source-remote.d.ts +5 -0
  68. package/dist/runtime/template-source-remote.js +33 -0
  69. package/dist/runtime/template-source.js +35 -4
  70. package/dist/runtime/workspace-inventory.d.ts +43 -1
  71. package/dist/runtime/workspace-inventory.js +132 -1
  72. package/dist/runtime/workspace-project.d.ts +1 -1
  73. package/dist/runtime/workspace-project.js +3 -3
  74. package/package.json +9 -4
  75. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +728 -49
  76. package/templates/query-loop/src/validator-toolkit.ts.mustache +0 -1
@@ -1,9 +1,16 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import { PACKAGE_MANAGER_IDS, getPackageManager, } from './package-managers.js';
2
3
  import { normalizeBlockSlug, resolveScaffoldIdentifiers, validateBlockSlug, validateNamespace, } from './scaffold-identifiers.js';
3
4
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from './template-registry.js';
4
5
  import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from './template-defaults.js';
5
6
  import { toSnakeCase, toTitleCase, } from './string-case.js';
6
7
  const WORKSPACE_TEMPLATE_ALIAS = 'workspace';
8
+ const TEMPLATE_SELECTION_HINT = `--template <${[
9
+ ...TEMPLATE_IDS,
10
+ WORKSPACE_TEMPLATE_ALIAS,
11
+ ].join('|')}|./path|github:owner/repo/path[#ref]|npm-package>`;
12
+ const TEMPLATE_SUGGESTION_IDS = [...TEMPLATE_IDS, WORKSPACE_TEMPLATE_ALIAS];
13
+ const QUERY_POST_TYPE_RULE = 'Use lowercase, 1-20 chars, and only a-z, 0-9, "_" or "-".';
7
14
  /**
8
15
  * Detect the current author name from local Git config.
9
16
  *
@@ -44,12 +51,15 @@ export function getDefaultAnswers(projectName, templateId) {
44
51
  };
45
52
  }
46
53
  function validateQueryPostType(value) {
47
- const normalizedValue = value.trim().toLowerCase();
54
+ const rawValue = value.trim();
55
+ const normalizedValue = rawValue.toLowerCase();
48
56
  if (normalizedValue.length === 0) {
49
57
  return 'Query post type is required.';
50
58
  }
51
59
  if (!/^[a-z0-9_-]{1,20}$/u.test(normalizedValue)) {
52
- return 'Query post type must be lowercase, 1-20 chars, and only a-z, 0-9, "_" or "-".';
60
+ return rawValue === normalizedValue
61
+ ? `Query post type "${rawValue}" is invalid. ${QUERY_POST_TYPE_RULE}`
62
+ : `Query post type "${rawValue}" normalizes to "${normalizedValue}", which is invalid. ${QUERY_POST_TYPE_RULE}`;
53
63
  }
54
64
  return true;
55
65
  }
@@ -57,17 +67,71 @@ function normalizeQueryPostType(value) {
57
67
  if (typeof value !== 'string') {
58
68
  return undefined;
59
69
  }
60
- const normalizedValue = value.trim().toLowerCase();
61
- if (validateQueryPostType(normalizedValue) !== true) {
62
- throw new Error('Query post type must be lowercase, 1-20 chars, and only a-z, 0-9, "_" or "-".');
70
+ const validationResult = validateQueryPostType(value);
71
+ if (validationResult !== true) {
72
+ throw new Error(validationResult);
63
73
  }
64
- return normalizedValue;
74
+ return value.trim().toLowerCase();
65
75
  }
66
76
  function normalizeTemplateSelection(templateId) {
67
77
  return templateId === WORKSPACE_TEMPLATE_ALIAS
68
78
  ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
69
79
  : templateId;
70
80
  }
81
+ function looksLikeExplicitExternalTemplateLocator(templateId) {
82
+ return (templateId.startsWith('./') ||
83
+ templateId.startsWith('../') ||
84
+ templateId.startsWith('/') ||
85
+ templateId.startsWith('@') ||
86
+ templateId.startsWith('github:') ||
87
+ templateId.includes('/'));
88
+ }
89
+ function getEditDistance(left, right) {
90
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
91
+ const current = new Array(right.length + 1);
92
+ for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
93
+ current[0] = leftIndex + 1;
94
+ for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
95
+ const substitutionCost = left[leftIndex] === right[rightIndex] ? 0 : 1;
96
+ current[rightIndex + 1] = Math.min(current[rightIndex] + 1, previous[rightIndex + 1] + 1, previous[rightIndex] + substitutionCost);
97
+ }
98
+ for (let index = 0; index < current.length; index += 1) {
99
+ previous[index] = current[index];
100
+ }
101
+ }
102
+ return previous[right.length];
103
+ }
104
+ function findMistypedBuiltInTemplateSuggestion(templateId) {
105
+ const normalizedTemplateId = templateId.trim().toLowerCase();
106
+ if (normalizedTemplateId.length === 0 ||
107
+ looksLikeExplicitExternalTemplateLocator(normalizedTemplateId)) {
108
+ return null;
109
+ }
110
+ let bestCandidate = null;
111
+ for (const candidateId of TEMPLATE_SUGGESTION_IDS) {
112
+ const distance = getEditDistance(normalizedTemplateId, candidateId);
113
+ if (bestCandidate === null ||
114
+ distance < bestCandidate.distance) {
115
+ bestCandidate = {
116
+ distance,
117
+ id: candidateId,
118
+ };
119
+ }
120
+ }
121
+ return bestCandidate && bestCandidate.distance <= 2
122
+ ? bestCandidate.id
123
+ : null;
124
+ }
125
+ function getMistypedBuiltInTemplateMessage(templateId) {
126
+ const suggestion = findMistypedBuiltInTemplateSuggestion(templateId);
127
+ if (!suggestion) {
128
+ return null;
129
+ }
130
+ const suggestionDescription = suggestion === WORKSPACE_TEMPLATE_ALIAS
131
+ ? 'official workspace scaffold'
132
+ : 'built-in scaffold';
133
+ return `Unknown template "${templateId}". Did you mean "${suggestion}"? Use \`--template ${suggestion}\` for the ${suggestionDescription}, or pass a local path, \`github:owner/repo/path[#ref]\`, or an npm package spec for an external template.`;
134
+ }
71
135
  /**
72
136
  * Resolve the scaffold template id from flags, defaults, and interactive selection.
73
137
  *
@@ -80,16 +144,23 @@ export async function resolveTemplateId({ templateId, yes = false, isInteractive
80
144
  if (isRemovedBuiltInTemplateId(templateId)) {
81
145
  throw new Error(getRemovedBuiltInTemplateMessage(templateId));
82
146
  }
147
+ if (normalizedTemplateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
148
+ return normalizedTemplateId;
149
+ }
83
150
  if (isBuiltInTemplateId(normalizedTemplateId)) {
84
151
  return getTemplateById(normalizedTemplateId).id;
85
152
  }
153
+ const mistypedBuiltInTemplateMessage = getMistypedBuiltInTemplateMessage(templateId);
154
+ if (mistypedBuiltInTemplateMessage) {
155
+ throw new Error(mistypedBuiltInTemplateMessage);
156
+ }
86
157
  return normalizedTemplateId;
87
158
  }
88
159
  if (yes) {
89
160
  return 'basic';
90
161
  }
91
162
  if (!isInteractive || !selectTemplate) {
92
- throw new Error(`Template is required in non-interactive mode. Use --template <${TEMPLATE_IDS.join('|')}|./path|github:owner/repo/path[#ref]|npm-package>.`);
163
+ throw new Error(`Template is required in non-interactive mode. Use ${TEMPLATE_SELECTION_HINT}.`);
93
164
  }
94
165
  return normalizeTemplateSelection(await selectTemplate());
95
166
  }
@@ -160,4 +231,3 @@ export async function collectScaffoldAnswers({ projectName, templateId, yes = fa
160
231
  title: await promptText('Block title', toTitleCase(identifiers.slug)),
161
232
  };
162
233
  }
163
- import { execSync } from 'node:child_process';
@@ -2,7 +2,7 @@ import { type BuiltInBlockArtifact } from "./built-in-block-artifacts.js";
2
2
  import type { BuiltInCodeArtifact } from "./built-in-block-code-artifacts.js";
3
3
  import { type BuiltInTemplateId } from "./template-registry.js";
4
4
  import type { PackageManagerId } from "./package-managers.js";
5
- import type { ScaffoldTemplateVariables } from "./scaffold.js";
5
+ import type { ScaffoldProgressEvent, ScaffoldTemplateVariables } from "./scaffold.js";
6
6
  export { buildGitignore, buildReadme, mergeTextLines, } from "./scaffold-documents.js";
7
7
  export interface InstallDependenciesOptions {
8
8
  packageManager: PackageManagerId;
@@ -16,7 +16,7 @@ export declare function writeStarterManifestFiles(targetDir: string, templateId:
16
16
  */
17
17
  export declare function seedBuiltInPersistenceArtifacts(targetDir: string, templateId: BuiltInTemplateId, variables: ScaffoldTemplateVariables): Promise<void>;
18
18
  export declare function normalizePackageManagerFiles(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
19
- export declare function normalizePackageJson(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
19
+ export declare function removeQueryLoopPlaceholderFiles(projectDir: string, templateId: string): Promise<void>;
20
20
  export declare function removeUnexpectedLockfiles(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
21
21
  /**
22
22
  * Recursively normalizes generated text files for the selected package manager
@@ -33,7 +33,7 @@ export declare function applyWorkspaceMigrationCapability(projectDir: string, pa
33
33
  * Applies a built-in scaffold into the target directory, including generated
34
34
  * code artifacts, starter manifests, preset files, and placeholder rewrites.
35
35
  */
36
- export declare function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir, packageManager, withMigrationUi, withTestPreset, withWpEnv, noInstall, installDependencies, repositoryReference, }: {
36
+ export declare function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir, packageManager, withMigrationUi, withTestPreset, withWpEnv, noInstall, installDependencies, repositoryReference, onProgress, }: {
37
37
  projectDir: string;
38
38
  templateDir: string;
39
39
  templateId: BuiltInTemplateId;
@@ -50,4 +50,5 @@ export declare function applyBuiltInScaffoldProjectFiles({ projectDir, templateD
50
50
  noInstall?: boolean;
51
51
  installDependencies?: ((options: InstallDependenciesOptions) => Promise<void>) | undefined;
52
52
  repositoryReference?: string;
53
+ onProgress?: ((event: ScaffoldProgressEvent) => void | Promise<void>) | undefined;
53
54
  }): Promise<void>;
@@ -10,12 +10,17 @@ import { ensureMigrationDirectories, writeInitialMigrationScaffold, writeMigrati
10
10
  import { syncPersistenceRestArtifacts, } from "./persistence-rest-artifacts.js";
11
11
  import { buildGitignore, buildReadme, mergeTextLines, } from "./scaffold-documents.js";
12
12
  import { getStarterManifestFiles, stringifyStarterManifest, } from "./starter-manifests.js";
13
+ import { formatNonEmptyTargetDirectoryError, } from "./scaffold-bootstrap.js";
13
14
  import { stringifyBuiltInBlockJsonDocument, } from "./built-in-block-artifacts.js";
14
15
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, } from "./template-registry.js";
15
16
  import { copyInterpolatedDirectory } from "./template-render.js";
16
- import { formatInstallCommand, formatPackageExecCommand, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
17
+ import { formatInstallCommand, formatPackageExecCommand, transformPackageManagerText, } from "./package-managers.js";
18
+ import { normalizePackageJson } from "./scaffold-package-manager-files.js";
17
19
  import { replaceRepositoryReferencePlaceholders, resolveScaffoldRepositoryReference, } from "./scaffold-repository-reference.js";
18
20
  export { buildGitignore, buildReadme, mergeTextLines, } from "./scaffold-documents.js";
21
+ async function reportScaffoldProgress(onProgress, event) {
22
+ await onProgress?.(event);
23
+ }
19
24
  const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
20
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
26
  const LOCKFILES = {
@@ -34,7 +39,7 @@ export async function ensureDirectory(targetDir, allowExisting = false) {
34
39
  }
35
40
  const entries = await fsp.readdir(targetDir);
36
41
  if (entries.length > 0) {
37
- throw new Error(`Target directory is not empty: ${targetDir}`);
42
+ throw new Error(formatNonEmptyTargetDirectoryError(targetDir));
38
43
  }
39
44
  }
40
45
  export async function writeStarterManifestFiles(targetDir, templateId, variables, artifacts) {
@@ -135,22 +140,13 @@ export async function normalizePackageManagerFiles(targetDir, packageManagerId)
135
140
  await fsp.rm(yarnRcPath, { force: true });
136
141
  }
137
142
  }
138
- export async function normalizePackageJson(targetDir, packageManagerId) {
139
- const packageJsonPath = path.join(targetDir, "package.json");
140
- if (!fs.existsSync(packageJsonPath)) {
143
+ export async function removeQueryLoopPlaceholderFiles(projectDir, templateId) {
144
+ if (templateId !== "query-loop") {
141
145
  return;
142
146
  }
143
- const packageManager = getPackageManager(packageManagerId);
144
- const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
145
- packageJson.packageManager = packageManager.packageManagerField;
146
- if (packageJson.scripts) {
147
- for (const [key, value] of Object.entries(packageJson.scripts)) {
148
- if (typeof value === "string") {
149
- packageJson.scripts[key] = transformPackageManagerText(value, packageManagerId);
150
- }
151
- }
152
- }
153
- await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
147
+ await fsp.rm(path.join(projectDir, "src", "validator-toolkit.ts"), {
148
+ force: true,
149
+ });
154
150
  }
155
151
  export async function removeUnexpectedLockfiles(targetDir, packageManagerId) {
156
152
  const keep = new Set(LOCKFILES[packageManagerId] ?? []);
@@ -254,8 +250,13 @@ export async function applyWorkspaceMigrationCapability(projectDir, packageManag
254
250
  * Applies a built-in scaffold into the target directory, including generated
255
251
  * code artifacts, starter manifests, preset files, and placeholder rewrites.
256
252
  */
257
- export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir = false, packageManager, withMigrationUi = false, withTestPreset = false, withWpEnv = false, noInstall = false, installDependencies, repositoryReference, }) {
253
+ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir = false, packageManager, withMigrationUi = false, withTestPreset = false, withWpEnv = false, noInstall = false, installDependencies, repositoryReference, onProgress, }) {
258
254
  await ensureDirectory(projectDir, allowExistingDir);
255
+ await reportScaffoldProgress(onProgress, {
256
+ detail: "Copying built-in template files and writing generated source modules.",
257
+ phase: "generate-files",
258
+ title: "Generating project files",
259
+ });
259
260
  await copyInterpolatedDirectory(templateDir, projectDir, variables);
260
261
  if (codeArtifacts && codeArtifacts.length > 0) {
261
262
  await writeBuiltInCodeArtifacts(projectDir, codeArtifacts);
@@ -263,6 +264,11 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
263
264
  if (artifacts && artifacts.length > 0) {
264
265
  await writeBuiltInStructuralArtifacts(projectDir, artifacts);
265
266
  }
267
+ await reportScaffoldProgress(onProgress, {
268
+ detail: "Writing starter manifests, local presets, and seeded template artifacts.",
269
+ phase: "seed-artifacts",
270
+ title: "Seeding scaffold artifacts",
271
+ });
266
272
  await writeStarterManifestFiles(projectDir, templateId, variables, artifacts);
267
273
  await seedBuiltInPersistenceArtifacts(projectDir, templateId, variables);
268
274
  await applyLocalDevPresetFiles({
@@ -279,6 +285,11 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
279
285
  variables,
280
286
  });
281
287
  }
288
+ await reportScaffoldProgress(onProgress, {
289
+ detail: "Writing README, normalizing package metadata, and aligning package-manager files.",
290
+ phase: "finalize-project",
291
+ title: "Finalizing scaffold output",
292
+ });
282
293
  const readmePath = path.join(projectDir, "README.md");
283
294
  if (!fs.existsSync(readmePath)) {
284
295
  await fsp.writeFile(readmePath, readmeContent ??
@@ -302,12 +313,18 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
302
313
  withTestPreset,
303
314
  withWpEnv,
304
315
  });
316
+ await removeQueryLoopPlaceholderFiles(projectDir, templateId);
305
317
  await normalizePackageManagerFiles(projectDir, packageManager);
306
318
  await removeUnexpectedLockfiles(projectDir, packageManager);
307
319
  await replaceTextRecursively(projectDir, packageManager, {
308
320
  repositoryReference,
309
321
  });
310
322
  if (!noInstall) {
323
+ await reportScaffoldProgress(onProgress, {
324
+ detail: "Installing project dependencies with the selected package manager.",
325
+ phase: "install-dependencies",
326
+ title: "Installing dependencies",
327
+ });
311
328
  const installer = installDependencies ?? defaultInstallDependencies;
312
329
  await installer({
313
330
  projectDir,
@@ -9,6 +9,14 @@ import type { BuiltInTemplateId } from "./template-registry.js";
9
9
  * @returns A promise that resolves once the directory precondition is satisfied.
10
10
  */
11
11
  export declare function ensureScaffoldDirectory(targetDir: string, allowExisting?: boolean): Promise<void>;
12
+ /**
13
+ * Format the actionable error message used when a scaffold target directory
14
+ * already exists and is not empty.
15
+ *
16
+ * @param targetDir Absolute path to the target directory being evaluated.
17
+ * @returns A human-readable error string with next-step guidance.
18
+ */
19
+ export declare function formatNonEmptyTargetDirectoryError(targetDir: string): string;
12
20
  /**
13
21
  * Writes built-in starter manifest files into a scaffolded project.
14
22
  *
@@ -28,6 +36,13 @@ export declare function writeStarterManifestFiles(targetDir: string, templateId:
28
36
  * @returns A promise that resolves after any required persistence artifacts are generated.
29
37
  */
30
38
  export declare function seedBuiltInPersistenceArtifacts(targetDir: string, templateId: BuiltInTemplateId, variables: ScaffoldTemplateVariables): Promise<void>;
39
+ /**
40
+ * Detects whether a scaffolded project declares the workspace project model.
41
+ *
42
+ * @param projectDir Absolute scaffold target directory.
43
+ * @returns `true` when the project metadata identifies a workspace scaffold.
44
+ */
45
+ export declare function isWorkspaceProject(projectDir: string): boolean;
31
46
  /**
32
47
  * Detects whether a scaffolded project is the official workspace template.
33
48
  *
@@ -25,9 +25,26 @@ export async function ensureScaffoldDirectory(targetDir, allowExisting = false)
25
25
  }
26
26
  const entries = await fsp.readdir(targetDir);
27
27
  if (entries.length > 0) {
28
- throw new Error(`Target directory is not empty: ${targetDir}`);
28
+ throw new Error(formatNonEmptyTargetDirectoryError(targetDir));
29
29
  }
30
30
  }
31
+ function readGeneratedPackageJson(projectDir) {
32
+ const packageJsonPath = path.join(projectDir, "package.json");
33
+ if (!fs.existsSync(packageJsonPath)) {
34
+ return null;
35
+ }
36
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
37
+ }
38
+ /**
39
+ * Format the actionable error message used when a scaffold target directory
40
+ * already exists and is not empty.
41
+ *
42
+ * @param targetDir Absolute path to the target directory being evaluated.
43
+ * @returns A human-readable error string with next-step guidance.
44
+ */
45
+ export function formatNonEmptyTargetDirectoryError(targetDir) {
46
+ return `Target directory is not empty: ${targetDir}. Choose a new project directory, or empty this directory before rerunning the scaffold.`;
47
+ }
31
48
  /**
32
49
  * Writes built-in starter manifest files into a scaffolded project.
33
50
  *
@@ -77,6 +94,15 @@ export async function seedBuiltInPersistenceArtifacts(targetDir, templateId, var
77
94
  });
78
95
  });
79
96
  }
97
+ /**
98
+ * Detects whether a scaffolded project declares the workspace project model.
99
+ *
100
+ * @param projectDir Absolute scaffold target directory.
101
+ * @returns `true` when the project metadata identifies a workspace scaffold.
102
+ */
103
+ export function isWorkspaceProject(projectDir) {
104
+ return readGeneratedPackageJson(projectDir)?.wpTypia?.projectType === "workspace";
105
+ }
80
106
  /**
81
107
  * Detects whether a scaffolded project is the official workspace template.
82
108
  *
@@ -84,12 +110,8 @@ export async function seedBuiltInPersistenceArtifacts(targetDir, templateId, var
84
110
  * @returns `true` when the project metadata identifies the official workspace template.
85
111
  */
86
112
  export function isOfficialWorkspaceProject(projectDir) {
87
- const packageJsonPath = path.join(projectDir, "package.json");
88
- if (!fs.existsSync(packageJsonPath)) {
89
- return false;
90
- }
91
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
92
- return (packageJson.wpTypia?.projectType === "workspace" &&
113
+ const packageJson = readGeneratedPackageJson(projectDir);
114
+ return (packageJson?.wpTypia?.projectType === "workspace" &&
93
115
  packageJson.wpTypia?.templatePackage === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE);
94
116
  }
95
117
  /**
@@ -1,6 +1,7 @@
1
1
  import { getPrimaryDevelopmentScript } from './local-dev-presets.js';
2
2
  import { getCompoundExtensionWorkflowSection, getInitialCommitCommands, getInitialCommitNote, getOptionalOnboardingNote, getOptionalOnboardingSteps, getQuickStartWorkflowNote, getPhpRestExtensionPointsSection, getTemplateSourceOfTruthNote, } from './scaffold-onboarding.js';
3
- import { formatInstallCommand, formatRunScript, } from './package-managers.js';
3
+ import { formatPackageExecCommand, formatInstallCommand, formatRunScript, } from './package-managers.js';
4
+ import { getPackageVersions } from './package-versions.js';
4
5
  /**
5
6
  * Builds the generated README markdown for one scaffolded project.
6
7
  *
@@ -22,12 +23,31 @@ export function buildReadme(templateId, variables, packageManager, { withMigrati
22
23
  const publicPersistencePolicyNote = variables.isPublicPersistencePolicy === 'true'
23
24
  ? 'Public persistence writes use signed short-lived tokens, per-request ids, and coarse rate limiting by default. Add application-specific abuse controls before using the same pattern for high-value metrics or experiments.'
24
25
  : null;
26
+ const alternateRenderTargetSection = variables.hasAlternateRenderTargets === 'true'
27
+ ? `## Alternate Render Targets\n\nThis scaffold keeps \`${templateId === 'compound' ? `src/blocks/${variables.slugKebabCase}/render.php` : 'src/render.php'}\` as the default web render boundary and also generates ${[
28
+ variables.hasAlternateEmailRenderTarget === 'true'
29
+ ? `\`${templateId === 'compound' ? `src/blocks/${variables.slugKebabCase}/render-email.php` : 'src/render-email.php'}\``
30
+ : null,
31
+ variables.hasAlternateMjmlRenderTarget === 'true'
32
+ ? `\`${templateId === 'compound' ? `src/blocks/${variables.slugKebabCase}/render-mjml.php` : 'src/render-mjml.php'}\``
33
+ : null,
34
+ variables.hasAlternatePlainTextRenderTarget === 'true'
35
+ ? `\`${templateId === 'compound' ? `src/blocks/${variables.slugKebabCase}/render-text.php` : 'src/render-text.php'}\``
36
+ : null,
37
+ ]
38
+ .filter((value) => Boolean(value))
39
+ .join(', ')}. All of those entries delegate through \`${templateId === 'compound' ? `src/blocks/${variables.slugKebabCase}/render-targets.php` : 'src/render-targets.php'}\`, so attribute normalization, validation, and render-target adapter hooks stay aligned across web, email, MJML, and plain-text integrations.`
40
+ : '';
25
41
  const compoundExtensionWorkflowSection = getCompoundExtensionWorkflowSection(packageManager, templateId);
42
+ const compoundInnerBlocksSection = templateId === 'compound'
43
+ ? `## Compound InnerBlocks Presets\n\nThis scaffold starts with the \`${variables.compoundInnerBlocksPreset}\` preset for compound container authoring. Static nested relationships still belong in each generated \`block.json\` via \`allowedBlocks\`, \`parent\`, and \`ancestor\`, while \`src/blocks/${variables.slugKebabCase}/children.ts\` owns editor-only \`InnerBlocks\` behavior such as \`orientation\`, \`templateLock\`, \`defaultBlock\`, and \`directInsert\`.\n\n- \`freeform\`: unlocked inserter flow with the starter child template.\n- \`ordered\`: vertical ordered flow with \`templateLock="insert"\` and direct inserts.\n- \`horizontal\`: row-like nested authoring with direct inserts.\n- \`locked-structure\`: fully locked starter structure.\n\nWhen you need to change that authoring behavior later, update the preset helpers in \`src/blocks/${variables.slugKebabCase}/children.ts\` and keep fixed child constraints metadata-owned instead of duplicating them in editor props. If you need tighter wrapper ownership or sibling markup control, switch the generated edit components to \`useInnerBlocksProps\` and reuse \`getRootInnerBlocksPropsOptions()\` / \`getChildInnerBlocksPropsOptions( metadata.name )\` so the same preset behavior carries forward without rebuilding the option set by hand.`
44
+ : '';
26
45
  const phpRestExtensionPointsSection = getPhpRestExtensionPointsSection(templateId, {
27
46
  compoundPersistenceEnabled,
28
47
  slug: variables.slug,
29
48
  });
30
49
  const developmentScript = getPrimaryDevelopmentScript(templateId);
50
+ const noStepsHeading = templateId === 'query-loop' ? 'Variation Workflow' : 'Artifact Refresh';
31
51
  const wpEnvSection = withWpEnv
32
52
  ? `## Local WordPress\n\n\`\`\`bash\n${formatRunScript(packageManager, 'wp-env:start')}\n${formatRunScript(packageManager, 'wp-env:stop')}\n${formatRunScript(packageManager, 'wp-env:reset')}\n\`\`\``
33
53
  : '';
@@ -41,7 +61,7 @@ export function buildReadme(templateId, variables, packageManager, { withMigrati
41
61
  ? `## Advanced Sync\n\n\`\`\`bash\n${optionalOnboardingSteps.join('\n')}\n\`\`\`\n\n${getOptionalOnboardingNote(packageManager, templateId, {
42
62
  compoundPersistenceEnabled,
43
63
  })}`
44
- : `## Artifact Refresh\n\n${getOptionalOnboardingNote(packageManager, templateId, {
64
+ : `## ${noStepsHeading}\n\n${getOptionalOnboardingNote(packageManager, templateId, {
45
65
  compoundPersistenceEnabled,
46
66
  })}`;
47
67
  return `# ${variables.title}
@@ -68,6 +88,7 @@ ${getQuickStartWorkflowNote(packageManager, templateId, {
68
88
  \`\`\`bash
69
89
  ${formatRunScript(packageManager, 'build')}
70
90
  ${formatRunScript(packageManager, 'typecheck')}
91
+ ${formatPackageExecCommand(packageManager, `wp-typia@${getPackageVersions().wpTypiaPackageExactVersion}`, 'doctor')}
71
92
  \`\`\`
72
93
 
73
94
  ${advancedSyncSection}
@@ -80,7 +101,7 @@ ${initialCommitCommands.join('\n')}
80
101
 
81
102
  ${getInitialCommitNote()}
82
103
 
83
- ${sourceOfTruthNote}${publicPersistencePolicyNote ? `\n\n${publicPersistencePolicyNote}` : ''}${migrationSection ? `\n\n${migrationSection}` : ''}${compoundExtensionWorkflowSection ? `\n\n${compoundExtensionWorkflowSection}` : ''}${wpEnvSection ? `\n\n${wpEnvSection}` : ''}${testPresetSection ? `\n\n${testPresetSection}` : ''}${phpRestExtensionPointsSection ? `\n\n${phpRestExtensionPointsSection}` : ''}
104
+ ${sourceOfTruthNote}${publicPersistencePolicyNote ? `\n\n${publicPersistencePolicyNote}` : ''}${alternateRenderTargetSection ? `\n\n${alternateRenderTargetSection}` : ''}${compoundInnerBlocksSection ? `\n\n${compoundInnerBlocksSection}` : ''}${migrationSection ? `\n\n${migrationSection}` : ''}${compoundExtensionWorkflowSection ? `\n\n${compoundExtensionWorkflowSection}` : ''}${wpEnvSection ? `\n\n${wpEnvSection}` : ''}${testPresetSection ? `\n\n${testPresetSection}` : ''}${phpRestExtensionPointsSection ? `\n\n${phpRestExtensionPointsSection}` : ''}
84
105
  `;
85
106
  }
86
107
  /**
@@ -10,6 +10,23 @@ export declare function validateTextDomain(input: string): true | string;
10
10
  export declare function validatePhpPrefix(input: string): true | string;
11
11
  export declare function assertValidIdentifier(label: string, value: string, validate: (value: string) => true | string): string;
12
12
  export declare function normalizeBlockSlug(input: string): string;
13
+ /**
14
+ * Normalize one human-entered block slug and reject values that collapse to an
15
+ * empty generated slug.
16
+ *
17
+ * @param options Normalization context for one block-like name.
18
+ * @param options.input Raw user input before kebab-case normalization.
19
+ * @param options.label Human-readable field label used in thrown errors.
20
+ * @param options.usage Example CLI usage shown when the input is blank.
21
+ * @returns A non-empty normalized slug.
22
+ * @throws When the input is blank after trimming.
23
+ * @throws When normalization removes every character from the original input.
24
+ */
25
+ export declare function resolveNonEmptyNormalizedBlockSlug(options: {
26
+ input: string;
27
+ label: string;
28
+ usage: string;
29
+ }): string;
13
30
  export declare function resolveValidatedBlockSlug(value: string): string;
14
31
  export declare function resolveValidatedNamespace(value: string): string;
15
32
  export declare function resolveValidatedTextDomain(value: string): string;
@@ -34,6 +34,28 @@ export function assertValidIdentifier(label, value, validate) {
34
34
  export function normalizeBlockSlug(input) {
35
35
  return toKebabCase(input);
36
36
  }
37
+ /**
38
+ * Normalize one human-entered block slug and reject values that collapse to an
39
+ * empty generated slug.
40
+ *
41
+ * @param options Normalization context for one block-like name.
42
+ * @param options.input Raw user input before kebab-case normalization.
43
+ * @param options.label Human-readable field label used in thrown errors.
44
+ * @param options.usage Example CLI usage shown when the input is blank.
45
+ * @returns A non-empty normalized slug.
46
+ * @throws When the input is blank after trimming.
47
+ * @throws When normalization removes every character from the original input.
48
+ */
49
+ export function resolveNonEmptyNormalizedBlockSlug(options) {
50
+ const normalizedSlug = normalizeBlockSlug(options.input);
51
+ if (normalizedSlug.length > 0) {
52
+ return normalizedSlug;
53
+ }
54
+ if (options.input.trim().length === 0) {
55
+ throw new Error(`${options.label} is required. Use \`${options.usage}\`.`);
56
+ }
57
+ throw new Error(`${options.label} "${options.input.trim()}" normalizes to an empty slug. Use letters or numbers so wp-typia can generate a block slug.`);
58
+ }
37
59
  export function resolveValidatedBlockSlug(value) {
38
60
  return assertValidIdentifier("Block slug", normalizeBlockSlug(value), validateBlockSlug);
39
61
  }
@@ -1,4 +1,5 @@
1
- import { formatRunScript } from "./package-managers.js";
1
+ import { formatPackageExecCommand, formatRunScript, } from "./package-managers.js";
2
+ import { getPackageVersions } from "./package-versions.js";
2
3
  import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
3
4
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
4
5
  const INITIAL_COMMIT_COMMANDS = [
@@ -6,6 +7,9 @@ const INITIAL_COMMIT_COMMANDS = [
6
7
  "git add .",
7
8
  'git commit -m "Initial scaffold"',
8
9
  ];
10
+ function getDoctorVerificationCommand(packageManager) {
11
+ return formatPackageExecCommand(packageManager, `wp-typia@${getPackageVersions().wpTypiaPackageExactVersion}`, "doctor");
12
+ }
9
13
  function templateHasPersistenceSync(templateId, { compoundPersistenceEnabled = false } = {}) {
10
14
  return templateId === "persistence" || (templateId === "compound" && compoundPersistenceEnabled);
11
15
  }
@@ -40,29 +44,31 @@ export function getOptionalOnboardingSteps(packageManager, templateId, options =
40
44
  * Returns the quick-start note explaining the scaffold's primary local loop.
41
45
  */
42
46
  export function getQuickStartWorkflowNote(packageManager, templateId = "basic", options = {}) {
47
+ const doctorCommand = getDoctorVerificationCommand(packageManager);
43
48
  if (templateId === "query-loop") {
44
- return `${formatRunScript(packageManager, "start")} runs the editor build/watch loop that registers your Query Loop variation in the block editor. This scaffold is editor-facing by design: update \`src/index.ts\` when you want to change the variation namespace, default query, allowed controls, or the minimal inline starter layout, update \`src/patterns/*.php\` when you want richer connected layout presets in the inserter, use \`src/query-extension.ts\` when the variation needs custom query params or optional editor-side hooks, and mirror frontend/editor preview query mapping in \`inc/query-runtime.php\`.`;
49
+ return `${formatRunScript(packageManager, "dev")} runs the editor build/watch loop that registers your Query Loop variation in the block editor. This scaffold intentionally skips \`src/types.ts\`, \`block.json\`, and Typia manifests because the source of truth lives in the variation registration flow. Update \`src/index.ts\` for variation defaults, \`src/patterns/*.php\` for richer connected layouts, \`src/query-extension.ts\` for custom query params, and \`inc/query-runtime.php\` for frontend/editor preview parity. Use ${doctorCommand} when you want a quick environment and workspace sanity check.`;
45
50
  }
46
51
  const developmentScript = getPrimaryDevelopmentScript(templateId);
47
52
  const devCommand = formatRunScript(packageManager, developmentScript);
48
53
  const startCommand = formatRunScript(packageManager, "start");
49
54
  if (developmentScript === "start") {
50
- return `${startCommand} is the primary local entry point for this template. If the template also exposes a dedicated watch mode or alternate editor workflow, follow that template-specific documentation alongside the generated project scripts.`;
55
+ return `${startCommand} is the primary local entry point for this template. Use ${doctorCommand} when you want a quick environment and workspace sanity check before build or commit.`;
51
56
  }
52
57
  if (developmentScript !== "dev") {
53
- return `${devCommand} is the primary local entry point for this template. Use ${startCommand} when you want the scaffold's one-shot startup flow instead of the watch-oriented workflow.`;
58
+ return `${devCommand} is the primary local entry point for this template. Use ${startCommand} for the one-shot startup flow, and ${doctorCommand} when you want a quick verification pass before build or commit.`;
54
59
  }
55
60
  if (templateHasPersistenceSync(templateId, options)) {
56
- return `${devCommand} keeps the editor, type-derived artifacts, and REST-derived artifacts moving together during local development. Use ${startCommand} when you want a one-shot sync plus editor startup without the long-running watch loop.`;
61
+ return `${devCommand} keeps the editor, type-derived artifacts, and REST-derived artifacts moving together during local development. Use ${startCommand} for a one-shot sync plus editor startup, and ${doctorCommand} when you want an explicit verification pass before build or commit.`;
57
62
  }
58
- return `${devCommand} keeps the editor and type-derived artifacts moving together during local development. Use ${startCommand} when you want a one-shot sync plus editor startup without the long-running watch loop.`;
63
+ return `${devCommand} keeps the editor and type-derived artifacts moving together during local development. Use ${startCommand} for a one-shot sync plus editor startup, and ${doctorCommand} when you want an explicit verification pass before build or commit.`;
59
64
  }
60
65
  /**
61
66
  * Returns the onboarding note explaining when manual sync is optional.
62
67
  */
63
68
  export function getOptionalOnboardingNote(packageManager, templateId = "basic", options = {}) {
69
+ const doctorCommand = getDoctorVerificationCommand(packageManager);
64
70
  if (templateId === "query-loop") {
65
- return `This scaffold does not generate \`block.json\` or Typia manifests. Edit \`src/index.ts\` to change the variation contract, edit \`src/patterns/*.php\` when you want richer connected layouts beyond the inline fallback, edit \`src/query-extension.ts\` when you need variation-specific query params or custom editor hooks, and edit \`inc/query-runtime.php\` when those params need frontend or editor preview parity. Then rerun ${formatRunScript(packageManager, "build")}, ${formatRunScript(packageManager, "start")}, or ${formatRunScript(packageManager, "typecheck")} as needed.`;
71
+ return `This scaffold owns a \`core/query\` variation, so it does not generate a \`sync\` script, \`src/types.ts\`, \`block.json\`, or Typia manifests. Edit \`src/index.ts\`, \`src/patterns/*.php\`, \`src/query-extension.ts\`, and \`inc/query-runtime.php\` as needed, then rerun ${formatRunScript(packageManager, "build")}, ${formatRunScript(packageManager, "typecheck")}, or ${doctorCommand}.`;
66
72
  }
67
73
  const optionalSyncScripts = getOptionalSyncScriptNames(templateId, options);
68
74
  const hasUnifiedSync = optionalSyncScripts.includes("sync");
@@ -76,7 +82,7 @@ export function getOptionalOnboardingNote(packageManager, templateId = "basic",
76
82
  const typecheckCommand = formatRunScript(packageManager, "typecheck");
77
83
  const strictSyncCommand = formatRunScript(packageManager, "sync-types", "--strict --report json");
78
84
  const advancedPersistenceNote = templateHasPersistenceSync(templateId, options)
79
- ? ` ${syncRestCommand} remains available for advanced REST-only refreshes, but it now fails fast when type-derived artifacts are stale; run \`${syncCommand}\` or \`${syncTypesCommand}\` first.`
85
+ ? ` ${syncRestCommand} remains available for REST-only refreshes after ${syncTypesCommand}.`
80
86
  : "";
81
87
  const isCustomTemplate = !isBuiltInTemplateId(templateId) &&
82
88
  templateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE;
@@ -86,12 +92,12 @@ export function getOptionalOnboardingNote(packageManager, templateId = "basic",
86
92
  ? `Run ${syncSteps.join(" then ")} manually before build, typecheck, or commit. ${syncCheckCommand} verifies the current type-derived artifacts without rewriting them.${optionalSyncScripts.includes("sync-rest") ? ` ${syncRestCommand} remains available for REST-only refreshes after ${syncTypesCommand}.` : ""}`
87
93
  : null;
88
94
  if (fallbackCustomTemplateNote) {
89
- return fallbackCustomTemplateNote;
95
+ return `${fallbackCustomTemplateNote} Use ${doctorCommand} when you want a quick environment and workspace sanity check.`;
90
96
  }
91
97
  if (isCustomTemplate && syncSteps.length === 0) {
92
- return "No optional sync command was detected for this custom template. Follow the template's own artifact-refresh guidance before build, typecheck, or your first commit.";
98
+ return `No optional sync command was detected for this custom template. Use ${doctorCommand} for a quick environment and workspace sanity check, then follow the template's own artifact-refresh guidance before build, typecheck, or your first commit.`;
93
99
  }
94
- return `You usually do not need to run ${syncCommand} during a normal ${formatRunScript(packageManager, developmentScript)} session. Run ${syncCommand} manually when you want a reviewable artifact refresh before ${formatRunScript(packageManager, "build")}, ${typecheckCommand}, or your first commit. ${syncTypesCommand} stays warn-only by default; use \`${failOnLossySyncCommand}\` to fail only on lossy WordPress projections, or \`${strictSyncCommand}\` for the stricter CI-oriented report.${advancedPersistenceNote} They do not create migration history. If this directory is new, create your first Git commit after that refresh.`;
100
+ return `You usually do not need to run ${syncCommand} during a normal ${formatRunScript(packageManager, developmentScript)} session. Run ${syncCommand} before ${formatRunScript(packageManager, "build")}, ${typecheckCommand}, or ${doctorCommand} when you want a reviewable refresh. ${syncTypesCommand} stays warn-only by default; use \`${failOnLossySyncCommand}\` or \`${strictSyncCommand}\` for stricter CI checks.${advancedPersistenceNote} Generated syncs do not create migration history, so refresh before your first commit if this directory is new.`;
95
101
  }
96
102
  /**
97
103
  * Returns the recommended version-control commands for a fresh scaffold.
@@ -110,7 +116,7 @@ export function getInitialCommitNote() {
110
116
  */
111
117
  export function getTemplateSourceOfTruthNote(templateId, { compoundPersistenceEnabled = false } = {}) {
112
118
  if (templateId === "query-loop") {
113
- return "`src/index.ts` remains the source of truth for the Query Loop variation name, default query attributes, `allowedControls`, and the minimal inline starter `innerBlocks`. Use `src/patterns/*.php` for richer connected layout presets that stay tied to the same variation namespace, use `src/query-extension.ts` for custom query seed values or optional editor-only hook registration, and use `inc/query-runtime.php` to keep frontend and editor preview query mapping aligned for those custom keys. The generated plugin bootstrap should stay focused on script registration, pattern loading, and explicit runtime glue for the variation.";
119
+ return "`src/index.ts` remains the source of truth for the Query Loop variation name, default query attributes, `allowedControls`, and the minimal inline starter `innerBlocks`. This scaffold intentionally does not generate `src/types.ts`, `block.json`, or Typia manifests because those artifacts belong to standalone block families, not `core/query` variation ownership. Use `src/patterns/*.php` for richer connected layout presets that stay tied to the same variation namespace, use `src/query-extension.ts` for custom query seed values or optional editor-only hook registration, and use `inc/query-runtime.php` to keep frontend and editor preview query mapping aligned for those custom keys. The generated plugin bootstrap should stay focused on script registration, pattern loading, and explicit runtime glue for the variation.";
114
120
  }
115
121
  if (templateId === "compound") {
116
122
  const compoundBase = "`src/blocks/*/types.ts` files remain the source of truth for each block's `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include starter `typia.manifest.json` files so editor imports resolve before the first sync.";
@@ -135,9 +141,15 @@ export function getCompoundExtensionWorkflowSection(packageManager, templateId)
135
141
 
136
142
  \`\`\`bash
137
143
  ${formatRunScript(packageManager, "add-child", '--slug faq-item --title "FAQ Item"')}
144
+
145
+ ${formatRunScript(packageManager, "add-child", '--slug section --title "Section" --container --inserter visible')}
146
+
147
+ ${formatRunScript(packageManager, "add-child", '--slug clause --title "Clause" --ancestor section --dry-run')}
148
+
149
+ ${formatRunScript(packageManager, "add-child", '--slug clause --title "Clause" --ancestor section')}
138
150
  \`\`\`
139
151
 
140
- This scaffolds a new hidden child block type, updates \`scripts/block-config.ts\` and \`src/blocks/*/children.ts\`, and leaves the default seeded child template unchanged.`;
152
+ This scaffolds additional compound child block types, updates \`scripts/block-config.ts\` and \`src/blocks/*/children.ts\`, and now supports root-level hidden children, visible container children, and nested ancestor chains for richer document-style block hierarchies. Pass \`--dry-run\` when you want a validated child-graph preview and planned write list before the script mutates files.`;
141
153
  }
142
154
  function formatPhpRestExtensionPointsSection({ apiTypesPath, extraNote, mainPhpPath, mainPhpScope, transportPath, }) {
143
155
  const schemaJsonGlob = apiTypesPath.replace(/api-types\.ts$/u, "api-schemas/*.schema.json");
@@ -38,7 +38,12 @@ export async function normalizePackageJson(targetDir, packageManagerId) {
38
38
  }
39
39
  const packageManager = getPackageManager(packageManagerId);
40
40
  const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
41
- packageJson.packageManager = packageManager.packageManagerField;
41
+ if (packageManagerId === "npm") {
42
+ delete packageJson.packageManager;
43
+ }
44
+ else {
45
+ packageJson.packageManager = packageManager.packageManagerField;
46
+ }
42
47
  if (packageJson.scripts) {
43
48
  for (const [key, value] of Object.entries(packageJson.scripts)) {
44
49
  if (typeof value === "string") {