@wp-typia/project-tools 0.19.3 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +23 -0
  2. package/dist/runtime/ability-spec.d.ts +90 -0
  3. package/dist/runtime/ability-spec.js +51 -0
  4. package/dist/runtime/ai-artifacts.d.ts +39 -0
  5. package/dist/runtime/ai-artifacts.js +68 -0
  6. package/dist/runtime/ai-feature-artifacts.d.ts +85 -0
  7. package/dist/runtime/ai-feature-artifacts.js +139 -0
  8. package/dist/runtime/ai-feature-capability.d.ts +114 -0
  9. package/dist/runtime/ai-feature-capability.js +150 -0
  10. package/dist/runtime/block-generator-service-spec.js +6 -0
  11. package/dist/runtime/cli-add-shared.d.ts +32 -1
  12. package/dist/runtime/cli-add-shared.js +44 -0
  13. package/dist/runtime/cli-add-workspace-ability.d.ts +8 -0
  14. package/dist/runtime/cli-add-workspace-ability.js +810 -0
  15. package/dist/runtime/cli-add-workspace-ai-anchors.d.ts +22 -0
  16. package/dist/runtime/cli-add-workspace-ai-anchors.js +277 -0
  17. package/dist/runtime/cli-add-workspace-ai-source-emitters.d.ts +28 -0
  18. package/dist/runtime/cli-add-workspace-ai-source-emitters.js +346 -0
  19. package/dist/runtime/cli-add-workspace-ai.d.ts +14 -0
  20. package/dist/runtime/cli-add-workspace-ai.js +484 -0
  21. package/dist/runtime/cli-add-workspace.d.ts +10 -0
  22. package/dist/runtime/cli-add-workspace.js +10 -0
  23. package/dist/runtime/cli-add.d.ts +1 -1
  24. package/dist/runtime/cli-add.js +1 -1
  25. package/dist/runtime/cli-core.d.ts +3 -1
  26. package/dist/runtime/cli-core.js +3 -1
  27. package/dist/runtime/cli-doctor-workspace.js +140 -1
  28. package/dist/runtime/cli-help.js +4 -0
  29. package/dist/runtime/index.d.ts +3 -1
  30. package/dist/runtime/index.js +2 -1
  31. package/dist/runtime/scaffold-compatibility.d.ts +65 -0
  32. package/dist/runtime/scaffold-compatibility.js +152 -0
  33. package/dist/runtime/scaffold-template-variable-groups.d.ts +2 -0
  34. package/dist/runtime/scaffold-template-variables.js +6 -0
  35. package/dist/runtime/scaffold.d.ts +3 -0
  36. package/dist/runtime/typia-llm.d.ts +213 -0
  37. package/dist/runtime/typia-llm.js +348 -0
  38. package/dist/runtime/wordpress-ai.d.ts +122 -0
  39. package/dist/runtime/wordpress-ai.js +177 -0
  40. package/dist/runtime/workspace-inventory.d.ts +51 -4
  41. package/dist/runtime/workspace-inventory.js +157 -4
  42. package/package.json +12 -2
  43. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +3 -3
  44. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +3 -3
  45. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +3 -3
  46. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +3 -3
  47. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +3 -3
  48. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +3 -3
  49. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +3 -3
  50. package/templates/query-loop/{{slugKebabCase}}.php.mustache +3 -3
