wp-typia 0.20.2 → 0.20.4

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.
@@ -1,5 +1,7 @@
1
1
  // @bun
2
2
  import {
3
+ OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY,
4
+ REQUIRED_WORKSPACE_ABILITY_COMPATIBILITY,
3
5
  assertExternalLayerCompositionOptions,
4
6
  copyInterpolatedDirectory,
5
7
  getDefaultAnswers,
@@ -8,17 +10,23 @@ import {
8
10
  parseAlternateRenderTargets,
9
11
  parseCompoundInnerBlocksPreset,
10
12
  parseTemplateLocator,
13
+ renderScaffoldCompatibilityConfig,
14
+ require_semver,
11
15
  resolveExternalTemplateLayers,
12
16
  resolveLocalCliPathOption,
13
17
  resolveOptionalInteractiveExternalLayerId,
18
+ resolveScaffoldCompatibilityPolicy,
14
19
  resolveTemplateSeed,
15
20
  scaffoldProject,
16
- syncPersistenceRestArtifacts
17
- } from "./cli-32rf304y.js";
18
- import"./cli-tesygdnr.js";
21
+ syncPersistenceRestArtifacts,
22
+ updatePluginHeaderCompatibility
23
+ } from "./cli-c5021kqy.js";
24
+ import {
25
+ getPackageVersions
26
+ } from "./cli-tesygdnr.js";
19
27
  import {
20
28
  snapshotProjectVersion
21
- } from "./cli-qpt5dt0x.js";
29
+ } from "./cli-2rev5hqm.js";
22
30
  import {
23
31
  ensureMigrationDirectories,
24
32
  parseMigrationConfig,
@@ -38,6 +46,8 @@ import {
38
46
  ADD_KIND_IDS,
39
47
  EDITOR_PLUGIN_SLOT_IDS,
40
48
  appendWorkspaceInventoryEntries,
49
+ assertAbilityDoesNotExist,
50
+ assertAiFeatureDoesNotExist,
41
51
  assertBindingSourceDoesNotExist,
42
52
  assertEditorPluginDoesNotExist,
43
53
  assertPatternDoesNotExist,
@@ -67,14 +77,17 @@ import {
67
77
  snapshotWorkspaceFiles,
68
78
  toKebabCase,
69
79
  toTitleCase
70
- } from "./cli-rg481yks.js";
80
+ } from "./cli-3w3qxq9w.js";
71
81
  import {
72
82
  createManagedTempRoot
73
83
  } from "./cli-t73q5aqz.js";
74
84
  import {
75
85
  resolveWorkspaceProject
76
86
  } from "./cli-pd5pqgre.js";
77
- import"./cli-xnn9xjcy.js";
87
+ import {
88
+ __reExport,
89
+ __toESM
90
+ } from "./cli-xnn9xjcy.js";
78
91
  // ../wp-typia-project-tools/src/runtime/cli-add-block.ts
79
92
  import fs2 from "fs";
80
93
  import { promises as fsp2 } from "fs";
@@ -746,9 +759,9 @@ async function runAddBlockCommand({
746
759
  }
747
760
  }
748
761
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace.ts
749
- import fs4 from "fs";
750
- import { promises as fsp5 } from "fs";
751
- import path8 from "path";
762
+ import fs5 from "fs";
763
+ import { promises as fsp8 } from "fs";
764
+ import path12 from "path";
752
765
 
753
766
  // ../wp-typia-project-tools/src/runtime/cli-add-workspace-assets.ts
754
767
  import fs3 from "fs";
@@ -2747,7 +2760,6 @@ async function runAddRestResourceCommand({
2747
2760
  const validatorsFilePath = path7.join(restResourceDir, "api-validators.ts");
2748
2761
  const apiFilePath = path7.join(restResourceDir, "api.ts");
2749
2762
  const dataFilePath = path7.join(restResourceDir, "data.ts");
2750
- const clientFilePath = path7.join(restResourceDir, "api-client.ts");
2751
2763
  const phpFilePath = path7.join(workspace.projectDir, "inc", "rest", `${restResourceSlug}.php`);
2752
2764
  const mutationSnapshot = {
2753
2765
  fileSources: await snapshotWorkspaceFiles([
@@ -2799,139 +2811,2246 @@ async function runAddRestResourceCommand({
2799
2811
  throw error;
2800
2812
  }
2801
2813
  }
2802
-
2803
- // ../wp-typia-project-tools/src/runtime/cli-add-workspace.ts
2804
- var VARIATIONS_IMPORT_LINE = "import { registerWorkspaceVariations } from './variations';";
2805
- var VARIATIONS_CALL_LINE = "registerWorkspaceVariations();";
2806
- function buildVariationConfigEntry(blockSlug, variationSlug) {
2814
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ability.ts
2815
+ var import_semver = __toESM(require_semver(), 1);
2816
+ import fs4 from "fs";
2817
+ import { promises as fsp5 } from "fs";
2818
+ import path8 from "path";
2819
+ import { syncTypeSchemas as syncTypeSchemas2 } from "@wp-typia/block-runtime/metadata-core";
2820
+ var ABILITY_SERVER_GLOB = "/inc/abilities/*.php";
2821
+ var ABILITY_EDITOR_SCRIPT = "build/abilities/index.js";
2822
+ var ABILITY_EDITOR_ASSET = "build/abilities/index.asset.php";
2823
+ var ABILITY_REGISTRY_END_MARKER = "// wp-typia add ability entries end";
2824
+ var ABILITY_REGISTRY_START_MARKER = "// wp-typia add ability entries start";
2825
+ var WP_ABILITIES_PACKAGE_VERSION = "^0.10.0";
2826
+ var WP_CORE_ABILITIES_PACKAGE_VERSION = "^0.9.0";
2827
+ var WP_ABILITIES_SCRIPT_MODULE_ID = "@wordpress/abilities";
2828
+ var WP_CORE_ABILITIES_SCRIPT_MODULE_ID = "@wordpress/core-abilities";
2829
+ function escapeRegex3(value) {
2830
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
2831
+ }
2832
+ function quotePhpString3(value) {
2833
+ return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
2834
+ }
2835
+ function findPhpFunctionRange2(source, functionName) {
2836
+ const functionPattern = new RegExp(`function\\s+${escapeRegex3(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u");
2837
+ const match = functionPattern.exec(source);
2838
+ if (!match) {
2839
+ return null;
2840
+ }
2841
+ const openingBraceIndex = match.index + match[0].length - 1;
2842
+ let depth = 0;
2843
+ for (let index = openingBraceIndex;index < source.length; index += 1) {
2844
+ const character = source[index];
2845
+ if (character === "{") {
2846
+ depth += 1;
2847
+ } else if (character === "}") {
2848
+ depth -= 1;
2849
+ if (depth === 0) {
2850
+ const end = index + 1;
2851
+ return {
2852
+ end,
2853
+ source: source.slice(match.index, end),
2854
+ start: match.index
2855
+ };
2856
+ }
2857
+ }
2858
+ }
2859
+ return null;
2860
+ }
2861
+ function replacePhpFunctionDefinition2(source, functionName, replacement) {
2862
+ const functionRange = findPhpFunctionRange2(source, functionName);
2863
+ if (!functionRange) {
2864
+ return source;
2865
+ }
2866
+ return `${source.slice(0, functionRange.start)}${replacement.trimStart()}${source.slice(functionRange.end)}`;
2867
+ }
2868
+ function resolveManagedDependencyVersion(existingVersion, requiredVersion) {
2869
+ if (!existingVersion) {
2870
+ return requiredVersion;
2871
+ }
2872
+ const existingMinimum = import_semver.default.minVersion(existingVersion);
2873
+ const requiredMinimum = import_semver.default.minVersion(requiredVersion);
2874
+ if (!existingMinimum || !requiredMinimum) {
2875
+ return requiredVersion;
2876
+ }
2877
+ return import_semver.default.gte(existingMinimum, requiredMinimum) ? existingVersion : requiredVersion;
2878
+ }
2879
+ function toPascalCaseFromAbilitySlug(abilitySlug) {
2880
+ return normalizeBlockSlug(abilitySlug).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
2881
+ }
2882
+ function toAbilityCategorySlug(workspaceNamespace) {
2883
+ const normalizedNamespace = workspaceNamespace.replace(/[^a-z0-9-]+/gu, "-").replace(/-{2,}/gu, "-").replace(/^-|-$/gu, "");
2884
+ return `${normalizedNamespace || "workspace"}-workflows`;
2885
+ }
2886
+ function buildAbilityConfigEntry(abilitySlug, compatibilityPolicy) {
2887
+ const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
2807
2888
  return [
2808
2889
  "\t{",
2809
- ` block: ${quoteTsString(blockSlug)},`,
2810
- ` file: ${quoteTsString(`src/blocks/${blockSlug}/variations/${variationSlug}.ts`)},`,
2811
- ` slug: ${quoteTsString(variationSlug)},`,
2890
+ ` clientFile: ${quoteTsString(`src/abilities/${abilitySlug}/client.ts`)},`,
2891
+ ` compatibility: ${renderScaffoldCompatibilityConfig(compatibilityPolicy)},`,
2892
+ ` configFile: ${quoteTsString(`src/abilities/${abilitySlug}/ability.config.json`)},`,
2893
+ ` dataFile: ${quoteTsString(`src/abilities/${abilitySlug}/data.ts`)},`,
2894
+ ` inputSchemaFile: ${quoteTsString(`src/abilities/${abilitySlug}/input.schema.json`)},`,
2895
+ ` inputTypeName: ${quoteTsString(`${pascalCase}AbilityInput`)},`,
2896
+ ` outputSchemaFile: ${quoteTsString(`src/abilities/${abilitySlug}/output.schema.json`)},`,
2897
+ ` outputTypeName: ${quoteTsString(`${pascalCase}AbilityOutput`)},`,
2898
+ ` phpFile: ${quoteTsString(`inc/abilities/${abilitySlug}.php`)},`,
2899
+ ` slug: ${quoteTsString(abilitySlug)},`,
2900
+ ` typesFile: ${quoteTsString(`src/abilities/${abilitySlug}/types.ts`)},`,
2812
2901
  "\t},"
2813
2902
  ].join(`
2814
2903
  `);
2815
2904
  }
2816
- function buildVariationConstName(variationSlug) {
2817
- const identifierSegments = toKebabCase(variationSlug).split("-").filter(Boolean);
2818
- return `workspaceVariation_${identifierSegments.join("_")}`;
2819
- }
2820
- function getVariationConstBindings(variationSlugs) {
2821
- const seenConstNames = new Map;
2822
- return variationSlugs.map((variationSlug) => {
2823
- const constName = buildVariationConstName(variationSlug);
2824
- const previousSlug = seenConstNames.get(constName);
2825
- if (previousSlug && previousSlug !== variationSlug) {
2826
- throw new Error(`Variation slugs "${previousSlug}" and "${variationSlug}" generate the same registry identifier "${constName}". Rename one of the variations.`);
2905
+ function buildAbilityConfigSource(abilitySlug, workspaceNamespace) {
2906
+ const abilityTitle = toTitleCase(abilitySlug);
2907
+ return `${JSON.stringify({
2908
+ abilityId: `${workspaceNamespace}/${abilitySlug}`,
2909
+ category: {
2910
+ description: `Typed editor and admin workflows exposed by the ${workspaceNamespace} workspace.`,
2911
+ label: `${toTitleCase(workspaceNamespace)} Workflows`,
2912
+ slug: toAbilityCategorySlug(workspaceNamespace)
2913
+ },
2914
+ description: `Runs the ${abilityTitle} workflow using a typed server callback.`,
2915
+ label: abilityTitle,
2916
+ meta: {
2917
+ annotations: {
2918
+ destructive: false,
2919
+ idempotent: true,
2920
+ readonly: false
2921
+ },
2922
+ mcp: {
2923
+ public: false
2924
+ },
2925
+ showInRest: true
2827
2926
  }
2828
- seenConstNames.set(constName, variationSlug);
2829
- return { constName, variationSlug };
2830
- });
2927
+ }, null, 2)}
2928
+ `;
2929
+ }
2930
+ function buildAbilityTypesSource(abilitySlug) {
2931
+ const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
2932
+ return `export interface ${pascalCase}AbilityInput {
2933
+ contextId: number;
2934
+ note?: string;
2831
2935
  }
2832
- function buildVariationSource(variationSlug, textDomain) {
2833
- const variationTitle = toTitleCase(variationSlug);
2834
- const variationConstName = buildVariationConstName(variationSlug);
2835
- return `import type { BlockVariation } from '@wp-typia/block-types/blocks/registration';
2836
- import { __ } from '@wordpress/i18n';
2837
2936
 
2838
- export const ${variationConstName} = {
2839
- name: ${quoteTsString(variationSlug)},
2840
- title: __( ${quoteTsString(variationTitle)}, ${quoteTsString(textDomain)} ),
2841
- description: __(
2842
- ${quoteTsString(`A starter variation for ${variationTitle}.`)},
2843
- ${quoteTsString(textDomain)},
2844
- ),
2845
- attributes: {},
2846
- scope: ['inserter'],
2847
- } satisfies BlockVariation;
2937
+ export interface ${pascalCase}AbilityOutput {
2938
+ processedContextId: number;
2939
+ receivedNote?: string;
2940
+ status: 'ready';
2941
+ summary: string;
2942
+ }
2848
2943
  `;
2849
2944
  }
2850
- function buildVariationIndexSource(variationSlugs) {
2851
- const variationBindings = getVariationConstBindings(variationSlugs);
2852
- const importLines = variationBindings.map(({ constName, variationSlug }) => {
2853
- return `import { ${constName} } from './${variationSlug}';`;
2854
- }).join(`
2855
- `);
2856
- const variationConstNames = variationBindings.map(({ constName }) => constName).join(`,
2857
- `);
2858
- return `import { registerBlockVariation } from '@wordpress/blocks';
2859
- import metadata from '../block.json';
2860
- ${importLines ? `
2861
- ${importLines}` : ""}
2945
+ function buildAbilityDataSource(abilitySlug) {
2946
+ const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
2947
+ const abilityConstBase = abilitySlug.toUpperCase().replace(/[^A-Z0-9]+/gu, "_").replace(/_{2,}/gu, "_").replace(/^_|_$/gu, "");
2948
+ return `import {
2949
+ executeAbility,
2950
+ getAbilities,
2951
+ getAbility as getRegisteredAbility,
2952
+ } from '@wordpress/abilities';
2953
+ import '@wordpress/core-abilities';
2862
2954
 
2863
- const WORKSPACE_VARIATIONS = [
2864
- ${variationConstNames}
2865
- // wp-typia add variation entries
2866
- ];
2955
+ import abilityConfig from './ability.config.json';
2867
2956
 
2868
- export function registerWorkspaceVariations() {
2869
- for (const variation of WORKSPACE_VARIATIONS) {
2870
- registerBlockVariation(metadata.name, variation);
2957
+ import type { ${pascalCase}AbilityInput, ${pascalCase}AbilityOutput } from './types';
2958
+
2959
+ interface WordPressAbilityDefinition {
2960
+ category?: string;
2961
+ description?: string;
2962
+ label?: string;
2963
+ meta?: Record<string, unknown>;
2964
+ name?: string;
2965
+ }
2966
+
2967
+ export const ${abilityConstBase}_ABILITY = abilityConfig;
2968
+ export const ${abilityConstBase}_ABILITY_CATEGORY = abilityConfig.category;
2969
+ export const ${abilityConstBase}_ABILITY_ID = abilityConfig.abilityId;
2970
+ export const ${abilityConstBase}_ABILITY_META = abilityConfig.meta;
2971
+ const ABILITY_DISCOVERY_POLL_INTERVAL_MS = 50;
2972
+ const ABILITY_DISCOVERY_TIMEOUT_MS = 5000;
2973
+
2974
+ export type {
2975
+ ${pascalCase}AbilityInput,
2976
+ ${pascalCase}AbilityOutput,
2977
+ };
2978
+
2979
+ function sleep( milliseconds: number ): Promise< void > {
2980
+ return new Promise( ( resolve ) => {
2981
+ setTimeout( resolve, milliseconds );
2982
+ } );
2983
+ }
2984
+
2985
+ async function waitFor${pascalCase}AbilityRegistration(): Promise< void > {
2986
+ const deadline = Date.now() + ABILITY_DISCOVERY_TIMEOUT_MS;
2987
+ while ( ! getRegisteredAbility( ${abilityConstBase}_ABILITY_ID ) ) {
2988
+ if ( Date.now() >= deadline ) {
2989
+ return;
2990
+ }
2991
+
2992
+ await sleep( ABILITY_DISCOVERY_POLL_INTERVAL_MS );
2993
+ }
2994
+ }
2995
+
2996
+ export async function list${pascalCase}CategoryAbilities(): Promise< WordPressAbilityDefinition[] > {
2997
+ await waitFor${pascalCase}AbilityRegistration();
2998
+
2999
+ return getAbilities( {
3000
+ category: ${abilityConstBase}_ABILITY_CATEGORY.slug,
3001
+ } ) as WordPressAbilityDefinition[];
3002
+ }
3003
+
3004
+ export async function get${pascalCase}Ability(): Promise<
3005
+ | WordPressAbilityDefinition
3006
+ | undefined
3007
+ > {
3008
+ await waitFor${pascalCase}AbilityRegistration();
3009
+
3010
+ return getRegisteredAbility( ${abilityConstBase}_ABILITY_ID ) as
3011
+ | WordPressAbilityDefinition
3012
+ | undefined;
3013
+ }
3014
+
3015
+ export async function require${pascalCase}Ability(): Promise< WordPressAbilityDefinition > {
3016
+ const ability = await get${pascalCase}Ability();
3017
+ if ( ability ) {
3018
+ return ability;
2871
3019
  }
3020
+
3021
+ throw new Error(
3022
+ [
3023
+ \`Ability "\${ ${abilityConstBase}_ABILITY_ID }" is not available yet.\`,
3024
+ 'Load the WordPress core abilities integration on this screen and confirm the server-side registration succeeded.',
3025
+ ].join( ' ' )
3026
+ );
3027
+ }
3028
+
3029
+ export async function run${pascalCase}Ability(
3030
+ input: ${pascalCase}AbilityInput
3031
+ ): Promise< ${pascalCase}AbilityOutput > {
3032
+ await waitFor${pascalCase}AbilityRegistration();
3033
+
3034
+ return ( await executeAbility(
3035
+ ${abilityConstBase}_ABILITY_ID,
3036
+ input
3037
+ ) ) as ${pascalCase}AbilityOutput;
2872
3038
  }
2873
3039
  `;
2874
3040
  }
2875
- async function ensureVariationRegistrationHook(blockIndexPath) {
2876
- await patchFile(blockIndexPath, (source) => {
2877
- let nextSource = source;
2878
- if (!nextSource.includes(VARIATIONS_IMPORT_LINE)) {
2879
- nextSource = `${VARIATIONS_IMPORT_LINE}
2880
- ${nextSource}`;
2881
- }
2882
- if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
2883
- const callInsertionPatterns = [
2884
- /(registerBlockType<[\s\S]*?\);\s*)/u,
2885
- /(registerBlockType\([\s\S]*?\);\s*)/u
2886
- ];
2887
- let inserted = false;
2888
- for (const pattern of callInsertionPatterns) {
2889
- const candidate = nextSource.replace(pattern, (match) => `${match}
2890
- ${VARIATIONS_CALL_LINE}
2891
- `);
2892
- if (candidate !== nextSource) {
2893
- nextSource = candidate;
2894
- inserted = true;
2895
- break;
2896
- }
2897
- }
2898
- if (!inserted) {
2899
- nextSource = `${nextSource.trimEnd()}
3041
+ function buildAbilityClientSource(abilitySlug) {
3042
+ const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
3043
+ return `/**
3044
+ * Re-export the typed ${pascalCase} ability client helpers.
3045
+ *
3046
+ * The helper methods load the WordPress core abilities integration and wait for
3047
+ * this server-registered ability before reading or executing it.
3048
+ */
3049
+ export * from './data';
3050
+ `;
3051
+ }
3052
+ function buildAbilitySyncScriptSource() {
3053
+ return `/* eslint-disable no-console */
3054
+ import { syncTypeSchemas } from '@wp-typia/block-runtime/metadata-core';
2900
3055
 
2901
- ${VARIATIONS_CALL_LINE}
3056
+ import {
3057
+ ABILITIES,
3058
+ type WorkspaceAbilityConfig,
3059
+ } from './block-config';
3060
+
3061
+ function parseCliOptions( argv: string[] ) {
3062
+ const options = {
3063
+ check: false,
3064
+ };
3065
+
3066
+ for ( const argument of argv ) {
3067
+ if ( argument === '--check' ) {
3068
+ options.check = true;
3069
+ continue;
3070
+ }
3071
+
3072
+ throw new Error( \`Unknown sync-abilities flag: \${ argument }\` );
3073
+ }
3074
+
3075
+ return options;
3076
+ }
3077
+
3078
+ function isWorkspaceAbility(
3079
+ ability: WorkspaceAbilityConfig
3080
+ ): ability is WorkspaceAbilityConfig & {
3081
+ clientFile: string;
3082
+ configFile: string;
3083
+ dataFile: string;
3084
+ inputSchemaFile: string;
3085
+ inputTypeName: string;
3086
+ outputSchemaFile: string;
3087
+ outputTypeName: string;
3088
+ phpFile: string;
3089
+ typesFile: string;
3090
+ } {
3091
+ return (
3092
+ typeof ability.clientFile === 'string' &&
3093
+ typeof ability.configFile === 'string' &&
3094
+ typeof ability.dataFile === 'string' &&
3095
+ typeof ability.inputSchemaFile === 'string' &&
3096
+ typeof ability.inputTypeName === 'string' &&
3097
+ typeof ability.outputSchemaFile === 'string' &&
3098
+ typeof ability.outputTypeName === 'string' &&
3099
+ typeof ability.phpFile === 'string' &&
3100
+ typeof ability.typesFile === 'string'
3101
+ );
3102
+ }
3103
+
3104
+ async function main() {
3105
+ const options = parseCliOptions( process.argv.slice( 2 ) );
3106
+ const abilities = ABILITIES.filter( isWorkspaceAbility );
3107
+
3108
+ if ( ABILITIES.length > 0 && abilities.length === 0 ) {
3109
+ console.warn(
3110
+ '\u26A0\uFE0F Ability inventory entries exist, but none include the required typed schema files. Check scripts/block-config.ts before relying on sync-abilities.'
3111
+ );
3112
+ }
3113
+
3114
+ if ( abilities.length === 0 ) {
3115
+ console.log(
3116
+ options.check
3117
+ ? '\u2139\uFE0F No typed workflow abilities are registered yet. "sync-abilities --check" is already clean.'
3118
+ : '\u2139\uFE0F No typed workflow abilities are registered yet.'
3119
+ );
3120
+ return;
3121
+ }
3122
+
3123
+ for ( const ability of abilities ) {
3124
+ await syncTypeSchemas(
3125
+ {
3126
+ jsonSchemaFile: ability.inputSchemaFile,
3127
+ projectRoot: process.cwd(),
3128
+ sourceTypeName: ability.inputTypeName,
3129
+ typesFile: ability.typesFile,
3130
+ },
3131
+ {
3132
+ check: options.check,
3133
+ }
3134
+ );
3135
+
3136
+ await syncTypeSchemas(
3137
+ {
3138
+ jsonSchemaFile: ability.outputSchemaFile,
3139
+ projectRoot: process.cwd(),
3140
+ sourceTypeName: ability.outputTypeName,
3141
+ typesFile: ability.typesFile,
3142
+ },
3143
+ {
3144
+ check: options.check,
3145
+ }
3146
+ );
3147
+ }
3148
+
3149
+ console.log(
3150
+ options.check
3151
+ ? '\u2705 Ability input and output schemas are already up to date for all registered workflow abilities!'
3152
+ : '\u2705 Ability input and output schemas generated for all registered workflow abilities!'
3153
+ );
3154
+ }
3155
+
3156
+ main().catch( ( error ) => {
3157
+ console.error( '\u274C Ability schema sync failed:', error );
3158
+ process.exit( 1 );
3159
+ } );
2902
3160
  `;
2903
- }
2904
- }
2905
- if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
2906
- throw new Error(`Unable to inject ${VARIATIONS_CALL_LINE} into ${path8.basename(blockIndexPath)}.`);
2907
- }
2908
- return nextSource;
2909
- });
2910
3161
  }
2911
- async function writeVariationRegistry(projectDir, blockSlug, variationSlug) {
2912
- const variationsDir = path8.join(projectDir, "src", "blocks", blockSlug, "variations");
2913
- const variationsIndexPath = path8.join(variationsDir, "index.ts");
2914
- await fsp5.mkdir(variationsDir, { recursive: true });
2915
- const existingVariationSlugs = fs4.readdirSync(variationsDir).filter((entry) => entry.endsWith(".ts") && entry !== "index.ts").map((entry) => entry.replace(/\.ts$/u, ""));
2916
- const nextVariationSlugs = Array.from(new Set([...existingVariationSlugs, variationSlug])).sort();
2917
- await fsp5.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
3162
+ function buildAbilityPhpSource(abilitySlug, workspace) {
3163
+ const abilityTitle = toTitleCase(abilitySlug);
3164
+ const abilityPhpId = abilitySlug.replace(/-/g, "_");
3165
+ const categoryRegisterFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_register_ability_category`;
3166
+ const abilityRegisterFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_register_ability`;
3167
+ const configLoaderFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_load_ability_config`;
3168
+ const schemaLoaderFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_load_ability_schema`;
3169
+ const permissionFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_can_execute_ability`;
3170
+ const executeFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_execute_ability`;
3171
+ const metaFactoryFunctionName = `${workspace.workspace.phpPrefix}_${abilityPhpId}_build_ability_meta`;
3172
+ return `<?php
3173
+ if ( ! defined( 'ABSPATH' ) ) {
3174
+ return;
2918
3175
  }
2919
- async function runAddVariationCommand({
2920
- blockName,
2921
- cwd = process.cwd(),
2922
- variationName
2923
- }) {
2924
- const workspace = resolveWorkspaceProject(cwd);
2925
- const blockSlug = normalizeBlockSlug(blockName);
2926
- const variationSlug = assertValidGeneratedSlug("Variation name", normalizeBlockSlug(variationName), "wp-typia add variation <name> --block <block-slug>");
2927
- const inventory = readWorkspaceInventory(workspace.projectDir);
2928
- resolveWorkspaceBlock(inventory, blockSlug);
2929
- assertVariationDoesNotExist(workspace.projectDir, blockSlug, variationSlug, inventory);
2930
- const blockConfigPath = path8.join(workspace.projectDir, "scripts", "block-config.ts");
2931
- const blockIndexPath = path8.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
2932
- const variationsDir = path8.join(workspace.projectDir, "src", "blocks", blockSlug, "variations");
2933
- const variationFilePath = path8.join(variationsDir, `${variationSlug}.ts`);
2934
- const variationsIndexPath = path8.join(variationsDir, "index.ts");
3176
+
3177
+ if ( ! function_exists( '${configLoaderFunctionName}' ) ) {
3178
+ function ${configLoaderFunctionName}() {
3179
+ $project_root = dirname( __DIR__, 2 );
3180
+ $config_path = $project_root . '/src/abilities/${abilitySlug}/ability.config.json';
3181
+ if ( ! file_exists( $config_path ) ) {
3182
+ return null;
3183
+ }
3184
+
3185
+ $decoded = json_decode( file_get_contents( $config_path ), true );
3186
+ return is_array( $decoded ) ? $decoded : null;
3187
+ }
3188
+ }
3189
+
3190
+ if ( ! function_exists( '${schemaLoaderFunctionName}' ) ) {
3191
+ function ${schemaLoaderFunctionName}( $schema_name ) {
3192
+ $project_root = dirname( __DIR__, 2 );
3193
+ $schema_path = $project_root . '/src/abilities/${abilitySlug}/' . $schema_name;
3194
+ if ( ! file_exists( $schema_path ) ) {
3195
+ return null;
3196
+ }
3197
+
3198
+ $decoded = json_decode( file_get_contents( $schema_path ), true );
3199
+ return is_array( $decoded ) ? $decoded : null;
3200
+ }
3201
+ }
3202
+
3203
+ if ( ! function_exists( '${metaFactoryFunctionName}' ) ) {
3204
+ function ${metaFactoryFunctionName}( array $config ) {
3205
+ $meta = array(
3206
+ 'annotations' => isset( $config['meta']['annotations'] ) && is_array( $config['meta']['annotations'] )
3207
+ ? $config['meta']['annotations']
3208
+ : array(
3209
+ 'destructive' => false,
3210
+ 'idempotent' => true,
3211
+ 'readonly' => false,
3212
+ ),
3213
+ 'show_in_rest' => ! empty( $config['meta']['showInRest'] ),
3214
+ );
3215
+
3216
+ if ( ! empty( $config['meta']['mcp']['public'] ) ) {
3217
+ $meta['mcp'] = array(
3218
+ 'public' => true,
3219
+ );
3220
+ }
3221
+
3222
+ return $meta;
3223
+ }
3224
+ }
3225
+
3226
+ if ( ! function_exists( '${permissionFunctionName}' ) ) {
3227
+ function ${permissionFunctionName}( $input = array() ) {
3228
+ unset( $input );
3229
+
3230
+ return current_user_can( 'edit_posts' );
3231
+ }
3232
+ }
3233
+
3234
+ if ( ! function_exists( '${executeFunctionName}' ) ) {
3235
+ function ${executeFunctionName}( $input = array() ) {
3236
+ $payload = is_array( $input ) ? $input : array();
3237
+ $context_id = isset( $payload['contextId'] ) ? (int) $payload['contextId'] : 0;
3238
+ $note = isset( $payload['note'] ) && is_string( $payload['note'] )
3239
+ ? trim( $payload['note'] )
3240
+ : '';
3241
+ $result = array(
3242
+ 'processedContextId' => $context_id,
3243
+ 'status' => 'ready',
3244
+ 'summary' => sprintf(
3245
+ /* translators: 1: workflow title, 2: context id */
3246
+ __( '%1$s processed context %2$d.', ${quotePhpString3(workspace.workspace.textDomain)} ),
3247
+ ${quotePhpString3(abilityTitle)},
3248
+ $context_id
3249
+ ),
3250
+ );
3251
+
3252
+ if ( '' !== $note ) {
3253
+ $result['receivedNote'] = $note;
3254
+ }
3255
+
3256
+ return $result;
3257
+ }
3258
+ }
3259
+
3260
+ if ( ! function_exists( '${categoryRegisterFunctionName}' ) ) {
3261
+ function ${categoryRegisterFunctionName}() {
3262
+ if ( ! function_exists( 'wp_register_ability_category' ) ) {
3263
+ return;
3264
+ }
3265
+
3266
+ $config = ${configLoaderFunctionName}();
3267
+ if (
3268
+ ! is_array( $config ) ||
3269
+ empty( $config['category']['slug'] ) ||
3270
+ empty( $config['category']['label'] )
3271
+ ) {
3272
+ return;
3273
+ }
3274
+
3275
+ wp_register_ability_category(
3276
+ (string) $config['category']['slug'],
3277
+ array(
3278
+ 'description' => isset( $config['category']['description'] ) && is_string( $config['category']['description'] )
3279
+ ? $config['category']['description']
3280
+ : '',
3281
+ 'label' => (string) $config['category']['label'],
3282
+ )
3283
+ );
3284
+ }
3285
+ }
3286
+
3287
+ if ( ! function_exists( '${abilityRegisterFunctionName}' ) ) {
3288
+ function ${abilityRegisterFunctionName}() {
3289
+ if ( ! function_exists( 'wp_register_ability' ) ) {
3290
+ return;
3291
+ }
3292
+
3293
+ $config = ${configLoaderFunctionName}();
3294
+ if (
3295
+ ! is_array( $config ) ||
3296
+ empty( $config['abilityId'] ) ||
3297
+ empty( $config['category']['slug'] ) ||
3298
+ empty( $config['label'] ) ||
3299
+ empty( $config['description'] )
3300
+ ) {
3301
+ return;
3302
+ }
3303
+
3304
+ $input_schema = ${schemaLoaderFunctionName}( 'input.schema.json' );
3305
+ $output_schema = ${schemaLoaderFunctionName}( 'output.schema.json' );
3306
+ if ( ! is_array( $output_schema ) ) {
3307
+ return;
3308
+ }
3309
+
3310
+ $args = array(
3311
+ 'category' => (string) $config['category']['slug'],
3312
+ 'description' => (string) $config['description'],
3313
+ 'execute_callback' => ${quotePhpString3(executeFunctionName)},
3314
+ 'label' => (string) $config['label'],
3315
+ 'meta' => ${metaFactoryFunctionName}( $config ),
3316
+ 'output_schema' => $output_schema,
3317
+ 'permission_callback' => ${quotePhpString3(permissionFunctionName)},
3318
+ );
3319
+
3320
+ if ( is_array( $input_schema ) ) {
3321
+ $args['input_schema'] = $input_schema;
3322
+ }
3323
+
3324
+ wp_register_ability(
3325
+ (string) $config['abilityId'],
3326
+ $args
3327
+ );
3328
+ }
3329
+ }
3330
+
3331
+ add_action( 'wp_abilities_api_categories_init', '${categoryRegisterFunctionName}' );
3332
+ add_action( 'wp_abilities_api_init', '${abilityRegisterFunctionName}' );
3333
+ `;
3334
+ }
3335
+ function buildAbilityRegistrySource(abilitySlugs) {
3336
+ const exportLines = abilitySlugs.map((abilitySlug) => `export * from './${abilitySlug}/client';`).join(`
3337
+ `);
3338
+ return [
3339
+ ABILITY_REGISTRY_START_MARKER,
3340
+ exportLines,
3341
+ ABILITY_REGISTRY_END_MARKER
3342
+ ].filter((line) => line.length > 0).join(`
3343
+ `).concat(`
3344
+ `);
3345
+ }
3346
+ function resolveAbilityRegistryPath(projectDir) {
3347
+ const abilitiesDir = path8.join(projectDir, "src", "abilities");
3348
+ return [path8.join(abilitiesDir, "index.ts"), path8.join(abilitiesDir, "index.js")].find((candidatePath) => fs4.existsSync(candidatePath)) ?? path8.join(abilitiesDir, "index.ts");
3349
+ }
3350
+ function readAbilityRegistrySlugs(registryPath) {
3351
+ if (!fs4.existsSync(registryPath)) {
3352
+ return [];
3353
+ }
3354
+ const source = fs4.readFileSync(registryPath, "utf8");
3355
+ return Array.from(source.matchAll(/^\s*export\s+\*\s+from\s+['"]\.\/([^/'"]+)\/client['"];?\s*$/gmu)).map((match) => match[1]);
3356
+ }
3357
+ async function writeAbilityRegistry(projectDir, abilitySlug) {
3358
+ const abilitiesDir = path8.join(projectDir, "src", "abilities");
3359
+ const registryPath = resolveAbilityRegistryPath(projectDir);
3360
+ await fsp5.mkdir(abilitiesDir, { recursive: true });
3361
+ const existingAbilitySlugs = readWorkspaceInventory(projectDir).abilities.map((entry) => entry.slug);
3362
+ const existingRegistrySlugs = readAbilityRegistrySlugs(registryPath);
3363
+ const nextAbilitySlugs = Array.from(new Set([...existingAbilitySlugs, ...existingRegistrySlugs, abilitySlug])).sort();
3364
+ const generatedSection = buildAbilityRegistrySource(nextAbilitySlugs);
3365
+ const existingSource = fs4.existsSync(registryPath) ? fs4.readFileSync(registryPath, "utf8") : "";
3366
+ const generatedSectionPattern = new RegExp(`${escapeRegex3(ABILITY_REGISTRY_START_MARKER)}[\\s\\S]*?${escapeRegex3(ABILITY_REGISTRY_END_MARKER)}\\n?`, "u");
3367
+ const nextSource = existingSource ? generatedSectionPattern.test(existingSource) ? existingSource.replace(generatedSectionPattern, generatedSection) : `${existingSource.trimEnd()}
3368
+
3369
+ ${generatedSection}` : generatedSection;
3370
+ await fsp5.writeFile(registryPath, nextSource, "utf8");
3371
+ }
3372
+ async function ensureAbilityBootstrapAnchors(workspace) {
3373
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
3374
+ await patchFile(bootstrapPath, (source) => {
3375
+ let nextSource = source;
3376
+ const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
3377
+ const loadFunctionName = `${workspace.workspace.phpPrefix}_load_workflow_abilities`;
3378
+ const enqueueFunctionName = `${workspace.workspace.phpPrefix}_enqueue_workflow_abilities`;
3379
+ const loadHook = `add_action( 'plugins_loaded', '${loadFunctionName}' );`;
3380
+ const adminEnqueueHook = `add_action( 'admin_enqueue_scripts', '${enqueueFunctionName}' );`;
3381
+ const editorEnqueueHook = `add_action( 'enqueue_block_editor_assets', '${enqueueFunctionName}' );`;
3382
+ const loadFunction = `
3383
+
3384
+ function ${loadFunctionName}() {
3385
+ foreach ( glob( __DIR__ . '${ABILITY_SERVER_GLOB}' ) ?: array() as $ability_module ) {
3386
+ require_once $ability_module;
3387
+ }
3388
+ }
3389
+ `;
3390
+ const enqueueFunction = `
3391
+
3392
+ function ${enqueueFunctionName}() {
3393
+ if ( ! class_exists( 'WP_Ability' ) ) {
3394
+ return;
3395
+ }
3396
+
3397
+ $script_path = __DIR__ . '/${ABILITY_EDITOR_SCRIPT}';
3398
+ $asset_path = __DIR__ . '/${ABILITY_EDITOR_ASSET}';
3399
+
3400
+ if ( ! file_exists( $script_path ) || ! file_exists( $asset_path ) ) {
3401
+ return;
3402
+ }
3403
+
3404
+ $asset = require $asset_path;
3405
+ if ( ! is_array( $asset ) ) {
3406
+ $asset = array();
3407
+ }
3408
+
3409
+ $dependencies = isset( $asset['dependencies'] ) && is_array( $asset['dependencies'] )
3410
+ ? $asset['dependencies']
3411
+ : array();
3412
+
3413
+ foreach ( array( '${WP_CORE_ABILITIES_SCRIPT_MODULE_ID}', '${WP_ABILITIES_SCRIPT_MODULE_ID}' ) as $ability_dependency ) {
3414
+ $has_dependency = false;
3415
+ foreach ( $dependencies as $dependency ) {
3416
+ $dependency_id = is_array( $dependency ) && isset( $dependency['id'] )
3417
+ ? $dependency['id']
3418
+ : $dependency;
3419
+ if ( $dependency_id === $ability_dependency ) {
3420
+ $has_dependency = true;
3421
+ break;
3422
+ }
3423
+ }
3424
+ if ( ! $has_dependency ) {
3425
+ $dependencies[] = $ability_dependency;
3426
+ }
3427
+ }
3428
+
3429
+ if ( ! function_exists( 'wp_enqueue_script_module' ) ) {
3430
+ return;
3431
+ }
3432
+
3433
+ wp_enqueue_script_module(
3434
+ '${workspaceBaseName}-abilities',
3435
+ plugins_url( '${ABILITY_EDITOR_SCRIPT}', __FILE__ ),
3436
+ $dependencies,
3437
+ isset( $asset['version'] ) ? $asset['version'] : filemtime( $script_path )
3438
+ );
3439
+ }
3440
+ `;
3441
+ const insertionAnchors = [
3442
+ /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
3443
+ /\?>\s*$/u
3444
+ ];
3445
+ const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex3(functionName)}\\s*\\(`, "u").test(nextSource);
3446
+ const insertPhpSnippet = (snippet) => {
3447
+ for (const anchor of insertionAnchors) {
3448
+ const candidate = nextSource.replace(anchor, (match) => `${snippet}
3449
+ ${match}`);
3450
+ if (candidate !== nextSource) {
3451
+ nextSource = candidate;
3452
+ return;
3453
+ }
3454
+ }
3455
+ nextSource = `${nextSource.trimEnd()}
3456
+ ${snippet}
3457
+ `;
3458
+ };
3459
+ const appendPhpSnippet = (snippet) => {
3460
+ const closingTagPattern = /\?>\s*$/u;
3461
+ if (closingTagPattern.test(nextSource)) {
3462
+ nextSource = nextSource.replace(closingTagPattern, `${snippet}
3463
+ ?>`);
3464
+ return;
3465
+ }
3466
+ nextSource = `${nextSource.trimEnd()}
3467
+ ${snippet}
3468
+ `;
3469
+ };
3470
+ if (!hasPhpFunctionDefinition(loadFunctionName)) {
3471
+ insertPhpSnippet(loadFunction);
3472
+ }
3473
+ if (!hasPhpFunctionDefinition(enqueueFunctionName)) {
3474
+ insertPhpSnippet(enqueueFunction);
3475
+ } else if (!findPhpFunctionRange2(nextSource, enqueueFunctionName)?.source.includes("wp_enqueue_script_module")) {
3476
+ nextSource = replacePhpFunctionDefinition2(nextSource, enqueueFunctionName, enqueueFunction);
3477
+ }
3478
+ if (!nextSource.includes(loadHook)) {
3479
+ appendPhpSnippet(loadHook);
3480
+ }
3481
+ if (!nextSource.includes(adminEnqueueHook)) {
3482
+ appendPhpSnippet(adminEnqueueHook);
3483
+ }
3484
+ if (!nextSource.includes(editorEnqueueHook)) {
3485
+ appendPhpSnippet(editorEnqueueHook);
3486
+ }
3487
+ return nextSource;
3488
+ });
3489
+ }
3490
+ async function ensureAbilityPackageScripts(workspace) {
3491
+ const packageJsonPath = path8.join(workspace.projectDir, "package.json");
3492
+ const packageJson = JSON.parse(await fsp5.readFile(packageJsonPath, "utf8"));
3493
+ const nextScripts = {
3494
+ ...packageJson.scripts ?? {},
3495
+ "sync-abilities": packageJson.scripts?.["sync-abilities"] ?? "tsx scripts/sync-abilities.ts"
3496
+ };
3497
+ const nextDependencies = {
3498
+ ...packageJson.dependencies ?? {},
3499
+ [WP_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_ABILITIES_SCRIPT_MODULE_ID], WP_ABILITIES_PACKAGE_VERSION),
3500
+ [WP_CORE_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_CORE_ABILITIES_SCRIPT_MODULE_ID], WP_CORE_ABILITIES_PACKAGE_VERSION)
3501
+ };
3502
+ if (JSON.stringify(nextScripts) === JSON.stringify(packageJson.scripts ?? {}) && JSON.stringify(nextDependencies) === JSON.stringify(packageJson.dependencies ?? {})) {
3503
+ return;
3504
+ }
3505
+ packageJson.scripts = nextScripts;
3506
+ packageJson.dependencies = nextDependencies;
3507
+ await fsp5.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
3508
+ `, "utf8");
3509
+ }
3510
+ async function ensureAbilitySyncProjectAnchors(workspace) {
3511
+ const syncProjectScriptPath = path8.join(workspace.projectDir, "scripts", "sync-project.ts");
3512
+ await patchFile(syncProjectScriptPath, (source) => {
3513
+ let nextSource = source;
3514
+ const syncRestConst = "const syncRestScriptPath = path.join( 'scripts', 'sync-rest-contracts.ts' );";
3515
+ const syncAbilitiesConst = "const syncAbilitiesScriptPath = path.join( 'scripts', 'sync-abilities.ts' );";
3516
+ const syncRestBlockPattern = /if \( fs\.existsSync\( path\.resolve\( process\.cwd\(\), syncRestScriptPath \) \) \) \{\n\s*runSyncScript\( syncRestScriptPath, options \);\n\s*\}/u;
3517
+ const syncAbilitiesBlock = [
3518
+ "if ( fs.existsSync( path.resolve( process.cwd(), syncAbilitiesScriptPath ) ) ) {",
3519
+ "\trunSyncScript( syncAbilitiesScriptPath, options );",
3520
+ "}"
3521
+ ].join(`
3522
+ `);
3523
+ if (!nextSource.includes(syncAbilitiesConst)) {
3524
+ if (!nextSource.includes(syncRestConst)) {
3525
+ throw new Error([
3526
+ `ensureAbilitySyncProjectAnchors could not patch ${path8.basename(syncProjectScriptPath)}.`,
3527
+ "Missing the expected sync-rest script constant in scripts/sync-project.ts.",
3528
+ "Restore the generated template or wire sync-abilities manually before retrying."
3529
+ ].join(" "));
3530
+ }
3531
+ nextSource = nextSource.replace(syncRestConst, `${syncRestConst}
3532
+ ${syncAbilitiesConst}`);
3533
+ }
3534
+ if (!nextSource.includes("runSyncScript( syncAbilitiesScriptPath, options );")) {
3535
+ if (!syncRestBlockPattern.test(nextSource)) {
3536
+ throw new Error([
3537
+ `ensureAbilitySyncProjectAnchors could not patch ${path8.basename(syncProjectScriptPath)}.`,
3538
+ "Missing the expected sync-rest invocation block in scripts/sync-project.ts.",
3539
+ "Restore the generated template or wire sync-abilities manually before retrying."
3540
+ ].join(" "));
3541
+ }
3542
+ nextSource = nextSource.replace(syncRestBlockPattern, (match) => `${match}
3543
+
3544
+ ${syncAbilitiesBlock}`);
3545
+ }
3546
+ return nextSource;
3547
+ });
3548
+ }
3549
+ async function ensureAbilityBuildScriptAnchors(workspace) {
3550
+ const buildScriptPath = path8.join(workspace.projectDir, "scripts", "build-workspace.mjs");
3551
+ await patchFile(buildScriptPath, (source) => {
3552
+ let nextSource = source;
3553
+ if (/['"]src\/abilities\/index\.(?:ts|js)['"]/u.test(nextSource)) {
3554
+ return nextSource;
3555
+ }
3556
+ const sharedEntriesPattern = /(for\s*\(\s*const\s+relativePath\s+of\s+\[)([\s\S]*?)(\]\s*\)\s*\{)/u;
3557
+ const match = nextSource.match(sharedEntriesPattern);
3558
+ if (!match || !match[2].includes("src/bindings/index.ts") || !match[2].includes("src/editor-plugins/index.ts")) {
3559
+ throw new Error([
3560
+ `ensureAbilityBuildScriptAnchors could not patch ${path8.basename(buildScriptPath)}.`,
3561
+ "Missing the expected shared editor entries array in scripts/build-workspace.mjs.",
3562
+ "Restore the generated template or wire abilities/index manually before retrying."
3563
+ ].join(" "));
3564
+ }
3565
+ nextSource = nextSource.replace(sharedEntriesPattern, `$1
3566
+ 'src/bindings/index.ts',
3567
+ 'src/bindings/index.js',
3568
+ 'src/editor-plugins/index.ts',
3569
+ 'src/editor-plugins/index.js',
3570
+ 'src/abilities/index.ts',
3571
+ 'src/abilities/index.js',
3572
+ $3`);
3573
+ return nextSource;
3574
+ });
3575
+ }
3576
+ async function ensureAbilityWebpackAnchors(workspace) {
3577
+ const webpackConfigPath = path8.join(workspace.projectDir, "webpack.config.js");
3578
+ await patchFile(webpackConfigPath, (source) => {
3579
+ if (/['"]abilities\/index['"]/u.test(source)) {
3580
+ return source;
3581
+ }
3582
+ const optionalModuleReturnPattern = /(function\s+getOptionalModuleEntries\s*\(\)\s*\{[\s\S]*?)(\n\treturn Object\.fromEntries\(\s*entries\s*\);\n\})/u;
3583
+ if (optionalModuleReturnPattern.test(source)) {
3584
+ return source.replace(optionalModuleReturnPattern, `$1
3585
+
3586
+ for ( const [ entryName, candidates ] of [
3587
+ [
3588
+ 'abilities/index',
3589
+ [ 'src/abilities/index.ts', 'src/abilities/index.js' ],
3590
+ ],
3591
+ ] ) {
3592
+ for ( const relativePath of candidates ) {
3593
+ const entryPath = path.resolve( process.cwd(), relativePath );
3594
+ if ( ! fs.existsSync( entryPath ) ) {
3595
+ continue;
3596
+ }
3597
+
3598
+ entries.push( [ entryName, entryPath ] );
3599
+ break;
3600
+ }
3601
+ }
3602
+ $2`);
3603
+ }
3604
+ const sharedEntriesPattern = /for\s*\(\s*const\s+\[\s*entryName\s*,\s*candidates\s*\]\s+of\s+\[([\s\S]*?)\]\s*\)\s*\{/u;
3605
+ const match = source.match(sharedEntriesPattern);
3606
+ if (!match || !match[1].includes("bindings/index") || !match[1].includes("editor-plugins/index")) {
3607
+ throw new Error([
3608
+ `ensureAbilityWebpackAnchors could not patch ${path8.basename(webpackConfigPath)}.`,
3609
+ "Missing the expected shared editor entries block in webpack.config.js.",
3610
+ "Restore the generated template or wire abilities/index manually before retrying."
3611
+ ].join(" "));
3612
+ }
3613
+ return source.replace(sharedEntriesPattern, `for ( const [ entryName, candidates ] of [
3614
+ [
3615
+ 'bindings/index',
3616
+ [ 'src/bindings/index.ts', 'src/bindings/index.js' ],
3617
+ ],
3618
+ [
3619
+ 'editor-plugins/index',
3620
+ [ 'src/editor-plugins/index.ts', 'src/editor-plugins/index.js' ],
3621
+ ],
3622
+ [
3623
+ 'abilities/index',
3624
+ [ 'src/abilities/index.ts', 'src/abilities/index.js' ],
3625
+ ],
3626
+ ] ) {`);
3627
+ });
3628
+ }
3629
+ async function runAddAbilityCommand({
3630
+ abilityName,
3631
+ cwd = process.cwd()
3632
+ }) {
3633
+ const workspace = resolveWorkspaceProject(cwd);
3634
+ const abilitySlug = assertValidGeneratedSlug("Ability name", normalizeBlockSlug(abilityName), "wp-typia add ability <name>");
3635
+ const inventory = readWorkspaceInventory(workspace.projectDir);
3636
+ assertAbilityDoesNotExist(workspace.projectDir, abilitySlug, inventory);
3637
+ const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(REQUIRED_WORKSPACE_ABILITY_COMPATIBILITY);
3638
+ const blockConfigPath = path8.join(workspace.projectDir, "scripts", "block-config.ts");
3639
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
3640
+ const buildScriptPath = path8.join(workspace.projectDir, "scripts", "build-workspace.mjs");
3641
+ const packageJsonPath = path8.join(workspace.projectDir, "package.json");
3642
+ const syncAbilitiesScriptPath = path8.join(workspace.projectDir, "scripts", "sync-abilities.ts");
3643
+ const syncProjectScriptPath = path8.join(workspace.projectDir, "scripts", "sync-project.ts");
3644
+ const webpackConfigPath = path8.join(workspace.projectDir, "webpack.config.js");
3645
+ const abilitiesIndexPath = resolveAbilityRegistryPath(workspace.projectDir);
3646
+ const abilityDir = path8.join(workspace.projectDir, "src", "abilities", abilitySlug);
3647
+ const configFilePath = path8.join(abilityDir, "ability.config.json");
3648
+ const typesFilePath = path8.join(abilityDir, "types.ts");
3649
+ const dataFilePath = path8.join(abilityDir, "data.ts");
3650
+ const clientFilePath = path8.join(abilityDir, "client.ts");
3651
+ const phpFilePath = path8.join(workspace.projectDir, "inc", "abilities", `${abilitySlug}.php`);
3652
+ const mutationSnapshot = {
3653
+ fileSources: await snapshotWorkspaceFiles([
3654
+ blockConfigPath,
3655
+ bootstrapPath,
3656
+ buildScriptPath,
3657
+ packageJsonPath,
3658
+ syncAbilitiesScriptPath,
3659
+ syncProjectScriptPath,
3660
+ webpackConfigPath,
3661
+ abilitiesIndexPath
3662
+ ]),
3663
+ snapshotDirs: [],
3664
+ targetPaths: [abilityDir, phpFilePath, syncAbilitiesScriptPath]
3665
+ };
3666
+ try {
3667
+ await fsp5.mkdir(abilityDir, { recursive: true });
3668
+ await fsp5.mkdir(path8.dirname(phpFilePath), { recursive: true });
3669
+ await ensureAbilityBootstrapAnchors(workspace);
3670
+ await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
3671
+ await ensureAbilityPackageScripts(workspace);
3672
+ await ensureAbilitySyncProjectAnchors(workspace);
3673
+ await ensureAbilityBuildScriptAnchors(workspace);
3674
+ await ensureAbilityWebpackAnchors(workspace);
3675
+ await fsp5.writeFile(syncAbilitiesScriptPath, buildAbilitySyncScriptSource(), "utf8");
3676
+ await fsp5.writeFile(configFilePath, buildAbilityConfigSource(abilitySlug, workspace.workspace.namespace), "utf8");
3677
+ await fsp5.writeFile(typesFilePath, buildAbilityTypesSource(abilitySlug), "utf8");
3678
+ await fsp5.writeFile(dataFilePath, buildAbilityDataSource(abilitySlug), "utf8");
3679
+ await fsp5.writeFile(clientFilePath, buildAbilityClientSource(abilitySlug), "utf8");
3680
+ await fsp5.writeFile(phpFilePath, buildAbilityPhpSource(abilitySlug, workspace), "utf8");
3681
+ const pascalCase = toPascalCaseFromAbilitySlug(abilitySlug);
3682
+ await syncTypeSchemas2({
3683
+ jsonSchemaFile: `src/abilities/${abilitySlug}/input.schema.json`,
3684
+ projectRoot: workspace.projectDir,
3685
+ sourceTypeName: `${pascalCase}AbilityInput`,
3686
+ typesFile: `src/abilities/${abilitySlug}/types.ts`
3687
+ });
3688
+ await syncTypeSchemas2({
3689
+ jsonSchemaFile: `src/abilities/${abilitySlug}/output.schema.json`,
3690
+ projectRoot: workspace.projectDir,
3691
+ sourceTypeName: `${pascalCase}AbilityOutput`,
3692
+ typesFile: `src/abilities/${abilitySlug}/types.ts`
3693
+ });
3694
+ await writeAbilityRegistry(workspace.projectDir, abilitySlug);
3695
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
3696
+ abilityEntries: [buildAbilityConfigEntry(abilitySlug, compatibilityPolicy)]
3697
+ });
3698
+ return {
3699
+ abilitySlug,
3700
+ projectDir: workspace.projectDir
3701
+ };
3702
+ } catch (error) {
3703
+ await rollbackWorkspaceMutation(mutationSnapshot);
3704
+ throw error;
3705
+ }
3706
+ }
3707
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai.ts
3708
+ import { promises as fsp7 } from "fs";
3709
+ import path11 from "path";
3710
+
3711
+ // ../wp-typia-project-tools/src/runtime/ai-feature-artifacts.ts
3712
+ import { mkdir, readFile, writeFile } from "fs/promises";
3713
+ import path9 from "path";
3714
+ import {
3715
+ defineEndpointManifest as defineEndpointManifest2,
3716
+ syncEndpointClient as syncEndpointClient2,
3717
+ syncRestOpenApi as syncRestOpenApi2,
3718
+ syncTypeSchemas as syncTypeSchemas3
3719
+ } from "@wp-typia/block-runtime/metadata-core";
3720
+
3721
+ // ../wp-typia-project-tools/src/runtime/schema-core.ts
3722
+ var exports_schema_core = {};
3723
+ __reExport(exports_schema_core, schema_core);
3724
+ import"@wp-typia/block-runtime/schema-core";
3725
+
3726
+ // ../wp-typia-project-tools/src/runtime/wordpress-ai.ts
3727
+ function projectWordPressAiSchema(schema) {
3728
+ return exports_schema_core.projectJsonSchemaDocument(schema, {
3729
+ profile: "ai-structured-output"
3730
+ });
3731
+ }
3732
+ // ../wp-typia-project-tools/src/runtime/ai-feature-artifacts.ts
3733
+ function normalizeGeneratedArtifactContent(content) {
3734
+ return content.replace(/\r\n?/g, `
3735
+ `);
3736
+ }
3737
+ async function reconcileGeneratedArtifact(options) {
3738
+ if (!options.check) {
3739
+ await mkdir(path9.dirname(options.filePath), {
3740
+ recursive: true
3741
+ });
3742
+ await writeFile(options.filePath, options.content, "utf8");
3743
+ return;
3744
+ }
3745
+ try {
3746
+ const current = normalizeGeneratedArtifactContent(await readFile(options.filePath, "utf8"));
3747
+ const expected = normalizeGeneratedArtifactContent(options.content);
3748
+ if (current !== expected) {
3749
+ throw new Error(`Generated AI feature artifact is stale: ${options.label} (${options.filePath}).`);
3750
+ }
3751
+ } catch (error) {
3752
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
3753
+ if (code === "ENOENT") {
3754
+ throw new Error(`Generated AI feature artifact is missing: ${options.label} (${options.filePath}).`);
3755
+ }
3756
+ throw error;
3757
+ }
3758
+ }
3759
+ function assertJsonObject(value, label) {
3760
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3761
+ throw new Error(`Expected ${label} to decode to a JSON object.`);
3762
+ }
3763
+ return value;
3764
+ }
3765
+ function buildAiFeatureEndpointManifest(variables) {
3766
+ return defineEndpointManifest2({
3767
+ contracts: {
3768
+ "feature-request": {
3769
+ sourceTypeName: `${variables.pascalCase}AiFeatureRequest`
3770
+ },
3771
+ "feature-response": {
3772
+ sourceTypeName: `${variables.pascalCase}AiFeatureResponse`
3773
+ },
3774
+ "feature-result": {
3775
+ sourceTypeName: `${variables.pascalCase}AiFeatureResult`
3776
+ }
3777
+ },
3778
+ endpoints: [
3779
+ {
3780
+ auth: "authenticated",
3781
+ bodyContract: "feature-request",
3782
+ method: "POST",
3783
+ operationId: `run${variables.pascalCase}AiFeature`,
3784
+ path: `/${variables.namespace}/ai/${variables.slugKebabCase}`,
3785
+ responseContract: "feature-response",
3786
+ summary: `Run the ${variables.title} AI feature endpoint.`,
3787
+ tags: [`${variables.title} AI`],
3788
+ wordpressAuth: {
3789
+ mechanism: "rest-nonce"
3790
+ }
3791
+ }
3792
+ ],
3793
+ info: {
3794
+ title: `${variables.title} AI Feature API`,
3795
+ version: "1.0.0"
3796
+ }
3797
+ });
3798
+ }
3799
+ async function syncAiFeatureRestArtifacts({
3800
+ clientFile,
3801
+ executionOptions,
3802
+ outputDir,
3803
+ projectDir,
3804
+ typesFile,
3805
+ validatorsFile,
3806
+ variables
3807
+ }) {
3808
+ const manifest = buildAiFeatureEndpointManifest(variables);
3809
+ for (const [baseName, contract] of Object.entries(manifest.contracts)) {
3810
+ await syncTypeSchemas3({
3811
+ jsonSchemaFile: path9.join(outputDir, "api-schemas", `${baseName}.schema.json`),
3812
+ openApiFile: path9.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
3813
+ projectRoot: projectDir,
3814
+ sourceTypeName: contract.sourceTypeName,
3815
+ typesFile
3816
+ }, executionOptions);
3817
+ }
3818
+ await syncRestOpenApi2({
3819
+ manifest,
3820
+ openApiFile: path9.join(outputDir, "api.openapi.json"),
3821
+ projectRoot: projectDir,
3822
+ typesFile
3823
+ }, executionOptions);
3824
+ await syncEndpointClient2({
3825
+ clientFile,
3826
+ manifest,
3827
+ projectRoot: projectDir,
3828
+ typesFile,
3829
+ validatorsFile
3830
+ }, executionOptions);
3831
+ }
3832
+ async function syncAiFeatureSchemaArtifact({
3833
+ aiSchemaFile,
3834
+ check = false,
3835
+ outputDir,
3836
+ projectDir
3837
+ }) {
3838
+ const sourceSchemaPath = path9.join(projectDir, outputDir, "api-schemas", "feature-result.schema.json");
3839
+ const responseSchema = assertJsonObject(JSON.parse(await readFile(sourceSchemaPath, "utf8")), sourceSchemaPath);
3840
+ const aiSchema = projectWordPressAiSchema(responseSchema);
3841
+ await reconcileGeneratedArtifact({
3842
+ check,
3843
+ content: `${JSON.stringify(aiSchema, null, 2)}
3844
+ `,
3845
+ filePath: path9.join(projectDir, aiSchemaFile),
3846
+ label: "AI feature schema"
3847
+ });
3848
+ return {
3849
+ aiSchema,
3850
+ aiSchemaPath: path9.join(projectDir, aiSchemaFile),
3851
+ check,
3852
+ sourceSchemaPath
3853
+ };
3854
+ }
3855
+
3856
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai-source-emitters.ts
3857
+ function toPascalCaseFromAiFeatureSlug(slug) {
3858
+ return normalizeBlockSlug(slug).split("-").filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
3859
+ }
3860
+ function indentMultiline2(source, prefix) {
3861
+ return source.split(`
3862
+ `).map((line) => `${prefix}${line}`).join(`
3863
+ `);
3864
+ }
3865
+ function buildAiFeatureConfigEntry(aiFeatureSlug, namespace) {
3866
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
3867
+ const title = toTitleCase(aiFeatureSlug);
3868
+ const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY);
3869
+ const manifest = buildAiFeatureEndpointManifest({
3870
+ namespace,
3871
+ pascalCase,
3872
+ slugKebabCase: aiFeatureSlug,
3873
+ title
3874
+ });
3875
+ return [
3876
+ "\t{",
3877
+ ` aiSchemaFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/ai-schemas/feature-result.ai.schema.json`)},`,
3878
+ ` apiFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api.ts`)},`,
3879
+ ` clientFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api-client.ts`)},`,
3880
+ ` compatibility: ${renderScaffoldCompatibilityConfig(compatibilityPolicy)},`,
3881
+ ` dataFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/data.ts`)},`,
3882
+ ` namespace: ${quoteTsString(namespace)},`,
3883
+ ` openApiFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api.openapi.json`)},`,
3884
+ ` phpFile: ${quoteTsString(`inc/ai-features/${aiFeatureSlug}.php`)},`,
3885
+ "\t\trestManifest: defineEndpointManifest(",
3886
+ indentMultiline2(JSON.stringify(manifest, null, "\t"), "\t\t\t"),
3887
+ "\t\t),",
3888
+ ` slug: ${quoteTsString(aiFeatureSlug)},`,
3889
+ ` typesFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api-types.ts`)},`,
3890
+ ` validatorsFile: ${quoteTsString(`src/ai-features/${aiFeatureSlug}/api-validators.ts`)},`,
3891
+ "\t},"
3892
+ ].join(`
3893
+ `);
3894
+ }
3895
+ function buildAiFeatureTypesSource(aiFeatureSlug) {
3896
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
3897
+ return `import { tags } from 'typia';
3898
+
3899
+ export interface ${pascalCase}AiFeatureRequest {
3900
+ brief: string & tags.MinLength< 1 > & tags.MaxLength< 4000 >;
3901
+ context?: string & tags.MaxLength< 4000 >;
3902
+ }
3903
+
3904
+ export interface ${pascalCase}AiFeatureResult {
3905
+ title: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
3906
+ summary: string & tags.MinLength< 1 > & tags.MaxLength< 2000 >;
3907
+ confidence?: number & tags.Minimum< 0 > & tags.Maximum< 1 >;
3908
+ }
3909
+
3910
+ export interface ${pascalCase}AiFeatureTokenUsage {
3911
+ completionTokens: number & tags.Type< 'uint32' >;
3912
+ promptTokens: number & tags.Type< 'uint32' >;
3913
+ totalTokens: number & tags.Type< 'uint32' >;
3914
+ thoughtTokens?: number & tags.Type< 'uint32' >;
3915
+ }
3916
+
3917
+ export interface ${pascalCase}AiFeatureTelemetry {
3918
+ modelId: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
3919
+ modelName: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
3920
+ providerId: string & tags.MinLength< 1 > & tags.MaxLength< 80 >;
3921
+ providerName: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
3922
+ providerType: 'client' | 'cloud' | 'server';
3923
+ resultId: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
3924
+ tokenUsage: ${pascalCase}AiFeatureTokenUsage;
3925
+ }
3926
+
3927
+ export interface ${pascalCase}AiFeatureResponse {
3928
+ result: ${pascalCase}AiFeatureResult;
3929
+ telemetry: ${pascalCase}AiFeatureTelemetry;
3930
+ }
3931
+ `;
3932
+ }
3933
+ function buildAiFeatureValidatorsSource(aiFeatureSlug) {
3934
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
3935
+ return `import typia from 'typia';
3936
+
3937
+ import { toValidationResult } from '@wp-typia/rest';
3938
+ import type {
3939
+ ${pascalCase}AiFeatureRequest,
3940
+ ${pascalCase}AiFeatureResponse,
3941
+ ${pascalCase}AiFeatureResult,
3942
+ } from './api-types';
3943
+
3944
+ const validateFeatureRequest = typia.createValidate< ${pascalCase}AiFeatureRequest >();
3945
+ const validateFeatureResult = typia.createValidate< ${pascalCase}AiFeatureResult >();
3946
+ const validateFeatureResponse = typia.createValidate< ${pascalCase}AiFeatureResponse >();
3947
+
3948
+ export const apiValidators = {
3949
+ featureRequest: ( input: unknown ) =>
3950
+ toValidationResult< ${pascalCase}AiFeatureRequest >(
3951
+ validateFeatureRequest( input )
3952
+ ),
3953
+ featureResult: ( input: unknown ) =>
3954
+ toValidationResult< ${pascalCase}AiFeatureResult >(
3955
+ validateFeatureResult( input )
3956
+ ),
3957
+ featureResponse: ( input: unknown ) =>
3958
+ toValidationResult< ${pascalCase}AiFeatureResponse >(
3959
+ validateFeatureResponse( input )
3960
+ ),
3961
+ };
3962
+ `;
3963
+ }
3964
+ function buildAiFeatureApiSource(aiFeatureSlug) {
3965
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
3966
+ return `import {
3967
+ callEndpoint,
3968
+ resolveRestRouteUrl,
3969
+ } from '@wp-typia/rest';
3970
+
3971
+ import type {
3972
+ ${pascalCase}AiFeatureRequest,
3973
+ } from './api-types';
3974
+ import {
3975
+ run${pascalCase}AiFeatureEndpoint,
3976
+ } from './api-client';
3977
+
3978
+ function resolveRestNonce( fallback?: string ): string | undefined {
3979
+ if ( typeof fallback === 'string' && fallback.length > 0 ) {
3980
+ return fallback;
3981
+ }
3982
+
3983
+ if ( typeof window === 'undefined' ) {
3984
+ return undefined;
3985
+ }
3986
+
3987
+ const wpApiSettings = (
3988
+ window as typeof window & {
3989
+ wpApiSettings?: { nonce?: string };
3990
+ }
3991
+ ).wpApiSettings;
3992
+
3993
+ return typeof wpApiSettings?.nonce === 'string' &&
3994
+ wpApiSettings.nonce.length > 0
3995
+ ? wpApiSettings.nonce
3996
+ : undefined;
3997
+ }
3998
+
3999
+ export const aiFeatureRunEndpoint = {
4000
+ ...run${pascalCase}AiFeatureEndpoint,
4001
+ buildRequestOptions: () => {
4002
+ const nonce = resolveRestNonce();
4003
+ return {
4004
+ headers: nonce
4005
+ ? {
4006
+ 'X-WP-Nonce': nonce,
4007
+ }
4008
+ : undefined,
4009
+ url: resolveRestRouteUrl( run${pascalCase}AiFeatureEndpoint.path ),
4010
+ };
4011
+ },
4012
+ };
4013
+
4014
+ export function runAiFeature( request: ${pascalCase}AiFeatureRequest ) {
4015
+ return callEndpoint( aiFeatureRunEndpoint, request );
4016
+ }
4017
+ `;
4018
+ }
4019
+ function buildAiFeatureDataSource(aiFeatureSlug) {
4020
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
4021
+ return `import {
4022
+ useEndpointMutation,
4023
+ type UseEndpointMutationOptions,
4024
+ } from '@wp-typia/rest/react';
4025
+
4026
+ import type {
4027
+ ${pascalCase}AiFeatureRequest,
4028
+ ${pascalCase}AiFeatureResponse,
4029
+ } from './api-types';
4030
+ import {
4031
+ aiFeatureRunEndpoint,
4032
+ } from './api';
4033
+
4034
+ export type UseRun${pascalCase}AiFeatureMutationOptions =
4035
+ UseEndpointMutationOptions<
4036
+ ${pascalCase}AiFeatureRequest,
4037
+ ${pascalCase}AiFeatureResponse,
4038
+ unknown
4039
+ >;
4040
+
4041
+ export function useRun${pascalCase}AiFeatureMutation(
4042
+ options: UseRun${pascalCase}AiFeatureMutationOptions = {}
4043
+ ) {
4044
+ return useEndpointMutation( aiFeatureRunEndpoint, options );
4045
+ }
4046
+ `;
4047
+ }
4048
+ function buildAiFeatureSyncScriptSource() {
4049
+ return `/* eslint-disable no-console */
4050
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4051
+ import path from 'node:path';
4052
+
4053
+ import { projectWordPressAiSchema } from '@wp-typia/project-tools/ai-artifacts';
4054
+
4055
+ import {
4056
+ AI_FEATURES,
4057
+ type WorkspaceAiFeatureConfig,
4058
+ } from './block-config';
4059
+
4060
+ function parseCliOptions( argv: string[] ) {
4061
+ const options = {
4062
+ check: false,
4063
+ };
4064
+
4065
+ for ( const argument of argv ) {
4066
+ if ( argument === '--check' ) {
4067
+ options.check = true;
4068
+ continue;
4069
+ }
4070
+
4071
+ throw new Error( \`Unknown sync-ai flag: \${ argument }\` );
4072
+ }
4073
+
4074
+ return options;
4075
+ }
4076
+
4077
+ function isWorkspaceAiFeature(
4078
+ feature: WorkspaceAiFeatureConfig
4079
+ ): feature is WorkspaceAiFeatureConfig & {
4080
+ aiSchemaFile: string;
4081
+ typesFile: string;
4082
+ } {
4083
+ return (
4084
+ typeof feature.aiSchemaFile === 'string' &&
4085
+ typeof feature.typesFile === 'string'
4086
+ );
4087
+ }
4088
+
4089
+ function normalizeGeneratedArtifactContent( content: string ) {
4090
+ return content.replace( /\\r\\n?/g, '\\n' );
4091
+ }
4092
+
4093
+ async function reconcileGeneratedArtifact( options: {
4094
+ check: boolean;
4095
+ content: string;
4096
+ filePath: string;
4097
+ label: string;
4098
+ } ) {
4099
+ if ( ! options.check ) {
4100
+ await mkdir( path.dirname( options.filePath ), {
4101
+ recursive: true,
4102
+ } );
4103
+ await writeFile( options.filePath, options.content, 'utf8' );
4104
+ return;
4105
+ }
4106
+
4107
+ const current = normalizeGeneratedArtifactContent(
4108
+ await readFile( options.filePath, 'utf8' )
4109
+ );
4110
+ const expected = normalizeGeneratedArtifactContent( options.content );
4111
+ if ( current !== expected ) {
4112
+ throw new Error(
4113
+ \`Generated AI feature artifact is stale: \${ options.label } (\${ options.filePath }).\`
4114
+ );
4115
+ }
4116
+ }
4117
+
4118
+ async function loadJsonDocument( filePath: string ) {
4119
+ const decoded = JSON.parse( await readFile( filePath, 'utf8' ) ) as unknown;
4120
+ if ( ! decoded || typeof decoded !== 'object' || Array.isArray( decoded ) ) {
4121
+ throw new Error( \`Expected \${ filePath } to decode to a JSON object.\` );
4122
+ }
4123
+
4124
+ return decoded as Parameters< typeof projectWordPressAiSchema >[ 0 ];
4125
+ }
4126
+
4127
+ async function main() {
4128
+ const options = parseCliOptions( process.argv.slice( 2 ) );
4129
+ const aiFeatures = AI_FEATURES.filter( isWorkspaceAiFeature );
4130
+ if ( AI_FEATURES.length > 0 && aiFeatures.length === 0 ) {
4131
+ console.warn(
4132
+ '\u26A0\uFE0F AI_FEATURES entries exist, but none satisfied the generated sync-ai guard. Check for missing aiSchemaFile/typesFile fields in scripts/block-config.ts.'
4133
+ );
4134
+ }
4135
+
4136
+ if ( aiFeatures.length === 0 ) {
4137
+ console.log(
4138
+ options.check
4139
+ ? '\u2139\uFE0F No workspace AI features are registered yet. \`sync-ai --check\` is already clean.'
4140
+ : '\u2139\uFE0F No workspace AI features are registered yet.'
4141
+ );
4142
+ return;
4143
+ }
4144
+
4145
+ for ( const feature of aiFeatures ) {
4146
+ const sourceSchemaPath = path.join(
4147
+ path.dirname( feature.typesFile ),
4148
+ 'api-schemas',
4149
+ 'feature-result.schema.json'
4150
+ );
4151
+ const sourceSchema = await loadJsonDocument( sourceSchemaPath );
4152
+ const aiSchema = projectWordPressAiSchema( sourceSchema );
4153
+ await reconcileGeneratedArtifact( {
4154
+ check: options.check,
4155
+ content: \`\${ JSON.stringify( aiSchema, null, 2 ) }\\n\`,
4156
+ filePath: feature.aiSchemaFile,
4157
+ label: feature.slug,
4158
+ } );
4159
+ }
4160
+
4161
+ console.log(
4162
+ options.check
4163
+ ? '\u2705 AI feature structured-output schemas are already synchronized.'
4164
+ : '\u2705 AI feature structured-output schemas were synchronized.'
4165
+ );
4166
+ }
4167
+
4168
+ main().catch( ( error ) => {
4169
+ console.error( '\u274C AI feature sync failed:', error );
4170
+ process.exit( 1 );
4171
+ } );
4172
+ `;
4173
+ }
4174
+
4175
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai-anchors.ts
4176
+ import { promises as fsp6 } from "fs";
4177
+ import path10 from "path";
4178
+ var AI_FEATURE_SERVER_GLOB = "/inc/ai-features/*.php";
4179
+ function escapeRegex4(value) {
4180
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
4181
+ }
4182
+ async function ensureAiFeatureBootstrapAnchors(workspace) {
4183
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
4184
+ await patchFile(bootstrapPath, (source) => {
4185
+ let nextSource = source;
4186
+ const registerFunctionName = `${workspace.workspace.phpPrefix}_register_ai_features`;
4187
+ const registerHook = `add_action( 'init', '${registerFunctionName}', 20 );`;
4188
+ const registerFunction = `
4189
+
4190
+ function ${registerFunctionName}() {
4191
+ foreach ( glob( __DIR__ . '${AI_FEATURE_SERVER_GLOB}' ) ?: array() as $ai_feature_module ) {
4192
+ require_once $ai_feature_module;
4193
+ }
4194
+ }
4195
+ `;
4196
+ const insertionAnchors = [
4197
+ /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
4198
+ /\?>\s*$/u
4199
+ ];
4200
+ const hasPhpFunctionDefinition = (functionName) => new RegExp(`function\\s+${escapeRegex4(functionName)}\\s*\\(`, "u").test(nextSource);
4201
+ const insertPhpSnippet = (snippet) => {
4202
+ for (const anchor of insertionAnchors) {
4203
+ const candidate = nextSource.replace(anchor, (match) => `${snippet}
4204
+ ${match}`);
4205
+ if (candidate !== nextSource) {
4206
+ nextSource = candidate;
4207
+ return;
4208
+ }
4209
+ }
4210
+ nextSource = `${nextSource.trimEnd()}
4211
+ ${snippet}
4212
+ `;
4213
+ };
4214
+ const appendPhpSnippet = (snippet) => {
4215
+ const closingTagPattern = /\?>\s*$/u;
4216
+ if (closingTagPattern.test(nextSource)) {
4217
+ nextSource = nextSource.replace(closingTagPattern, `${snippet}
4218
+ ?>`);
4219
+ return;
4220
+ }
4221
+ nextSource = `${nextSource.trimEnd()}
4222
+ ${snippet}
4223
+ `;
4224
+ };
4225
+ if (!hasPhpFunctionDefinition(registerFunctionName)) {
4226
+ insertPhpSnippet(registerFunction);
4227
+ } else if (!nextSource.includes(AI_FEATURE_SERVER_GLOB)) {
4228
+ throw new Error([
4229
+ `Unable to patch ${path10.basename(bootstrapPath)} in ensureAiFeatureBootstrapAnchors.`,
4230
+ `The existing ${registerFunctionName}() definition does not include ${AI_FEATURE_SERVER_GLOB}.`,
4231
+ "Restore the generated bootstrap shape or wire the AI feature loader manually before retrying."
4232
+ ].join(" "));
4233
+ }
4234
+ if (!nextSource.includes(registerHook)) {
4235
+ appendPhpSnippet(registerHook);
4236
+ }
4237
+ return nextSource;
4238
+ });
4239
+ }
4240
+ async function ensureAiFeaturePackageScripts(workspace) {
4241
+ const packageJsonPath = path10.join(workspace.projectDir, "package.json");
4242
+ const packageJson = JSON.parse(await fsp6.readFile(packageJsonPath, "utf8"));
4243
+ const nextScripts = {
4244
+ ...packageJson.scripts ?? {},
4245
+ "sync-ai": packageJson.scripts?.["sync-ai"] ?? "tsx scripts/sync-ai-features.ts"
4246
+ };
4247
+ const nextDevDependencies = {
4248
+ ...packageJson.devDependencies ?? {},
4249
+ "@wp-typia/project-tools": packageJson.devDependencies?.["@wp-typia/project-tools"] ?? getPackageVersions().projectToolsPackageVersion
4250
+ };
4251
+ const addedSyncAiScript = packageJson.scripts?.["sync-ai"] === undefined;
4252
+ const addedProjectToolsDependency = packageJson.devDependencies?.["@wp-typia/project-tools"] === undefined;
4253
+ if (JSON.stringify(nextScripts) === JSON.stringify(packageJson.scripts ?? {}) && JSON.stringify(nextDevDependencies) === JSON.stringify(packageJson.devDependencies ?? {})) {
4254
+ return {
4255
+ addedProjectToolsDependency: false,
4256
+ addedSyncAiScript: false
4257
+ };
4258
+ }
4259
+ packageJson.scripts = nextScripts;
4260
+ packageJson.devDependencies = nextDevDependencies;
4261
+ await fsp6.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}
4262
+ `, "utf8");
4263
+ return {
4264
+ addedProjectToolsDependency,
4265
+ addedSyncAiScript
4266
+ };
4267
+ }
4268
+ async function ensureAiFeatureSyncProjectAnchors(workspace) {
4269
+ const syncProjectScriptPath = path10.join(workspace.projectDir, "scripts", "sync-project.ts");
4270
+ await patchFile(syncProjectScriptPath, (source) => {
4271
+ let nextSource = source;
4272
+ const syncRestConst = "const syncRestScriptPath = path.join( 'scripts', 'sync-rest-contracts.ts' );";
4273
+ const syncAiConst = "const syncAiScriptPath = path.join( 'scripts', 'sync-ai-features.ts' );";
4274
+ const syncRestBlockPattern = /if \( fs\.existsSync\( path\.resolve\( process\.cwd\(\), syncRestScriptPath \) \) \) \{\n\s*runSyncScript\( syncRestScriptPath, options \);\n\s*\}/u;
4275
+ const syncAiBlock = [
4276
+ "if ( fs.existsSync( path.resolve( process.cwd(), syncAiScriptPath ) ) ) {",
4277
+ "\trunSyncScript( syncAiScriptPath, options );",
4278
+ "}"
4279
+ ].join(`
4280
+ `);
4281
+ if (!nextSource.includes(syncAiConst)) {
4282
+ if (!nextSource.includes(syncRestConst)) {
4283
+ throw new Error([
4284
+ `ensureAiFeatureSyncProjectAnchors could not patch ${path10.basename(syncProjectScriptPath)}.`,
4285
+ "Missing the expected sync-rest script constant in scripts/sync-project.ts.",
4286
+ "Restore the generated template or wire sync-ai manually before retrying."
4287
+ ].join(" "));
4288
+ }
4289
+ nextSource = nextSource.replace(syncRestConst, `${syncRestConst}
4290
+ ${syncAiConst}`);
4291
+ }
4292
+ if (!nextSource.includes("runSyncScript( syncAiScriptPath, options );")) {
4293
+ if (!syncRestBlockPattern.test(nextSource)) {
4294
+ throw new Error([
4295
+ `ensureAiFeatureSyncProjectAnchors could not patch ${path10.basename(syncProjectScriptPath)}.`,
4296
+ "Missing the expected sync-rest invocation block in scripts/sync-project.ts.",
4297
+ "Restore the generated template or wire sync-ai manually before retrying."
4298
+ ].join(" "));
4299
+ }
4300
+ nextSource = nextSource.replace(syncRestBlockPattern, (match) => `${match}
4301
+
4302
+ ${syncAiBlock}`);
4303
+ }
4304
+ return nextSource;
4305
+ });
4306
+ }
4307
+ function assertSyncRestAnchor2(nextSource, target, anchorDescription, hasAnchor, syncRestScriptPath) {
4308
+ if (!nextSource.includes(target) && !hasAnchor) {
4309
+ throw new Error([
4310
+ `ensureAiFeatureSyncRestAnchors could not patch ${path10.basename(syncRestScriptPath)}.`,
4311
+ `Missing expected ${anchorDescription} anchor in scripts/sync-rest-contracts.ts.`,
4312
+ "Restore the generated template or add the AI feature wiring manually before retrying."
4313
+ ].join(" "));
4314
+ }
4315
+ }
4316
+ function replaceRequiredSyncRestSource2(nextSource, target, anchor, replacement, anchorDescription, syncRestScriptPath) {
4317
+ if (nextSource.includes(target)) {
4318
+ return nextSource;
4319
+ }
4320
+ const hasAnchor = typeof anchor === "string" ? nextSource.includes(anchor) : anchor.test(nextSource);
4321
+ assertSyncRestAnchor2(nextSource, target, anchorDescription, hasAnchor, syncRestScriptPath);
4322
+ return nextSource.replace(anchor, replacement);
4323
+ }
4324
+ async function ensureAiFeatureSyncRestAnchors(workspace) {
4325
+ const syncRestScriptPath = path10.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
4326
+ await patchFile(syncRestScriptPath, (source) => {
4327
+ let nextSource = source;
4328
+ const importAnchor = [
4329
+ "import {",
4330
+ "\tBLOCKS,",
4331
+ "\tREST_RESOURCES,",
4332
+ "\ttype WorkspaceBlockConfig,",
4333
+ "\ttype WorkspaceRestResourceConfig,",
4334
+ "} from './block-config';"
4335
+ ].join(`
4336
+ `);
4337
+ const helperInsertionAnchor = "async function assertTypeArtifactsCurrent";
4338
+ const restResourcesAnchor = "const restResources = REST_RESOURCES.filter( isWorkspaceRestResource );";
4339
+ const noResourcesPattern = /if \( restBlocks.length === 0 && restResources.length === 0 \) \{[\s\S]*?\n\t\treturn;\n\t\}/u;
4340
+ const consoleLogPattern = /\n\tconsole\.log\(\n\t\toptions\.check/u;
4341
+ nextSource = replaceRequiredSyncRestSource2(nextSource, "AI_FEATURES", importAnchor, [
4342
+ "import {",
4343
+ "\tAI_FEATURES,",
4344
+ "\tBLOCKS,",
4345
+ "\tREST_RESOURCES,",
4346
+ "\ttype WorkspaceAiFeatureConfig,",
4347
+ "\ttype WorkspaceBlockConfig,",
4348
+ "\ttype WorkspaceRestResourceConfig,",
4349
+ "} from './block-config';"
4350
+ ].join(`
4351
+ `), "workspace inventory import", syncRestScriptPath);
4352
+ nextSource = replaceRequiredSyncRestSource2(nextSource, "function isWorkspaceAiFeature(", helperInsertionAnchor, [
4353
+ "function isWorkspaceAiFeature(",
4354
+ "\tfeature: WorkspaceAiFeatureConfig",
4355
+ "): feature is WorkspaceAiFeatureConfig & {",
4356
+ "\taiSchemaFile: string;",
4357
+ "\tclientFile: string;",
4358
+ "\topenApiFile: string;",
4359
+ "\trestManifest: NonNullable< WorkspaceAiFeatureConfig[ 'restManifest' ] >;",
4360
+ "\ttypesFile: string;",
4361
+ "\tvalidatorsFile: string;",
4362
+ "} {",
4363
+ "\treturn (",
4364
+ "\t\ttypeof feature.aiSchemaFile === 'string' &&",
4365
+ "\t\ttypeof feature.clientFile === 'string' &&",
4366
+ "\t\ttypeof feature.openApiFile === 'string' &&",
4367
+ "\t\ttypeof feature.typesFile === 'string' &&",
4368
+ "\t\ttypeof feature.validatorsFile === 'string' &&",
4369
+ "\t\ttypeof feature.restManifest === 'object' &&",
4370
+ "\t\tfeature.restManifest !== null",
4371
+ "\t);",
4372
+ "}",
4373
+ "",
4374
+ "async function assertTypeArtifactsCurrent"
4375
+ ].join(`
4376
+ `), "type artifact assertion helper", syncRestScriptPath);
4377
+ nextSource = replaceRequiredSyncRestSource2(nextSource, "const aiFeatures = AI_FEATURES.filter( isWorkspaceAiFeature );", restResourcesAnchor, [
4378
+ "const restResources = REST_RESOURCES.filter( isWorkspaceRestResource );",
4379
+ "const aiFeatures = AI_FEATURES.filter( isWorkspaceAiFeature );"
4380
+ ].join(`
4381
+ `), "rest resource filter", syncRestScriptPath);
4382
+ nextSource = replaceRequiredSyncRestSource2(nextSource, "restBlocks.length === 0 && restResources.length === 0 && aiFeatures.length === 0", noResourcesPattern, [
4383
+ "if ( restBlocks.length === 0 && restResources.length === 0 && aiFeatures.length === 0 ) {",
4384
+ "\t\tconsole.log(",
4385
+ "\t\t\toptions.check",
4386
+ "\t\t\t\t? '\u2139\uFE0F No REST-enabled workspace blocks, plugin-level REST resources, or AI features are registered yet. `sync-rest --check` is already clean.'",
4387
+ "\t\t\t\t: '\u2139\uFE0F No REST-enabled workspace blocks, plugin-level REST resources, or AI features are registered yet.'",
4388
+ "\t\t);",
4389
+ "\t\treturn;",
4390
+ "\t}"
4391
+ ].join(`
4392
+ `), "no-resources guard", syncRestScriptPath);
4393
+ nextSource = replaceRequiredSyncRestSource2(nextSource, "for ( const feature of aiFeatures ) {", consoleLogPattern, [
4394
+ "",
4395
+ "\tfor ( const feature of aiFeatures ) {",
4396
+ "\t\tconst contracts = feature.restManifest.contracts;",
4397
+ "",
4398
+ "\t\tfor ( const [ baseName, contract ] of Object.entries( contracts ) ) {",
4399
+ "\t\t\tawait syncTypeSchemas(",
4400
+ "\t\t\t\t{",
4401
+ "\t\t\t\t\tjsonSchemaFile: path.join(",
4402
+ "\t\t\t\t\t\tpath.dirname( feature.typesFile ),",
4403
+ "\t\t\t\t\t\t'api-schemas',",
4404
+ "\t\t\t\t\t\t`${ baseName }.schema.json`",
4405
+ "\t\t\t\t\t),",
4406
+ "\t\t\t\t\topenApiFile: path.join(",
4407
+ "\t\t\t\t\t\tpath.dirname( feature.typesFile ),",
4408
+ "\t\t\t\t\t\t'api-schemas',",
4409
+ "\t\t\t\t\t\t`${ baseName }.openapi.json`",
4410
+ "\t\t\t\t\t),",
4411
+ "\t\t\t\t\tsourceTypeName: contract.sourceTypeName,",
4412
+ "\t\t\t\t\ttypesFile: feature.typesFile,",
4413
+ "\t\t\t\t},",
4414
+ "\t\t\t\t{",
4415
+ "\t\t\t\t\tcheck: options.check,",
4416
+ "\t\t\t\t}",
4417
+ "\t\t\t);",
4418
+ "\t\t}",
4419
+ "",
4420
+ "\t\tawait syncRestOpenApi(",
4421
+ "\t\t\t{",
4422
+ "\t\t\t\tmanifest: feature.restManifest,",
4423
+ "\t\t\t\topenApiFile: feature.openApiFile,",
4424
+ "\t\t\t\ttypesFile: feature.typesFile,",
4425
+ "\t\t\t},",
4426
+ "\t\t\t{",
4427
+ "\t\t\t\tcheck: options.check,",
4428
+ "\t\t\t}",
4429
+ "\t\t);",
4430
+ "",
4431
+ "\t\tawait syncEndpointClient(",
4432
+ "\t\t\t{",
4433
+ "\t\t\t\tclientFile: feature.clientFile,",
4434
+ "\t\t\t\tmanifest: feature.restManifest,",
4435
+ "\t\t\t\ttypesFile: feature.typesFile,",
4436
+ "\t\t\t\tvalidatorsFile: feature.validatorsFile,",
4437
+ "\t\t\t},",
4438
+ "\t\t\t{",
4439
+ "\t\t\t\tcheck: options.check,",
4440
+ "\t\t\t}",
4441
+ "\t\t);",
4442
+ "\t}",
4443
+ "",
4444
+ "\tconsole.log(",
4445
+ "\t\toptions.check"
4446
+ ].join(`
4447
+ `), "final sync summary", syncRestScriptPath);
4448
+ nextSource = replaceRequiredSyncRestSource2(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);
4449
+ return nextSource;
4450
+ });
4451
+ }
4452
+
4453
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace-ai.ts
4454
+ function quotePhpString4(value) {
4455
+ return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
4456
+ }
4457
+ function buildAiFeaturePhpSource(aiFeatureSlug, namespace, phpPrefix, textDomain) {
4458
+ const aiFeatureTitle = toTitleCase(aiFeatureSlug);
4459
+ const aiFeaturePhpId = aiFeatureSlug.replace(/-/g, "_");
4460
+ const loadSchemaFunctionName = `${phpPrefix}_${aiFeaturePhpId}_load_ai_feature_schema`;
4461
+ const loadAiSchemaFunctionName = `${phpPrefix}_${aiFeaturePhpId}_load_ai_response_schema`;
4462
+ const normalizeSchemaFunctionName = `${phpPrefix}_${aiFeaturePhpId}_sanitize_ai_feature_schema`;
4463
+ const validatePayloadFunctionName = `${phpPrefix}_${aiFeaturePhpId}_validate_ai_feature_payload`;
4464
+ const canManageFunctionName = `${phpPrefix}_${aiFeaturePhpId}_can_manage_ai_feature`;
4465
+ const buildPromptFunctionName = `${phpPrefix}_${aiFeaturePhpId}_build_ai_feature_prompt`;
4466
+ const normalizeProviderTypeFunctionName = `${phpPrefix}_${aiFeaturePhpId}_normalize_provider_type`;
4467
+ const buildTelemetryFunctionName = `${phpPrefix}_${aiFeaturePhpId}_build_ai_feature_telemetry`;
4468
+ const isSupportedFunctionName = `${phpPrefix}_${aiFeaturePhpId}_is_ai_feature_supported`;
4469
+ const adminNoticeFunctionName = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_admin_notice`;
4470
+ const handlerFunctionName = `${phpPrefix}_${aiFeaturePhpId}_handle_run_ai_feature`;
4471
+ const registerRoutesFunctionName = `${phpPrefix}_${aiFeaturePhpId}_register_ai_feature_routes`;
4472
+ return `<?php
4473
+ if ( ! defined( 'ABSPATH' ) ) {
4474
+ return;
4475
+ }
4476
+
4477
+ if ( ! function_exists( '${loadSchemaFunctionName}' ) ) {
4478
+ function ${loadSchemaFunctionName}( $schema_name ) {
4479
+ $project_root = dirname( __DIR__, 2 );
4480
+ $schema_path = $project_root . '/src/ai-features/${aiFeatureSlug}/api-schemas/' . $schema_name . '.schema.json';
4481
+ if ( ! file_exists( $schema_path ) ) {
4482
+ return null;
4483
+ }
4484
+
4485
+ $decoded = json_decode( file_get_contents( $schema_path ), true );
4486
+ return is_array( $decoded ) ? $decoded : null;
4487
+ }
4488
+ }
4489
+
4490
+ if ( ! function_exists( '${loadAiSchemaFunctionName}' ) ) {
4491
+ function ${loadAiSchemaFunctionName}() {
4492
+ $project_root = dirname( __DIR__, 2 );
4493
+ $schema_path = $project_root . '/src/ai-features/${aiFeatureSlug}/ai-schemas/feature-result.ai.schema.json';
4494
+ if ( ! file_exists( $schema_path ) ) {
4495
+ return null;
4496
+ }
4497
+
4498
+ $decoded = json_decode( file_get_contents( $schema_path ), true );
4499
+ return is_array( $decoded ) ? $decoded : null;
4500
+ }
4501
+ }
4502
+
4503
+ if ( ! function_exists( '${normalizeSchemaFunctionName}' ) ) {
4504
+ function ${normalizeSchemaFunctionName}( $schema ) {
4505
+ if ( ! is_array( $schema ) ) {
4506
+ return $schema;
4507
+ }
4508
+
4509
+ unset( $schema['$schema'], $schema['title'] );
4510
+
4511
+ if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
4512
+ foreach ( $schema['properties'] as $key => $property_schema ) {
4513
+ $schema['properties'][ $key ] = ${normalizeSchemaFunctionName}( $property_schema );
4514
+ }
4515
+ }
4516
+
4517
+ if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
4518
+ $schema['items'] = ${normalizeSchemaFunctionName}( $schema['items'] );
4519
+ }
4520
+
4521
+ return $schema;
4522
+ }
4523
+ }
4524
+
4525
+ if ( ! function_exists( '${validatePayloadFunctionName}' ) ) {
4526
+ function ${validatePayloadFunctionName}( $value, $schema_name, $param_name ) {
4527
+ $schema = ${loadSchemaFunctionName}( $schema_name );
4528
+ if ( ! is_array( $schema ) ) {
4529
+ return new WP_Error( 'missing_schema', 'Missing AI feature schema.', array( 'status' => 500 ) );
4530
+ }
4531
+
4532
+ $rest_schema = ${normalizeSchemaFunctionName}( $schema );
4533
+ $validation = rest_validate_value_from_schema( $value, $rest_schema, $param_name );
4534
+ if ( is_wp_error( $validation ) ) {
4535
+ return $validation;
4536
+ }
4537
+
4538
+ return rest_sanitize_value_from_schema( $value, $rest_schema, $param_name );
4539
+ }
4540
+ }
4541
+
4542
+ if ( ! function_exists( '${canManageFunctionName}' ) ) {
4543
+ function ${canManageFunctionName}() {
4544
+ return current_user_can( 'edit_posts' );
4545
+ }
4546
+ }
4547
+
4548
+ if ( ! function_exists( '${buildPromptFunctionName}' ) ) {
4549
+ function ${buildPromptFunctionName}( array $payload ) {
4550
+ return sprintf(
4551
+ 'You are helping with the %1$s AI workflow. Read the JSON request payload and return JSON that matches the provided schema. Request payload: %2$s',
4552
+ ${quotePhpString4(aiFeatureTitle)},
4553
+ wp_json_encode( $payload )
4554
+ );
4555
+ }
4556
+ }
4557
+
4558
+ if ( ! function_exists( '${normalizeProviderTypeFunctionName}' ) ) {
4559
+ function ${normalizeProviderTypeFunctionName}( $provider_type ) {
4560
+ if ( is_object( $provider_type ) && isset( $provider_type->value ) && is_string( $provider_type->value ) ) {
4561
+ return $provider_type->value;
4562
+ }
4563
+
4564
+ return is_string( $provider_type ) && '' !== $provider_type ? $provider_type : 'cloud';
4565
+ }
4566
+ }
4567
+
4568
+ if ( ! function_exists( '${buildTelemetryFunctionName}' ) ) {
4569
+ function ${buildTelemetryFunctionName}( $result ) {
4570
+ if (
4571
+ ! is_object( $result ) ||
4572
+ ! method_exists( $result, 'getId' ) ||
4573
+ ! method_exists( $result, 'getModelMetadata' ) ||
4574
+ ! method_exists( $result, 'getProviderMetadata' ) ||
4575
+ ! method_exists( $result, 'getTokenUsage' )
4576
+ ) {
4577
+ return new WP_Error(
4578
+ 'ai_client_result_shape',
4579
+ 'The current WordPress AI Client result object is missing telemetry helpers.',
4580
+ array( 'status' => 502 )
4581
+ );
4582
+ }
4583
+
4584
+ $model_metadata = $result->getModelMetadata();
4585
+ $provider_metadata = $result->getProviderMetadata();
4586
+ $token_usage = $result->getTokenUsage();
4587
+
4588
+ if (
4589
+ ! is_object( $model_metadata ) ||
4590
+ ! method_exists( $model_metadata, 'getId' ) ||
4591
+ ! method_exists( $model_metadata, 'getName' ) ||
4592
+ ! is_object( $provider_metadata ) ||
4593
+ ! method_exists( $provider_metadata, 'getId' ) ||
4594
+ ! method_exists( $provider_metadata, 'getName' ) ||
4595
+ ! method_exists( $provider_metadata, 'getType' ) ||
4596
+ ! is_object( $token_usage ) ||
4597
+ ! method_exists( $token_usage, 'getCompletionTokens' ) ||
4598
+ ! method_exists( $token_usage, 'getPromptTokens' ) ||
4599
+ ! method_exists( $token_usage, 'getTotalTokens' )
4600
+ ) {
4601
+ return new WP_Error(
4602
+ 'ai_client_result_shape',
4603
+ 'The current WordPress AI Client telemetry objects are missing expected getters.',
4604
+ array( 'status' => 502 )
4605
+ );
4606
+ }
4607
+
4608
+ $telemetry = array(
4609
+ 'modelId' => (string) $model_metadata->getId(),
4610
+ 'modelName' => (string) $model_metadata->getName(),
4611
+ 'providerId' => (string) $provider_metadata->getId(),
4612
+ 'providerName' => (string) $provider_metadata->getName(),
4613
+ 'providerType' => ${normalizeProviderTypeFunctionName}( $provider_metadata->getType() ),
4614
+ 'resultId' => (string) $result->getId(),
4615
+ 'tokenUsage' => array(
4616
+ 'completionTokens' => (int) $token_usage->getCompletionTokens(),
4617
+ 'promptTokens' => (int) $token_usage->getPromptTokens(),
4618
+ 'totalTokens' => (int) $token_usage->getTotalTokens(),
4619
+ ),
4620
+ );
4621
+
4622
+ if ( method_exists( $token_usage, 'getThoughtTokens' ) ) {
4623
+ $thought_tokens = $token_usage->getThoughtTokens();
4624
+ if ( null !== $thought_tokens ) {
4625
+ $telemetry['tokenUsage']['thoughtTokens'] = (int) $thought_tokens;
4626
+ }
4627
+ }
4628
+
4629
+ return $telemetry;
4630
+ }
4631
+ }
4632
+
4633
+ if ( ! function_exists( '${isSupportedFunctionName}' ) ) {
4634
+ function ${isSupportedFunctionName}() {
4635
+ static $is_supported = null;
4636
+ if ( null !== $is_supported ) {
4637
+ return $is_supported;
4638
+ }
4639
+
4640
+ if ( ! function_exists( 'wp_ai_client_prompt' ) ) {
4641
+ $is_supported = false;
4642
+ return $is_supported;
4643
+ }
4644
+
4645
+ $schema = ${loadAiSchemaFunctionName}();
4646
+ if ( ! is_array( $schema ) ) {
4647
+ $is_supported = false;
4648
+ return $is_supported;
4649
+ }
4650
+
4651
+ $prompt = wp_ai_client_prompt( 'AI feature support probe.' );
4652
+ if ( ! is_object( $prompt ) || ! method_exists( $prompt, 'as_json_response' ) ) {
4653
+ $is_supported = false;
4654
+ return $is_supported;
4655
+ }
4656
+
4657
+ $structured_prompt = $prompt->as_json_response( $schema );
4658
+ if ( ! is_object( $structured_prompt ) ) {
4659
+ $is_supported = false;
4660
+ return $is_supported;
4661
+ }
4662
+
4663
+ if ( method_exists( $structured_prompt, 'is_supported_for_text_generation' ) ) {
4664
+ $is_supported = (bool) $structured_prompt->is_supported_for_text_generation();
4665
+ return $is_supported;
4666
+ }
4667
+
4668
+ $is_supported = method_exists( $structured_prompt, 'generate_text_result' );
4669
+ return $is_supported;
4670
+ }
4671
+ }
4672
+
4673
+ if ( ! function_exists( '${adminNoticeFunctionName}' ) ) {
4674
+ function ${adminNoticeFunctionName}() {
4675
+ if ( ! current_user_can( 'manage_options' ) || ${isSupportedFunctionName}() ) {
4676
+ return;
4677
+ }
4678
+
4679
+ $message = sprintf(
4680
+ /* translators: %s: AI feature name. */
4681
+ __( 'The %s AI feature is optional and remains disabled until the WordPress AI Client is available with structured text generation support for the generated schema.', ${quotePhpString4(textDomain)} ),
4682
+ ${quotePhpString4(aiFeatureTitle)}
4683
+ );
4684
+ printf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html( $message ) );
4685
+ }
4686
+ }
4687
+
4688
+ if ( ! function_exists( '${handlerFunctionName}' ) ) {
4689
+ function ${handlerFunctionName}( WP_REST_Request $request ) {
4690
+ $payload = ${validatePayloadFunctionName}( $request->get_json_params(), 'feature-request', 'body' );
4691
+ if ( is_wp_error( $payload ) ) {
4692
+ return $payload;
4693
+ }
4694
+
4695
+ if ( ! ${isSupportedFunctionName}() ) {
4696
+ return new WP_Error(
4697
+ 'ai_client_unavailable',
4698
+ 'The WordPress AI Client is unavailable or does not support this feature endpoint.',
4699
+ array( 'status' => 501 )
4700
+ );
4701
+ }
4702
+
4703
+ $ai_schema = ${loadAiSchemaFunctionName}();
4704
+ if ( ! is_array( $ai_schema ) ) {
4705
+ return new WP_Error(
4706
+ 'ai_client_schema_missing',
4707
+ 'The generated AI response schema is missing for this feature endpoint.',
4708
+ array( 'status' => 500 )
4709
+ );
4710
+ }
4711
+
4712
+ $prompt = wp_ai_client_prompt( ${buildPromptFunctionName}( $payload ) );
4713
+ if ( ! is_object( $prompt ) ) {
4714
+ return new WP_Error(
4715
+ 'ai_client_unavailable',
4716
+ 'The WordPress AI Client prompt builder is unavailable on this site.',
4717
+ array( 'status' => 501 )
4718
+ );
4719
+ }
4720
+
4721
+ if ( method_exists( $prompt, 'using_temperature' ) ) {
4722
+ $prompt = $prompt->using_temperature( 0.2 );
4723
+ }
4724
+ if ( ! method_exists( $prompt, 'as_json_response' ) ) {
4725
+ return new WP_Error(
4726
+ 'ai_client_unavailable',
4727
+ 'The current WordPress AI Client does not expose as_json_response().',
4728
+ array( 'status' => 501 )
4729
+ );
4730
+ }
4731
+
4732
+ $structured_prompt = $prompt->as_json_response( $ai_schema );
4733
+ if ( ! is_object( $structured_prompt ) ) {
4734
+ return new WP_Error(
4735
+ 'ai_client_unavailable',
4736
+ 'The current WordPress AI Client could not prepare a structured-output prompt.',
4737
+ array( 'status' => 501 )
4738
+ );
4739
+ }
4740
+
4741
+ if (
4742
+ method_exists( $structured_prompt, 'is_supported_for_text_generation' ) &&
4743
+ ! $structured_prompt->is_supported_for_text_generation()
4744
+ ) {
4745
+ return new WP_Error(
4746
+ 'ai_client_unavailable',
4747
+ 'The current WordPress AI Client provider or model does not support this structured-output feature.',
4748
+ array( 'status' => 501 )
4749
+ );
4750
+ }
4751
+ if ( ! method_exists( $structured_prompt, 'generate_text_result' ) ) {
4752
+ return new WP_Error(
4753
+ 'ai_client_unavailable',
4754
+ 'The current WordPress AI Client does not expose generate_text_result() after as_json_response().',
4755
+ array( 'status' => 501 )
4756
+ );
4757
+ }
4758
+
4759
+ $result = $structured_prompt->generate_text_result();
4760
+ if ( is_wp_error( $result ) ) {
4761
+ return $result;
4762
+ }
4763
+ if ( ! is_object( $result ) || ! method_exists( $result, 'toText' ) ) {
4764
+ return new WP_Error(
4765
+ 'ai_client_result_shape',
4766
+ 'The current WordPress AI Client result does not expose toText().',
4767
+ array( 'status' => 502 )
4768
+ );
4769
+ }
4770
+
4771
+ $decoded_result = json_decode( $result->toText(), true );
4772
+ if ( ! is_array( $decoded_result ) ) {
4773
+ return new WP_Error(
4774
+ 'ai_client_invalid_json',
4775
+ 'The AI feature response did not decode to a JSON object.',
4776
+ array( 'status' => 502 )
4777
+ );
4778
+ }
4779
+
4780
+ $normalized_result = ${validatePayloadFunctionName}( $decoded_result, 'feature-result', 'result' );
4781
+ if ( is_wp_error( $normalized_result ) ) {
4782
+ return new WP_Error(
4783
+ 'ai_client_invalid_response',
4784
+ $normalized_result->get_error_message(),
4785
+ array( 'status' => 502 )
4786
+ );
4787
+ }
4788
+
4789
+ $telemetry = ${buildTelemetryFunctionName}( $result );
4790
+ if ( is_wp_error( $telemetry ) ) {
4791
+ return $telemetry;
4792
+ }
4793
+
4794
+ $response = ${validatePayloadFunctionName}(
4795
+ array(
4796
+ 'result' => $normalized_result,
4797
+ 'telemetry' => $telemetry,
4798
+ ),
4799
+ 'feature-response',
4800
+ 'response'
4801
+ );
4802
+ if ( is_wp_error( $response ) ) {
4803
+ return new WP_Error(
4804
+ 'ai_client_invalid_response',
4805
+ $response->get_error_message(),
4806
+ array( 'status' => 502 )
4807
+ );
4808
+ }
4809
+
4810
+ return rest_ensure_response( $response );
4811
+ }
4812
+ }
4813
+
4814
+ if ( ! function_exists( '${registerRoutesFunctionName}' ) ) {
4815
+ function ${registerRoutesFunctionName}() {
4816
+ register_rest_route(
4817
+ ${quotePhpString4(namespace)},
4818
+ '/ai/${aiFeatureSlug}',
4819
+ array(
4820
+ array(
4821
+ 'methods' => WP_REST_Server::CREATABLE,
4822
+ 'callback' => '${handlerFunctionName}',
4823
+ 'permission_callback' => '${canManageFunctionName}',
4824
+ )
4825
+ )
4826
+ );
4827
+ }
4828
+ }
4829
+
4830
+ add_action( 'admin_notices', '${adminNoticeFunctionName}' );
4831
+ add_action( 'rest_api_init', '${registerRoutesFunctionName}' );
4832
+ `;
4833
+ }
4834
+ async function runAddAiFeatureCommand({
4835
+ aiFeatureName,
4836
+ cwd = process.cwd(),
4837
+ namespace
4838
+ }) {
4839
+ const workspace = resolveWorkspaceProject(cwd);
4840
+ const aiFeatureSlug = assertValidGeneratedSlug("AI feature name", normalizeBlockSlug(aiFeatureName), "wp-typia add ai-feature <name> [--namespace <vendor/v1>]");
4841
+ const resolvedNamespace = resolveRestResourceNamespace(workspace.workspace.namespace, namespace);
4842
+ const compatibilityPolicy = resolveScaffoldCompatibilityPolicy(OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY);
4843
+ const inventory = readWorkspaceInventory(workspace.projectDir);
4844
+ assertAiFeatureDoesNotExist(workspace.projectDir, aiFeatureSlug, inventory);
4845
+ const blockConfigPath = path11.join(workspace.projectDir, "scripts", "block-config.ts");
4846
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
4847
+ const packageJsonPath = path11.join(workspace.projectDir, "package.json");
4848
+ const syncAiScriptPath = path11.join(workspace.projectDir, "scripts", "sync-ai-features.ts");
4849
+ const syncProjectScriptPath = path11.join(workspace.projectDir, "scripts", "sync-project.ts");
4850
+ const syncRestScriptPath = path11.join(workspace.projectDir, "scripts", "sync-rest-contracts.ts");
4851
+ const aiFeatureDir = path11.join(workspace.projectDir, "src", "ai-features", aiFeatureSlug);
4852
+ const typesFilePath = path11.join(aiFeatureDir, "api-types.ts");
4853
+ const validatorsFilePath = path11.join(aiFeatureDir, "api-validators.ts");
4854
+ const apiFilePath = path11.join(aiFeatureDir, "api.ts");
4855
+ const dataFilePath = path11.join(aiFeatureDir, "data.ts");
4856
+ const phpFilePath = path11.join(workspace.projectDir, "inc", "ai-features", `${aiFeatureSlug}.php`);
4857
+ const mutationSnapshot = {
4858
+ fileSources: await snapshotWorkspaceFiles([
4859
+ blockConfigPath,
4860
+ bootstrapPath,
4861
+ packageJsonPath,
4862
+ syncAiScriptPath,
4863
+ syncProjectScriptPath,
4864
+ syncRestScriptPath
4865
+ ]),
4866
+ snapshotDirs: [],
4867
+ targetPaths: [aiFeatureDir, phpFilePath, syncAiScriptPath]
4868
+ };
4869
+ try {
4870
+ await fsp7.mkdir(aiFeatureDir, { recursive: true });
4871
+ await fsp7.mkdir(path11.dirname(phpFilePath), { recursive: true });
4872
+ await ensureAiFeatureBootstrapAnchors(workspace);
4873
+ await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
4874
+ const packageScriptChanges = await ensureAiFeaturePackageScripts(workspace);
4875
+ await ensureAiFeatureSyncProjectAnchors(workspace);
4876
+ await ensureAiFeatureSyncRestAnchors(workspace);
4877
+ await fsp7.writeFile(syncAiScriptPath, buildAiFeatureSyncScriptSource(), "utf8");
4878
+ await fsp7.writeFile(typesFilePath, buildAiFeatureTypesSource(aiFeatureSlug), "utf8");
4879
+ await fsp7.writeFile(validatorsFilePath, buildAiFeatureValidatorsSource(aiFeatureSlug), "utf8");
4880
+ await fsp7.writeFile(apiFilePath, buildAiFeatureApiSource(aiFeatureSlug), "utf8");
4881
+ await fsp7.writeFile(dataFilePath, buildAiFeatureDataSource(aiFeatureSlug), "utf8");
4882
+ await fsp7.writeFile(phpFilePath, buildAiFeaturePhpSource(aiFeatureSlug, resolvedNamespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain), "utf8");
4883
+ const pascalCase = toPascalCaseFromAiFeatureSlug(aiFeatureSlug);
4884
+ await syncAiFeatureRestArtifacts({
4885
+ clientFile: `src/ai-features/${aiFeatureSlug}/api-client.ts`,
4886
+ outputDir: path11.join("src", "ai-features", aiFeatureSlug),
4887
+ projectDir: workspace.projectDir,
4888
+ typesFile: `src/ai-features/${aiFeatureSlug}/api-types.ts`,
4889
+ validatorsFile: `src/ai-features/${aiFeatureSlug}/api-validators.ts`,
4890
+ variables: {
4891
+ namespace: resolvedNamespace,
4892
+ pascalCase,
4893
+ slugKebabCase: aiFeatureSlug,
4894
+ title: toTitleCase(aiFeatureSlug)
4895
+ }
4896
+ });
4897
+ await syncAiFeatureSchemaArtifact({
4898
+ aiSchemaFile: `src/ai-features/${aiFeatureSlug}/ai-schemas/feature-result.ai.schema.json`,
4899
+ outputDir: path11.join("src", "ai-features", aiFeatureSlug),
4900
+ projectDir: workspace.projectDir
4901
+ });
4902
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
4903
+ aiFeatureEntries: [
4904
+ buildAiFeatureConfigEntry(aiFeatureSlug, resolvedNamespace)
4905
+ ],
4906
+ transformSource: ensureBlockConfigCanAddRestManifests
4907
+ });
4908
+ return {
4909
+ aiFeatureSlug,
4910
+ namespace: resolvedNamespace,
4911
+ projectDir: workspace.projectDir,
4912
+ warnings: packageScriptChanges.addedProjectToolsDependency ? [
4913
+ "Added `@wp-typia/project-tools` to devDependencies for `sync-ai`. If this workspace was already installed, rerun your package manager install command before the first `wp-typia sync ai`."
4914
+ ] : []
4915
+ };
4916
+ } catch (error) {
4917
+ await rollbackWorkspaceMutation(mutationSnapshot);
4918
+ throw error;
4919
+ }
4920
+ }
4921
+
4922
+ // ../wp-typia-project-tools/src/runtime/cli-add-workspace.ts
4923
+ var VARIATIONS_IMPORT_LINE = "import { registerWorkspaceVariations } from './variations';";
4924
+ var VARIATIONS_CALL_LINE = "registerWorkspaceVariations();";
4925
+ function buildVariationConfigEntry(blockSlug, variationSlug) {
4926
+ return [
4927
+ "\t{",
4928
+ ` block: ${quoteTsString(blockSlug)},`,
4929
+ ` file: ${quoteTsString(`src/blocks/${blockSlug}/variations/${variationSlug}.ts`)},`,
4930
+ ` slug: ${quoteTsString(variationSlug)},`,
4931
+ "\t},"
4932
+ ].join(`
4933
+ `);
4934
+ }
4935
+ function buildVariationConstName(variationSlug) {
4936
+ const identifierSegments = toKebabCase(variationSlug).split("-").filter(Boolean);
4937
+ return `workspaceVariation_${identifierSegments.join("_")}`;
4938
+ }
4939
+ function getVariationConstBindings(variationSlugs) {
4940
+ const seenConstNames = new Map;
4941
+ return variationSlugs.map((variationSlug) => {
4942
+ const constName = buildVariationConstName(variationSlug);
4943
+ const previousSlug = seenConstNames.get(constName);
4944
+ if (previousSlug && previousSlug !== variationSlug) {
4945
+ throw new Error(`Variation slugs "${previousSlug}" and "${variationSlug}" generate the same registry identifier "${constName}". Rename one of the variations.`);
4946
+ }
4947
+ seenConstNames.set(constName, variationSlug);
4948
+ return { constName, variationSlug };
4949
+ });
4950
+ }
4951
+ function buildVariationSource(variationSlug, textDomain) {
4952
+ const variationTitle = toTitleCase(variationSlug);
4953
+ const variationConstName = buildVariationConstName(variationSlug);
4954
+ return `import type { BlockVariation } from '@wp-typia/block-types/blocks/registration';
4955
+ import { __ } from '@wordpress/i18n';
4956
+
4957
+ export const ${variationConstName} = {
4958
+ name: ${quoteTsString(variationSlug)},
4959
+ title: __( ${quoteTsString(variationTitle)}, ${quoteTsString(textDomain)} ),
4960
+ description: __(
4961
+ ${quoteTsString(`A starter variation for ${variationTitle}.`)},
4962
+ ${quoteTsString(textDomain)},
4963
+ ),
4964
+ attributes: {},
4965
+ scope: ['inserter'],
4966
+ } satisfies BlockVariation;
4967
+ `;
4968
+ }
4969
+ function buildVariationIndexSource(variationSlugs) {
4970
+ const variationBindings = getVariationConstBindings(variationSlugs);
4971
+ const importLines = variationBindings.map(({ constName, variationSlug }) => {
4972
+ return `import { ${constName} } from './${variationSlug}';`;
4973
+ }).join(`
4974
+ `);
4975
+ const variationConstNames = variationBindings.map(({ constName }) => constName).join(`,
4976
+ `);
4977
+ return `import { registerBlockVariation } from '@wordpress/blocks';
4978
+ import metadata from '../block.json';
4979
+ ${importLines ? `
4980
+ ${importLines}` : ""}
4981
+
4982
+ const WORKSPACE_VARIATIONS = [
4983
+ ${variationConstNames}
4984
+ // wp-typia add variation entries
4985
+ ];
4986
+
4987
+ export function registerWorkspaceVariations() {
4988
+ for (const variation of WORKSPACE_VARIATIONS) {
4989
+ registerBlockVariation(metadata.name, variation);
4990
+ }
4991
+ }
4992
+ `;
4993
+ }
4994
+ async function ensureVariationRegistrationHook(blockIndexPath) {
4995
+ await patchFile(blockIndexPath, (source) => {
4996
+ let nextSource = source;
4997
+ if (!nextSource.includes(VARIATIONS_IMPORT_LINE)) {
4998
+ nextSource = `${VARIATIONS_IMPORT_LINE}
4999
+ ${nextSource}`;
5000
+ }
5001
+ if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
5002
+ const callInsertionPatterns = [
5003
+ /(registerBlockType<[\s\S]*?\);\s*)/u,
5004
+ /(registerBlockType\([\s\S]*?\);\s*)/u
5005
+ ];
5006
+ let inserted = false;
5007
+ for (const pattern of callInsertionPatterns) {
5008
+ const candidate = nextSource.replace(pattern, (match) => `${match}
5009
+ ${VARIATIONS_CALL_LINE}
5010
+ `);
5011
+ if (candidate !== nextSource) {
5012
+ nextSource = candidate;
5013
+ inserted = true;
5014
+ break;
5015
+ }
5016
+ }
5017
+ if (!inserted) {
5018
+ nextSource = `${nextSource.trimEnd()}
5019
+
5020
+ ${VARIATIONS_CALL_LINE}
5021
+ `;
5022
+ }
5023
+ }
5024
+ if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
5025
+ throw new Error(`Unable to inject ${VARIATIONS_CALL_LINE} into ${path12.basename(blockIndexPath)}.`);
5026
+ }
5027
+ return nextSource;
5028
+ });
5029
+ }
5030
+ async function writeVariationRegistry(projectDir, blockSlug, variationSlug) {
5031
+ const variationsDir = path12.join(projectDir, "src", "blocks", blockSlug, "variations");
5032
+ const variationsIndexPath = path12.join(variationsDir, "index.ts");
5033
+ await fsp8.mkdir(variationsDir, { recursive: true });
5034
+ const existingVariationSlugs = fs5.readdirSync(variationsDir).filter((entry) => entry.endsWith(".ts") && entry !== "index.ts").map((entry) => entry.replace(/\.ts$/u, ""));
5035
+ const nextVariationSlugs = Array.from(new Set([...existingVariationSlugs, variationSlug])).sort();
5036
+ await fsp8.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
5037
+ }
5038
+ async function runAddVariationCommand({
5039
+ blockName,
5040
+ cwd = process.cwd(),
5041
+ variationName
5042
+ }) {
5043
+ const workspace = resolveWorkspaceProject(cwd);
5044
+ const blockSlug = normalizeBlockSlug(blockName);
5045
+ const variationSlug = assertValidGeneratedSlug("Variation name", normalizeBlockSlug(variationName), "wp-typia add variation <name> --block <block-slug>");
5046
+ const inventory = readWorkspaceInventory(workspace.projectDir);
5047
+ resolveWorkspaceBlock(inventory, blockSlug);
5048
+ assertVariationDoesNotExist(workspace.projectDir, blockSlug, variationSlug, inventory);
5049
+ const blockConfigPath = path12.join(workspace.projectDir, "scripts", "block-config.ts");
5050
+ const blockIndexPath = path12.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
5051
+ const variationsDir = path12.join(workspace.projectDir, "src", "blocks", blockSlug, "variations");
5052
+ const variationFilePath = path12.join(variationsDir, `${variationSlug}.ts`);
5053
+ const variationsIndexPath = path12.join(variationsDir, "index.ts");
2935
5054
  const mutationSnapshot = {
2936
5055
  fileSources: await snapshotWorkspaceFiles([
2937
5056
  blockConfigPath,
@@ -2942,8 +5061,8 @@ async function runAddVariationCommand({
2942
5061
  targetPaths: [variationFilePath]
2943
5062
  };
2944
5063
  try {
2945
- await fsp5.mkdir(variationsDir, { recursive: true });
2946
- await fsp5.writeFile(variationFilePath, buildVariationSource(variationSlug, workspace.workspace.textDomain), "utf8");
5064
+ await fsp8.mkdir(variationsDir, { recursive: true });
5065
+ await fsp8.writeFile(variationFilePath, buildVariationSource(variationSlug, workspace.workspace.textDomain), "utf8");
2947
5066
  await writeVariationRegistry(workspace.projectDir, blockSlug, variationSlug);
2948
5067
  await ensureVariationRegistrationHook(blockIndexPath);
2949
5068
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
@@ -2976,7 +5095,7 @@ async function runAddHookedBlockCommand({
2976
5095
  throw new Error("`wp-typia add hooked-block` cannot hook a block relative to its own block name.");
2977
5096
  }
2978
5097
  const { blockJson, blockJsonPath } = readWorkspaceBlockJson(workspace.projectDir, blockSlug);
2979
- const blockJsonRelativePath = path8.relative(workspace.projectDir, blockJsonPath);
5098
+ const blockJsonRelativePath = path12.relative(workspace.projectDir, blockJsonPath);
2980
5099
  const blockHooks = getMutableBlockHooks(blockJson, blockJsonRelativePath);
2981
5100
  if (Object.prototype.hasOwnProperty.call(blockHooks, resolvedAnchorBlockName)) {
2982
5101
  throw new Error(`${blockJsonRelativePath} already defines a blockHooks entry for "${resolvedAnchorBlockName}".`);
@@ -2988,7 +5107,7 @@ async function runAddHookedBlockCommand({
2988
5107
  };
2989
5108
  try {
2990
5109
  blockHooks[resolvedAnchorBlockName] = resolvedPosition;
2991
- await fsp5.writeFile(blockJsonPath, JSON.stringify(blockJson, null, "\t"), "utf8");
5110
+ await fsp8.writeFile(blockJsonPath, JSON.stringify(blockJson, null, "\t"), "utf8");
2992
5111
  return {
2993
5112
  anchorBlockName: resolvedAnchorBlockName,
2994
5113
  blockSlug,
@@ -3009,6 +5128,8 @@ export {
3009
5128
  runAddEditorPluginCommand,
3010
5129
  runAddBlockCommand,
3011
5130
  runAddBindingSourceCommand,
5131
+ runAddAiFeatureCommand,
5132
+ runAddAbilityCommand,
3012
5133
  getWorkspaceBlockSelectOptions,
3013
5134
  formatAddHelpText,
3014
5135
  EDITOR_PLUGIN_SLOT_IDS,
@@ -3016,4 +5137,4 @@ export {
3016
5137
  ADD_BLOCK_TEMPLATE_IDS
3017
5138
  };
3018
5139
 
3019
- //# debugId=862EE94117F7BF5964756E2164756E21
5140
+ //# debugId=B375BD42A8A3F6C764756E2164756E21