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.
- package/README.md +2 -0
- package/dist-bunli/.bunli/commands.gen.js +3500 -654
- package/dist-bunli/{cli-qpt5dt0x.js → cli-2rev5hqm.js} +1 -1
- package/dist-bunli/{cli-rg481yks.js → cli-3w3qxq9w.js} +189 -2
- package/dist-bunli/{cli-n4m6yqz1.js → cli-68145vb5.js} +4 -4
- package/dist-bunli/{cli-add-dcr8ek9z.js → cli-add-a27wjrk4.js} +2248 -127
- package/dist-bunli/{cli-32rf304y.js → cli-c5021kqy.js} +276 -4
- package/dist-bunli/{cli-doctor-3myz5bd3.js → cli-doctor-31djnnxs.js} +118 -3
- package/dist-bunli/{cli-scaffold-w5f4zbz1.js → cli-scaffold-r0yxfhbq.js} +3 -3
- package/dist-bunli/cli.js +2 -2
- package/dist-bunli/{command-list-5xp9pjyy.js → command-list-kx7q3f18.js} +162 -34
- package/dist-bunli/{migrations-fanyw571.js → migrations-1p6mbkyw.js} +2 -2
- package/dist-bunli/node-cli.js +163 -32
- package/package.json +2 -2
|
@@ -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
|
-
|
|
18
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
750
|
-
import { promises as
|
|
751
|
-
import
|
|
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
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
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
|
-
`
|
|
2810
|
-
`
|
|
2811
|
-
`
|
|
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
|
|
2817
|
-
const
|
|
2818
|
-
return
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
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
|
-
|
|
2829
|
-
|
|
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
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
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
|
|
2851
|
-
const
|
|
2852
|
-
const
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
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
|
-
|
|
2864
|
-
${variationConstNames}
|
|
2865
|
-
// wp-typia add variation entries
|
|
2866
|
-
];
|
|
2955
|
+
import abilityConfig from './ability.config.json';
|
|
2867
2956
|
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
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
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2912
|
-
const
|
|
2913
|
-
const
|
|
2914
|
-
|
|
2915
|
-
const
|
|
2916
|
-
const
|
|
2917
|
-
|
|
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
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
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
|
|
2946
|
-
await
|
|
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 =
|
|
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
|
|
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=
|
|
5140
|
+
//# debugId=B375BD42A8A3F6C764756E2164756E21
|