@wp-typia/project-tools 0.22.10 → 0.23.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/cli-add-collision.d.ts +25 -0
  2. package/dist/runtime/cli-add-collision.js +76 -0
  3. package/dist/runtime/cli-add-help.js +11 -2
  4. package/dist/runtime/cli-add-kind-ids.d.ts +1 -1
  5. package/dist/runtime/cli-add-kind-ids.js +3 -0
  6. package/dist/runtime/cli-add-types.d.ts +117 -0
  7. package/dist/runtime/cli-add-types.js +26 -0
  8. package/dist/runtime/cli-add-validation.d.ts +90 -1
  9. package/dist/runtime/cli-add-validation.js +304 -1
  10. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +74 -19
  11. package/dist/runtime/cli-add-workspace-admin-view-source.js +11 -2
  12. package/dist/runtime/cli-add-workspace-admin-view-templates.d.ts +20 -2
  13. package/dist/runtime/cli-add-workspace-admin-view-templates.js +359 -3
  14. package/dist/runtime/cli-add-workspace-admin-view-types.d.ts +21 -0
  15. package/dist/runtime/cli-add-workspace-admin-view-types.js +22 -0
  16. package/dist/runtime/cli-add-workspace-ai-anchors.js +121 -31
  17. package/dist/runtime/cli-add-workspace-contract-source-emitters.d.ts +15 -0
  18. package/dist/runtime/cli-add-workspace-contract-source-emitters.js +42 -0
  19. package/dist/runtime/cli-add-workspace-contract.d.ts +15 -0
  20. package/dist/runtime/cli-add-workspace-contract.js +65 -0
  21. package/dist/runtime/cli-add-workspace-integration-env.d.ts +24 -0
  22. package/dist/runtime/cli-add-workspace-integration-env.js +391 -0
  23. package/dist/runtime/cli-add-workspace-post-meta-anchors.d.ts +23 -0
  24. package/dist/runtime/cli-add-workspace-post-meta-anchors.js +244 -0
  25. package/dist/runtime/cli-add-workspace-post-meta-source-emitters.d.ts +63 -0
  26. package/dist/runtime/cli-add-workspace-post-meta-source-emitters.js +179 -0
  27. package/dist/runtime/cli-add-workspace-post-meta.d.ts +15 -0
  28. package/dist/runtime/cli-add-workspace-post-meta.js +107 -0
  29. package/dist/runtime/cli-add-workspace-rest-anchors.d.ts +1 -0
  30. package/dist/runtime/cli-add-workspace-rest-anchors.js +285 -21
  31. package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +90 -2
  32. package/dist/runtime/cli-add-workspace-rest-source-emitters.js +302 -29
  33. package/dist/runtime/cli-add-workspace-rest.d.ts +15 -2
  34. package/dist/runtime/cli-add-workspace-rest.js +329 -21
  35. package/dist/runtime/cli-add-workspace.d.ts +15 -0
  36. package/dist/runtime/cli-add-workspace.js +15 -0
  37. package/dist/runtime/cli-add.d.ts +1 -1
  38. package/dist/runtime/cli-add.js +1 -1
  39. package/dist/runtime/cli-core.d.ts +2 -1
  40. package/dist/runtime/cli-core.js +2 -1
  41. package/dist/runtime/cli-doctor-environment.js +1 -3
  42. package/dist/runtime/cli-doctor-workspace-features.js +128 -10
  43. package/dist/runtime/cli-doctor-workspace-package.d.ts +25 -3
  44. package/dist/runtime/cli-doctor-workspace-package.js +35 -13
  45. package/dist/runtime/cli-doctor-workspace-shared.d.ts +2 -0
  46. package/dist/runtime/cli-doctor-workspace-shared.js +2 -0
  47. package/dist/runtime/cli-doctor-workspace.js +8 -3
  48. package/dist/runtime/cli-help.js +7 -0
  49. package/dist/runtime/cli-init-templates.js +11 -1
  50. package/dist/runtime/contract-artifacts.d.ts +14 -0
  51. package/dist/runtime/contract-artifacts.js +15 -0
  52. package/dist/runtime/index.d.ts +1 -1
  53. package/dist/runtime/index.js +1 -1
  54. package/dist/runtime/rest-resource-artifacts.d.ts +57 -1
  55. package/dist/runtime/rest-resource-artifacts.js +97 -1
  56. package/dist/runtime/template-render.d.ts +1 -1
  57. package/dist/runtime/template-render.js +1 -1
  58. package/dist/runtime/template-source-cache-markers.d.ts +37 -0
  59. package/dist/runtime/template-source-cache-markers.js +125 -0
  60. package/dist/runtime/template-source-cache.d.ts +1 -4
  61. package/dist/runtime/template-source-cache.js +16 -122
  62. package/dist/runtime/template-source-external.d.ts +4 -2
  63. package/dist/runtime/template-source-external.js +4 -2
  64. package/dist/runtime/template-source-remote.d.ts +8 -4
  65. package/dist/runtime/template-source-remote.js +8 -4
  66. package/dist/runtime/workspace-inventory-mutations.js +52 -3
  67. package/dist/runtime/workspace-inventory-parser.d.ts +3 -2
  68. package/dist/runtime/workspace-inventory-parser.js +126 -5
  69. package/dist/runtime/workspace-inventory-read.d.ts +9 -2
  70. package/dist/runtime/workspace-inventory-read.js +9 -2
  71. package/dist/runtime/workspace-inventory-templates.d.ts +16 -1
  72. package/dist/runtime/workspace-inventory-templates.js +74 -4
  73. package/dist/runtime/workspace-inventory-types.d.ts +51 -2
  74. package/dist/runtime/workspace-inventory.d.ts +2 -2
  75. package/dist/runtime/workspace-inventory.js +1 -1
  76. package/package.json +2 -2