@@ -0,0 +1,22 @@
1
+ import type { WorkspaceProject } from "./workspace-project.js";
2
+ /**
3
+ * Patch the workspace bootstrap file so it loads generated AI feature PHP modules.
4
+ */
5
+ export declare function ensureAiFeatureBootstrapAnchors(workspace: WorkspaceProject): Promise<void>;
6
+ /**
7
+ * Patch `package.json` with `sync-ai` plus the project-tools dependency used by generated AI sync scripts.
8
+ */
9
+ export declare function ensureAiFeaturePackageScripts(workspace: WorkspaceProject): Promise<{
10
+ /** True when `@wp-typia/project-tools` was newly added to `devDependencies`. */
11
+ addedProjectToolsDependency: boolean;
12
+ /** True when the workspace did not already define a `sync-ai` script. */
13
+ addedSyncAiScript: boolean;
14
+ }>;
15
+ /**
16
+ * Patch `scripts/sync-project.ts` after package scripts so generated workspaces invoke `sync-ai` when present.
17
+ */
18
+ export declare function ensureAiFeatureSyncProjectAnchors(workspace: WorkspaceProject): Promise<void>;
19
+ /**
20
+ * Patch `scripts/sync-rest-contracts.ts` after sync-project wiring so AI feature REST artifacts join the split sync flow.
21
+ */
22
+ export declare function ensureAiFeatureSyncRestAnchors(workspace: WorkspaceProject): Promise<void>;
@@ -0,0 +1,277 @@
1
+ import { promises as fsp } from "node:fs";
2
+ import path from "node:path";
3
+ import { getPackageVersions } from "./package-versions.js";
4
+ import { getWorkspaceBootstrapPath, patchFile, } from "./cli-add-shared.js";
5
+ const AI_FEATURE_SERVER_GLOB = "/inc/ai-features/*.php";
6
+ function escapeRegex(value) {
7
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
8
+ }
9
+ /**
10
+ * Patch the workspace bootstrap file so it loads generated AI feature PHP modules.
11
+ */
12
+ export async function ensureAiFeatureBootstrapAnchors(workspace) {
13
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
14
+ await patchFile(bootstrapPath, (source) => {
15
+ let nextSource = source;
16
+ const registerFunctionName = `${workspace.workspace.phpPrefix}_register_ai_features`;
17
+ const registerHook = `add_action( 'init', '${registerFunctionName}', 20 );`;
18
+ const registerFunction = `
19
+
20
+ function ${registerFunctionName}() {
21
+ \tforeach ( glob( __DIR__ . '${AI_FEATURE_SERVER_GLOB}' ) ?: array() as $ai_feature_module ) {
22
+ \t\trequire_once $ai_feature_module;
23
+ \t}
24
+ }
25
+ `;
26
+ const insertionAnchors = [
27
+ /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
28
+ /\?>\s*$/u,
29
+ ];
30
+ const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u").test(nextSource);
31
+ const insertPhpSnippet = (snippet) => {
32
+ for (const anchor of insertionAnchors) {
33
+ const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
34
+ if (candidate !== nextSource) {
35
+ nextSource = candidate;
36
+ return;
37
+ }
38
+ }
39
+ nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
40
+ };
41
+ const appendPhpSnippet = (snippet) => {
42
+ const closingTagPattern = /\?>\s*$/u;
43
+ if (closingTagPattern.test(nextSource)) {
44
+ nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
45
+ return;
46
+ }
47
+ nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
48
+ };
49
+ if (!hasPhpFunctionDefinition(registerFunctionName)) {
50
+ insertPhpSnippet(registerFunction);
51
+ }
52
+ else if (!nextSource.includes(AI_FEATURE_SERVER_GLOB)) {
53
+ throw new Error([
54
+ `Unable to patch ${path.basename(bootstrapPath)} in ensureAiFeatureBootstrapAnchors.`,
55
+ `The existing ${registerFunctionName}() definition does not include ${AI_FEATURE_SERVER_GLOB}.`,
56
+ "Restore the generated bootstrap shape or wire the AI feature loader manually before retrying.",
57
+ ].join(" "));
58
+ }
59
+ if (!nextSource.includes(registerHook)) {
60
+ appendPhpSnippet(registerHook);
61
+ }
62
+ return nextSource;
63
+ });
64
+ }
65
+ /**
66
+ * Patch `package.json` with `sync-ai` plus the project-tools dependency used by generated AI sync scripts.
67
+ */
68
+ export async function ensureAiFeaturePackageScripts(workspace) {
69
+ const packageJsonPath = path.join(workspace.projectDir, "package.json");
70
+ const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
71
+ const nextScripts = {
72
+ ...(packageJson.scripts ?? {}),
73
+ "sync-ai": packageJson.scripts?.["sync-ai"] ?? "tsx scripts/sync-ai-features.ts",
74
+ };
75
+ const nextDevDependencies = {
76
+ ...(packageJson.devDependencies ?? {}),
77
+ "@wp-typia/project-tools": packageJson.devDependencies?.["@wp-typia/project-tools"] ??
78
+ getPackageVersions().projectToolsPackageVersion,
79
+ };
80
+ const addedSyncAiScript = packageJson.scripts?.["sync-ai"] === undefined;
81
+ const addedProjectToolsDependency = packageJson.devDependencies?.["@wp-typia/project-tools"] === undefined;
82
+ if (JSON.stringify(nextScripts) === JSON.stringify(packageJson.scripts ?? {}) &&
83
+ JSON.stringify(nextDevDependencies) ===
84
+ JSON.stringify(packageJson.devDependencies ?? {})) {
85
+ return {
86
+ addedProjectToolsDependency: false,
87
+ addedSyncAiScript: false,
88
+ };
89
+ }
90
+ packageJson.scripts = nextScripts;
91
+ packageJson.devDependencies = nextDevDependencies;
92
+ await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
93
+ return {
94
+ addedProjectToolsDependency,
95
+ addedSyncAiScript,
96
+ };
97
+ }
98
+ /**
99
+ * Patch `scripts/sync-project.ts` after package scripts so generated workspaces invoke `sync-ai` when present.
100
+ */
101
+ export async function ensureAiFeatureSyncProjectAnchors(workspace) {
102
+ const syncProjectScriptPath = path.join(workspace.projectDir, "scripts", "sync-project.ts");
103
+ await patchFile(syncProjectScriptPath, (source) => {
104
+ let nextSource = source;
105
+ const syncRestConst = "const syncRestScriptPath = path.join( 'scripts', 'sync-rest-contracts.ts' );";
106
+ const syncAiConst = "const syncAiScriptPath = path.join( 'scripts', 'sync-ai-features.ts' );";
107
+ const syncRestBlockPattern = /if \( fs\.existsSync\( path\.resolve\( process\.cwd\(\), syncRestScriptPath \) \) \) \{\n\s*runSyncScript\( syncRestScriptPath, options \);\n\s*\}/u;
108
+ const syncAiBlock = [
109
+ "if ( fs.existsSync( path.resolve( process.cwd(), syncAiScriptPath ) ) ) {",
110
+ "\trunSyncScript( syncAiScriptPath, options );",
111
+ "}",
112
+ ].join("\n");
113
+ if (!nextSource.includes(syncAiConst)) {
114
+ if (!nextSource.includes(syncRestConst)) {
115
+ throw new Error([
116
+ `ensureAiFeatureSyncProjectAnchors could not patch ${path.basename(syncProjectScriptPath)}.`,
117
+ "Missing the expected sync-rest script constant in scripts/sync-project.ts.",
118
+ "Restore the generated template or wire sync-ai manually before retrying.",
119
+ ].join(" "));
120
+ }
121
+ nextSource = nextSource.replace(syncRestConst, `${syncRestConst}\n${syncAiConst}`);
122
+ }
123
+ if (!nextSource.includes("runSyncScript( syncAiScriptPath, options );")) {
124
+ if (!syncRestBlockPattern.test(nextSource)) {
125
+ throw new Error([
126
+ `ensureAiFeatureSyncProjectAnchors could not patch ${path.basename(syncProjectScriptPath)}.`,
127
+ "Missing the expected sync-rest invocation block in scripts/sync-project.ts.",
128
+ "Restore the generated template or wire sync-ai manually before retrying.",
129
+ ].join(" "));
130
+ }
131
+ nextSource = nextSource.replace(syncRestBlockPattern, (match) => `${match}\n\n${syncAiBlock}`);
132
+ }
133
+ return nextSource;
134
+ });
135
+ }
136
+ function assertSyncRestAnchor(nextSource, target, anchorDescription, hasAnchor, syncRestScriptPath) {
137
+ if (!nextSource.includes(target) && !hasAnchor) {
138
+ throw new Error([
139
+ `ensureAiFeatureSyncRestAnchors could not patch ${path.basename(syncRestScriptPath)}.`,
140
+ `Missing expected ${anchorDescription} anchor in scripts/sync-rest-contracts.ts.`,
141
+ "Restore the generated template or add the AI feature wiring manually before retrying.",
142
+ ].join(" "));
143
+ }
144
+ }
145
+ function replaceRequiredSyncRestSource(nextSource, target, anchor, replacement, anchorDescription, syncRestScriptPath) {
146
+ if (nextSource.includes(target)) {
147
+ return nextSource;
148
+ }
149
+ const hasAnchor = typeof anchor === "string" ? nextSource.includes(anchor) : anchor.test(nextSource);
150
+ assertSyncRestAnchor(nextSource, target, anchorDescription, hasAnchor, syncRestScriptPath);
151
+ return nextSource.replace(anchor, replacement);
152
+ }
153
+ /**
154
+ * Patch `scripts/sync-rest-contracts.ts` after sync-project wiring so AI feature REST artifacts join the split sync flow.
155
+ */
156
+ export async function ensureAiFeatureSyncRestAnchors(workspace) {
157
+ const syncRestScriptPath = path.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
158
+ await patchFile(syncRestScriptPath, (source) => {
159
+ let nextSource = source;
160
+ const importAnchor = [
161
+ "import {",
162
+ "\tBLOCKS,",
163
+ "\tREST_RESOURCES,",
164
+ "\ttype WorkspaceBlockConfig,",
165
+ "\ttype WorkspaceRestResourceConfig,",
166
+ "} from './block-config';",
167
+ ].join("\n");
168
+ const helperInsertionAnchor = "async function assertTypeArtifactsCurrent";
169
+ const restResourcesAnchor = "const restResources = REST_RESOURCES.filter( isWorkspaceRestResource );";
170
+ const noResourcesPattern = /if \( restBlocks.length === 0 && restResources.length === 0 \) \{[\s\S]*?\n\t\treturn;\n\t\}/u;
171
+ const consoleLogPattern = /\n\tconsole\.log\(\n\t\toptions\.check/u;
172
+ nextSource = replaceRequiredSyncRestSource(nextSource, "AI_FEATURES", importAnchor, [
173
+ "import {",
174
+ "\tAI_FEATURES,",
175
+ "\tBLOCKS,",
176
+ "\tREST_RESOURCES,",
177
+ "\ttype WorkspaceAiFeatureConfig,",
178
+ "\ttype WorkspaceBlockConfig,",
179
+ "\ttype WorkspaceRestResourceConfig,",
180
+ "} from './block-config';",
181
+ ].join("\n"), "workspace inventory import", syncRestScriptPath);
182
+ nextSource = replaceRequiredSyncRestSource(nextSource, "function isWorkspaceAiFeature(", helperInsertionAnchor, [
183
+ "function isWorkspaceAiFeature(",
184
+ "\tfeature: WorkspaceAiFeatureConfig",
185
+ "): feature is WorkspaceAiFeatureConfig & {",
186
+ "\taiSchemaFile: string;",
187
+ "\tclientFile: string;",
188
+ "\topenApiFile: string;",
189
+ "\trestManifest: NonNullable< WorkspaceAiFeatureConfig[ 'restManifest' ] >;",
190
+ "\ttypesFile: string;",
191
+ "\tvalidatorsFile: string;",
192
+ "} {",
193
+ "\treturn (",
194
+ "\t\ttypeof feature.aiSchemaFile === 'string' &&",
195
+ "\t\ttypeof feature.clientFile === 'string' &&",
196
+ "\t\ttypeof feature.openApiFile === 'string' &&",
197
+ "\t\ttypeof feature.typesFile === 'string' &&",
198
+ "\t\ttypeof feature.validatorsFile === 'string' &&",
199
+ "\t\ttypeof feature.restManifest === 'object' &&",
200
+ "\t\tfeature.restManifest !== null",
201
+ "\t);",
202
+ "}",
203
+ "",
204
+ "async function assertTypeArtifactsCurrent",
205
+ ].join("\n"), "type artifact assertion helper", syncRestScriptPath);
206
+ nextSource = replaceRequiredSyncRestSource(nextSource, "const aiFeatures = AI_FEATURES.filter( isWorkspaceAiFeature );", restResourcesAnchor, [
207
+ "const restResources = REST_RESOURCES.filter( isWorkspaceRestResource );",
208
+ "const aiFeatures = AI_FEATURES.filter( isWorkspaceAiFeature );",
209
+ ].join("\n"), "rest resource filter", syncRestScriptPath);
210
+ nextSource = replaceRequiredSyncRestSource(nextSource, "restBlocks.length === 0 && restResources.length === 0 && aiFeatures.length === 0", noResourcesPattern, [
211
+ "if ( restBlocks.length === 0 && restResources.length === 0 && aiFeatures.length === 0 ) {",
212
+ "\t\tconsole.log(",
213
+ "\t\t\toptions.check",
214
+ "\t\t\t\t? 'ℹ️ No REST-enabled workspace blocks, plugin-level REST resources, or AI features are registered yet. `sync-rest --check` is already clean.'",
215
+ "\t\t\t\t: 'ℹ️ No REST-enabled workspace blocks, plugin-level REST resources, or AI features are registered yet.'",
216
+ "\t\t);",
217
+ "\t\treturn;",
218
+ "\t}",
219
+ ].join("\n"), "no-resources guard", syncRestScriptPath);
220
+ nextSource = replaceRequiredSyncRestSource(nextSource, "for ( const feature of aiFeatures ) {", consoleLogPattern, [
221
+ "",
222
+ "\tfor ( const feature of aiFeatures ) {",
223
+ "\t\tconst contracts = feature.restManifest.contracts;",
224
+ "",
225
+ "\t\tfor ( const [ baseName, contract ] of Object.entries( contracts ) ) {",
226
+ "\t\t\tawait syncTypeSchemas(",
227
+ "\t\t\t\t{",
228
+ "\t\t\t\t\tjsonSchemaFile: path.join(",
229
+ "\t\t\t\t\t\tpath.dirname( feature.typesFile ),",
230
+ "\t\t\t\t\t\t'api-schemas',",
231
+ "\t\t\t\t\t\t`${ baseName }.schema.json`",
232
+ "\t\t\t\t\t),",
233
+ "\t\t\t\t\topenApiFile: path.join(",
234
+ "\t\t\t\t\t\tpath.dirname( feature.typesFile ),",
235
+ "\t\t\t\t\t\t'api-schemas',",
236
+ "\t\t\t\t\t\t`${ baseName }.openapi.json`",
237
+ "\t\t\t\t\t),",
238
+ "\t\t\t\t\tsourceTypeName: contract.sourceTypeName,",
239
+ "\t\t\t\t\ttypesFile: feature.typesFile,",
240
+ "\t\t\t\t},",
241
+ "\t\t\t\t{",
242
+ "\t\t\t\t\tcheck: options.check,",
243
+ "\t\t\t\t}",
244
+ "\t\t\t);",
245
+ "\t\t}",
246
+ "",
247
+ "\t\tawait syncRestOpenApi(",
248
+ "\t\t\t{",
249
+ "\t\t\t\tmanifest: feature.restManifest,",
250
+ "\t\t\t\topenApiFile: feature.openApiFile,",
251
+ "\t\t\t\ttypesFile: feature.typesFile,",
252
+ "\t\t\t},",
253
+ "\t\t\t{",
254
+ "\t\t\t\tcheck: options.check,",
255
+ "\t\t\t}",
256
+ "\t\t);",
257
+ "",
258
+ "\t\tawait syncEndpointClient(",
259
+ "\t\t\t{",
260
+ "\t\t\t\tclientFile: feature.clientFile,",
261
+ "\t\t\t\tmanifest: feature.restManifest,",
262
+ "\t\t\t\ttypesFile: feature.typesFile,",
263
+ "\t\t\t\tvalidatorsFile: feature.validatorsFile,",
264
+ "\t\t\t},",
265
+ "\t\t\t{",
266
+ "\t\t\t\tcheck: options.check,",
267
+ "\t\t\t}",
268
+ "\t\t);",
269
+ "\t}",
270
+ "",
271
+ "\tconsole.log(",
272
+ "\t\toptions.check",
273
+ ].join("\n"), "final sync summary", syncRestScriptPath);
274
+ nextSource = replaceRequiredSyncRestSource(nextSource, "workspace blocks, plugin-level resources, and AI features", "workspace blocks and plugin-level resources", "workspace blocks, plugin-level resources, and AI features", "sync summary copy", syncRestScriptPath);
275
+ return nextSource;
276
+ });
277
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Convert an AI feature slug into the PascalCase identifier used by generated types.
3
+ */
4
+ export declare function toPascalCaseFromAiFeatureSlug(slug: string): string;
5
+ /**
6
+ * Build the workspace inventory entry written into `scripts/block-config.ts` for one AI feature.
7
+ */
8
+ export declare function buildAiFeatureConfigEntry(aiFeatureSlug: string, namespace: string): string;
9
+ /**
10
+ * Generate TypeScript request, response, and telemetry contracts for an AI feature scaffold.
11
+ */
12
+ export declare function buildAiFeatureTypesSource(aiFeatureSlug: string): string;
13
+ /**
14
+ * Generate runtime validators for the AI feature request/result/response contracts.
15
+ */
16
+ export declare function buildAiFeatureValidatorsSource(aiFeatureSlug: string): string;
17
+ /**
18
+ * Generate the typed client wrapper that calls the scaffolded AI feature endpoint.
19
+ */
20
+ export declare function buildAiFeatureApiSource(aiFeatureSlug: string): string;
21
+ /**
22
+ * Generate React endpoint-mutation hooks for the scaffolded AI feature client wrapper.
23
+ */
24
+ export declare function buildAiFeatureDataSource(aiFeatureSlug: string): string;
25
+ /**
26
+ * Generate the `scripts/sync-ai-features.ts` source that projects AI-safe schemas for workspace features.
27
+ */
28
+ export declare function buildAiFeatureSyncScriptSource(): string;
@@ -0,0 +1,346 @@
1
+ import { normalizeBlockSlug, quoteTsString, } from "./cli-add-shared.js";
2
+ import { buildAiFeatureEndpointManifest } from "./ai-feature-artifacts.js";
3
+ import { OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY, renderScaffoldCompatibilityConfig, resolveScaffoldCompatibilityPolicy, } from "./scaffold-compatibility.js";
4
+ import { toTitleCase } from "./string-case.js";
5
+ /**
6
+ * Convert an AI feature slug into the PascalCase identifier used by generated types.
7
+ */
8
+ export function toPascalCaseFromAiFeatureSlug(slug) {
9
+ return normalizeBlockSlug(slug)
10
+ .split("-")
11
+ .filter(Boolean)
12
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
13
+ .join("");
14
+ }
15
+ function indentMultiline(source, prefix) {
16
+ return source
17
+ .split("\n")
18
+ .map((line) => `${prefix}${line}`)
19
+ .join("\n");
20
+ }
21
+ /**
22
+ * Build the workspace inventory entry written into `scripts/block-config.ts` for one AI feature.
23
+ */
24
+ export function buildAiFeatureConfigEntry(aiFeatureSlug, namespace) {
25
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
26
+ const title = toTitleCase(aiFeatureSlug);
27
+ const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY);
28
+ const manifest = buildAiFeatureEndpointManifest({
29
+ namespace,
30
+ pascalCase,
31
+ slugKebabCase: aiFeatureSlug,
32
+ title,
33
+ });
34
+ return [
35
+ "\t{",
36
+ `\t\taiSchemaFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/ai-schemas/feature-result.ai.schema.json`)},`,
37
+ `\t\tapiFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api.ts`)},`,
38
+ `\t\tclientFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api-client.ts`)},`,
39
+ `\t\tcompatibility: ${renderScaffoldCompatibilityConfig(compatibilityPolicy)},`,
40
+ `\t\tdataFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/data.ts`)},`,
41
+ `\t\tnamespace: ${quoteTsString(namespace)},`,
42
+ `\t\topenApiFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api.openapi.json`)},`,
43
+ `\t\tphpFile: ${quoteTsString(`inc/ai-features/${aiFeatureSlug}.php`)},`,
44
+ "\t\trestManifest: defineEndpointManifest(",
45
+ indentMultiline(JSON.stringify(manifest, null, "\t"), "\t\t\t"),
46
+ "\t\t),",
47
+ `\t\tslug: ${quoteTsString(aiFeatureSlug)},`,
48
+ `\t\ttypesFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api-types.ts`)},`,
49
+ `\t\tvalidatorsFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api-validators.ts`)},`,
50
+ "\t},",
51
+ ].join("\n");
52
+ }
53
+ /**
54
+ * Generate TypeScript request, response, and telemetry contracts for an AI feature scaffold.
55
+ */
56
+ export function buildAiFeatureTypesSource(aiFeatureSlug) {
57
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
58
+ return `import { tags } from 'typia';
59
+
60
+ export interface ${pascalCase}AiFeatureRequest {
61
+ \tbrief: string & tags.MinLength< 1 > & tags.MaxLength< 4000 >;
62
+ \tcontext?: string & tags.MaxLength< 4000 >;
63
+ }
64
+
65
+ export interface ${pascalCase}AiFeatureResult {
66
+ \ttitle: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
67
+ \tsummary: string & tags.MinLength< 1 > & tags.MaxLength< 2000 >;
68
+ \tconfidence?: number & tags.Minimum< 0 > & tags.Maximum< 1 >;
69
+ }
70
+
71
+ export interface ${pascalCase}AiFeatureTokenUsage {
72
+ \tcompletionTokens: number & tags.Type< 'uint32' >;
73
+ \tpromptTokens: number & tags.Type< 'uint32' >;
74
+ \ttotalTokens: number & tags.Type< 'uint32' >;
75
+ \tthoughtTokens?: number & tags.Type< 'uint32' >;
76
+ }
77
+
78
+ export interface ${pascalCase}AiFeatureTelemetry {
79
+ \tmodelId: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
80
+ \tmodelName: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
81
+ \tproviderId: string & tags.MinLength< 1 > & tags.MaxLength< 80 >;
82
+ \tproviderName: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
83
+ \tproviderType: 'client' | 'cloud' | 'server';
84
+ \tresultId: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
85
+ \ttokenUsage: ${pascalCase}AiFeatureTokenUsage;
86
+ }
87
+
88
+ export interface ${pascalCase}AiFeatureResponse {
89
+ \tresult: ${pascalCase}AiFeatureResult;
90
+ \ttelemetry: ${pascalCase}AiFeatureTelemetry;
91
+ }
92
+ `;
93
+ }
94
+ /**
95
+ * Generate runtime validators for the AI feature request/result/response contracts.
96
+ */
97
+ export function buildAiFeatureValidatorsSource(aiFeatureSlug) {
98
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
99
+ return `import typia from 'typia';
100
+
101
+ import { toValidationResult } from '@wp-typia/rest';
102
+ import type {
103
+ \t${pascalCase}AiFeatureRequest,
104
+ \t${pascalCase}AiFeatureResponse,
105
+ \t${pascalCase}AiFeatureResult,
106
+ } from './api-types';
107
+
108
+ const validateFeatureRequest = typia.createValidate< ${pascalCase}AiFeatureRequest >();
109
+ const validateFeatureResult = typia.createValidate< ${pascalCase}AiFeatureResult >();
110
+ const validateFeatureResponse = typia.createValidate< ${pascalCase}AiFeatureResponse >();
111
+
112
+ export const apiValidators = {
113
+ \tfeatureRequest: ( input: unknown ) =>
114
+ \t\ttoValidationResult< ${pascalCase}AiFeatureRequest >(
115
+ \t\t\tvalidateFeatureRequest( input )
116
+ \t\t),
117
+ \tfeatureResult: ( input: unknown ) =>
118
+ \t\ttoValidationResult< ${pascalCase}AiFeatureResult >(
119
+ \t\t\tvalidateFeatureResult( input )
120
+ \t\t),
121
+ \tfeatureResponse: ( input: unknown ) =>
122
+ \t\ttoValidationResult< ${pascalCase}AiFeatureResponse >(
123
+ \t\t\tvalidateFeatureResponse( input )
124
+ \t\t),
125
+ };
126
+ `;
127
+ }
128
+ /**
129
+ * Generate the typed client wrapper that calls the scaffolded AI feature endpoint.
130
+ */
131
+ export function buildAiFeatureApiSource(aiFeatureSlug) {
132
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
133
+ return `import {
134
+ \tcallEndpoint,
135
+ \tresolveRestRouteUrl,
136
+ } from '@wp-typia/rest';
137
+
138
+ import type {
139
+ \t${pascalCase}AiFeatureRequest,
140
+ } from './api-types';
141
+ import {
142
+ \trun${pascalCase}AiFeatureEndpoint,
143
+ } from './api-client';
144
+
145
+ function resolveRestNonce( fallback?: string ): string | undefined {
146
+ \tif ( typeof fallback === 'string' && fallback.length > 0 ) {
147
+ \t\treturn fallback;
148
+ \t}
149
+
150
+ \tif ( typeof window === 'undefined' ) {
151
+ \t\treturn undefined;
152
+ \t}
153
+
154
+ \tconst wpApiSettings = (
155
+ \t\twindow as typeof window & {
156
+ \t\t\twpApiSettings?: { nonce?: string };
157
+ \t\t}
158
+ \t).wpApiSettings;
159
+
160
+ \treturn typeof wpApiSettings?.nonce === 'string' &&
161
+ \t\twpApiSettings.nonce.length > 0
162
+ \t\t? wpApiSettings.nonce
163
+ \t\t: undefined;
164
+ }
165
+
166
+ export const aiFeatureRunEndpoint = {
167
+ \t...run${pascalCase}AiFeatureEndpoint,
168
+ \tbuildRequestOptions: () => {
169
+ \t\tconst nonce = resolveRestNonce();
170
+ \t\treturn {
171
+ \t\t\theaders: nonce
172
+ \t\t\t\t? {
173
+ \t\t\t\t\t'X-WP-Nonce': nonce,
174
+ \t\t\t\t}
175
+ \t\t\t\t: undefined,
176
+ \t\t\turl: resolveRestRouteUrl( run${pascalCase}AiFeatureEndpoint.path ),
177
+ \t\t};
178
+ \t},
179
+ };
180
+
181
+ export function runAiFeature( request: ${pascalCase}AiFeatureRequest ) {
182
+ \treturn callEndpoint( aiFeatureRunEndpoint, request );
183
+ }
184
+ `;
185
+ }
186
+ /**
187
+ * Generate React endpoint-mutation hooks for the scaffolded AI feature client wrapper.
188
+ */
189
+ export function buildAiFeatureDataSource(aiFeatureSlug) {
190
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
191
+ return `import {
192
+ \tuseEndpointMutation,
193
+ \ttype UseEndpointMutationOptions,
194
+ } from '@wp-typia/rest/react';
195
+
196
+ import type {
197
+ \t${pascalCase}AiFeatureRequest,
198
+ \t${pascalCase}AiFeatureResponse,
199
+ } from './api-types';
200
+ import {
201
+ \taiFeatureRunEndpoint,
202
+ } from './api';
203
+
204
+ export type UseRun${pascalCase}AiFeatureMutationOptions =
205
+ \tUseEndpointMutationOptions<
206
+ \t\t${pascalCase}AiFeatureRequest,
207
+ \t\t${pascalCase}AiFeatureResponse,
208
+ \t\tunknown
209
+ \t>;
210
+
211
+ export function useRun${pascalCase}AiFeatureMutation(
212
+ \toptions: UseRun${pascalCase}AiFeatureMutationOptions = {}
213
+ ) {
214
+ \treturn useEndpointMutation( aiFeatureRunEndpoint, options );
215
+ }
216
+ `;
217
+ }
218
+ /**
219
+ * Generate the `scripts/sync-ai-features.ts` source that projects AI-safe schemas for workspace features.
220
+ */
221
+ export function buildAiFeatureSyncScriptSource() {
222
+ return `/* eslint-disable no-console */
223
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
224
+ import path from 'node:path';
225
+
226
+ import { projectWordPressAiSchema } from '@wp-typia/project-tools/ai-artifacts';
227
+
228
+ import {
229
+ \tAI_FEATURES,
230
+ \ttype WorkspaceAiFeatureConfig,
231
+ } from './block-config';
232
+
233
+ function parseCliOptions( argv: string[] ) {
234
+ \tconst options = {
235
+ \t\tcheck: false,
236
+ \t};
237
+
238
+ \tfor ( const argument of argv ) {
239
+ \t\tif ( argument === '--check' ) {
240
+ \t\t\toptions.check = true;
241
+ \t\t\tcontinue;
242
+ \t\t}
243
+
244
+ \t\tthrow new Error( \`Unknown sync-ai flag: \${ argument }\` );
245
+ \t}
246
+
247
+ \treturn options;
248
+ }
249
+
250
+ function isWorkspaceAiFeature(
251
+ \tfeature: WorkspaceAiFeatureConfig
252
+ ): feature is WorkspaceAiFeatureConfig & {
253
+ \taiSchemaFile: string;
254
+ \ttypesFile: string;
255
+ } {
256
+ \treturn (
257
+ \t\ttypeof feature.aiSchemaFile === 'string' &&
258
+ \t\ttypeof feature.typesFile === 'string'
259
+ \t);
260
+ }
261
+
262
+ function normalizeGeneratedArtifactContent( content: string ) {
263
+ \treturn content.replace( /\\r\\n?/g, '\\n' );
264
+ }
265
+
266
+ async function reconcileGeneratedArtifact( options: {
267
+ \tcheck: boolean;
268
+ \tcontent: string;
269
+ \tfilePath: string;
270
+ \tlabel: string;
271
+ } ) {
272
+ \tif ( ! options.check ) {
273
+ \t\tawait mkdir( path.dirname( options.filePath ), {
274
+ \t\t\trecursive: true,
275
+ \t\t} );
276
+ \t\tawait writeFile( options.filePath, options.content, 'utf8' );
277
+ \t\treturn;
278
+ \t}
279
+
280
+ \tconst current = normalizeGeneratedArtifactContent(
281
+ \t\tawait readFile( options.filePath, 'utf8' )
282
+ \t);
283
+ \tconst expected = normalizeGeneratedArtifactContent( options.content );
284
+ \tif ( current !== expected ) {
285
+ \t\tthrow new Error(
286
+ \t\t\t\`Generated AI feature artifact is stale: \${ options.label } (\${ options.filePath }).\`
287
+ \t\t);
288
+ \t}
289
+ }
290
+
291
+ async function loadJsonDocument( filePath: string ) {
292
+ \tconst decoded = JSON.parse( await readFile( filePath, 'utf8' ) ) as unknown;
293
+ \tif ( ! decoded || typeof decoded !== 'object' || Array.isArray( decoded ) ) {
294
+ \t\tthrow new Error( \`Expected \${ filePath } to decode to a JSON object.\` );
295
+ \t}
296
+
297
+ \treturn decoded as Parameters< typeof projectWordPressAiSchema >[ 0 ];
298
+ }
299
+
300
+ async function main() {
301
+ \tconst options = parseCliOptions( process.argv.slice( 2 ) );
302
+ \tconst aiFeatures = AI_FEATURES.filter( isWorkspaceAiFeature );
303
+ \tif ( AI_FEATURES.length > 0 && aiFeatures.length === 0 ) {
304
+ \t\tconsole.warn(
305
+ \t\t\t'⚠️ AI_FEATURES entries exist, but none satisfied the generated sync-ai guard. Check for missing aiSchemaFile/typesFile fields in scripts/block-config.ts.'
306
+ \t\t);
307
+ \t}
308
+
309
+ \tif ( aiFeatures.length === 0 ) {
310
+ \t\tconsole.log(
311
+ \t\t\toptions.check
312
+ \t\t\t\t? 'ℹ️ No workspace AI features are registered yet. \`sync-ai --check\` is already clean.'
313
+ \t\t\t\t: 'ℹ️ No workspace AI features are registered yet.'
314
+ \t\t);
315
+ \t\treturn;
316
+ \t}
317
+
318
+ \tfor ( const feature of aiFeatures ) {
319
+ \t\tconst sourceSchemaPath = path.join(
320
+ \t\t\tpath.dirname( feature.typesFile ),
321
+ \t\t\t'api-schemas',
322
+ \t\t\t'feature-result.schema.json'
323
+ \t\t);
324
+ \t\tconst sourceSchema = await loadJsonDocument( sourceSchemaPath );
325
+ \t\tconst aiSchema = projectWordPressAiSchema( sourceSchema );
326
+ \t\tawait reconcileGeneratedArtifact( {
327
+ \t\t\tcheck: options.check,
328
+ \t\t\tcontent: \`\${ JSON.stringify( aiSchema, null, 2 ) }\\n\`,
329
+ \t\t\tfilePath: feature.aiSchemaFile,
330
+ \t\t\tlabel: feature.slug,
331
+ \t\t} );
332
+ \t}
333
+
334
+ \tconsole.log(
335
+ \t\toptions.check
336
+ \t\t\t? '✅ AI feature structured-output schemas are already synchronized.'
337
+ \t\t\t: '✅ AI feature structured-output schemas were synchronized.'
338
+ \t);
339
+ }
340
+
341
+ main().catch( ( error ) => {
342
+ \tconsole.error( '❌ AI feature sync failed:', error );
343
+ \tprocess.exit( 1 );
344
+ } );
345
+ `;
346
+ }