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