@@ -1,50 +1,68 @@
1
1
  import { promises as fsp } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ensureBlockConfigCanAddRestManifests } from "./cli-add-block-legacy-validator.js";
4
- import { assertRestResourceDoesNotExist, assertValidGeneratedSlug, assertValidRestResourceMethods, getWorkspaceBootstrapPath, normalizeBlockSlug, resolveRestResourceNamespace, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
4
+ import { assertValidManualRestContractAuth, assertValidManualRestContractHttpMethod, assertRestResourceDoesNotExist, assertValidGeneratedSlug, assertValidRestResourceMethods, assertValidTypeScriptIdentifier, getWorkspaceBootstrapPath, normalizeBlockSlug, resolveGeneratedRestResourceRoutePattern, resolveManualRestContractPathPattern, resolveOptionalPhpCallbackReference, resolveOptionalPhpClassReference, resolveRestResourceNamespace, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
5
5
  import { ensureRestResourceBootstrapAnchors, ensureRestResourceSyncScriptAnchors, } from "./cli-add-workspace-rest-anchors.js";
6
- import { buildRestResourceApiSource, buildRestResourceConfigEntry, buildRestResourceDataSource, buildRestResourceTypesSource, buildRestResourceValidatorsSource, } from "./cli-add-workspace-rest-source-emitters.js";
6
+ import { buildManualRestContractApiSource, buildManualRestContractConfigEntry, buildManualRestContractTypesSource, buildManualRestContractValidatorsSource, buildRestResourceApiSource, buildRestResourceConfigEntry, buildRestResourceDataSource, buildRestResourceTypesSource, buildRestResourceValidatorsSource, } from "./cli-add-workspace-rest-source-emitters.js";
7
7
  import { quotePhpString } from "./php-utils.js";
8
- import { syncRestResourceArtifacts } from "./rest-resource-artifacts.js";
8
+ import { syncManualRestContractArtifacts, syncRestResourceArtifacts, } from "./rest-resource-artifacts.js";
9
9
  import { toPascalCase, toTitleCase } from "./string-case.js";
10
10
  import { appendWorkspaceInventoryEntries, readWorkspaceInventoryAsync, } from "./workspace-inventory.js";
11
11
  import { resolveWorkspaceProject } from "./workspace-project.js";
12
- function buildRestResourceRouteRegistrations(restResourceSlug, methods, functions) {
12
+ const MANUAL_REST_REQUEST_BODY_FIELD_NAMES = new Set(["payload", "comment"]);
13
+ const MANUAL_REST_RESPONSE_FIELD_NAMES = new Set([
14
+ "id",
15
+ "status",
16
+ "message",
17
+ "updatedAt",
18
+ ]);
19
+ function buildRestResourceRouteRegistrations(restResourceSlug, methods, functions, options) {
13
20
  const collectionRoutes = [];
14
21
  const itemRoutes = [];
22
+ const readPermissionCallback = options.permissionCallback
23
+ ? quotePhpString(options.permissionCallback)
24
+ : quotePhpString("__return_true");
25
+ const writePermissionCallback = options.permissionCallback
26
+ ? quotePhpString(options.permissionCallback)
27
+ : options.controllerVariableName
28
+ ? `array( ${options.controllerVariableName}, 'can_manage_rest_resource' )`
29
+ : quotePhpString(functions.canWriteFunctionName);
30
+ const buildRouteCallback = (functionName, methodName) => options.controllerVariableName
31
+ ? `array( ${options.controllerVariableName}, '${methodName}' )`
32
+ : quotePhpString(functionName);
15
33
  if (methods.includes("list")) {
16
34
  collectionRoutes.push(`\t\tarray(
17
35
  \t\t\t'methods' => WP_REST_Server::READABLE,
18
- \t\t\t'callback' => '${functions.listHandlerName}',
19
- \t\t\t'permission_callback' => '__return_true',
36
+ \t\t\t'callback' => ${buildRouteCallback(functions.listHandlerName, "list_items")},
37
+ \t\t\t'permission_callback' => ${readPermissionCallback},
20
38
  \t\t)`);
21
39
  }
22
40
  if (methods.includes("create")) {
23
41
  collectionRoutes.push(`\t\tarray(
24
42
  \t\t\t'methods' => WP_REST_Server::CREATABLE,
25
- \t\t\t'callback' => '${functions.createHandlerName}',
26
- \t\t\t'permission_callback' => '${functions.canWriteFunctionName}',
43
+ \t\t\t'callback' => ${buildRouteCallback(functions.createHandlerName, "create_item")},
44
+ \t\t\t'permission_callback' => ${writePermissionCallback},
27
45
  \t\t)`);
28
46
  }
29
47
  if (methods.includes("read")) {
30
48
  itemRoutes.push(`\t\tarray(
31
49
  \t\t\t'methods' => WP_REST_Server::READABLE,
32
- \t\t\t'callback' => '${functions.readHandlerName}',
33
- \t\t\t'permission_callback' => '__return_true',
50
+ \t\t\t'callback' => ${buildRouteCallback(functions.readHandlerName, "read_item")},
51
+ \t\t\t'permission_callback' => ${readPermissionCallback},
34
52
  \t\t)`);
35
53
  }
36
54
  if (methods.includes("update")) {
37
55
  itemRoutes.push(`\t\tarray(
38
56
  \t\t\t'methods' => WP_REST_Server::EDITABLE,
39
- \t\t\t'callback' => '${functions.updateHandlerName}',
40
- \t\t\t'permission_callback' => '${functions.canWriteFunctionName}',
57
+ \t\t\t'callback' => ${buildRouteCallback(functions.updateHandlerName, "update_item")},
58
+ \t\t\t'permission_callback' => ${writePermissionCallback},
41
59
  \t\t)`);
42
60
  }
43
61
  if (methods.includes("delete")) {
44
62
  itemRoutes.push(`\t\tarray(
45
63
  \t\t\t'methods' => WP_REST_Server::DELETABLE,
46
- \t\t\t'callback' => '${functions.deleteHandlerName}',
47
- \t\t\t'permission_callback' => '${functions.canWriteFunctionName}',
64
+ \t\t\t'callback' => ${buildRouteCallback(functions.deleteHandlerName, "delete_item")},
65
+ \t\t\t'permission_callback' => ${writePermissionCallback},
48
66
  \t\t)`);
49
67
  }
50
68
  const registrations = [];
@@ -60,7 +78,7 @@ ${collectionRoutes.join(",\n")}
60
78
  if (itemRoutes.length > 0) {
61
79
  registrations.push(`\tregister_rest_route(
62
80
  \t\t$namespace,
63
- \t\t'/${restResourceSlug}/item',
81
+ \t\t${quotePhpString(options.routePattern)},
64
82
  \t\tarray(
65
83
  ${itemRoutes.join(",\n")}
66
84
  \t\t)
@@ -68,7 +86,57 @@ ${itemRoutes.join(",\n")}
68
86
  }
69
87
  return registrations.join("\n\n");
70
88
  }
71
- function buildRestResourcePhpSource(restResourceSlug, namespace, phpPrefix, textDomain, methods) {
89
+ function normalizeGlobalPhpClassName(classReference) {
90
+ const normalized = classReference.startsWith("\\")
91
+ ? classReference.slice(1)
92
+ : classReference;
93
+ return /^[A-Za-z_][A-Za-z0-9_]*$/u.test(normalized) ? normalized : undefined;
94
+ }
95
+ function toPhpClassConstantReference(classReference) {
96
+ const normalized = classReference.startsWith("\\")
97
+ ? classReference
98
+ : `\\${classReference}`;
99
+ return `${normalized}::class`;
100
+ }
101
+ function buildRestResourceControllerClassSource(options) {
102
+ const controllerClassName = normalizeGlobalPhpClassName(options.controllerClass);
103
+ if (!controllerClassName) {
104
+ return "";
105
+ }
106
+ const extendsClause = options.controllerExtends
107
+ ? ` extends ${options.controllerExtends.startsWith("\\") ? options.controllerExtends : `\\${options.controllerExtends}`}`
108
+ : "";
109
+ return `
110
+ if ( ! class_exists( ${quotePhpString(controllerClassName)} ) ) {
111
+ \tclass ${controllerClassName}${extendsClause} {
112
+ \t\tpublic function can_manage_rest_resource() {
113
+ \t\t\treturn ${options.functions.canWriteFunctionName}();
114
+ \t\t}
115
+
116
+ \t\tpublic function list_items( WP_REST_Request $request ) {
117
+ \t\t\treturn ${options.functions.listHandlerName}( $request );
118
+ \t\t}
119
+
120
+ \t\tpublic function read_item( WP_REST_Request $request ) {
121
+ \t\t\treturn ${options.functions.readHandlerName}( $request );
122
+ \t\t}
123
+
124
+ \t\tpublic function create_item( WP_REST_Request $request ) {
125
+ \t\t\treturn ${options.functions.createHandlerName}( $request );
126
+ \t\t}
127
+
128
+ \t\tpublic function update_item( WP_REST_Request $request ) {
129
+ \t\t\treturn ${options.functions.updateHandlerName}( $request );
130
+ \t\t}
131
+
132
+ \t\tpublic function delete_item( WP_REST_Request $request ) {
133
+ \t\t\treturn ${options.functions.deleteHandlerName}( $request );
134
+ \t\t}
135
+ \t}
136
+ }
137
+ `;
138
+ }
139
+ function buildRestResourcePhpSource(restResourceSlug, namespace, phpPrefix, textDomain, methods, options) {
72
140
  const restResourceTitle = toTitleCase(restResourceSlug);
73
141
  const restResourcePhpId = restResourceSlug.replace(/-/g, "_");
74
142
  const canWriteFunctionName = `${phpPrefix}_${restResourcePhpId}_can_manage_rest_resource`;
@@ -85,6 +153,7 @@ function buildRestResourcePhpSource(restResourceSlug, namespace, phpPrefix, text
85
153
  const updateHandlerName = `${phpPrefix}_${restResourcePhpId}_handle_update_rest_resource`;
86
154
  const deleteHandlerName = `${phpPrefix}_${restResourcePhpId}_handle_delete_rest_resource`;
87
155
  const registerRoutesFunctionName = `${phpPrefix}_${restResourcePhpId}_register_rest_routes`;
156
+ const controllerVariableName = options.controllerClass ? "$controller" : undefined;
88
157
  const routeRegistrations = buildRestResourceRouteRegistrations(restResourceSlug, methods, {
89
158
  canWriteFunctionName,
90
159
  createHandlerName,
@@ -92,7 +161,38 @@ function buildRestResourcePhpSource(restResourceSlug, namespace, phpPrefix, text
92
161
  listHandlerName,
93
162
  readHandlerName,
94
163
  updateHandlerName,
164
+ }, {
165
+ ...(controllerVariableName ? { controllerVariableName } : {}),
166
+ ...(options.permissionCallback
167
+ ? { permissionCallback: options.permissionCallback }
168
+ : {}),
169
+ routePattern: options.routePattern,
95
170
  });
171
+ const controllerClassSource = options.controllerClass
172
+ ? buildRestResourceControllerClassSource({
173
+ controllerClass: options.controllerClass,
174
+ ...(options.controllerExtends
175
+ ? { controllerExtends: options.controllerExtends }
176
+ : {}),
177
+ functions: {
178
+ canWriteFunctionName,
179
+ createHandlerName,
180
+ deleteHandlerName,
181
+ listHandlerName,
182
+ readHandlerName,
183
+ updateHandlerName,
184
+ },
185
+ })
186
+ : "";
187
+ const controllerBootstrapSource = options.controllerClass
188
+ ? `\t\t$controller_class = ${toPhpClassConstantReference(options.controllerClass)};
189
+ \t\tif ( ! class_exists( $controller_class ) ) {
190
+ \t\t\treturn;
191
+ \t\t}
192
+ \t\t$controller = new $controller_class();
193
+
194
+ `
195
+ : "";
96
196
  return `<?php
97
197
  if ( ! defined( 'ABSPATH' ) ) {
98
198
  \treturn;
@@ -411,10 +511,12 @@ if ( ! function_exists( '${deleteHandlerName}' ) ) {
411
511
  \t}
412
512
  }
413
513
 
514
+ ${controllerClassSource}
414
515
  if ( ! function_exists( '${registerRoutesFunctionName}' ) ) {
415
516
  \tfunction ${registerRoutesFunctionName}() {
416
517
  \t\t$namespace = ${quotePhpString(namespace)};
417
518
 
519
+ ${controllerBootstrapSource}
418
520
  ${routeRegistrations}
419
521
  \t}
420
522
  }
@@ -429,20 +531,189 @@ add_action( 'rest_api_init', '${registerRoutesFunctionName}' );
429
531
  * @param options Command options for the REST resource scaffold workflow.
430
532
  * @returns Resolved scaffold metadata for the created REST resource.
431
533
  */
432
- export async function runAddRestResourceCommand({ cwd = process.cwd(), methods, namespace, restResourceName, }) {
534
+ export async function runAddRestResourceCommand({ auth, bodyTypeName, controllerClass, controllerExtends, cwd = process.cwd(), manual, method, methods, namespace, permissionCallback, pathPattern, queryTypeName, restResourceName, responseTypeName, routePattern, secretFieldName, secretStateFieldName, }) {
433
535
  const workspace = resolveWorkspaceProject(cwd);
434
536
  const restResourceSlug = assertValidGeneratedSlug("REST resource name", normalizeBlockSlug(restResourceName), "wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <list,read,create>]");
435
- const resolvedMethods = assertValidRestResourceMethods(methods);
436
537
  const resolvedNamespace = resolveRestResourceNamespace(workspace.workspace.namespace, namespace);
437
538
  const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
438
539
  assertRestResourceDoesNotExist(workspace.projectDir, restResourceSlug, inventory);
439
540
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
440
- const bootstrapPath = getWorkspaceBootstrapPath(workspace);
441
541
  const syncRestScriptPath = path.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
442
542
  const restResourceDir = path.join(workspace.projectDir, "src", "rest", restResourceSlug);
443
543
  const typesFilePath = path.join(restResourceDir, "api-types.ts");
444
544
  const validatorsFilePath = path.join(restResourceDir, "api-validators.ts");
445
545
  const apiFilePath = path.join(restResourceDir, "api.ts");
546
+ if (manual) {
547
+ if (controllerClass || controllerExtends || permissionCallback || routePattern) {
548
+ throw new Error("Manual REST contracts do not generate PHP route glue. Use generated rest-resource mode for --route-pattern, --permission-callback, --controller-class, or --controller-extends.");
549
+ }
550
+ const pascalCase = toPascalCase(restResourceSlug);
551
+ const resolvedAuth = assertValidManualRestContractAuth(auth);
552
+ const resolvedMethod = assertValidManualRestContractHttpMethod(method);
553
+ const resolvedPathPattern = resolveManualRestContractPathPattern(restResourceSlug, pathPattern);
554
+ const resolvedQueryTypeName = assertValidTypeScriptIdentifier("Manual REST contract query type", queryTypeName ?? `${pascalCase}Query`, "wp-typia add rest-resource <name> --manual [--query-type <ExportedQueryType>]");
555
+ const resolvedResponseTypeName = assertValidTypeScriptIdentifier("Manual REST contract response type", responseTypeName ?? `${pascalCase}Response`, "wp-typia add rest-resource <name> --manual [--response-type <ExportedResponseType>]");
556
+ const defaultsToBody = bodyTypeName == null && ["PATCH", "POST", "PUT"].includes(resolvedMethod);
557
+ const resolvedBodyTypeName = bodyTypeName != null || defaultsToBody
558
+ ? assertValidTypeScriptIdentifier("Manual REST contract body type", bodyTypeName ?? `${pascalCase}Request`, "wp-typia add rest-resource <name> --manual [--body-type <ExportedBodyType>]")
559
+ : undefined;
560
+ if (resolvedMethod === "GET" && resolvedBodyTypeName) {
561
+ throw new Error("Manual REST contract GET routes cannot define a body type. Remove --body-type or use POST, PUT, or PATCH.");
562
+ }
563
+ if (secretStateFieldName && !secretFieldName) {
564
+ throw new Error("Manual REST contract --secret-state-field requires --secret-field.");
565
+ }
566
+ if (secretFieldName && !resolvedBodyTypeName) {
567
+ throw new Error("Manual REST contract secret fields require a request body. Use POST, PUT, or PATCH so a request body is generated.");
568
+ }
569
+ const resolvedSecretFieldName = secretFieldName
570
+ ? assertValidTypeScriptIdentifier("Manual REST contract secret field", secretFieldName, "wp-typia add rest-resource <name> --manual --method POST --secret-field <field>")
571
+ : undefined;
572
+ const resolvedSecretStateFieldName = resolvedSecretFieldName
573
+ ? assertValidTypeScriptIdentifier("Manual REST contract secret state field", secretStateFieldName ?? `has${toPascalCase(resolvedSecretFieldName)}`, "wp-typia add rest-resource <name> --manual --method POST --secret-state-field <field>")
574
+ : undefined;
575
+ if (resolvedSecretFieldName &&
576
+ MANUAL_REST_REQUEST_BODY_FIELD_NAMES.has(resolvedSecretFieldName)) {
577
+ throw new Error(`Manual REST contract secret field must not reuse scaffolded request body fields: ${Array.from(MANUAL_REST_REQUEST_BODY_FIELD_NAMES).join(", ")}.`);
578
+ }
579
+ if (resolvedSecretStateFieldName &&
580
+ MANUAL_REST_RESPONSE_FIELD_NAMES.has(resolvedSecretStateFieldName)) {
581
+ throw new Error(`Manual REST contract secret state field must not reuse scaffolded response fields: ${Array.from(MANUAL_REST_RESPONSE_FIELD_NAMES).join(", ")}.`);
582
+ }
583
+ if (resolvedSecretFieldName &&
584
+ resolvedSecretStateFieldName &&
585
+ resolvedSecretFieldName === resolvedSecretStateFieldName) {
586
+ throw new Error("Manual REST contract secret state field must be different from the raw secret field.");
587
+ }
588
+ const manualTypeNames = [
589
+ resolvedQueryTypeName,
590
+ resolvedResponseTypeName,
591
+ resolvedBodyTypeName,
592
+ ].filter((value) => value != null);
593
+ const duplicateManualTypeNames = manualTypeNames.filter((name, index) => manualTypeNames.indexOf(name) !== index);
594
+ if (duplicateManualTypeNames.length > 0) {
595
+ throw new Error(`Manual REST contract type names must be unique: ${Array.from(new Set(duplicateManualTypeNames)).join(", ")}. Use distinct --query-type, --body-type, and --response-type values.`);
596
+ }
597
+ const mutationSnapshot = {
598
+ fileSources: await snapshotWorkspaceFiles([
599
+ blockConfigPath,
600
+ syncRestScriptPath,
601
+ ]),
602
+ snapshotDirs: [],
603
+ targetPaths: [restResourceDir],
604
+ };
605
+ try {
606
+ await fsp.mkdir(restResourceDir, { recursive: true });
607
+ await ensureRestResourceSyncScriptAnchors(workspace);
608
+ await fsp.writeFile(typesFilePath, buildManualRestContractTypesSource({
609
+ ...(resolvedBodyTypeName
610
+ ? { bodyTypeName: resolvedBodyTypeName }
611
+ : {}),
612
+ queryTypeName: resolvedQueryTypeName,
613
+ responseTypeName: resolvedResponseTypeName,
614
+ restResourceSlug,
615
+ ...(resolvedSecretFieldName
616
+ ? { secretFieldName: resolvedSecretFieldName }
617
+ : {}),
618
+ ...(resolvedSecretStateFieldName
619
+ ? { secretStateFieldName: resolvedSecretStateFieldName }
620
+ : {}),
621
+ }), "utf8");
622
+ await fsp.writeFile(validatorsFilePath, buildManualRestContractValidatorsSource({
623
+ ...(resolvedBodyTypeName
624
+ ? { bodyTypeName: resolvedBodyTypeName }
625
+ : {}),
626
+ queryTypeName: resolvedQueryTypeName,
627
+ responseTypeName: resolvedResponseTypeName,
628
+ }), "utf8");
629
+ await fsp.writeFile(apiFilePath, buildManualRestContractApiSource({
630
+ ...(resolvedBodyTypeName
631
+ ? { bodyTypeName: resolvedBodyTypeName }
632
+ : {}),
633
+ queryTypeName: resolvedQueryTypeName,
634
+ restResourceSlug,
635
+ }), "utf8");
636
+ await syncManualRestContractArtifacts({
637
+ clientFile: `src/rest/${restResourceSlug}/api-client.ts`,
638
+ outputDir: restResourceDir,
639
+ projectDir: workspace.projectDir,
640
+ typesFile: `src/rest/${restResourceSlug}/api-types.ts`,
641
+ validatorsFile: `src/rest/${restResourceSlug}/api-validators.ts`,
642
+ variables: {
643
+ auth: resolvedAuth,
644
+ ...(resolvedBodyTypeName
645
+ ? { bodyTypeName: resolvedBodyTypeName }
646
+ : {}),
647
+ method: resolvedMethod,
648
+ namespace: resolvedNamespace,
649
+ pascalCase,
650
+ pathPattern: resolvedPathPattern,
651
+ queryTypeName: resolvedQueryTypeName,
652
+ responseTypeName: resolvedResponseTypeName,
653
+ slugKebabCase: restResourceSlug,
654
+ title: toTitleCase(restResourceSlug),
655
+ },
656
+ });
657
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
658
+ restResourceEntries: [
659
+ buildManualRestContractConfigEntry({
660
+ auth: resolvedAuth,
661
+ ...(resolvedBodyTypeName
662
+ ? { bodyTypeName: resolvedBodyTypeName }
663
+ : {}),
664
+ method: resolvedMethod,
665
+ namespace: resolvedNamespace,
666
+ pathPattern: resolvedPathPattern,
667
+ queryTypeName: resolvedQueryTypeName,
668
+ responseTypeName: resolvedResponseTypeName,
669
+ restResourceSlug,
670
+ ...(resolvedSecretFieldName
671
+ ? { secretFieldName: resolvedSecretFieldName }
672
+ : {}),
673
+ ...(resolvedSecretStateFieldName
674
+ ? { secretStateFieldName: resolvedSecretStateFieldName }
675
+ : {}),
676
+ }),
677
+ ],
678
+ transformSource: ensureBlockConfigCanAddRestManifests,
679
+ });
680
+ return {
681
+ auth: resolvedAuth,
682
+ ...(resolvedBodyTypeName
683
+ ? { bodyTypeName: resolvedBodyTypeName }
684
+ : {}),
685
+ method: resolvedMethod,
686
+ methods: [],
687
+ mode: "manual",
688
+ namespace: resolvedNamespace,
689
+ pathPattern: resolvedPathPattern,
690
+ projectDir: workspace.projectDir,
691
+ queryTypeName: resolvedQueryTypeName,
692
+ restResourceSlug,
693
+ responseTypeName: resolvedResponseTypeName,
694
+ ...(resolvedSecretFieldName
695
+ ? { secretFieldName: resolvedSecretFieldName }
696
+ : {}),
697
+ ...(resolvedSecretStateFieldName
698
+ ? { secretStateFieldName: resolvedSecretStateFieldName }
699
+ : {}),
700
+ };
701
+ }
702
+ catch (error) {
703
+ await rollbackWorkspaceMutation(mutationSnapshot);
704
+ throw error;
705
+ }
706
+ }
707
+ const resolvedMethods = assertValidRestResourceMethods(methods);
708
+ const resolvedRoutePattern = resolveGeneratedRestResourceRoutePattern(restResourceSlug, routePattern);
709
+ const hasCustomRoutePattern = typeof routePattern === "string" && routePattern.trim().length > 0;
710
+ const resolvedPermissionCallback = resolveOptionalPhpCallbackReference("Generated REST resource permission callback", permissionCallback);
711
+ const resolvedControllerClass = resolveOptionalPhpClassReference("Generated REST resource controller class", controllerClass);
712
+ const resolvedControllerExtends = resolveOptionalPhpClassReference("Generated REST resource controller base class", controllerExtends);
713
+ if (resolvedControllerExtends && !resolvedControllerClass) {
714
+ throw new Error("Generated REST resource controller base class requires --controller-class.");
715
+ }
716
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
446
717
  const dataFilePath = path.join(restResourceDir, "data.ts");
447
718
  const phpFilePath = path.join(workspace.projectDir, "inc", "rest", `${restResourceSlug}.php`);
448
719
  const mutationSnapshot = {
@@ -463,7 +734,18 @@ export async function runAddRestResourceCommand({ cwd = process.cwd(), methods,
463
734
  await fsp.writeFile(validatorsFilePath, buildRestResourceValidatorsSource(restResourceSlug, resolvedMethods), "utf8");
464
735
  await fsp.writeFile(apiFilePath, buildRestResourceApiSource(restResourceSlug, resolvedMethods), "utf8");
465
736
  await fsp.writeFile(dataFilePath, buildRestResourceDataSource(restResourceSlug, resolvedMethods), "utf8");
466
- await fsp.writeFile(phpFilePath, buildRestResourcePhpSource(restResourceSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain, resolvedMethods), "utf8");
737
+ await fsp.writeFile(phpFilePath, buildRestResourcePhpSource(restResourceSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain, resolvedMethods, {
738
+ ...(resolvedControllerClass
739
+ ? { controllerClass: resolvedControllerClass }
740
+ : {}),
741
+ ...(resolvedControllerExtends
742
+ ? { controllerExtends: resolvedControllerExtends }
743
+ : {}),
744
+ ...(resolvedPermissionCallback
745
+ ? { permissionCallback: resolvedPermissionCallback }
746
+ : {}),
747
+ routePattern: resolvedRoutePattern,
748
+ }), "utf8");
467
749
  await syncRestResourceArtifacts({
468
750
  clientFile: `src/rest/${restResourceSlug}/api-client.ts`,
469
751
  methods: resolvedMethods,
@@ -474,21 +756,47 @@ export async function runAddRestResourceCommand({ cwd = process.cwd(), methods,
474
756
  variables: {
475
757
  namespace: resolvedNamespace,
476
758
  pascalCase: toPascalCase(restResourceSlug),
759
+ ...(hasCustomRoutePattern ? { routePattern: resolvedRoutePattern } : {}),
477
760
  slugKebabCase: restResourceSlug,
478
761
  title: toTitleCase(restResourceSlug),
479
762
  },
480
763
  });
481
764
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
482
765
  restResourceEntries: [
483
- buildRestResourceConfigEntry(restResourceSlug, resolvedNamespace, resolvedMethods),
766
+ buildRestResourceConfigEntry({
767
+ ...(resolvedControllerClass
768
+ ? { controllerClass: resolvedControllerClass }
769
+ : {}),
770
+ ...(resolvedControllerExtends
771
+ ? { controllerExtends: resolvedControllerExtends }
772
+ : {}),
773
+ methods: resolvedMethods,
774
+ namespace: resolvedNamespace,
775
+ ...(resolvedPermissionCallback
776
+ ? { permissionCallback: resolvedPermissionCallback }
777
+ : {}),
778
+ restResourceSlug,
779
+ ...(hasCustomRoutePattern ? { routePattern: resolvedRoutePattern } : {}),
780
+ }),
484
781
  ],
485
782
  transformSource: ensureBlockConfigCanAddRestManifests,
486
783
  });
487
784
  return {
785
+ ...(resolvedControllerClass
786
+ ? { controllerClass: resolvedControllerClass }
787
+ : {}),
788
+ ...(resolvedControllerExtends
789
+ ? { controllerExtends: resolvedControllerExtends }
790
+ : {}),
488
791
  methods: resolvedMethods,
792
+ mode: "generated",
489
793
  namespace: resolvedNamespace,
794
+ ...(resolvedPermissionCallback
795
+ ? { permissionCallback: resolvedPermissionCallback }
796
+ : {}),
490
797
  projectDir: workspace.projectDir,
491
798
  restResourceSlug,
799
+ ...(hasCustomRoutePattern ? { routePattern: resolvedRoutePattern } : {}),
492
800
  };
493
801
  }
494
802
  catch (error) {
@@ -10,11 +10,26 @@ export { runAddAdminViewCommand, } from "./cli-add-workspace-admin-view.js";
10
10
  * `cli-add-workspace-assets` module.
11
11
  */
12
12
  export { runAddEditorPluginCommand, runAddBindingSourceCommand, runAddPatternCommand, } from "./cli-add-workspace-assets.js";
13
+ /**
14
+ * Re-export the local integration environment scaffold workflow from the
15
+ * focused integration-env runtime helper module.
16
+ */
17
+ export { runAddIntegrationEnvCommand } from "./cli-add-workspace-integration-env.js";
13
18
  /**
14
19
  * Re-export the plugin-level REST resource scaffold workflow from the focused
15
20
  * rest-resource runtime helper module.
16
21
  */
17
22
  export { runAddRestResourceCommand } from "./cli-add-workspace-rest.js";
23
+ /**
24
+ * Re-export the standalone contract scaffold workflow from the focused
25
+ * contract runtime helper module.
26
+ */
27
+ export { runAddContractCommand } from "./cli-add-workspace-contract.js";
28
+ /**
29
+ * Re-export the typed post-meta contract scaffold workflow from the focused
30
+ * post-meta runtime helper module.
31
+ */
32
+ export { runAddPostMetaCommand } from "./cli-add-workspace-post-meta.js";
18
33
  /**
19
34
  * Re-export the typed workflow ability scaffold workflow from the focused
20
35
  * ability runtime helper module.
@@ -417,11 +417,26 @@ export { runAddAdminViewCommand, } from "./cli-add-workspace-admin-view.js";
417
417
  * `cli-add-workspace-assets` module.
418
418
  */
419
419
  export { runAddEditorPluginCommand, runAddBindingSourceCommand, runAddPatternCommand, } from "./cli-add-workspace-assets.js";
420
+ /**
421
+ * Re-export the local integration environment scaffold workflow from the
422
+ * focused integration-env runtime helper module.
423
+ */
424
+ export { runAddIntegrationEnvCommand } from "./cli-add-workspace-integration-env.js";
420
425
  /**
421
426
  * Re-export the plugin-level REST resource scaffold workflow from the focused
422
427
  * rest-resource runtime helper module.
423
428
  */
424
429
  export { runAddRestResourceCommand } from "./cli-add-workspace-rest.js";
430
+ /**
431
+ * Re-export the standalone contract scaffold workflow from the focused
432
+ * contract runtime helper module.
433
+ */
434
+ export { runAddContractCommand } from "./cli-add-workspace-contract.js";
435
+ /**
436
+ * Re-export the typed post-meta contract scaffold workflow from the focused
437
+ * post-meta runtime helper module.
438
+ */
439
+ export { runAddPostMetaCommand } from "./cli-add-workspace-post-meta.js";
425
440
  /**
426
441
  * Re-export the typed workflow ability scaffold workflow from the focused
427
442
  * ability runtime helper module.
@@ -10,6 +10,6 @@
10
10
  export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, suggestAddBlockTemplateId, } from "./cli-add-shared.js";
11
11
  export type { AddBlockTemplateId, AddKindId, EditorPluginSlotId, } from "./cli-add-shared.js";
12
12
  export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
13
- export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
13
+ export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddContractCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddIntegrationEnvCommand, runAddPatternCommand, runAddPostMetaCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
14
14
  export { getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, } from "./workspace-inventory.js";
15
15
  export type { WorkspaceBlockSelectOption } from "./workspace-inventory.js";
@@ -9,5 +9,5 @@
9
9
  */
10
10
  export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, suggestAddBlockTemplateId, } from "./cli-add-shared.js";
11
11
  export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
12
- export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
12
+ export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddContractCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddIntegrationEnvCommand, runAddPatternCommand, runAddPostMetaCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
13
13
  export { getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, } from "./workspace-inventory.js";
@@ -15,6 +15,7 @@
15
15
  * `seedWorkspaceMigrationProject` for
16
16
  * explicit `wp-typia add` flows,
17
17
  * `runAddAiFeatureCommand` for server-owned WordPress AI feature scaffolds,
18
+ * `runAddContractCommand` for standalone TypeScript schema artifacts,
18
19
  * `runAddRestResourceCommand` for plugin-level REST resource scaffolds,
19
20
  * `getDoctorChecks`, `runDoctor`, and `DoctorCheck` for diagnostics,
20
21
  * `createCliCommandError`, `createCliDiagnosticCodeError`,
@@ -32,7 +33,7 @@
32
33
  export { getDoctorChecks, runDoctor, type DoctorCheck } from "./cli-doctor.js";
33
34
  export { createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODE_METADATA, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getCliDiagnosticCodeMetadata, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
34
35
  export type { CliDiagnosticCode, CliDiagnosticCodeError, CliDiagnosticMessage, } from "./cli-diagnostics.js";
35
- export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, runAddAdminViewCommand, runAddAbilityCommand, runAddBindingSourceCommand, runAddAiFeatureCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
36
+ export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, runAddAdminViewCommand, runAddAbilityCommand, runAddBindingSourceCommand, runAddAiFeatureCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddContractCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddPostMetaCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
36
37
  export { COMPOUND_INNER_BLOCKS_PRESET_IDS, getCompoundInnerBlocksPresetDefinition, } from "./compound-inner-blocks.js";
37
38
  export type { CompoundInnerBlocksPresetId } from "./compound-inner-blocks.js";
38
39
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
@@ -15,6 +15,7 @@
15
15
  * `seedWorkspaceMigrationProject` for
16
16
  * explicit `wp-typia add` flows,
17
17
  * `runAddAiFeatureCommand` for server-owned WordPress AI feature scaffolds,
18
+ * `runAddContractCommand` for standalone TypeScript schema artifacts,
18
19
  * `runAddRestResourceCommand` for plugin-level REST resource scaffolds,
19
20
  * `getDoctorChecks`, `runDoctor`, and `DoctorCheck` for diagnostics,
20
21
  * `createCliCommandError`, `createCliDiagnosticCodeError`,
@@ -31,7 +32,7 @@
31
32
  */
32
33
  export { getDoctorChecks, runDoctor } from "./cli-doctor.js";
33
34
  export { createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODE_METADATA, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getCliDiagnosticCodeMetadata, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
34
- export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, runAddAdminViewCommand, runAddAbilityCommand, runAddBindingSourceCommand, runAddAiFeatureCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
35
+ export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, runAddAdminViewCommand, runAddAbilityCommand, runAddBindingSourceCommand, runAddAiFeatureCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddContractCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddPostMetaCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
35
36
  export { COMPOUND_INNER_BLOCKS_PRESET_IDS, getCompoundInnerBlocksPresetDefinition, } from "./compound-inner-blocks.js";
36
37
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
37
38
  export { formatHelpText } from "./cli-help.js";
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, } from "./template-builtins.js";
7
+ import { createDoctorCheck } from "./cli-doctor-workspace-shared.js";
7
8
  import { isBuiltInTemplateId, listTemplates } from "./template-registry.js";
8
9
  function readCommandVersion(command, args = ["--version"]) {
9
10
  try {
@@ -40,9 +41,6 @@ async function checkTempDirectory() {
40
41
  return false;
41
42
  }
42
43
  }
43
- function createDoctorCheck(label, status, detail) {
44
- return { detail, label, status };
45
- }
46
44
  function getTemplateDoctorChecks() {
47
45
  const checks = [];
48
46
  for (const template of listTemplates()) {