@lesliechan721/aw-plugin-core 0.1.0-beta.1
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/dist/file-rules.d.ts +2 -0
- package/dist/file-rules.js +42 -0
- package/dist/file-rules.js.map +1 -0
- package/dist/generated/plugin-manifest-v1.d.ts +368 -0
- package/dist/generated/plugin-manifest-v1.js +3 -0
- package/dist/generated/plugin-manifest-v1.js.map +1 -0
- package/dist/hooks-contract.d.ts +189 -0
- package/dist/hooks-contract.js +967 -0
- package/dist/hooks-contract.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest-contract.d.ts +96 -0
- package/dist/manifest-contract.js +2730 -0
- package/dist/manifest-contract.js.map +1 -0
- package/dist/manifest-type-contract.d.ts +7 -0
- package/dist/manifest-type-contract.js +2 -0
- package/dist/manifest-type-contract.js.map +1 -0
- package/dist/manifest.d.ts +27 -0
- package/dist/manifest.js +59 -0
- package/dist/manifest.js.map +1 -0
- package/dist/permission-command.d.ts +3 -0
- package/dist/permission-command.js +11 -0
- package/dist/permission-command.js.map +1 -0
- package/dist/platform-entries.d.ts +13 -0
- package/dist/platform-entries.js +20 -0
- package/dist/platform-entries.js.map +1 -0
- package/dist/platform-support.d.ts +6 -0
- package/dist/platform-support.js +38 -0
- package/dist/platform-support.js.map +1 -0
- package/dist/plugin-release.d.ts +30 -0
- package/dist/plugin-release.js +187 -0
- package/dist/plugin-release.js.map +1 -0
- package/dist/plugin-script-contract.d.ts +33 -0
- package/dist/plugin-script-contract.js +599 -0
- package/dist/plugin-script-contract.js.map +1 -0
- package/dist/portable-skill-validation.d.ts +7 -0
- package/dist/portable-skill-validation.js +111 -0
- package/dist/portable-skill-validation.js.map +1 -0
- package/dist/read-json.d.ts +1 -0
- package/dist/read-json.js +5 -0
- package/dist/read-json.js.map +1 -0
- package/dist/runtime-export-source.d.ts +14 -0
- package/dist/runtime-export-source.js +73 -0
- package/dist/runtime-export-source.js.map +1 -0
- package/dist/skill-authoring.d.ts +2 -0
- package/dist/skill-authoring.js +13 -0
- package/dist/skill-authoring.js.map +1 -0
- package/dist/skill-frontmatter-capabilities.d.ts +36 -0
- package/dist/skill-frontmatter-capabilities.js +120 -0
- package/dist/skill-frontmatter-capabilities.js.map +1 -0
- package/dist/skill-frontmatter-capabilities.json +200 -0
- package/dist/slash-commands.d.ts +43 -0
- package/dist/slash-commands.js +394 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/template-engine.d.ts +2 -0
- package/dist/template-engine.js +26 -0
- package/dist/template-engine.js.map +1 -0
- package/dist/types.d.ts +428 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace.d.ts +105 -0
- package/dist/workspace.js +963 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +46 -0
- package/schemas/plugin-manifest-v1.json +329 -0
|
@@ -0,0 +1,2730 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { normalizePluginManifestSchemaVersion, PLUGIN_MANIFEST_SCHEMA_VERSION, SOURCE_PLUGIN_MANIFEST_CONTRACT_VERSION, } from './manifest.js';
|
|
5
|
+
import { readJsonFile } from './read-json.js';
|
|
6
|
+
import { parsePermissionCommand } from './permission-command.js';
|
|
7
|
+
import { applyFileRules } from './file-rules.js';
|
|
8
|
+
import { resolvePlatformEntries } from './platform-entries.js';
|
|
9
|
+
import { validatePortableSkillDirectoriesRoot, validatePortableSkillName, } from './portable-skill-validation.js';
|
|
10
|
+
import { assertHookMatcherAllowed, assertNativeHookEvent, assertPublicHookEvent, normalizeRenderedHookGroupIdentity, renderHookDefinitionGroup, resolvePlatformHookDefinitions, } from './hooks-contract.js';
|
|
11
|
+
import { inferLifecycleScriptRunnerFromPath, isPluginLifecycleOsSelector, isPluginScriptRunner, PLUGIN_AUTO_RUNNER_EXTENSIONS, PLUGIN_LIFECYCLE_EVENTS, PLUGIN_SCRIPT_TIMEOUT_DEFAULT_MS, PLUGIN_SCRIPT_TIMEOUT_MAX_MS, PLUGIN_SCRIPT_TIMEOUT_MIN_MS, resolveLifecycleScriptForOs, } from './plugin-script-contract.js';
|
|
12
|
+
import { DEFAULT_SLASH_COMMANDS_PATH, resolveSlashCommands, } from './slash-commands.js';
|
|
13
|
+
import { isSkillAuthorOnlyPath } from './skill-authoring.js';
|
|
14
|
+
import { assertPluginInputTree, validateRuntimeExportSource } from './runtime-export-source.js';
|
|
15
|
+
import { formatMarketplaceProductsForError, normalizeMarketplaceProductToken, resolveSupportedPlatforms, supportsPlatform, } from './platform-support.js';
|
|
16
|
+
export const SOURCE_MANIFEST_PATH = path.join('.agents-plugin', 'plugin.json');
|
|
17
|
+
export { SOURCE_PLUGIN_MANIFEST_CONTRACT_VERSION };
|
|
18
|
+
export const MARKETPLACE_SOURCE_PATH = path.join('plugins', 'marketplace-source.json');
|
|
19
|
+
const MANIFEST_POLICY_PRODUCTS = new Set(['CODEX', 'CLAUDE']);
|
|
20
|
+
const MARKETPLACE_AUTHENTICATION_POLICIES = new Set([
|
|
21
|
+
'ON_INSTALL',
|
|
22
|
+
'ON_USE',
|
|
23
|
+
]);
|
|
24
|
+
const CATALOG_CHANNELS = new Set(['latest', 'beta']);
|
|
25
|
+
const NODE_RUNNABLE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
|
26
|
+
const PLATFORM_PRODUCT_BY_NAME = {
|
|
27
|
+
claude: 'CLAUDE',
|
|
28
|
+
codex: 'CODEX',
|
|
29
|
+
};
|
|
30
|
+
const PLUGIN_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
31
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
32
|
+
export const DEFAULT_SOURCE_PLUGIN_AW_MIN_VERSION = '0.0.0';
|
|
33
|
+
const STRICT_SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:0|[1-9]\d*|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
34
|
+
export const STRICT_SEMVER_VALIDATION_CODE = 'invalidStrictSemVer';
|
|
35
|
+
export class StrictSemVerValidationError extends Error {
|
|
36
|
+
code = STRICT_SEMVER_VALIDATION_CODE;
|
|
37
|
+
fieldPath;
|
|
38
|
+
value;
|
|
39
|
+
constructor(issue) {
|
|
40
|
+
super(issue.message);
|
|
41
|
+
this.name = 'StrictSemVerValidationError';
|
|
42
|
+
this.fieldPath = issue.fieldPath;
|
|
43
|
+
this.value = issue.value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function formatStrictSemVerValidationValue(value) {
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
return JSON.stringify(value);
|
|
49
|
+
}
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
return 'undefined';
|
|
52
|
+
}
|
|
53
|
+
const serialized = JSON.stringify(value);
|
|
54
|
+
return serialized ?? String(value);
|
|
55
|
+
}
|
|
56
|
+
export function createStrictSemVerValidationIssue(value, fieldPath) {
|
|
57
|
+
return {
|
|
58
|
+
code: STRICT_SEMVER_VALIDATION_CODE,
|
|
59
|
+
fieldPath,
|
|
60
|
+
value,
|
|
61
|
+
message: `Invalid ${fieldPath}: expected a strict SemVer 2.0.0 string. Received ${formatStrictSemVerValidationValue(value)}.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function getStrictSemVerValidationIssue(value, fieldPath) {
|
|
65
|
+
return tryParseStrictSemVer(value)
|
|
66
|
+
? null
|
|
67
|
+
: createStrictSemVerValidationIssue(value, fieldPath);
|
|
68
|
+
}
|
|
69
|
+
export function isStrictSemVerValidationError(error) {
|
|
70
|
+
return error instanceof StrictSemVerValidationError
|
|
71
|
+
|| (error instanceof Error
|
|
72
|
+
&& 'code' in error
|
|
73
|
+
&& error.code === STRICT_SEMVER_VALIDATION_CODE);
|
|
74
|
+
}
|
|
75
|
+
export function compareStrictSemVerFields(options) {
|
|
76
|
+
const leftIssue = getStrictSemVerValidationIssue(options.left.value, options.left.fieldPath);
|
|
77
|
+
const rightIssue = getStrictSemVerValidationIssue(options.right.value, options.right.fieldPath);
|
|
78
|
+
if (leftIssue || rightIssue) {
|
|
79
|
+
return {
|
|
80
|
+
kind: 'invalid',
|
|
81
|
+
issues: [leftIssue, rightIssue].filter((issue) => issue !== null),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const left = tryParseStrictSemVer(options.left.value);
|
|
85
|
+
const right = tryParseStrictSemVer(options.right.value);
|
|
86
|
+
if (!left || !right) {
|
|
87
|
+
throw new Error('Strict SemVer comparison reached an unreachable invalid state.');
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
kind: 'ordered',
|
|
91
|
+
relation: compareStrictSemVer(left, right),
|
|
92
|
+
left,
|
|
93
|
+
right,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function assertObject(value, fieldName) {
|
|
97
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
98
|
+
throw new Error(`Invalid ${fieldName}: expected an object.`);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function assertAllowedKeys(value, fieldName, allowedKeys) {
|
|
103
|
+
for (const key of Object.keys(value)) {
|
|
104
|
+
if (!allowedKeys.includes(key)) {
|
|
105
|
+
throw new Error(`Invalid ${fieldName}: unknown field "${key}".`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function ensureString(value, fieldName) {
|
|
110
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
111
|
+
throw new Error(`Invalid ${fieldName}: expected a non-empty string.`);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
export function assertStrictSemVer(value, fieldPath) {
|
|
116
|
+
return parseStrictSemVer(value, fieldPath).raw;
|
|
117
|
+
}
|
|
118
|
+
export function assertCatalogVersion(value, fieldPath) {
|
|
119
|
+
const parsed = parseStrictSemVer(value, fieldPath);
|
|
120
|
+
const isStable = parsed.prerelease.length === 0 && parsed.build.length === 0;
|
|
121
|
+
const isBeta = parsed.prerelease.length === 2
|
|
122
|
+
&& parsed.prerelease[0] === 'beta'
|
|
123
|
+
&& /^[1-9]\d*$/u.test(parsed.prerelease[1] ?? '')
|
|
124
|
+
&& parsed.build.length === 0;
|
|
125
|
+
if (!isStable && !isBeta) {
|
|
126
|
+
throw new Error(`Invalid ${fieldPath}: expected a stable SemVer or an exact x.y.z-beta.N version.`);
|
|
127
|
+
}
|
|
128
|
+
return parsed.raw;
|
|
129
|
+
}
|
|
130
|
+
export function tryParseStrictSemVer(value) {
|
|
131
|
+
if (typeof value !== 'string') {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const match = STRICT_SEMVER_PATTERN.exec(value);
|
|
135
|
+
if (!match) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
raw: value,
|
|
140
|
+
major: Number(match[1]),
|
|
141
|
+
minor: Number(match[2]),
|
|
142
|
+
patch: Number(match[3]),
|
|
143
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
144
|
+
build: match[5] ? match[5].split('.') : [],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function parseStrictSemVer(value, fieldPath) {
|
|
148
|
+
const issue = getStrictSemVerValidationIssue(value, fieldPath);
|
|
149
|
+
if (issue) {
|
|
150
|
+
throw new StrictSemVerValidationError(issue);
|
|
151
|
+
}
|
|
152
|
+
return tryParseStrictSemVer(value);
|
|
153
|
+
}
|
|
154
|
+
function isNumericSemVerIdentifier(value) {
|
|
155
|
+
return /^\d+$/.test(value);
|
|
156
|
+
}
|
|
157
|
+
function comparePrereleaseIdentifiers(left, right) {
|
|
158
|
+
const leftNumeric = isNumericSemVerIdentifier(left);
|
|
159
|
+
const rightNumeric = isNumericSemVerIdentifier(right);
|
|
160
|
+
if (leftNumeric && rightNumeric) {
|
|
161
|
+
if (left.length !== right.length) {
|
|
162
|
+
return left.length - right.length;
|
|
163
|
+
}
|
|
164
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
165
|
+
}
|
|
166
|
+
if (leftNumeric) {
|
|
167
|
+
return -1;
|
|
168
|
+
}
|
|
169
|
+
if (rightNumeric) {
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
173
|
+
}
|
|
174
|
+
export function compareStrictSemVer(left, right) {
|
|
175
|
+
const resolvedLeft = typeof left === 'string'
|
|
176
|
+
? parseStrictSemVer(left, 'left version')
|
|
177
|
+
: left;
|
|
178
|
+
const resolvedRight = typeof right === 'string'
|
|
179
|
+
? parseStrictSemVer(right, 'right version')
|
|
180
|
+
: right;
|
|
181
|
+
if (resolvedLeft.major !== resolvedRight.major) {
|
|
182
|
+
return resolvedLeft.major - resolvedRight.major;
|
|
183
|
+
}
|
|
184
|
+
if (resolvedLeft.minor !== resolvedRight.minor) {
|
|
185
|
+
return resolvedLeft.minor - resolvedRight.minor;
|
|
186
|
+
}
|
|
187
|
+
if (resolvedLeft.patch !== resolvedRight.patch) {
|
|
188
|
+
return resolvedLeft.patch - resolvedRight.patch;
|
|
189
|
+
}
|
|
190
|
+
if (resolvedLeft.prerelease.length === 0 && resolvedRight.prerelease.length === 0) {
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
if (resolvedLeft.prerelease.length === 0) {
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
if (resolvedRight.prerelease.length === 0) {
|
|
197
|
+
return -1;
|
|
198
|
+
}
|
|
199
|
+
const maxLength = Math.max(resolvedLeft.prerelease.length, resolvedRight.prerelease.length);
|
|
200
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
201
|
+
const leftIdentifier = resolvedLeft.prerelease[index];
|
|
202
|
+
const rightIdentifier = resolvedRight.prerelease[index];
|
|
203
|
+
if (leftIdentifier === undefined) {
|
|
204
|
+
return -1;
|
|
205
|
+
}
|
|
206
|
+
if (rightIdentifier === undefined) {
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
const result = comparePrereleaseIdentifiers(leftIdentifier, rightIdentifier);
|
|
210
|
+
if (result !== 0) {
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
function ensureStringArray(value, fieldName) {
|
|
217
|
+
if (!Array.isArray(value)) {
|
|
218
|
+
throw new Error(`Invalid ${fieldName}: expected an array.`);
|
|
219
|
+
}
|
|
220
|
+
return value.map((entry, index) => ensureString(entry, `${fieldName}[${index}]`));
|
|
221
|
+
}
|
|
222
|
+
function normalizePermissionCommandArray(value, fieldName) {
|
|
223
|
+
if (value === undefined) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const entries = ensureStringArray(value, fieldName);
|
|
227
|
+
for (const [index, entry] of entries.entries()) {
|
|
228
|
+
parsePermissionCommand(entry, `${fieldName}[${index}]`);
|
|
229
|
+
}
|
|
230
|
+
return entries;
|
|
231
|
+
}
|
|
232
|
+
function normalizeComparableStringArray(value) {
|
|
233
|
+
if (!value) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
return [...new Set(value.map((entry) => entry.trim()).filter(Boolean))];
|
|
237
|
+
}
|
|
238
|
+
function stableStringify(value) {
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
return `[${value.map((entry) => stableStringify(entry)).join(',')}]`;
|
|
241
|
+
}
|
|
242
|
+
if (value && typeof value === 'object') {
|
|
243
|
+
const record = value;
|
|
244
|
+
return `{${Object.keys(record)
|
|
245
|
+
.sort()
|
|
246
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
247
|
+
.join(',')}}`;
|
|
248
|
+
}
|
|
249
|
+
return JSON.stringify(value);
|
|
250
|
+
}
|
|
251
|
+
function ensureDigestString(value, fieldName) {
|
|
252
|
+
const digest = ensureString(value, fieldName);
|
|
253
|
+
if (!DIGEST_PATTERN.test(digest)) {
|
|
254
|
+
throw new Error(`Invalid ${fieldName}: expected a sha256 digest string.`);
|
|
255
|
+
}
|
|
256
|
+
return digest;
|
|
257
|
+
}
|
|
258
|
+
function isPrereleaseVersion(value) {
|
|
259
|
+
return parseStrictSemVer(value, 'version').prerelease.length > 0;
|
|
260
|
+
}
|
|
261
|
+
function assertCatalogChannel(value, fieldName) {
|
|
262
|
+
const channel = ensureString(value, fieldName);
|
|
263
|
+
if (!CATALOG_CHANNELS.has(channel)) {
|
|
264
|
+
throw new Error(`Invalid ${fieldName}: expected "latest" or "beta".`);
|
|
265
|
+
}
|
|
266
|
+
return channel;
|
|
267
|
+
}
|
|
268
|
+
function createPluginCompatibility(minVersion) {
|
|
269
|
+
return {
|
|
270
|
+
aw: {
|
|
271
|
+
minVersion,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function normalizePluginCompatibility(value, fieldName, options) {
|
|
276
|
+
if (value === undefined) {
|
|
277
|
+
if (options.allowMissing) {
|
|
278
|
+
return createPluginCompatibility(assertStrictSemVer(options.defaultMinVersion ?? DEFAULT_SOURCE_PLUGIN_AW_MIN_VERSION, `${fieldName}.aw.minVersion`));
|
|
279
|
+
}
|
|
280
|
+
throw new Error(`Invalid ${fieldName}.aw.minVersion: expected a strict SemVer 2.0.0 string. Received undefined.`);
|
|
281
|
+
}
|
|
282
|
+
const input = assertObject(value, fieldName);
|
|
283
|
+
assertAllowedKeys(input, fieldName, ['aw']);
|
|
284
|
+
const aw = assertObject(input.aw, `${fieldName}.aw`);
|
|
285
|
+
assertAllowedKeys(aw, `${fieldName}.aw`, ['minVersion']);
|
|
286
|
+
return createPluginCompatibility(assertStrictSemVer(aw.minVersion, `${fieldName}.aw.minVersion`));
|
|
287
|
+
}
|
|
288
|
+
function assertPluginMinVersionWithinCatalogBounds(options) {
|
|
289
|
+
if (compareStrictSemVer(options.pluginMinVersion, options.catalogMinVersion) > 0) {
|
|
290
|
+
throw new Error(`Invalid ${options.fieldName}: plugin "${options.pluginName}" requires aw ${options.pluginMinVersion}, which is higher than catalog minimum ${options.catalogMinVersion}.`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function normalizeMarketplaceProducts(value, fieldName) {
|
|
294
|
+
if (value === undefined) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
const products = ensureStringArray(value, fieldName);
|
|
298
|
+
for (const [index, product] of products.entries()) {
|
|
299
|
+
const normalized = normalizeMarketplaceProductToken(product);
|
|
300
|
+
if (!MANIFEST_POLICY_PRODUCTS.has(normalized)) {
|
|
301
|
+
throw new Error(`Invalid ${fieldName}[${index}]: expected one of "CODEX" or "CLAUDE".`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return normalizeComparableStringArray(products);
|
|
305
|
+
}
|
|
306
|
+
export function assertPlatformFieldSupported(options) {
|
|
307
|
+
if (supportsPlatform(options.manifest, options.requiredPlatform)) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Invalid plugin "${options.pluginName}": ${options.fieldPath} requires ${PLATFORM_PRODUCT_BY_NAME[options.requiredPlatform]} support, but ${formatMarketplaceProductsForError(options.manifest)}.`);
|
|
311
|
+
}
|
|
312
|
+
function hasNonEmptyPlatformOverride(value) {
|
|
313
|
+
return value !== null && value !== undefined && Object.keys(value).length > 0;
|
|
314
|
+
}
|
|
315
|
+
function omitUndefinedValues(value) {
|
|
316
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
317
|
+
}
|
|
318
|
+
function assertAgentOverridePlatformsSupported(options) {
|
|
319
|
+
for (const platform of ['claude', 'codex']) {
|
|
320
|
+
if (hasNonEmptyPlatformOverride(options.agentOverrides?.[platform])) {
|
|
321
|
+
assertPlatformFieldSupported({
|
|
322
|
+
pluginName: options.pluginName,
|
|
323
|
+
manifest: options.manifest,
|
|
324
|
+
fieldPath: `${options.fieldPath}.agentOverrides.${platform}`,
|
|
325
|
+
requiredPlatform: platform,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export function assertManifestPlatformSupport(pluginName, manifest) {
|
|
331
|
+
if (manifest.buildConfig?.fileRules?.claude !== undefined) {
|
|
332
|
+
assertPlatformFieldSupported({
|
|
333
|
+
pluginName,
|
|
334
|
+
manifest,
|
|
335
|
+
fieldPath: 'buildConfig.fileRules.claude',
|
|
336
|
+
requiredPlatform: 'claude',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (manifest.buildConfig?.fileRules?.codex !== undefined) {
|
|
340
|
+
assertPlatformFieldSupported({
|
|
341
|
+
pluginName,
|
|
342
|
+
manifest,
|
|
343
|
+
fieldPath: 'buildConfig.fileRules.codex',
|
|
344
|
+
requiredPlatform: 'codex',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (manifest.buildConfig?.manifestOverrides?.claude !== undefined) {
|
|
348
|
+
assertPlatformFieldSupported({
|
|
349
|
+
pluginName,
|
|
350
|
+
manifest,
|
|
351
|
+
fieldPath: 'buildConfig.manifestOverrides.claude',
|
|
352
|
+
requiredPlatform: 'claude',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (manifest.buildConfig?.manifestOverrides?.codex !== undefined) {
|
|
356
|
+
assertPlatformFieldSupported({
|
|
357
|
+
pluginName,
|
|
358
|
+
manifest,
|
|
359
|
+
fieldPath: 'buildConfig.manifestOverrides.codex',
|
|
360
|
+
requiredPlatform: 'codex',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
manifest.buildConfig?.skillRuntimeFiles?.forEach((entry, index) => {
|
|
364
|
+
for (const platform of entry.platforms ?? []) {
|
|
365
|
+
assertPlatformFieldSupported({
|
|
366
|
+
pluginName,
|
|
367
|
+
manifest,
|
|
368
|
+
fieldPath: `buildConfig.skillRuntimeFiles[${index}].platforms`,
|
|
369
|
+
requiredPlatform: platform,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
if (manifest.apps) {
|
|
374
|
+
assertPlatformFieldSupported({
|
|
375
|
+
pluginName,
|
|
376
|
+
manifest,
|
|
377
|
+
fieldPath: 'apps',
|
|
378
|
+
requiredPlatform: 'codex',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if ((manifest.extensions?.nativeHooks?.claude?.length ?? 0) > 0) {
|
|
382
|
+
assertPlatformFieldSupported({
|
|
383
|
+
pluginName,
|
|
384
|
+
manifest,
|
|
385
|
+
fieldPath: 'extensions.nativeHooks.claude',
|
|
386
|
+
requiredPlatform: 'claude',
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if ((manifest.extensions?.nativeHooks?.codex?.length ?? 0) > 0) {
|
|
390
|
+
assertPlatformFieldSupported({
|
|
391
|
+
pluginName,
|
|
392
|
+
manifest,
|
|
393
|
+
fieldPath: 'extensions.nativeHooks.codex',
|
|
394
|
+
requiredPlatform: 'codex',
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
for (const [fieldName, entries] of [
|
|
398
|
+
['extensions.instructions', manifest.extensions?.instructions],
|
|
399
|
+
['extensions.permissions', manifest.extensions?.permissions],
|
|
400
|
+
['extensions.hooks', manifest.extensions?.hooks],
|
|
401
|
+
['extensions.agents', manifest.extensions?.agents],
|
|
402
|
+
]) {
|
|
403
|
+
entries?.forEach((entry, index) => {
|
|
404
|
+
assertAgentOverridePlatformsSupported({
|
|
405
|
+
pluginName,
|
|
406
|
+
manifest,
|
|
407
|
+
fieldPath: `${fieldName}[${index}]`,
|
|
408
|
+
agentOverrides: entry.agentOverrides,
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
manifest.artifacts?.targets?.forEach((target, index) => {
|
|
413
|
+
if (!target.platform)
|
|
414
|
+
return;
|
|
415
|
+
assertPlatformFieldSupported({
|
|
416
|
+
pluginName,
|
|
417
|
+
manifest,
|
|
418
|
+
fieldPath: `artifacts.targets[${index}].platform`,
|
|
419
|
+
requiredPlatform: target.platform,
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
export function ensurePluginName(value, fieldName) {
|
|
424
|
+
const name = ensureString(value, fieldName);
|
|
425
|
+
if (!PLUGIN_NAME_PATTERN.test(name)) {
|
|
426
|
+
throw new Error(`Invalid ${fieldName}: expected lowercase hyphen-case.`);
|
|
427
|
+
}
|
|
428
|
+
return name;
|
|
429
|
+
}
|
|
430
|
+
function normalizePluginChildPath(pluginRoot, value, fieldName) {
|
|
431
|
+
if (path.isAbsolute(value)) {
|
|
432
|
+
throw new Error(`Invalid ${fieldName}: absolute paths are not allowed.`);
|
|
433
|
+
}
|
|
434
|
+
if (!value.startsWith('./')) {
|
|
435
|
+
throw new Error(`Invalid ${fieldName}: path must start with "./".`);
|
|
436
|
+
}
|
|
437
|
+
const normalized = path.posix.normalize(value);
|
|
438
|
+
if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
|
|
439
|
+
throw new Error(`Invalid ${fieldName}: path must stay inside the plugin root.`);
|
|
440
|
+
}
|
|
441
|
+
const resolved = path.resolve(pluginRoot, normalized);
|
|
442
|
+
const relativeToRoot = path.relative(pluginRoot, resolved);
|
|
443
|
+
if (relativeToRoot === '' || relativeToRoot === '.' || relativeToRoot.startsWith('..')) {
|
|
444
|
+
throw new Error(`Invalid ${fieldName}: path must point to a child path inside the plugin root.`);
|
|
445
|
+
}
|
|
446
|
+
return normalized.startsWith('./') ? normalized : `./${normalized}`;
|
|
447
|
+
}
|
|
448
|
+
export function normalizePluginRelativePath(pluginRoot, value, fieldName) {
|
|
449
|
+
const normalized = normalizePluginChildPath(pluginRoot, value, fieldName);
|
|
450
|
+
const ignoredSegment = normalized.split(/[\\/]/u).find((segment) => segment === '.git' || segment === 'node_modules');
|
|
451
|
+
if (ignoredSegment) {
|
|
452
|
+
throw new Error(`Invalid ${fieldName}: source paths must not enter ${ignoredSegment}.`);
|
|
453
|
+
}
|
|
454
|
+
return normalized;
|
|
455
|
+
}
|
|
456
|
+
function normalizePathArray(pluginRoot, value, fieldName) {
|
|
457
|
+
if (value === undefined) {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
return ensureStringArray(value, fieldName).map((entry, index) => normalizePluginRelativePath(pluginRoot, entry, `${fieldName}[${index}]`));
|
|
461
|
+
}
|
|
462
|
+
function normalizePluginInterface(value, fieldName) {
|
|
463
|
+
const input = assertObject(value, fieldName);
|
|
464
|
+
assertAllowedKeys(input, fieldName, ['displayName', 'shortDescription', 'capabilities']);
|
|
465
|
+
return {
|
|
466
|
+
displayName: typeof input.displayName === 'string' ? input.displayName : undefined,
|
|
467
|
+
shortDescription: typeof input.shortDescription === 'string' ? input.shortDescription : undefined,
|
|
468
|
+
capabilities: input.capabilities === undefined
|
|
469
|
+
? undefined
|
|
470
|
+
: ensureStringArray(input.capabilities, `${fieldName}.capabilities`),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function normalizeMarketplacePolicy(value, fieldName) {
|
|
474
|
+
const input = assertObject(value, fieldName);
|
|
475
|
+
assertAllowedKeys(input, fieldName, ['installation', 'authentication', 'products']);
|
|
476
|
+
const authentication = ensureString(input.authentication, `${fieldName}.authentication`);
|
|
477
|
+
if (!MARKETPLACE_AUTHENTICATION_POLICIES.has(authentication)) {
|
|
478
|
+
throw new Error(`Invalid ${fieldName}.authentication: expected "ON_INSTALL" or "ON_USE".`);
|
|
479
|
+
}
|
|
480
|
+
const installation = ensureString(input.installation, `${fieldName}.installation`);
|
|
481
|
+
if (!['AVAILABLE', 'NOT_AVAILABLE', 'INSTALLED_BY_DEFAULT'].includes(installation)) {
|
|
482
|
+
throw new Error(`Invalid ${fieldName}.installation: expected "AVAILABLE", "NOT_AVAILABLE", or "INSTALLED_BY_DEFAULT".`);
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
installation: installation,
|
|
486
|
+
authentication: authentication,
|
|
487
|
+
products: normalizeMarketplaceProducts(input.products, `${fieldName}.products`),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function normalizeMarketplaceMeta(value, fieldName) {
|
|
491
|
+
const input = assertObject(value, fieldName);
|
|
492
|
+
assertAllowedKeys(input, fieldName, ['category', 'tags', 'strict', 'policy']);
|
|
493
|
+
if (typeof input.strict !== 'boolean') {
|
|
494
|
+
throw new Error(`Invalid ${fieldName}.strict: expected a boolean.`);
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
category: ensureString(input.category, `${fieldName}.category`),
|
|
498
|
+
tags: input.tags === undefined
|
|
499
|
+
? undefined
|
|
500
|
+
: normalizeComparableStringArray(ensureStringArray(input.tags, `${fieldName}.tags`)),
|
|
501
|
+
strict: input.strict,
|
|
502
|
+
policy: normalizeMarketplacePolicy(input.policy, `${fieldName}.policy`),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function normalizeTemplateVarByPlatform(value, fieldName) {
|
|
506
|
+
const input = assertObject(value, fieldName);
|
|
507
|
+
assertAllowedKeys(input, fieldName, ['claude', 'codex']);
|
|
508
|
+
const claude = input.claude;
|
|
509
|
+
const codex = input.codex;
|
|
510
|
+
if ((typeof claude !== 'string' && typeof claude !== 'boolean') ||
|
|
511
|
+
(typeof codex !== 'string' && typeof codex !== 'boolean')) {
|
|
512
|
+
throw new Error(`Invalid ${fieldName}: template vars must be string or boolean per platform.`);
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
claude,
|
|
516
|
+
codex,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function normalizeBuildConfigTemplates(value, fieldName) {
|
|
520
|
+
const input = assertObject(value, fieldName);
|
|
521
|
+
assertAllowedKeys(input, fieldName, ['vars', 'include']);
|
|
522
|
+
const varsInput = assertObject(input.vars ?? {}, `${fieldName}.vars`);
|
|
523
|
+
const vars = {};
|
|
524
|
+
for (const [key, entry] of Object.entries(varsInput)) {
|
|
525
|
+
vars[key] = normalizeTemplateVarByPlatform(entry, `${fieldName}.vars.${key}`);
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
vars,
|
|
529
|
+
include: ensureStringArray(input.include ?? [], `${fieldName}.include`),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function normalizeFileRuleSet(value, fieldName) {
|
|
533
|
+
const input = assertObject(value, fieldName);
|
|
534
|
+
assertAllowedKeys(input, fieldName, ['include', 'exclude']);
|
|
535
|
+
return {
|
|
536
|
+
include: ensureStringArray(input.include ?? [], `${fieldName}.include`),
|
|
537
|
+
exclude: ensureStringArray(input.exclude ?? [], `${fieldName}.exclude`),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function normalizeClaudeManifestOverrides(pluginRoot, value, fieldName) {
|
|
541
|
+
const input = assertObject(value, fieldName);
|
|
542
|
+
assertAllowedKeys(input, fieldName, ['commands', 'outputStyles', 'lspServers', 'userConfig', 'channels']);
|
|
543
|
+
const output = {};
|
|
544
|
+
if (input.commands !== undefined) {
|
|
545
|
+
output.commands = normalizePluginRelativePath(pluginRoot, ensureString(input.commands, `${fieldName}.commands`), `${fieldName}.commands`);
|
|
546
|
+
}
|
|
547
|
+
if (input.outputStyles !== undefined) {
|
|
548
|
+
output.outputStyles = normalizePluginRelativePath(pluginRoot, ensureString(input.outputStyles, `${fieldName}.outputStyles`), `${fieldName}.outputStyles`);
|
|
549
|
+
}
|
|
550
|
+
if (input.lspServers !== undefined) {
|
|
551
|
+
if (typeof input.lspServers === 'string') {
|
|
552
|
+
output.lspServers = normalizePluginRelativePath(pluginRoot, ensureString(input.lspServers, `${fieldName}.lspServers`), `${fieldName}.lspServers`);
|
|
553
|
+
}
|
|
554
|
+
else if (Array.isArray(input.lspServers)) {
|
|
555
|
+
output.lspServers = input.lspServers.map((entry, index) => typeof entry === 'string'
|
|
556
|
+
? normalizePluginRelativePath(pluginRoot, ensureString(entry, `${fieldName}.lspServers[${index}]`), `${fieldName}.lspServers[${index}]`)
|
|
557
|
+
: assertObject(entry, `${fieldName}.lspServers[${index}]`));
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
output.lspServers = assertObject(input.lspServers, `${fieldName}.lspServers`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (input.userConfig !== undefined) {
|
|
564
|
+
output.userConfig = assertObject(input.userConfig, `${fieldName}.userConfig`);
|
|
565
|
+
}
|
|
566
|
+
if (input.channels !== undefined) {
|
|
567
|
+
if (Array.isArray(input.channels)) {
|
|
568
|
+
output.channels = input.channels.map((entry, index) => assertObject(entry, `${fieldName}.channels[${index}]`));
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
output.channels = assertObject(input.channels, `${fieldName}.channels`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return output;
|
|
575
|
+
}
|
|
576
|
+
function normalizeRuntimeExportName(value, fieldName) {
|
|
577
|
+
return ensurePluginName(value, fieldName);
|
|
578
|
+
}
|
|
579
|
+
function normalizeCanonicalPluginFilePath(pluginRoot, value, fieldName) {
|
|
580
|
+
const raw = ensureString(value, fieldName);
|
|
581
|
+
if (raw.includes('\\')) {
|
|
582
|
+
throw new Error(`Invalid ${fieldName}: expected a canonical POSIX path.`);
|
|
583
|
+
}
|
|
584
|
+
const normalized = normalizePluginRelativePath(pluginRoot, raw, fieldName);
|
|
585
|
+
if (normalized !== raw) {
|
|
586
|
+
throw new Error(`Invalid ${fieldName}: expected a canonical POSIX path.`);
|
|
587
|
+
}
|
|
588
|
+
return normalized;
|
|
589
|
+
}
|
|
590
|
+
function normalizeImportReference(value, fieldName, reservedImports) {
|
|
591
|
+
const input = assertObject(value, fieldName);
|
|
592
|
+
assertAllowedKeys(input, fieldName, ['plugin', 'export']);
|
|
593
|
+
const plugin = ensureString(input.plugin, `${fieldName}.plugin`);
|
|
594
|
+
const exportName = normalizeRuntimeExportName(input.export, `${fieldName}.export`);
|
|
595
|
+
if (plugin.startsWith('@')) {
|
|
596
|
+
const allowedExports = reservedImports && Object.hasOwn(reservedImports, plugin)
|
|
597
|
+
? reservedImports[plugin]
|
|
598
|
+
: undefined;
|
|
599
|
+
if (!allowedExports) {
|
|
600
|
+
throw new Error(`Invalid ${fieldName}.plugin: unknown reserved provider "${plugin}".`);
|
|
601
|
+
}
|
|
602
|
+
if (!allowedExports.includes(exportName)) {
|
|
603
|
+
throw new Error(`Invalid ${fieldName}.export: unknown reserved export "${exportName}" for provider "${plugin}".`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
ensurePluginName(plugin, `${fieldName}.plugin`);
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
plugin,
|
|
611
|
+
export: exportName,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function normalizeRuntimeExports(pluginRoot, value, fieldName) {
|
|
615
|
+
if (!Array.isArray(value)) {
|
|
616
|
+
throw new Error(`Invalid ${fieldName}: expected an array.`);
|
|
617
|
+
}
|
|
618
|
+
const names = new Set();
|
|
619
|
+
return value.map((entry, index) => {
|
|
620
|
+
const entryField = `${fieldName}[${index}]`;
|
|
621
|
+
const input = assertObject(entry, entryField);
|
|
622
|
+
assertAllowedKeys(input, entryField, ['name', 'source']);
|
|
623
|
+
const name = normalizeRuntimeExportName(input.name, `${entryField}.name`);
|
|
624
|
+
if (names.has(name)) {
|
|
625
|
+
throw new Error(`Invalid ${fieldName}: duplicate export name "${name}".`);
|
|
626
|
+
}
|
|
627
|
+
names.add(name);
|
|
628
|
+
return {
|
|
629
|
+
name,
|
|
630
|
+
source: normalizeCanonicalPluginFilePath(pluginRoot, input.source, `${entryField}.source`),
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function normalizeBuildImports(value, fieldName, reservedImports) {
|
|
635
|
+
if (!Array.isArray(value)) {
|
|
636
|
+
throw new Error(`Invalid ${fieldName}: expected an array.`);
|
|
637
|
+
}
|
|
638
|
+
const envNames = new Set();
|
|
639
|
+
return value.map((entry, index) => {
|
|
640
|
+
const entryField = `${fieldName}[${index}]`;
|
|
641
|
+
const input = assertObject(entry, entryField);
|
|
642
|
+
assertAllowedKeys(input, entryField, ['plugin', 'export', 'env']);
|
|
643
|
+
const reference = normalizeImportReference({ plugin: input.plugin, export: input.export }, entryField, reservedImports);
|
|
644
|
+
const env = ensureString(input.env, `${entryField}.env`);
|
|
645
|
+
if (!/^AW_PLUGIN_IMPORT_[A-Z0-9_]+$/u.test(env)) {
|
|
646
|
+
throw new Error(`Invalid ${entryField}.env: expected an AW_PLUGIN_IMPORT_ prefixed variable name.`);
|
|
647
|
+
}
|
|
648
|
+
if (envNames.has(env)) {
|
|
649
|
+
throw new Error(`Invalid ${fieldName}: duplicate env name "${env}".`);
|
|
650
|
+
}
|
|
651
|
+
envNames.add(env);
|
|
652
|
+
return { ...reference, env };
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
function normalizeSkillRuntimeTarget(value, fieldName) {
|
|
656
|
+
const target = ensureString(value, fieldName);
|
|
657
|
+
if (target.includes('\\')
|
|
658
|
+
|| target.startsWith('./')
|
|
659
|
+
|| path.posix.isAbsolute(target)
|
|
660
|
+
|| path.posix.normalize(target) !== target
|
|
661
|
+
|| !/^(?:references|assets|scripts)\/.+/u.test(target)
|
|
662
|
+
|| target.endsWith('/')) {
|
|
663
|
+
throw new Error(`Invalid ${fieldName}: expected a canonical file path below references/, assets/, or scripts/.`);
|
|
664
|
+
}
|
|
665
|
+
return target;
|
|
666
|
+
}
|
|
667
|
+
function normalizeSkillRuntimeFiles(pluginRoot, value, fieldName, reservedImports) {
|
|
668
|
+
if (!Array.isArray(value)) {
|
|
669
|
+
throw new Error(`Invalid ${fieldName}: expected an array.`);
|
|
670
|
+
}
|
|
671
|
+
const targets = new Set();
|
|
672
|
+
return value.map((entry, index) => {
|
|
673
|
+
const entryField = `${fieldName}[${index}]`;
|
|
674
|
+
const input = assertObject(entry, entryField);
|
|
675
|
+
assertAllowedKeys(input, entryField, ['skill', 'target', 'platforms', 'distributions', 'source', 'import']);
|
|
676
|
+
const skill = ensurePluginName(input.skill, `${entryField}.skill`);
|
|
677
|
+
const target = normalizeSkillRuntimeTarget(input.target, `${entryField}.target`);
|
|
678
|
+
const collisionKey = `${skill}/${target}`;
|
|
679
|
+
if (targets.has(collisionKey)) {
|
|
680
|
+
throw new Error(`Invalid ${fieldName}: duplicate target "${collisionKey}".`);
|
|
681
|
+
}
|
|
682
|
+
targets.add(collisionKey);
|
|
683
|
+
const hasSource = input.source !== undefined;
|
|
684
|
+
const hasImport = input.import !== undefined;
|
|
685
|
+
if (hasSource === hasImport) {
|
|
686
|
+
throw new Error(`Invalid ${entryField}: exactly one of source or import is required.`);
|
|
687
|
+
}
|
|
688
|
+
let platforms;
|
|
689
|
+
if (input.platforms !== undefined) {
|
|
690
|
+
const values = ensureStringArray(input.platforms, `${entryField}.platforms`);
|
|
691
|
+
platforms = values.map((platform, platformIndex) => {
|
|
692
|
+
if (platform !== 'claude' && platform !== 'codex') {
|
|
693
|
+
throw new Error(`Invalid ${entryField}.platforms[${platformIndex}]: expected claude or codex.`);
|
|
694
|
+
}
|
|
695
|
+
return platform;
|
|
696
|
+
});
|
|
697
|
+
if (new Set(platforms).size !== platforms.length) {
|
|
698
|
+
throw new Error(`Invalid ${entryField}.platforms: duplicate platform.`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
let distributions;
|
|
702
|
+
if (input.distributions !== undefined) {
|
|
703
|
+
const values = ensureStringArray(input.distributions, `${entryField}.distributions`);
|
|
704
|
+
distributions = values.map((distribution, distributionIndex) => {
|
|
705
|
+
if (distribution !== 'agent-skills') {
|
|
706
|
+
throw new Error(`Invalid ${entryField}.distributions[${distributionIndex}]: expected agent-skills.`);
|
|
707
|
+
}
|
|
708
|
+
return distribution;
|
|
709
|
+
});
|
|
710
|
+
if (new Set(distributions).size !== distributions.length) {
|
|
711
|
+
throw new Error(`Invalid ${entryField}.distributions: duplicate distribution.`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
skill,
|
|
716
|
+
target,
|
|
717
|
+
...(platforms ? { platforms } : {}),
|
|
718
|
+
...(distributions ? { distributions } : {}),
|
|
719
|
+
...(hasSource
|
|
720
|
+
? { source: normalizeCanonicalPluginFilePath(pluginRoot, input.source, `${entryField}.source`) }
|
|
721
|
+
: { import: normalizeImportReference(input.import, `${entryField}.import`, reservedImports) }),
|
|
722
|
+
};
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function normalizeGeneratedSkillRuntimeFiles(value, fieldName) {
|
|
726
|
+
if (!Array.isArray(value)) {
|
|
727
|
+
throw new Error(`Invalid ${fieldName}: expected an array.`);
|
|
728
|
+
}
|
|
729
|
+
const targets = new Set();
|
|
730
|
+
return value.map((entry, index) => {
|
|
731
|
+
const entryField = `${fieldName}[${index}]`;
|
|
732
|
+
const input = assertObject(entry, entryField);
|
|
733
|
+
assertAllowedKeys(input, entryField, ['skill', 'target', 'distributions']);
|
|
734
|
+
const skill = ensurePluginName(input.skill, `${entryField}.skill`);
|
|
735
|
+
const target = normalizeSkillRuntimeTarget(input.target, `${entryField}.target`);
|
|
736
|
+
const collisionKey = `${skill}/${target}`;
|
|
737
|
+
if (targets.has(collisionKey)) {
|
|
738
|
+
throw new Error(`Invalid ${fieldName}: duplicate target "${collisionKey}".`);
|
|
739
|
+
}
|
|
740
|
+
targets.add(collisionKey);
|
|
741
|
+
const distributions = input.distributions === undefined
|
|
742
|
+
? undefined
|
|
743
|
+
: ensureStringArray(input.distributions, `${entryField}.distributions`).map((distribution, distributionIndex) => {
|
|
744
|
+
if (distribution !== 'agent-skills') {
|
|
745
|
+
throw new Error(`Invalid ${entryField}.distributions[${distributionIndex}]: expected agent-skills.`);
|
|
746
|
+
}
|
|
747
|
+
return distribution;
|
|
748
|
+
});
|
|
749
|
+
if (distributions && new Set(distributions).size !== distributions.length) {
|
|
750
|
+
throw new Error(`Invalid ${entryField}.distributions: duplicate distribution.`);
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
skill,
|
|
754
|
+
target,
|
|
755
|
+
...(distributions ? { distributions } : {}),
|
|
756
|
+
};
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
function normalizeBuildConfig(pluginRoot, value, fieldName, reservedImports) {
|
|
760
|
+
const input = assertObject(value, fieldName);
|
|
761
|
+
assertAllowedKeys(input, fieldName, [
|
|
762
|
+
'cacheable',
|
|
763
|
+
'templates',
|
|
764
|
+
'fileRules',
|
|
765
|
+
'manifestOverrides',
|
|
766
|
+
'runtimeExports',
|
|
767
|
+
'buildImports',
|
|
768
|
+
'skillRuntimeFiles',
|
|
769
|
+
'generatedSkillRuntimeFiles',
|
|
770
|
+
]);
|
|
771
|
+
const buildConfig = {};
|
|
772
|
+
if (input.cacheable !== undefined) {
|
|
773
|
+
if (input.cacheable !== true) {
|
|
774
|
+
throw new Error(`Invalid ${fieldName}.cacheable: expected true.`);
|
|
775
|
+
}
|
|
776
|
+
buildConfig.cacheable = true;
|
|
777
|
+
}
|
|
778
|
+
if (input.templates !== undefined) {
|
|
779
|
+
buildConfig.templates = normalizeBuildConfigTemplates(input.templates, `${fieldName}.templates`);
|
|
780
|
+
}
|
|
781
|
+
if (input.fileRules !== undefined) {
|
|
782
|
+
const fileRulesInput = assertObject(input.fileRules, `${fieldName}.fileRules`);
|
|
783
|
+
assertAllowedKeys(fileRulesInput, `${fieldName}.fileRules`, ['claude', 'codex']);
|
|
784
|
+
buildConfig.fileRules = {};
|
|
785
|
+
if (fileRulesInput.claude !== undefined) {
|
|
786
|
+
const normalizedRuleSet = normalizeFileRuleSet(fileRulesInput.claude, `${fieldName}.fileRules.claude`);
|
|
787
|
+
assertNoDeprecatedCommandFileRules(normalizedRuleSet, `${fieldName}.fileRules.claude`);
|
|
788
|
+
buildConfig.fileRules.claude = normalizedRuleSet;
|
|
789
|
+
}
|
|
790
|
+
if (fileRulesInput.codex !== undefined) {
|
|
791
|
+
const normalizedRuleSet = normalizeFileRuleSet(fileRulesInput.codex, `${fieldName}.fileRules.codex`);
|
|
792
|
+
assertNoDeprecatedCommandFileRules(normalizedRuleSet, `${fieldName}.fileRules.codex`);
|
|
793
|
+
buildConfig.fileRules.codex = normalizedRuleSet;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (input.manifestOverrides !== undefined) {
|
|
797
|
+
const overridesInput = assertObject(input.manifestOverrides, `${fieldName}.manifestOverrides`);
|
|
798
|
+
assertAllowedKeys(overridesInput, `${fieldName}.manifestOverrides`, ['claude', 'codex']);
|
|
799
|
+
buildConfig.manifestOverrides = {};
|
|
800
|
+
if (overridesInput.claude !== undefined) {
|
|
801
|
+
const claudeOverridesInput = assertObject(overridesInput.claude, `${fieldName}.manifestOverrides.claude`);
|
|
802
|
+
if (Object.hasOwn(claudeOverridesInput, 'commands')) {
|
|
803
|
+
throw new Error('Invalid buildConfig.manifestOverrides.claude.commands: source manifests must use "slashCommands" instead.');
|
|
804
|
+
}
|
|
805
|
+
buildConfig.manifestOverrides.claude = normalizeClaudeManifestOverrides(pluginRoot, claudeOverridesInput, `${fieldName}.manifestOverrides.claude`);
|
|
806
|
+
}
|
|
807
|
+
if (overridesInput.codex !== undefined) {
|
|
808
|
+
const codexOverrides = assertObject(overridesInput.codex, `${fieldName}.manifestOverrides.codex`);
|
|
809
|
+
assertAllowedKeys(codexOverrides, `${fieldName}.manifestOverrides.codex`, []);
|
|
810
|
+
buildConfig.manifestOverrides.codex = {};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (input.runtimeExports !== undefined) {
|
|
814
|
+
buildConfig.runtimeExports = normalizeRuntimeExports(pluginRoot, input.runtimeExports, `${fieldName}.runtimeExports`);
|
|
815
|
+
}
|
|
816
|
+
if (input.buildImports !== undefined) {
|
|
817
|
+
buildConfig.buildImports = normalizeBuildImports(input.buildImports, `${fieldName}.buildImports`, reservedImports);
|
|
818
|
+
}
|
|
819
|
+
if (input.skillRuntimeFiles !== undefined) {
|
|
820
|
+
buildConfig.skillRuntimeFiles = normalizeSkillRuntimeFiles(pluginRoot, input.skillRuntimeFiles, `${fieldName}.skillRuntimeFiles`, reservedImports);
|
|
821
|
+
}
|
|
822
|
+
if (input.generatedSkillRuntimeFiles !== undefined) {
|
|
823
|
+
buildConfig.generatedSkillRuntimeFiles = normalizeGeneratedSkillRuntimeFiles(input.generatedSkillRuntimeFiles, `${fieldName}.generatedSkillRuntimeFiles`);
|
|
824
|
+
}
|
|
825
|
+
return buildConfig;
|
|
826
|
+
}
|
|
827
|
+
function assertNoDeprecatedCommandFileRules(fileRuleSet, fieldName) {
|
|
828
|
+
for (const pattern of [...fileRuleSet.include, ...fileRuleSet.exclude]) {
|
|
829
|
+
const normalizedPattern = pattern.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
830
|
+
if (normalizedPattern === 'commands' || normalizedPattern.startsWith('commands/')) {
|
|
831
|
+
throw new Error(`Invalid ${fieldName}: legacy "commands/**" source rules are no longer supported; use "slashCommands" instead.`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function ensureBoolean(value, fieldName) {
|
|
836
|
+
if (typeof value !== 'boolean') {
|
|
837
|
+
throw new Error(`Invalid ${fieldName}: expected a boolean.`);
|
|
838
|
+
}
|
|
839
|
+
return value;
|
|
840
|
+
}
|
|
841
|
+
function ensureFiniteNumber(value, fieldName) {
|
|
842
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
843
|
+
throw new Error(`Invalid ${fieldName}: expected a number.`);
|
|
844
|
+
}
|
|
845
|
+
return value;
|
|
846
|
+
}
|
|
847
|
+
function normalizeScriptTimeout(value, fieldName) {
|
|
848
|
+
if (typeof value !== 'number' ||
|
|
849
|
+
!Number.isInteger(value) ||
|
|
850
|
+
value < PLUGIN_SCRIPT_TIMEOUT_MIN_MS ||
|
|
851
|
+
value > PLUGIN_SCRIPT_TIMEOUT_MAX_MS) {
|
|
852
|
+
throw new Error(`Invalid ${fieldName}: expected an integer between ${PLUGIN_SCRIPT_TIMEOUT_MIN_MS} and ${PLUGIN_SCRIPT_TIMEOUT_MAX_MS}.`);
|
|
853
|
+
}
|
|
854
|
+
return value;
|
|
855
|
+
}
|
|
856
|
+
function assertNodeRunnablePath(value, fieldName) {
|
|
857
|
+
const extension = path.posix.extname(value).toLowerCase();
|
|
858
|
+
if (!NODE_RUNNABLE_EXTENSIONS.has(extension)) {
|
|
859
|
+
throw new Error(`Invalid ${fieldName}: expected a Node runnable file extension (.js, .mjs, or .cjs).`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function assertAutoRunnablePath(value, fieldName) {
|
|
863
|
+
if (!inferLifecycleScriptRunnerFromPath(value)) {
|
|
864
|
+
throw new Error(`Invalid ${fieldName}: runner "auto" requires one of these file extensions: ${PLUGIN_AUTO_RUNNER_EXTENSIONS.join(', ')}.`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function assertNotInlineCommandLike(value, fieldName) {
|
|
868
|
+
if (/\s|[;&|<>`]/.test(value) || value.includes('$(')) {
|
|
869
|
+
throw new Error(`Invalid ${fieldName}: inline command strings are not allowed.`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function normalizePluginScriptPath(pluginRoot, value, fieldName) {
|
|
873
|
+
const rawPath = ensureString(value, fieldName);
|
|
874
|
+
assertNotInlineCommandLike(rawPath, fieldName);
|
|
875
|
+
return normalizePluginRelativePath(pluginRoot, rawPath, fieldName);
|
|
876
|
+
}
|
|
877
|
+
function normalizeScriptEntry(pluginRoot, value, fieldName, options = {}) {
|
|
878
|
+
const input = assertObject(value, fieldName);
|
|
879
|
+
if ('command' in input) {
|
|
880
|
+
throw new Error(`Invalid ${fieldName}: inline command strings are not allowed.`);
|
|
881
|
+
}
|
|
882
|
+
if ('timeoutMs' in input || 'timeout' in input) {
|
|
883
|
+
throw new Error(`Invalid ${fieldName}: per-event and per-selector timeouts are not supported.`);
|
|
884
|
+
}
|
|
885
|
+
assertAllowedKeys(input, fieldName, ['runner', 'path', 'args']);
|
|
886
|
+
const runner = ensureString(input.runner, `${fieldName}.runner`);
|
|
887
|
+
if (!isPluginScriptRunner(runner)) {
|
|
888
|
+
throw new Error(`Invalid ${fieldName}.runner: unsupported runner "${runner}".`);
|
|
889
|
+
}
|
|
890
|
+
if (options.requireNodeRunner && runner !== 'node') {
|
|
891
|
+
throw new Error(`Invalid ${fieldName}.runner: expected "node".`);
|
|
892
|
+
}
|
|
893
|
+
const scriptPath = normalizePluginScriptPath(pluginRoot, input.path, `${fieldName}.path`);
|
|
894
|
+
if (runner === 'auto') {
|
|
895
|
+
assertAutoRunnablePath(scriptPath, `${fieldName}.path`);
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
runner,
|
|
899
|
+
path: scriptPath,
|
|
900
|
+
args: input.args === undefined
|
|
901
|
+
? undefined
|
|
902
|
+
: ensureStringArray(input.args, `${fieldName}.args`),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function normalizeLifecycleScriptDefinition(pluginRoot, value, fieldName) {
|
|
906
|
+
if (typeof value === 'string') {
|
|
907
|
+
const scriptPath = normalizePluginScriptPath(pluginRoot, value, fieldName);
|
|
908
|
+
assertAutoRunnablePath(scriptPath, fieldName);
|
|
909
|
+
return {
|
|
910
|
+
default: {
|
|
911
|
+
runner: 'auto',
|
|
912
|
+
path: scriptPath,
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
const input = assertObject(value, fieldName);
|
|
917
|
+
if ('runner' in input || 'path' in input || 'command' in input) {
|
|
918
|
+
throw new Error(`Invalid ${fieldName}: lifecycle scripts must be declared under OS selectors.`);
|
|
919
|
+
}
|
|
920
|
+
if ('timeoutMs' in input || 'timeout' in input) {
|
|
921
|
+
throw new Error(`Invalid ${fieldName}: per-event and per-selector timeouts are not supported.`);
|
|
922
|
+
}
|
|
923
|
+
for (const key of Object.keys(input)) {
|
|
924
|
+
if (!isPluginLifecycleOsSelector(key)) {
|
|
925
|
+
throw new Error(`Invalid ${fieldName}: unknown OS selector "${key}".`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const output = {};
|
|
929
|
+
for (const [selector, entry] of Object.entries(input)) {
|
|
930
|
+
output[selector] = normalizeScriptEntry(pluginRoot, entry, `${fieldName}.${selector}`);
|
|
931
|
+
}
|
|
932
|
+
for (const osName of ['linux', 'darwin', 'win32']) {
|
|
933
|
+
if (!resolveLifecycleScriptForOs(output, osName)) {
|
|
934
|
+
throw new Error(`Invalid ${fieldName}: no lifecycle entry resolves on ${osName}.`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return output;
|
|
938
|
+
}
|
|
939
|
+
function normalizeLifecycleScripts(pluginRoot, value, fieldName) {
|
|
940
|
+
const input = assertObject(value, fieldName);
|
|
941
|
+
assertAllowedKeys(input, fieldName, [...PLUGIN_LIFECYCLE_EVENTS]);
|
|
942
|
+
const output = {};
|
|
943
|
+
for (const event of PLUGIN_LIFECYCLE_EVENTS) {
|
|
944
|
+
if (input[event] === undefined) {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
output[event] = normalizeLifecycleScriptDefinition(pluginRoot, input[event], `${fieldName}.${event}`);
|
|
948
|
+
}
|
|
949
|
+
return output;
|
|
950
|
+
}
|
|
951
|
+
function normalizeScriptHelpers(pluginRoot, value, fieldName) {
|
|
952
|
+
const input = assertObject(value, fieldName);
|
|
953
|
+
assertAllowedKeys(input, fieldName, ['config']);
|
|
954
|
+
if (input.config === undefined) {
|
|
955
|
+
throw new Error(`Invalid ${fieldName}: expected explicit "config" helper declaration.`);
|
|
956
|
+
}
|
|
957
|
+
const config = normalizePluginScriptPath(pluginRoot, input.config, `${fieldName}.config`);
|
|
958
|
+
assertNodeRunnablePath(config, `${fieldName}.config`);
|
|
959
|
+
return { config };
|
|
960
|
+
}
|
|
961
|
+
function normalizeNodeBuildScript(pluginRoot, value, fieldName) {
|
|
962
|
+
const entry = normalizeScriptEntry(pluginRoot, value, fieldName, {
|
|
963
|
+
requireNodeRunner: true,
|
|
964
|
+
});
|
|
965
|
+
assertNodeRunnablePath(entry.path, `${fieldName}.path`);
|
|
966
|
+
return {
|
|
967
|
+
runner: 'node',
|
|
968
|
+
path: entry.path,
|
|
969
|
+
args: entry.args,
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function normalizePackageDependencyRecord(value, fieldName) {
|
|
973
|
+
if (value === undefined) {
|
|
974
|
+
return {};
|
|
975
|
+
}
|
|
976
|
+
const input = assertObject(value, fieldName);
|
|
977
|
+
const output = {};
|
|
978
|
+
for (const [name, specifier] of Object.entries(input)) {
|
|
979
|
+
output[ensureString(name, `${fieldName} key`)] = ensureString(specifier, `${fieldName}.${name}`);
|
|
980
|
+
}
|
|
981
|
+
return output;
|
|
982
|
+
}
|
|
983
|
+
function isNonRegistryDependencySpecifier(specifier) {
|
|
984
|
+
const trimmed = specifier.trim();
|
|
985
|
+
return (/^(?:workspace|file|link|portal):/i.test(trimmed) ||
|
|
986
|
+
/^(?:git\+|git:|ssh:)/i.test(trimmed) ||
|
|
987
|
+
/^https?:\/\//i.test(trimmed) ||
|
|
988
|
+
/(?:\.tgz|\.tar\.gz)(?:[#?].*)?$/i.test(trimmed) ||
|
|
989
|
+
trimmed.startsWith('./') ||
|
|
990
|
+
trimmed.startsWith('../') ||
|
|
991
|
+
trimmed.startsWith('/') ||
|
|
992
|
+
trimmed.startsWith('~') ||
|
|
993
|
+
/^[\w.-]+\/[\w.-]+(?:#.*)?$/.test(trimmed));
|
|
994
|
+
}
|
|
995
|
+
function validatePackageDependencies(packageJson, fieldName) {
|
|
996
|
+
if (packageJson.optionalDependencies !== undefined) {
|
|
997
|
+
throw new Error(`Invalid ${fieldName}.optionalDependencies: optionalDependencies are not supported.`);
|
|
998
|
+
}
|
|
999
|
+
if (packageJson.bundledDependencies !== undefined || packageJson.bundleDependencies !== undefined) {
|
|
1000
|
+
throw new Error(`Invalid ${fieldName}.bundledDependencies: bundledDependencies are not supported.`);
|
|
1001
|
+
}
|
|
1002
|
+
for (const dependencyField of ['dependencies', 'devDependencies']) {
|
|
1003
|
+
const dependencies = normalizePackageDependencyRecord(packageJson[dependencyField], `${fieldName}.${dependencyField}`);
|
|
1004
|
+
for (const [name, specifier] of Object.entries(dependencies)) {
|
|
1005
|
+
if (isNonRegistryDependencySpecifier(specifier)) {
|
|
1006
|
+
throw new Error(`Invalid ${fieldName}.${dependencyField}.${name}: dependency specifier "${specifier}" is not registry-backed.`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function validateNodePackageRoot(pluginRoot, packageRoot, fieldName) {
|
|
1012
|
+
const absolutePackageRoot = path.join(pluginRoot, stripDotSlash(packageRoot));
|
|
1013
|
+
const stats = fs.statSync(absolutePackageRoot, { throwIfNoEntry: false });
|
|
1014
|
+
if (!stats?.isDirectory()) {
|
|
1015
|
+
throw new Error(`Invalid ${fieldName}: expected a package directory at "${packageRoot}".`);
|
|
1016
|
+
}
|
|
1017
|
+
const packageJsonPath = path.join(absolutePackageRoot, 'package.json');
|
|
1018
|
+
const lockfilePath = path.join(absolutePackageRoot, 'pnpm-lock.yaml');
|
|
1019
|
+
for (const lockfile of ['package-lock.json', 'yarn.lock', 'bun.lock', 'bun.lockb']) {
|
|
1020
|
+
if (fs.pathExistsSync(path.join(absolutePackageRoot, lockfile))) {
|
|
1021
|
+
throw new Error(`Invalid ${fieldName}: only pnpm-lock.yaml is supported, found ${lockfile}.`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (!fs.pathExistsSync(packageJsonPath)) {
|
|
1025
|
+
throw new Error(`Invalid ${fieldName}: missing package.json.`);
|
|
1026
|
+
}
|
|
1027
|
+
if (!fs.pathExistsSync(lockfilePath)) {
|
|
1028
|
+
throw new Error(`Invalid ${fieldName}: missing pnpm-lock.yaml.`);
|
|
1029
|
+
}
|
|
1030
|
+
let packageJson;
|
|
1031
|
+
try {
|
|
1032
|
+
packageJson = fs.readJsonSync(packageJsonPath);
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1036
|
+
throw new Error(`Invalid ${fieldName}.package.json: ${message}`);
|
|
1037
|
+
}
|
|
1038
|
+
validatePackageDependencies(assertObject(packageJson, `${fieldName}.package.json`), `${fieldName}.package.json`);
|
|
1039
|
+
}
|
|
1040
|
+
function normalizeNodeScripts(pluginRoot, value, fieldName) {
|
|
1041
|
+
const input = assertObject(value, fieldName);
|
|
1042
|
+
assertAllowedKeys(input, fieldName, ['packageRoot', 'build']);
|
|
1043
|
+
const output = {};
|
|
1044
|
+
if (input.packageRoot !== undefined) {
|
|
1045
|
+
output.packageRoot = normalizePluginRelativePath(pluginRoot, ensureString(input.packageRoot, `${fieldName}.packageRoot`), `${fieldName}.packageRoot`);
|
|
1046
|
+
validateNodePackageRoot(pluginRoot, output.packageRoot, `${fieldName}.packageRoot`);
|
|
1047
|
+
}
|
|
1048
|
+
if (input.build !== undefined) {
|
|
1049
|
+
output.build = normalizeNodeBuildScript(pluginRoot, input.build, `${fieldName}.build`);
|
|
1050
|
+
}
|
|
1051
|
+
return output;
|
|
1052
|
+
}
|
|
1053
|
+
function normalizeScriptTests(value, fieldName) {
|
|
1054
|
+
const input = assertObject(value, fieldName);
|
|
1055
|
+
assertAllowedKeys(input, fieldName, ['node']);
|
|
1056
|
+
return {
|
|
1057
|
+
node: input.node === undefined
|
|
1058
|
+
? undefined
|
|
1059
|
+
: ensureStringArray(input.node, `${fieldName}.node`),
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function normalizeArtifactRenderScript(pluginRoot, value, fieldName) {
|
|
1063
|
+
const entry = normalizeScriptEntry(pluginRoot, value, fieldName, {
|
|
1064
|
+
requireNodeRunner: true,
|
|
1065
|
+
});
|
|
1066
|
+
assertNodeRunnablePath(entry.path, `${fieldName}.path`);
|
|
1067
|
+
return {
|
|
1068
|
+
runner: 'node',
|
|
1069
|
+
path: entry.path,
|
|
1070
|
+
args: entry.args,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function normalizeArtifactScripts(pluginRoot, value, fieldName) {
|
|
1074
|
+
const input = assertObject(value, fieldName);
|
|
1075
|
+
assertAllowedKeys(input, fieldName, ['render']);
|
|
1076
|
+
return {
|
|
1077
|
+
render: input.render === undefined
|
|
1078
|
+
? undefined
|
|
1079
|
+
: normalizeArtifactRenderScript(pluginRoot, input.render, `${fieldName}.render`),
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function normalizePluginScripts(pluginRoot, value, fieldName) {
|
|
1083
|
+
const input = assertObject(value, fieldName);
|
|
1084
|
+
assertAllowedKeys(input, fieldName, ['timeoutMs', 'lifecycle', 'helpers', 'node', 'artifacts', 'tests']);
|
|
1085
|
+
const output = {
|
|
1086
|
+
timeoutMs: input.timeoutMs === undefined
|
|
1087
|
+
? PLUGIN_SCRIPT_TIMEOUT_DEFAULT_MS
|
|
1088
|
+
: normalizeScriptTimeout(input.timeoutMs, `${fieldName}.timeoutMs`),
|
|
1089
|
+
};
|
|
1090
|
+
if (input.lifecycle !== undefined) {
|
|
1091
|
+
output.lifecycle = normalizeLifecycleScripts(pluginRoot, input.lifecycle, `${fieldName}.lifecycle`);
|
|
1092
|
+
}
|
|
1093
|
+
if (input.helpers !== undefined) {
|
|
1094
|
+
output.helpers = normalizeScriptHelpers(pluginRoot, input.helpers, `${fieldName}.helpers`);
|
|
1095
|
+
}
|
|
1096
|
+
if (input.node !== undefined) {
|
|
1097
|
+
output.node = normalizeNodeScripts(pluginRoot, input.node, `${fieldName}.node`);
|
|
1098
|
+
}
|
|
1099
|
+
if (input.artifacts !== undefined) {
|
|
1100
|
+
output.artifacts = normalizeArtifactScripts(pluginRoot, input.artifacts, `${fieldName}.artifacts`);
|
|
1101
|
+
}
|
|
1102
|
+
if (input.tests !== undefined) {
|
|
1103
|
+
output.tests = normalizeScriptTests(input.tests, `${fieldName}.tests`);
|
|
1104
|
+
}
|
|
1105
|
+
return output;
|
|
1106
|
+
}
|
|
1107
|
+
function validateConfigKey(key, fieldName) {
|
|
1108
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(key)) {
|
|
1109
|
+
throw new Error(`Invalid ${fieldName}: config key "${key}" is not valid.`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function validateConfigDefault(type, value, values, min, max, fieldName) {
|
|
1113
|
+
if (type === 'string' || type === 'file' || type === 'directory') {
|
|
1114
|
+
return ensureString(value, fieldName);
|
|
1115
|
+
}
|
|
1116
|
+
if (type === 'boolean') {
|
|
1117
|
+
return ensureBoolean(value, fieldName);
|
|
1118
|
+
}
|
|
1119
|
+
if (type === 'number') {
|
|
1120
|
+
const numberValue = ensureFiniteNumber(value, fieldName);
|
|
1121
|
+
if (min !== undefined && numberValue < min) {
|
|
1122
|
+
throw new Error(`Invalid ${fieldName}: default is smaller than min.`);
|
|
1123
|
+
}
|
|
1124
|
+
if (max !== undefined && numberValue > max) {
|
|
1125
|
+
throw new Error(`Invalid ${fieldName}: default is greater than max.`);
|
|
1126
|
+
}
|
|
1127
|
+
return numberValue;
|
|
1128
|
+
}
|
|
1129
|
+
if (type === 'enum') {
|
|
1130
|
+
const enumValue = ensureString(value, fieldName);
|
|
1131
|
+
if (!values?.includes(enumValue)) {
|
|
1132
|
+
throw new Error(`Invalid ${fieldName}: enum default must be one of values.`);
|
|
1133
|
+
}
|
|
1134
|
+
return enumValue;
|
|
1135
|
+
}
|
|
1136
|
+
throw new Error(`Invalid ${fieldName}: unsupported config type "${type}".`);
|
|
1137
|
+
}
|
|
1138
|
+
function normalizePluginConfigPresentation(value, schema, fieldName) {
|
|
1139
|
+
const input = assertObject(value, fieldName);
|
|
1140
|
+
assertAllowedKeys(input, fieldName, ['groups', 'fields', 'presets']);
|
|
1141
|
+
const schemaKeys = new Set(Object.keys(schema));
|
|
1142
|
+
const presentation = {};
|
|
1143
|
+
if (input.groups !== undefined) {
|
|
1144
|
+
if (!Array.isArray(input.groups)) {
|
|
1145
|
+
throw new Error(`Invalid ${fieldName}.groups: expected an array.`);
|
|
1146
|
+
}
|
|
1147
|
+
const groupIds = new Set();
|
|
1148
|
+
const groupedFields = new Set();
|
|
1149
|
+
presentation.groups = input.groups.map((rawGroup, index) => {
|
|
1150
|
+
const groupFieldName = `${fieldName}.groups[${index}]`;
|
|
1151
|
+
const group = assertObject(rawGroup, groupFieldName);
|
|
1152
|
+
assertAllowedKeys(group, groupFieldName, [
|
|
1153
|
+
'id',
|
|
1154
|
+
'title',
|
|
1155
|
+
'description',
|
|
1156
|
+
'fields',
|
|
1157
|
+
'advanced',
|
|
1158
|
+
]);
|
|
1159
|
+
const id = ensureString(group.id, `${groupFieldName}.id`);
|
|
1160
|
+
if (groupIds.has(id)) {
|
|
1161
|
+
throw new Error(`Invalid ${groupFieldName}.id: duplicate group id "${id}".`);
|
|
1162
|
+
}
|
|
1163
|
+
groupIds.add(id);
|
|
1164
|
+
const fields = ensureStringArray(group.fields, `${groupFieldName}.fields`);
|
|
1165
|
+
for (const [fieldIndex, key] of fields.entries()) {
|
|
1166
|
+
if (!schemaKeys.has(key)) {
|
|
1167
|
+
throw new Error(`Invalid ${groupFieldName}.fields[${fieldIndex}]: unknown config key "${key}".`);
|
|
1168
|
+
}
|
|
1169
|
+
if (groupedFields.has(key)) {
|
|
1170
|
+
throw new Error(`Invalid ${groupFieldName}.fields[${fieldIndex}]: config key "${key}" is assigned to more than one group.`);
|
|
1171
|
+
}
|
|
1172
|
+
groupedFields.add(key);
|
|
1173
|
+
}
|
|
1174
|
+
return {
|
|
1175
|
+
id,
|
|
1176
|
+
title: ensureString(group.title, `${groupFieldName}.title`),
|
|
1177
|
+
description: group.description === undefined
|
|
1178
|
+
? undefined
|
|
1179
|
+
: ensureString(group.description, `${groupFieldName}.description`),
|
|
1180
|
+
fields,
|
|
1181
|
+
advanced: group.advanced === undefined
|
|
1182
|
+
? undefined
|
|
1183
|
+
: ensureBoolean(group.advanced, `${groupFieldName}.advanced`),
|
|
1184
|
+
};
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
if (input.fields !== undefined) {
|
|
1188
|
+
const fieldsInput = assertObject(input.fields, `${fieldName}.fields`);
|
|
1189
|
+
presentation.fields = {};
|
|
1190
|
+
for (const [key, rawPresentationField] of Object.entries(fieldsInput)) {
|
|
1191
|
+
if (!schemaKeys.has(key)) {
|
|
1192
|
+
throw new Error(`Invalid ${fieldName}.fields: unknown config key "${key}".`);
|
|
1193
|
+
}
|
|
1194
|
+
const presentationFieldName = `${fieldName}.fields.${key}`;
|
|
1195
|
+
const presentationField = assertObject(rawPresentationField, presentationFieldName);
|
|
1196
|
+
assertAllowedKeys(presentationField, presentationFieldName, [
|
|
1197
|
+
'valueLabels',
|
|
1198
|
+
'recommendedValue',
|
|
1199
|
+
'placeholder',
|
|
1200
|
+
'unit',
|
|
1201
|
+
'advanced',
|
|
1202
|
+
]);
|
|
1203
|
+
const schemaField = schema[key];
|
|
1204
|
+
let valueLabels;
|
|
1205
|
+
if (presentationField.valueLabels !== undefined) {
|
|
1206
|
+
if (schemaField.type !== 'enum') {
|
|
1207
|
+
throw new Error(`Invalid ${presentationFieldName}.valueLabels: valueLabels are only supported for enum config.`);
|
|
1208
|
+
}
|
|
1209
|
+
const labelsInput = assertObject(presentationField.valueLabels, `${presentationFieldName}.valueLabels`);
|
|
1210
|
+
valueLabels = {};
|
|
1211
|
+
for (const [token, rawLabel] of Object.entries(labelsInput)) {
|
|
1212
|
+
if (!schemaField.values?.includes(token)) {
|
|
1213
|
+
throw new Error(`Invalid ${presentationFieldName}.valueLabels: unknown enum token "${token}".`);
|
|
1214
|
+
}
|
|
1215
|
+
valueLabels[token] = ensureString(rawLabel, `${presentationFieldName}.valueLabels.${token}`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
presentation.fields[key] = {
|
|
1219
|
+
valueLabels,
|
|
1220
|
+
recommendedValue: presentationField.recommendedValue === undefined
|
|
1221
|
+
? undefined
|
|
1222
|
+
: validateConfigDefault(schemaField.type, presentationField.recommendedValue, schemaField.values, schemaField.min, schemaField.max, `${presentationFieldName}.recommendedValue`),
|
|
1223
|
+
placeholder: presentationField.placeholder === undefined
|
|
1224
|
+
? undefined
|
|
1225
|
+
: ensureString(presentationField.placeholder, `${presentationFieldName}.placeholder`),
|
|
1226
|
+
unit: presentationField.unit === undefined
|
|
1227
|
+
? undefined
|
|
1228
|
+
: ensureString(presentationField.unit, `${presentationFieldName}.unit`),
|
|
1229
|
+
advanced: presentationField.advanced === undefined
|
|
1230
|
+
? undefined
|
|
1231
|
+
: ensureBoolean(presentationField.advanced, `${presentationFieldName}.advanced`),
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (input.presets !== undefined) {
|
|
1236
|
+
if (!Array.isArray(input.presets)) {
|
|
1237
|
+
throw new Error(`Invalid ${fieldName}.presets: expected an array.`);
|
|
1238
|
+
}
|
|
1239
|
+
const presetIds = new Set();
|
|
1240
|
+
presentation.presets = input.presets.map((rawPreset, index) => {
|
|
1241
|
+
const presetFieldName = `${fieldName}.presets[${index}]`;
|
|
1242
|
+
const preset = assertObject(rawPreset, presetFieldName);
|
|
1243
|
+
assertAllowedKeys(preset, presetFieldName, ['id', 'title', 'description', 'values']);
|
|
1244
|
+
const id = ensureString(preset.id, `${presetFieldName}.id`);
|
|
1245
|
+
if (presetIds.has(id)) {
|
|
1246
|
+
throw new Error(`Invalid ${presetFieldName}.id: duplicate preset id "${id}".`);
|
|
1247
|
+
}
|
|
1248
|
+
presetIds.add(id);
|
|
1249
|
+
const valuesInput = assertObject(preset.values, `${presetFieldName}.values`);
|
|
1250
|
+
const values = {};
|
|
1251
|
+
for (const [key, rawValue] of Object.entries(valuesInput)) {
|
|
1252
|
+
const schemaField = schema[key];
|
|
1253
|
+
if (!schemaField) {
|
|
1254
|
+
throw new Error(`Invalid ${presetFieldName}.values: unknown config key "${key}".`);
|
|
1255
|
+
}
|
|
1256
|
+
if (schemaField.sensitive === true
|
|
1257
|
+
|| schemaField.type === 'file'
|
|
1258
|
+
|| schemaField.type === 'directory') {
|
|
1259
|
+
throw new Error(`Invalid ${presetFieldName}.values.${key}: presets cannot contain sensitive, file, or directory config values.`);
|
|
1260
|
+
}
|
|
1261
|
+
values[key] = validateConfigDefault(schemaField.type, rawValue, schemaField.values, schemaField.min, schemaField.max, `${presetFieldName}.values.${key}`);
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
id,
|
|
1265
|
+
title: ensureString(preset.title, `${presetFieldName}.title`),
|
|
1266
|
+
description: preset.description === undefined
|
|
1267
|
+
? undefined
|
|
1268
|
+
: ensureString(preset.description, `${presetFieldName}.description`),
|
|
1269
|
+
values,
|
|
1270
|
+
};
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
return presentation;
|
|
1274
|
+
}
|
|
1275
|
+
function normalizePluginConfigSchema(value, fieldName) {
|
|
1276
|
+
const input = assertObject(value, fieldName);
|
|
1277
|
+
assertAllowedKeys(input, fieldName, ['schema', 'presentation']);
|
|
1278
|
+
const schemaInput = assertObject(input.schema, `${fieldName}.schema`);
|
|
1279
|
+
const schema = {};
|
|
1280
|
+
const supportedTypes = new Set(['string', 'number', 'boolean', 'enum', 'file', 'directory']);
|
|
1281
|
+
for (const [key, rawField] of Object.entries(schemaInput)) {
|
|
1282
|
+
validateConfigKey(key, `${fieldName}.schema`);
|
|
1283
|
+
const schemaFieldName = `${fieldName}.schema.${key}`;
|
|
1284
|
+
const configField = assertObject(rawField, schemaFieldName);
|
|
1285
|
+
assertAllowedKeys(configField, schemaFieldName, [
|
|
1286
|
+
'type',
|
|
1287
|
+
'title',
|
|
1288
|
+
'description',
|
|
1289
|
+
'default',
|
|
1290
|
+
'required',
|
|
1291
|
+
'sensitive',
|
|
1292
|
+
'values',
|
|
1293
|
+
'min',
|
|
1294
|
+
'max',
|
|
1295
|
+
]);
|
|
1296
|
+
const type = ensureString(configField.type, `${schemaFieldName}.type`);
|
|
1297
|
+
if (!supportedTypes.has(type)) {
|
|
1298
|
+
throw new Error(`Invalid ${schemaFieldName}.type: expected string, number, boolean, enum, file, or directory.`);
|
|
1299
|
+
}
|
|
1300
|
+
const required = configField.required === undefined
|
|
1301
|
+
? undefined
|
|
1302
|
+
: ensureBoolean(configField.required, `${schemaFieldName}.required`);
|
|
1303
|
+
const sensitive = configField.sensitive === undefined
|
|
1304
|
+
? undefined
|
|
1305
|
+
: ensureBoolean(configField.sensitive, `${schemaFieldName}.sensitive`);
|
|
1306
|
+
const values = configField.values === undefined
|
|
1307
|
+
? undefined
|
|
1308
|
+
: ensureStringArray(configField.values, `${schemaFieldName}.values`);
|
|
1309
|
+
if (type !== 'enum' && values !== undefined) {
|
|
1310
|
+
throw new Error(`Invalid ${schemaFieldName}.values: values are only supported for enum config.`);
|
|
1311
|
+
}
|
|
1312
|
+
if (type === 'enum' && (!values || values.length === 0)) {
|
|
1313
|
+
throw new Error(`Invalid ${schemaFieldName}.values: enum config requires at least one value.`);
|
|
1314
|
+
}
|
|
1315
|
+
if (values && new Set(values).size !== values.length) {
|
|
1316
|
+
throw new Error(`Invalid ${schemaFieldName}.values: enum values must be unique.`);
|
|
1317
|
+
}
|
|
1318
|
+
const min = configField.min === undefined
|
|
1319
|
+
? undefined
|
|
1320
|
+
: ensureFiniteNumber(configField.min, `${schemaFieldName}.min`);
|
|
1321
|
+
const max = configField.max === undefined
|
|
1322
|
+
? undefined
|
|
1323
|
+
: ensureFiniteNumber(configField.max, `${schemaFieldName}.max`);
|
|
1324
|
+
if (type !== 'number' && (min !== undefined || max !== undefined)) {
|
|
1325
|
+
throw new Error(`Invalid ${schemaFieldName}: min/max are only supported for number config.`);
|
|
1326
|
+
}
|
|
1327
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
1328
|
+
throw new Error(`Invalid ${schemaFieldName}: min must be less than or equal to max.`);
|
|
1329
|
+
}
|
|
1330
|
+
schema[key] = {
|
|
1331
|
+
type: type,
|
|
1332
|
+
title: configField.title === undefined
|
|
1333
|
+
? undefined
|
|
1334
|
+
: ensureString(configField.title, `${schemaFieldName}.title`),
|
|
1335
|
+
description: configField.description === undefined
|
|
1336
|
+
? undefined
|
|
1337
|
+
: ensureString(configField.description, `${schemaFieldName}.description`),
|
|
1338
|
+
default: configField.default === undefined
|
|
1339
|
+
? undefined
|
|
1340
|
+
: validateConfigDefault(type, configField.default, values, min, max, `${schemaFieldName}.default`),
|
|
1341
|
+
required,
|
|
1342
|
+
sensitive,
|
|
1343
|
+
values,
|
|
1344
|
+
min,
|
|
1345
|
+
max,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
return {
|
|
1349
|
+
schema,
|
|
1350
|
+
presentation: input.presentation === undefined
|
|
1351
|
+
? undefined
|
|
1352
|
+
: normalizePluginConfigPresentation(input.presentation, schema, `${fieldName}.presentation`),
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
const ARTIFACT_TARGETS = new Set([
|
|
1356
|
+
'codex.developerInstructionsBlock',
|
|
1357
|
+
'codex.agentRole',
|
|
1358
|
+
'codex.skill',
|
|
1359
|
+
'claude.skill',
|
|
1360
|
+
'agent-skills.skill',
|
|
1361
|
+
]);
|
|
1362
|
+
const ARTIFACT_STATES = new Set(['present', 'disabled']);
|
|
1363
|
+
const DYNAMIC_ARTIFACT_KINDS = new Set(['instruction', 'codexRole', 'skill']);
|
|
1364
|
+
const ARTIFACT_PROJECT_MANAGED_KIND_BY_TARGET = {
|
|
1365
|
+
'codex.developerInstructionsBlock': 'instruction',
|
|
1366
|
+
'codex.agentRole': 'agent',
|
|
1367
|
+
'codex.skill': 'skill',
|
|
1368
|
+
};
|
|
1369
|
+
const ARTIFACT_KIND_BY_TARGET = {
|
|
1370
|
+
'codex.developerInstructionsBlock': 'instruction',
|
|
1371
|
+
'codex.agentRole': 'codexRole',
|
|
1372
|
+
'codex.skill': 'skill',
|
|
1373
|
+
'claude.skill': 'skill',
|
|
1374
|
+
'agent-skills.skill': 'skill',
|
|
1375
|
+
};
|
|
1376
|
+
function normalizeArtifactRuntimeImports(value, fieldName, reservedImports) {
|
|
1377
|
+
if (value === undefined)
|
|
1378
|
+
return undefined;
|
|
1379
|
+
if (!Array.isArray(value)) {
|
|
1380
|
+
throw new Error(`Invalid ${fieldName}: expected an array.`);
|
|
1381
|
+
}
|
|
1382
|
+
const names = new Set();
|
|
1383
|
+
return value.map((entry, index) => {
|
|
1384
|
+
const entryField = `${fieldName}[${index}]`;
|
|
1385
|
+
const input = assertObject(entry, entryField);
|
|
1386
|
+
assertAllowedKeys(input, entryField, ['name', 'plugin', 'export', 'resolved']);
|
|
1387
|
+
const name = ensurePluginName(input.name, `${entryField}.name`);
|
|
1388
|
+
if (names.has(name)) {
|
|
1389
|
+
throw new Error(`Invalid ${fieldName}: duplicate import name "${name}".`);
|
|
1390
|
+
}
|
|
1391
|
+
names.add(name);
|
|
1392
|
+
let resolved;
|
|
1393
|
+
if (input.resolved !== undefined) {
|
|
1394
|
+
const resolvedInput = assertObject(input.resolved, `${entryField}.resolved`);
|
|
1395
|
+
assertAllowedKeys(resolvedInput, `${entryField}.resolved`, [
|
|
1396
|
+
'path', 'digest', 'providerPluginId', 'providerVersion', 'source',
|
|
1397
|
+
]);
|
|
1398
|
+
resolved = {
|
|
1399
|
+
path: normalizePluginChildPath('', ensureString(resolvedInput.path, `${entryField}.resolved.path`), `${entryField}.resolved.path`),
|
|
1400
|
+
digest: ensureDigestString(resolvedInput.digest, `${entryField}.resolved.digest`),
|
|
1401
|
+
providerPluginId: ensureString(resolvedInput.providerPluginId, `${entryField}.resolved.providerPluginId`),
|
|
1402
|
+
providerVersion: assertStrictSemVer(resolvedInput.providerVersion, `${entryField}.resolved.providerVersion`),
|
|
1403
|
+
source: ensureString(resolvedInput.source, `${entryField}.resolved.source`),
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
name,
|
|
1408
|
+
...normalizeImportReference({ plugin: input.plugin, export: input.export }, entryField, reservedImports),
|
|
1409
|
+
...(resolved ? { resolved } : {}),
|
|
1410
|
+
};
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
function normalizePlatformName(value, fieldName) {
|
|
1414
|
+
const platform = ensureString(value, fieldName);
|
|
1415
|
+
if (platform !== 'codex' && platform !== 'claude') {
|
|
1416
|
+
throw new Error(`Invalid ${fieldName}: expected "codex" or "claude".`);
|
|
1417
|
+
}
|
|
1418
|
+
return platform;
|
|
1419
|
+
}
|
|
1420
|
+
function normalizeScopeArray(value, fieldName) {
|
|
1421
|
+
const scopes = ensureStringArray(value, fieldName);
|
|
1422
|
+
const normalized = [];
|
|
1423
|
+
for (const [index, scope] of scopes.entries()) {
|
|
1424
|
+
if (scope !== 'user' && scope !== 'project') {
|
|
1425
|
+
throw new Error(`Invalid ${fieldName}[${index}]: expected "user" or "project".`);
|
|
1426
|
+
}
|
|
1427
|
+
if (!normalized.includes(scope)) {
|
|
1428
|
+
normalized.push(scope);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (normalized.length === 0) {
|
|
1432
|
+
throw new Error(`Invalid ${fieldName}: expected at least one scope.`);
|
|
1433
|
+
}
|
|
1434
|
+
return normalized;
|
|
1435
|
+
}
|
|
1436
|
+
function normalizeArtifactTarget(value, fieldName) {
|
|
1437
|
+
const target = ensureString(value, fieldName);
|
|
1438
|
+
if (!ARTIFACT_TARGETS.has(target)) {
|
|
1439
|
+
throw new Error(`Invalid ${fieldName}: unsupported artifact target "${target}".`);
|
|
1440
|
+
}
|
|
1441
|
+
return target;
|
|
1442
|
+
}
|
|
1443
|
+
function normalizeArtifactStates(value, fieldName) {
|
|
1444
|
+
const states = ensureStringArray(value, fieldName);
|
|
1445
|
+
const normalized = [];
|
|
1446
|
+
for (const [index, state] of states.entries()) {
|
|
1447
|
+
if (!ARTIFACT_STATES.has(state)) {
|
|
1448
|
+
throw new Error(`Invalid ${fieldName}[${index}]: unsupported artifact state "${state}".`);
|
|
1449
|
+
}
|
|
1450
|
+
if (!normalized.includes(state)) {
|
|
1451
|
+
normalized.push(state);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (normalized.length === 0) {
|
|
1455
|
+
throw new Error(`Invalid ${fieldName}: expected at least one state.`);
|
|
1456
|
+
}
|
|
1457
|
+
return normalized;
|
|
1458
|
+
}
|
|
1459
|
+
function normalizeArtifactRefreshKeys(value, fieldName, schema) {
|
|
1460
|
+
const refreshKeys = ensureStringArray(value, fieldName);
|
|
1461
|
+
const schemaKeys = new Set(Object.keys(schema?.schema ?? {}));
|
|
1462
|
+
const normalized = [];
|
|
1463
|
+
for (const [index, key] of refreshKeys.entries()) {
|
|
1464
|
+
if (!schemaKeys.has(key)) {
|
|
1465
|
+
throw new Error(`Invalid ${fieldName}[${index}]: unknown config key "${key}".`);
|
|
1466
|
+
}
|
|
1467
|
+
if (!normalized.includes(key)) {
|
|
1468
|
+
normalized.push(key);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return normalized;
|
|
1472
|
+
}
|
|
1473
|
+
function normalizeDynamicArtifactKind(value, fieldName) {
|
|
1474
|
+
const kind = ensureString(value, fieldName);
|
|
1475
|
+
if (!DYNAMIC_ARTIFACT_KINDS.has(kind)) {
|
|
1476
|
+
throw new Error(`Invalid ${fieldName}: unsupported artifact kind "${kind}".`);
|
|
1477
|
+
}
|
|
1478
|
+
return kind;
|
|
1479
|
+
}
|
|
1480
|
+
function normalizeProjectManagedKind(value, fieldName) {
|
|
1481
|
+
const kind = ensureString(value, fieldName);
|
|
1482
|
+
if (!['skill', 'mcpServer', 'instruction', 'permission', 'hook', 'agent'].includes(kind)) {
|
|
1483
|
+
throw new Error(`Invalid ${fieldName}: unsupported Codex project managed kind "${kind}".`);
|
|
1484
|
+
}
|
|
1485
|
+
return kind;
|
|
1486
|
+
}
|
|
1487
|
+
function validateArtifactId(value, fieldName) {
|
|
1488
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(value)) {
|
|
1489
|
+
throw new Error(`Invalid ${fieldName}: expected a stable id using letters, numbers, ".", "_" or "-".`);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
function validateCodexRoleKey(value, fieldName) {
|
|
1493
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(value)) {
|
|
1494
|
+
throw new Error(`Invalid ${fieldName}: expected a valid Codex role key.`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
function validateCodexSkillKey(value, fieldName) {
|
|
1498
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(value)) {
|
|
1499
|
+
throw new Error(`Invalid ${fieldName}: expected a valid Codex skill key.`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
function normalizeArtifactTargetEntry(pluginRoot, entry, fieldName, schema, reservedImports) {
|
|
1503
|
+
const input = assertObject(entry, fieldName);
|
|
1504
|
+
assertAllowedKeys(input, fieldName, [
|
|
1505
|
+
'id',
|
|
1506
|
+
'platform',
|
|
1507
|
+
'scopes',
|
|
1508
|
+
'target',
|
|
1509
|
+
'kind',
|
|
1510
|
+
'projectManagedKind',
|
|
1511
|
+
'states',
|
|
1512
|
+
'refreshKeys',
|
|
1513
|
+
'requiresCodexProjectConfig',
|
|
1514
|
+
'roleKey',
|
|
1515
|
+
'skillKey',
|
|
1516
|
+
'sourceDirs',
|
|
1517
|
+
'distribution',
|
|
1518
|
+
'runtimeImports',
|
|
1519
|
+
]);
|
|
1520
|
+
const id = ensureString(input.id, `${fieldName}.id`);
|
|
1521
|
+
validateArtifactId(id, `${fieldName}.id`);
|
|
1522
|
+
const target = normalizeArtifactTarget(input.target, `${fieldName}.target`);
|
|
1523
|
+
const portable = target === 'agent-skills.skill';
|
|
1524
|
+
const platform = portable ? undefined : normalizePlatformName(input.platform, `${fieldName}.platform`);
|
|
1525
|
+
const distribution = portable
|
|
1526
|
+
? ensureString(input.distribution, `${fieldName}.distribution`)
|
|
1527
|
+
: undefined;
|
|
1528
|
+
if (portable) {
|
|
1529
|
+
if (distribution !== 'agent-skills') {
|
|
1530
|
+
throw new Error(`Invalid ${fieldName}.distribution: expected "agent-skills".`);
|
|
1531
|
+
}
|
|
1532
|
+
if (input.platform !== undefined) {
|
|
1533
|
+
throw new Error(`Invalid ${fieldName}.platform: distribution targets do not support platform.`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
const expectedPlatform = target === 'claude.skill' ? 'claude' : 'codex';
|
|
1538
|
+
if (platform !== expectedPlatform) {
|
|
1539
|
+
throw new Error(`Invalid ${fieldName}.platform: target "${target}" is only supported on "${expectedPlatform}".`);
|
|
1540
|
+
}
|
|
1541
|
+
if (input.distribution !== undefined) {
|
|
1542
|
+
throw new Error(`Invalid ${fieldName}.distribution: platform targets do not support distribution.`);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
const expectedKind = ARTIFACT_KIND_BY_TARGET[target];
|
|
1546
|
+
const kind = input.kind === undefined
|
|
1547
|
+
? expectedKind
|
|
1548
|
+
: normalizeDynamicArtifactKind(input.kind, `${fieldName}.kind`);
|
|
1549
|
+
if (kind !== expectedKind) {
|
|
1550
|
+
throw new Error(`Invalid ${fieldName}.kind: target "${target}" requires kind "${expectedKind}".`);
|
|
1551
|
+
}
|
|
1552
|
+
const expectedProjectManagedKind = ARTIFACT_PROJECT_MANAGED_KIND_BY_TARGET[target];
|
|
1553
|
+
const projectManagedKind = input.projectManagedKind === undefined
|
|
1554
|
+
? expectedProjectManagedKind
|
|
1555
|
+
: normalizeProjectManagedKind(input.projectManagedKind, `${fieldName}.projectManagedKind`);
|
|
1556
|
+
if (projectManagedKind !== expectedProjectManagedKind) {
|
|
1557
|
+
throw new Error(expectedProjectManagedKind
|
|
1558
|
+
? `Invalid ${fieldName}.projectManagedKind: target "${target}" must map to "${expectedProjectManagedKind}".`
|
|
1559
|
+
: `Invalid ${fieldName}.projectManagedKind: target "${target}" does not support projectManagedKind.`);
|
|
1560
|
+
}
|
|
1561
|
+
const roleKey = input.roleKey === undefined
|
|
1562
|
+
? undefined
|
|
1563
|
+
: ensureString(input.roleKey, `${fieldName}.roleKey`);
|
|
1564
|
+
const skillKey = input.skillKey === undefined
|
|
1565
|
+
? undefined
|
|
1566
|
+
: ensureString(input.skillKey, `${fieldName}.skillKey`);
|
|
1567
|
+
if (target === 'codex.agentRole') {
|
|
1568
|
+
if (!roleKey) {
|
|
1569
|
+
throw new Error(`Invalid ${fieldName}.roleKey: codex.agentRole targets must declare a static roleKey.`);
|
|
1570
|
+
}
|
|
1571
|
+
validateCodexRoleKey(roleKey, `${fieldName}.roleKey`);
|
|
1572
|
+
}
|
|
1573
|
+
else if (roleKey !== undefined) {
|
|
1574
|
+
throw new Error(`Invalid ${fieldName}.roleKey: roleKey is only supported for codex.agentRole targets.`);
|
|
1575
|
+
}
|
|
1576
|
+
if (target === 'codex.skill' || target === 'claude.skill' || target === 'agent-skills.skill') {
|
|
1577
|
+
if (!skillKey) {
|
|
1578
|
+
throw new Error(`Invalid ${fieldName}.skillKey: ${target} targets must declare a static skillKey.`);
|
|
1579
|
+
}
|
|
1580
|
+
if (portable)
|
|
1581
|
+
validatePortableSkillName(skillKey, `${fieldName}.skillKey`);
|
|
1582
|
+
else
|
|
1583
|
+
validateCodexSkillKey(skillKey, `${fieldName}.skillKey`);
|
|
1584
|
+
}
|
|
1585
|
+
else if (skillKey !== undefined) {
|
|
1586
|
+
throw new Error(`Invalid ${fieldName}.skillKey: skillKey is only supported for skill targets.`);
|
|
1587
|
+
}
|
|
1588
|
+
const sourceDirs = normalizePathArray(pluginRoot, input.sourceDirs, `${fieldName}.sourceDirs`);
|
|
1589
|
+
if (sourceDirs && target !== 'codex.skill' && target !== 'claude.skill' && target !== 'agent-skills.skill') {
|
|
1590
|
+
throw new Error(`Invalid ${fieldName}.sourceDirs: sourceDirs are only supported for skill targets.`);
|
|
1591
|
+
}
|
|
1592
|
+
if (sourceDirs && new Set(sourceDirs).size !== sourceDirs.length) {
|
|
1593
|
+
throw new Error(`Invalid ${fieldName}.sourceDirs: duplicate source directory.`);
|
|
1594
|
+
}
|
|
1595
|
+
if (sourceDirs && skillKey && !portable) {
|
|
1596
|
+
const canonicalRoot = `skills/${skillKey}`;
|
|
1597
|
+
const invalidSource = sourceDirs.find((sourceDir) => {
|
|
1598
|
+
const normalized = stripDotSlash(sourceDir);
|
|
1599
|
+
return normalized !== canonicalRoot && !normalized.startsWith(`${canonicalRoot}/`);
|
|
1600
|
+
});
|
|
1601
|
+
if (invalidSource) {
|
|
1602
|
+
throw new Error(`Invalid ${fieldName}.sourceDirs: "${invalidSource}" must stay inside "./${canonicalRoot}".`);
|
|
1603
|
+
}
|
|
1604
|
+
const authorOnlySource = sourceDirs.find((sourceDir) => isSkillAuthorOnlyPath(path.posix.relative(canonicalRoot, stripDotSlash(sourceDir))));
|
|
1605
|
+
if (authorOnlySource) {
|
|
1606
|
+
throw new Error(`Invalid ${fieldName}.sourceDirs: "${authorOnlySource}" points inside an author-only Skill directory.`);
|
|
1607
|
+
}
|
|
1608
|
+
if (!sourceDirs.some((sourceDir) => stripDotSlash(sourceDir) === canonicalRoot)) {
|
|
1609
|
+
throw new Error(`Invalid ${fieldName}.sourceDirs: dynamic Skill sources must include "./${canonicalRoot}" so README.md is published.`);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if ((platform === 'claude' || portable) && input.requiresCodexProjectConfig !== undefined) {
|
|
1613
|
+
throw new Error(`Invalid ${fieldName}.requiresCodexProjectConfig: Claude targets do not support requiresCodexProjectConfig.`);
|
|
1614
|
+
}
|
|
1615
|
+
if (!portable && input.runtimeImports !== undefined) {
|
|
1616
|
+
throw new Error(`Invalid ${fieldName}.runtimeImports: runtimeImports are only supported for agent-skills.skill targets.`);
|
|
1617
|
+
}
|
|
1618
|
+
return {
|
|
1619
|
+
id,
|
|
1620
|
+
platform,
|
|
1621
|
+
distribution: distribution,
|
|
1622
|
+
scopes: normalizeScopeArray(input.scopes, `${fieldName}.scopes`),
|
|
1623
|
+
target,
|
|
1624
|
+
kind,
|
|
1625
|
+
projectManagedKind,
|
|
1626
|
+
states: normalizeArtifactStates(input.states, `${fieldName}.states`),
|
|
1627
|
+
refreshKeys: normalizeArtifactRefreshKeys(input.refreshKeys, `${fieldName}.refreshKeys`, schema),
|
|
1628
|
+
requiresCodexProjectConfig: input.requiresCodexProjectConfig === undefined
|
|
1629
|
+
? undefined
|
|
1630
|
+
: ensureBoolean(input.requiresCodexProjectConfig, `${fieldName}.requiresCodexProjectConfig`),
|
|
1631
|
+
roleKey,
|
|
1632
|
+
skillKey,
|
|
1633
|
+
sourceDirs,
|
|
1634
|
+
runtimeImports: normalizeArtifactRuntimeImports(input.runtimeImports, `${fieldName}.runtimeImports`, reservedImports),
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
function normalizeArtifacts(pluginRoot, value, fieldName, schema, reservedImports) {
|
|
1638
|
+
const input = assertObject(value, fieldName);
|
|
1639
|
+
assertAllowedKeys(input, fieldName, ['targets']);
|
|
1640
|
+
const targetsInput = input.targets;
|
|
1641
|
+
const targets = targetsInput === undefined
|
|
1642
|
+
? undefined
|
|
1643
|
+
: (() => {
|
|
1644
|
+
if (!Array.isArray(targetsInput)) {
|
|
1645
|
+
throw new Error(`Invalid ${fieldName}.targets: expected an array.`);
|
|
1646
|
+
}
|
|
1647
|
+
const seen = new Set();
|
|
1648
|
+
const seenSkillOwners = new Set();
|
|
1649
|
+
return targetsInput.map((entry, index) => {
|
|
1650
|
+
const target = normalizeArtifactTargetEntry(pluginRoot, entry, `${fieldName}.targets[${index}]`, schema, reservedImports);
|
|
1651
|
+
const key = `${target.id}:${target.target}`;
|
|
1652
|
+
if (seen.has(key)) {
|
|
1653
|
+
throw new Error(`Invalid ${fieldName}.targets[${index}]: duplicate artifact id + target "${key}".`);
|
|
1654
|
+
}
|
|
1655
|
+
seen.add(key);
|
|
1656
|
+
if ((target.target === 'codex.skill' || target.target === 'claude.skill' || target.target === 'agent-skills.skill') && target.skillKey) {
|
|
1657
|
+
for (const scope of target.scopes) {
|
|
1658
|
+
const ownerKey = `${target.platform ?? target.distribution}:${scope}:${target.skillKey}`;
|
|
1659
|
+
if (seenSkillOwners.has(ownerKey)) {
|
|
1660
|
+
throw new Error(`Invalid ${fieldName}.targets[${index}]: duplicate dynamic skill owner "${ownerKey}".`);
|
|
1661
|
+
}
|
|
1662
|
+
seenSkillOwners.add(ownerKey);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return target;
|
|
1666
|
+
});
|
|
1667
|
+
})();
|
|
1668
|
+
return { targets };
|
|
1669
|
+
}
|
|
1670
|
+
function normalizePlatformAgentOverrides(value, fieldName, normalizer) {
|
|
1671
|
+
const input = assertObject(value, fieldName);
|
|
1672
|
+
assertAllowedKeys(input, fieldName, ['claude', 'codex']);
|
|
1673
|
+
const output = {};
|
|
1674
|
+
for (const platform of ['claude', 'codex']) {
|
|
1675
|
+
const entry = input[platform];
|
|
1676
|
+
if (entry === undefined) {
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (entry === null) {
|
|
1680
|
+
output[platform] = null;
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
output[platform] = omitUndefinedValues(normalizer(assertObject(entry, `${fieldName}.${platform}`), `${fieldName}.${platform}`));
|
|
1684
|
+
}
|
|
1685
|
+
return output;
|
|
1686
|
+
}
|
|
1687
|
+
function normalizeInstructionEntry(entry, fieldName) {
|
|
1688
|
+
const input = assertObject(entry, fieldName);
|
|
1689
|
+
assertAllowedKeys(input, fieldName, ['name', 'title', 'content', 'paths', 'agentOverrides']);
|
|
1690
|
+
return {
|
|
1691
|
+
name: ensureString(input.name, `${fieldName}.name`),
|
|
1692
|
+
title: ensureString(input.title, `${fieldName}.title`),
|
|
1693
|
+
content: ensureString(input.content, `${fieldName}.content`),
|
|
1694
|
+
paths: input.paths === undefined
|
|
1695
|
+
? undefined
|
|
1696
|
+
: ensureStringArray(input.paths, `${fieldName}.paths`),
|
|
1697
|
+
agentOverrides: input.agentOverrides === undefined
|
|
1698
|
+
? undefined
|
|
1699
|
+
: normalizePlatformAgentOverrides(input.agentOverrides, `${fieldName}.agentOverrides`, (overrideInput, overrideFieldName) => {
|
|
1700
|
+
assertAllowedKeys(overrideInput, overrideFieldName, ['name', 'title', 'content', 'paths']);
|
|
1701
|
+
return {
|
|
1702
|
+
name: overrideInput.name === undefined
|
|
1703
|
+
? undefined
|
|
1704
|
+
: ensureString(overrideInput.name, `${overrideFieldName}.name`),
|
|
1705
|
+
title: overrideInput.title === undefined
|
|
1706
|
+
? undefined
|
|
1707
|
+
: ensureString(overrideInput.title, `${overrideFieldName}.title`),
|
|
1708
|
+
content: overrideInput.content === undefined
|
|
1709
|
+
? undefined
|
|
1710
|
+
: ensureString(overrideInput.content, `${overrideFieldName}.content`),
|
|
1711
|
+
paths: overrideInput.paths === undefined
|
|
1712
|
+
? undefined
|
|
1713
|
+
: ensureStringArray(overrideInput.paths, `${overrideFieldName}.paths`),
|
|
1714
|
+
};
|
|
1715
|
+
}),
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function normalizePermissionEntry(entry, fieldName) {
|
|
1719
|
+
const input = assertObject(entry, fieldName);
|
|
1720
|
+
assertAllowedKeys(input, fieldName, ['name', 'allow', 'ask', 'deny', 'agentOverrides']);
|
|
1721
|
+
return {
|
|
1722
|
+
name: ensureString(input.name, `${fieldName}.name`),
|
|
1723
|
+
allow: normalizePermissionCommandArray(input.allow, `${fieldName}.allow`),
|
|
1724
|
+
ask: normalizePermissionCommandArray(input.ask, `${fieldName}.ask`),
|
|
1725
|
+
deny: normalizePermissionCommandArray(input.deny, `${fieldName}.deny`),
|
|
1726
|
+
agentOverrides: input.agentOverrides === undefined
|
|
1727
|
+
? undefined
|
|
1728
|
+
: normalizePlatformAgentOverrides(input.agentOverrides, `${fieldName}.agentOverrides`, (overrideInput, overrideFieldName) => {
|
|
1729
|
+
assertAllowedKeys(overrideInput, overrideFieldName, ['name', 'allow', 'ask', 'deny']);
|
|
1730
|
+
return {
|
|
1731
|
+
name: overrideInput.name === undefined
|
|
1732
|
+
? undefined
|
|
1733
|
+
: ensureString(overrideInput.name, `${overrideFieldName}.name`),
|
|
1734
|
+
allow: normalizePermissionCommandArray(overrideInput.allow, `${overrideFieldName}.allow`),
|
|
1735
|
+
ask: normalizePermissionCommandArray(overrideInput.ask, `${overrideFieldName}.ask`),
|
|
1736
|
+
deny: normalizePermissionCommandArray(overrideInput.deny, `${overrideFieldName}.deny`),
|
|
1737
|
+
};
|
|
1738
|
+
}),
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function normalizeHookEntry(entry, fieldName) {
|
|
1742
|
+
const input = assertObject(entry, fieldName);
|
|
1743
|
+
assertAllowedKeys(input, fieldName, ['event', 'matcher', 'command', 'agentOverrides']);
|
|
1744
|
+
const normalized = {
|
|
1745
|
+
event: assertPublicHookEvent(input.event, `${fieldName}.event`),
|
|
1746
|
+
matcher: input.matcher === undefined
|
|
1747
|
+
? undefined
|
|
1748
|
+
: ensureString(input.matcher, `${fieldName}.matcher`),
|
|
1749
|
+
command: ensureString(input.command, `${fieldName}.command`),
|
|
1750
|
+
agentOverrides: input.agentOverrides === undefined
|
|
1751
|
+
? undefined
|
|
1752
|
+
: normalizePlatformAgentOverrides(input.agentOverrides, `${fieldName}.agentOverrides`, (overrideInput, overrideFieldName) => {
|
|
1753
|
+
assertAllowedKeys(overrideInput, overrideFieldName, ['event', 'matcher', 'command']);
|
|
1754
|
+
return {
|
|
1755
|
+
event: overrideInput.event === undefined
|
|
1756
|
+
? undefined
|
|
1757
|
+
: assertPublicHookEvent(overrideInput.event, `${overrideFieldName}.event`),
|
|
1758
|
+
matcher: overrideInput.matcher === undefined
|
|
1759
|
+
? undefined
|
|
1760
|
+
: ensureString(overrideInput.matcher, `${overrideFieldName}.matcher`),
|
|
1761
|
+
command: overrideInput.command === undefined
|
|
1762
|
+
? undefined
|
|
1763
|
+
: ensureString(overrideInput.command, `${overrideFieldName}.command`),
|
|
1764
|
+
};
|
|
1765
|
+
}),
|
|
1766
|
+
};
|
|
1767
|
+
assertHookMatcherAllowed({
|
|
1768
|
+
kind: 'public',
|
|
1769
|
+
event: normalized.event,
|
|
1770
|
+
matcher: normalized.matcher,
|
|
1771
|
+
fieldName: `${fieldName}.matcher`,
|
|
1772
|
+
});
|
|
1773
|
+
for (const platform of ['claude', 'codex']) {
|
|
1774
|
+
const override = normalized.agentOverrides?.[platform];
|
|
1775
|
+
if (!override || override === null) {
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
assertHookMatcherAllowed({
|
|
1779
|
+
kind: 'public',
|
|
1780
|
+
event: override.event ?? normalized.event,
|
|
1781
|
+
matcher: override.matcher ?? normalized.matcher,
|
|
1782
|
+
fieldName: `${fieldName}.agentOverrides.${platform}.matcher`,
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
return normalized;
|
|
1786
|
+
}
|
|
1787
|
+
function normalizeNativeHookEntry(entry, platform, fieldName) {
|
|
1788
|
+
const input = assertObject(entry, fieldName);
|
|
1789
|
+
assertAllowedKeys(input, fieldName, ['event', 'matcher', 'command']);
|
|
1790
|
+
const event = assertNativeHookEvent(ensureString(input.event, `${fieldName}.event`), platform, `${fieldName}.event`);
|
|
1791
|
+
const matcher = input.matcher === undefined
|
|
1792
|
+
? undefined
|
|
1793
|
+
: ensureString(input.matcher, `${fieldName}.matcher`);
|
|
1794
|
+
assertHookMatcherAllowed({
|
|
1795
|
+
kind: 'native',
|
|
1796
|
+
platform,
|
|
1797
|
+
event,
|
|
1798
|
+
matcher,
|
|
1799
|
+
fieldName: `${fieldName}.matcher`,
|
|
1800
|
+
});
|
|
1801
|
+
return {
|
|
1802
|
+
event,
|
|
1803
|
+
matcher,
|
|
1804
|
+
command: ensureString(input.command, `${fieldName}.command`),
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
function normalizeNativeHooks(value, fieldName) {
|
|
1808
|
+
const input = assertObject(value, fieldName);
|
|
1809
|
+
assertAllowedKeys(input, fieldName, ['claude', 'codex']);
|
|
1810
|
+
const output = {};
|
|
1811
|
+
for (const platform of ['claude', 'codex']) {
|
|
1812
|
+
const entries = input[platform];
|
|
1813
|
+
if (entries === undefined) {
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
if (!Array.isArray(entries)) {
|
|
1817
|
+
throw new Error(`Invalid ${fieldName}.${platform}: expected an array.`);
|
|
1818
|
+
}
|
|
1819
|
+
output[platform] = entries.map((entry, index) => normalizeNativeHookEntry(entry, platform, `${fieldName}.${platform}[${index}]`));
|
|
1820
|
+
}
|
|
1821
|
+
return output;
|
|
1822
|
+
}
|
|
1823
|
+
function assertNoDuplicatePlatformHooks(extensions, fieldName) {
|
|
1824
|
+
for (const platform of ['claude', 'codex']) {
|
|
1825
|
+
const seen = new Set();
|
|
1826
|
+
for (const definition of resolvePlatformHookDefinitions({ extensions }, platform)) {
|
|
1827
|
+
const group = renderHookDefinitionGroup(definition, platform);
|
|
1828
|
+
const identity = normalizeRenderedHookGroupIdentity({
|
|
1829
|
+
platform,
|
|
1830
|
+
event: group.event,
|
|
1831
|
+
matcher: group.matcher,
|
|
1832
|
+
command: group.hooks[0].command,
|
|
1833
|
+
});
|
|
1834
|
+
const key = JSON.stringify([
|
|
1835
|
+
identity.event,
|
|
1836
|
+
identity.matcher ?? null,
|
|
1837
|
+
identity.command,
|
|
1838
|
+
]);
|
|
1839
|
+
if (seen.has(key)) {
|
|
1840
|
+
throw new Error(`Invalid ${fieldName}: duplicate ${platform} hook for event "${identity.event}" and command "${identity.command}".`);
|
|
1841
|
+
}
|
|
1842
|
+
seen.add(key);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
function normalizeAgentEntry(entry, fieldName) {
|
|
1847
|
+
const input = assertObject(entry, fieldName);
|
|
1848
|
+
assertAllowedKeys(input, fieldName, ['name', 'description', 'prompt', 'tools', 'agentOverrides']);
|
|
1849
|
+
return {
|
|
1850
|
+
name: ensureString(input.name, `${fieldName}.name`),
|
|
1851
|
+
description: ensureString(input.description, `${fieldName}.description`),
|
|
1852
|
+
prompt: ensureString(input.prompt, `${fieldName}.prompt`),
|
|
1853
|
+
tools: input.tools === undefined
|
|
1854
|
+
? undefined
|
|
1855
|
+
: ensureStringArray(input.tools, `${fieldName}.tools`),
|
|
1856
|
+
agentOverrides: input.agentOverrides === undefined
|
|
1857
|
+
? undefined
|
|
1858
|
+
: normalizePlatformAgentOverrides(input.agentOverrides, `${fieldName}.agentOverrides`, (overrideInput, overrideFieldName) => {
|
|
1859
|
+
assertAllowedKeys(overrideInput, overrideFieldName, [
|
|
1860
|
+
'name',
|
|
1861
|
+
'description',
|
|
1862
|
+
'prompt',
|
|
1863
|
+
'tools',
|
|
1864
|
+
]);
|
|
1865
|
+
return {
|
|
1866
|
+
name: overrideInput.name === undefined
|
|
1867
|
+
? undefined
|
|
1868
|
+
: ensureString(overrideInput.name, `${overrideFieldName}.name`),
|
|
1869
|
+
description: overrideInput.description === undefined
|
|
1870
|
+
? undefined
|
|
1871
|
+
: ensureString(overrideInput.description, `${overrideFieldName}.description`),
|
|
1872
|
+
prompt: overrideInput.prompt === undefined
|
|
1873
|
+
? undefined
|
|
1874
|
+
: ensureString(overrideInput.prompt, `${overrideFieldName}.prompt`),
|
|
1875
|
+
tools: overrideInput.tools === undefined
|
|
1876
|
+
? undefined
|
|
1877
|
+
: ensureStringArray(overrideInput.tools, `${overrideFieldName}.tools`),
|
|
1878
|
+
};
|
|
1879
|
+
}),
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
function normalizeExtensions(value, fieldName) {
|
|
1883
|
+
const input = assertObject(value, fieldName);
|
|
1884
|
+
assertAllowedKeys(input, fieldName, ['instructions', 'permissions', 'hooks', 'nativeHooks', 'agents']);
|
|
1885
|
+
const extensions = {
|
|
1886
|
+
instructions: Array.isArray(input.instructions)
|
|
1887
|
+
? input.instructions.map((entry, index) => normalizeInstructionEntry(entry, `${fieldName}.instructions[${index}]`))
|
|
1888
|
+
: [],
|
|
1889
|
+
permissions: Array.isArray(input.permissions)
|
|
1890
|
+
? input.permissions.map((entry, index) => normalizePermissionEntry(entry, `${fieldName}.permissions[${index}]`))
|
|
1891
|
+
: [],
|
|
1892
|
+
hooks: Array.isArray(input.hooks)
|
|
1893
|
+
? input.hooks.map((entry, index) => normalizeHookEntry(entry, `${fieldName}.hooks[${index}]`))
|
|
1894
|
+
: [],
|
|
1895
|
+
nativeHooks: input.nativeHooks === undefined
|
|
1896
|
+
? undefined
|
|
1897
|
+
: normalizeNativeHooks(input.nativeHooks, `${fieldName}.nativeHooks`),
|
|
1898
|
+
agents: Array.isArray(input.agents)
|
|
1899
|
+
? input.agents.map((entry, index) => normalizeAgentEntry(entry, `${fieldName}.agents[${index}]`))
|
|
1900
|
+
: [],
|
|
1901
|
+
};
|
|
1902
|
+
assertNoDuplicatePlatformHooks(extensions, fieldName);
|
|
1903
|
+
return extensions;
|
|
1904
|
+
}
|
|
1905
|
+
export function validateMarketplaceSourceMeta(rawValue) {
|
|
1906
|
+
const input = assertObject(rawValue, 'marketplace source metadata');
|
|
1907
|
+
assertAllowedKeys(input, 'marketplace source metadata', ['owner', 'interface', 'catalog']);
|
|
1908
|
+
const ownerInput = assertObject(input.owner, 'marketplace source metadata.owner');
|
|
1909
|
+
assertAllowedKeys(ownerInput, 'marketplace source metadata.owner', ['name', 'url']);
|
|
1910
|
+
const interfaceInput = assertObject(input.interface, 'marketplace source metadata.interface');
|
|
1911
|
+
assertAllowedKeys(interfaceInput, 'marketplace source metadata.interface', ['displayName']);
|
|
1912
|
+
const catalogInput = input.catalog === undefined
|
|
1913
|
+
? null
|
|
1914
|
+
: assertObject(input.catalog, 'marketplace source metadata.catalog');
|
|
1915
|
+
if (catalogInput)
|
|
1916
|
+
assertAllowedKeys(catalogInput, 'marketplace source metadata.catalog', ['version']);
|
|
1917
|
+
return {
|
|
1918
|
+
owner: {
|
|
1919
|
+
name: ensureString(ownerInput.name, 'marketplace source metadata.owner.name'),
|
|
1920
|
+
url: ensureString(ownerInput.url, 'marketplace source metadata.owner.url'),
|
|
1921
|
+
},
|
|
1922
|
+
interface: {
|
|
1923
|
+
displayName: ensureString(interfaceInput.displayName, 'marketplace source metadata.interface.displayName'),
|
|
1924
|
+
},
|
|
1925
|
+
catalog: {
|
|
1926
|
+
version: catalogInput === null
|
|
1927
|
+
? '0.1.0'
|
|
1928
|
+
: assertCatalogVersion(catalogInput.version, 'marketplace source metadata.catalog.version'),
|
|
1929
|
+
},
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
export function normalizeCatalogMarketplaceIdentity(rawValue, fieldName = 'catalog manifest marketplace') {
|
|
1933
|
+
const input = assertObject(rawValue, fieldName);
|
|
1934
|
+
assertAllowedKeys(input, fieldName, ['owner', 'interface']);
|
|
1935
|
+
const normalized = validateMarketplaceSourceMeta({
|
|
1936
|
+
owner: input.owner,
|
|
1937
|
+
interface: input.interface,
|
|
1938
|
+
catalog: { version: '0.1.0' },
|
|
1939
|
+
});
|
|
1940
|
+
return { owner: normalized.owner, interface: normalized.interface };
|
|
1941
|
+
}
|
|
1942
|
+
export async function readMarketplaceSourceMeta(repoRoot) {
|
|
1943
|
+
const sourcePath = path.join(repoRoot, MARKETPLACE_SOURCE_PATH);
|
|
1944
|
+
const rawValue = await readJsonFile(sourcePath);
|
|
1945
|
+
return validateMarketplaceSourceMeta(rawValue);
|
|
1946
|
+
}
|
|
1947
|
+
function hasDeclaredNativePluginCapability(manifest) {
|
|
1948
|
+
return Boolean((manifest.extensions?.instructions?.length ?? 0) > 0
|
|
1949
|
+
|| (manifest.extensions?.permissions?.length ?? 0) > 0
|
|
1950
|
+
|| (manifest.extensions?.hooks?.length ?? 0) > 0
|
|
1951
|
+
|| (manifest.extensions?.nativeHooks?.claude?.length ?? 0) > 0
|
|
1952
|
+
|| (manifest.extensions?.nativeHooks?.codex?.length ?? 0) > 0
|
|
1953
|
+
|| (manifest.extensions?.agents?.length ?? 0) > 0
|
|
1954
|
+
|| (manifest.artifacts?.targets ?? []).some((target) => target.platform !== undefined && target.states.includes('present')));
|
|
1955
|
+
}
|
|
1956
|
+
async function directoryContainsRegularFile(root, fileName, exact = false) {
|
|
1957
|
+
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
1958
|
+
for (const entry of entries) {
|
|
1959
|
+
if (entry.isSymbolicLink())
|
|
1960
|
+
continue;
|
|
1961
|
+
const entryPath = path.join(root, entry.name);
|
|
1962
|
+
if (entry.isFile() && (!fileName || (exact ? entry.name === fileName : entry.name.endsWith(fileName)))) {
|
|
1963
|
+
return true;
|
|
1964
|
+
}
|
|
1965
|
+
if (entry.isDirectory() && await directoryContainsRegularFile(entryPath, fileName, exact))
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
return false;
|
|
1969
|
+
}
|
|
1970
|
+
async function jsonAssetHasEntries(pluginRoot, relativePath, key) {
|
|
1971
|
+
if (!relativePath)
|
|
1972
|
+
return false;
|
|
1973
|
+
const content = await fs.readJson(path.join(pluginRoot, stripDotSlash(relativePath))).catch(() => undefined);
|
|
1974
|
+
if (!content || typeof content !== 'object' || Array.isArray(content))
|
|
1975
|
+
return false;
|
|
1976
|
+
const entries = content[key];
|
|
1977
|
+
return Boolean(entries && typeof entries === 'object' && !Array.isArray(entries)
|
|
1978
|
+
&& Object.keys(entries).length > 0);
|
|
1979
|
+
}
|
|
1980
|
+
export async function assertActualNativePluginCapability(pluginRoot, manifest, mode = 'source') {
|
|
1981
|
+
if (!manifest.distributions?.agentSkills || hasDeclaredNativePluginCapability(manifest))
|
|
1982
|
+
return;
|
|
1983
|
+
const skills = manifest.skills
|
|
1984
|
+
? await directoryContainsRegularFile(path.join(pluginRoot, stripDotSlash(manifest.skills)), 'SKILL.md', true)
|
|
1985
|
+
: false;
|
|
1986
|
+
const slashCommands = manifest.slashCommands
|
|
1987
|
+
? await directoryContainsRegularFile(path.join(pluginRoot, stripDotSlash(manifest.slashCommands)), '.md')
|
|
1988
|
+
: false;
|
|
1989
|
+
const [mcpServers, apps] = await Promise.all([
|
|
1990
|
+
jsonAssetHasEntries(pluginRoot, manifest.mcpServers, 'mcpServers'),
|
|
1991
|
+
jsonAssetHasEntries(pluginRoot, manifest.apps, 'apps'),
|
|
1992
|
+
]);
|
|
1993
|
+
let bundled = false;
|
|
1994
|
+
if (mode === 'bundle' && !skills && !slashCommands && !mcpServers && !apps) {
|
|
1995
|
+
for (const platform of ['claude', 'codex']) {
|
|
1996
|
+
const platformManifest = await fs.readJson(path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json')).catch(() => undefined);
|
|
1997
|
+
if (!platformManifest)
|
|
1998
|
+
continue;
|
|
1999
|
+
const platformSkills = typeof platformManifest.skills === 'string'
|
|
2000
|
+
&& await directoryContainsRegularFile(path.join(pluginRoot, stripDotSlash(platformManifest.skills)), 'SKILL.md', true);
|
|
2001
|
+
const commands = typeof platformManifest.commands === 'string'
|
|
2002
|
+
&& await directoryContainsRegularFile(path.join(pluginRoot, stripDotSlash(platformManifest.commands)), '.md');
|
|
2003
|
+
bundled = Boolean(platformSkills || commands
|
|
2004
|
+
|| (Array.isArray(platformManifest.agents) && platformManifest.agents.length > 0)
|
|
2005
|
+
|| (platformManifest.hooks && typeof platformManifest.hooks === 'object'
|
|
2006
|
+
&& Object.keys(platformManifest.hooks).length > 0)
|
|
2007
|
+
|| await jsonAssetHasEntries(pluginRoot, typeof platformManifest.mcpServers === 'string' ? platformManifest.mcpServers : undefined, 'mcpServers')
|
|
2008
|
+
|| await jsonAssetHasEntries(pluginRoot, typeof platformManifest.apps === 'string' ? platformManifest.apps : undefined, 'apps'));
|
|
2009
|
+
if (bundled)
|
|
2010
|
+
break;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
if (!skills && !slashCommands && !mcpServers && !apps && !bundled) {
|
|
2014
|
+
throw new Error('Invalid plugin manifest: portable-only plugins are not supported in V1. Declare at least one non-empty native capability.');
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
export function validatePluginManifest(pluginRoot, rawManifest, options = {}) {
|
|
2018
|
+
const input = assertObject(normalizePluginManifestSchemaVersion(rawManifest), 'plugin manifest');
|
|
2019
|
+
if (Object.hasOwn(input, 'commands')) {
|
|
2020
|
+
throw new Error('Invalid plugin manifest: top-level "commands" has been removed; use "slashCommands" instead.');
|
|
2021
|
+
}
|
|
2022
|
+
assertAllowedKeys(input, 'plugin manifest', [
|
|
2023
|
+
'schemaVersion',
|
|
2024
|
+
'name',
|
|
2025
|
+
'version',
|
|
2026
|
+
'description',
|
|
2027
|
+
'dependencies',
|
|
2028
|
+
'compatibility',
|
|
2029
|
+
'skills',
|
|
2030
|
+
'slashCommands',
|
|
2031
|
+
'mcpServers',
|
|
2032
|
+
'apps',
|
|
2033
|
+
'distributions',
|
|
2034
|
+
'marketplace',
|
|
2035
|
+
'interface',
|
|
2036
|
+
'extensions',
|
|
2037
|
+
'artifacts',
|
|
2038
|
+
'buildConfig',
|
|
2039
|
+
'scripts',
|
|
2040
|
+
'config',
|
|
2041
|
+
]);
|
|
2042
|
+
const manifest = {
|
|
2043
|
+
schemaVersion: PLUGIN_MANIFEST_SCHEMA_VERSION,
|
|
2044
|
+
name: ensureString(input.name, 'name'),
|
|
2045
|
+
version: assertStrictSemVer(input.version, 'plugin manifest.version'),
|
|
2046
|
+
description: ensureString(input.description, 'description'),
|
|
2047
|
+
compatibility: normalizePluginCompatibility(input.compatibility, 'compatibility', {
|
|
2048
|
+
allowMissing: (options.compatibilityMode ?? 'defaulted') === 'defaulted',
|
|
2049
|
+
defaultMinVersion: options.defaultMinVersion,
|
|
2050
|
+
}),
|
|
2051
|
+
marketplace: normalizeMarketplaceMeta(input.marketplace, 'marketplace'),
|
|
2052
|
+
};
|
|
2053
|
+
if (input.dependencies !== undefined) {
|
|
2054
|
+
const dependencies = ensureStringArray(input.dependencies, 'dependencies');
|
|
2055
|
+
if (dependencies.length > 64) {
|
|
2056
|
+
throw new Error('Invalid dependencies: expected at most 64 direct plugin dependencies.');
|
|
2057
|
+
}
|
|
2058
|
+
const seen = new Set();
|
|
2059
|
+
const normalizedDependencies = dependencies.map((dependency, index) => {
|
|
2060
|
+
const name = ensurePluginName(dependency, `dependencies[${index}]`);
|
|
2061
|
+
if (name === manifest.name) {
|
|
2062
|
+
throw new Error(`Invalid dependencies[${index}]: plugin "${manifest.name}" cannot depend on itself.`);
|
|
2063
|
+
}
|
|
2064
|
+
if (seen.has(name)) {
|
|
2065
|
+
throw new Error(`Invalid dependencies: duplicate plugin "${name}".`);
|
|
2066
|
+
}
|
|
2067
|
+
seen.add(name);
|
|
2068
|
+
return name;
|
|
2069
|
+
});
|
|
2070
|
+
if (normalizedDependencies.length > 0) {
|
|
2071
|
+
manifest.dependencies = normalizedDependencies;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
if (input.skills !== undefined) {
|
|
2075
|
+
manifest.skills = normalizePluginRelativePath(pluginRoot, ensureString(input.skills, 'skills'), 'skills');
|
|
2076
|
+
if (stripDotSlash(manifest.skills) !== 'skills') {
|
|
2077
|
+
throw new Error('Invalid plugin manifest: skills must be "./skills".');
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
if (input.slashCommands !== undefined) {
|
|
2081
|
+
manifest.slashCommands = normalizePluginRelativePath(pluginRoot, ensureString(input.slashCommands, 'slashCommands'), 'slashCommands');
|
|
2082
|
+
}
|
|
2083
|
+
if (input.mcpServers !== undefined) {
|
|
2084
|
+
manifest.mcpServers = normalizePluginRelativePath(pluginRoot, ensureString(input.mcpServers, 'mcpServers'), 'mcpServers');
|
|
2085
|
+
}
|
|
2086
|
+
if (input.apps !== undefined) {
|
|
2087
|
+
manifest.apps = normalizePluginRelativePath(pluginRoot, ensureString(input.apps, 'apps'), 'apps');
|
|
2088
|
+
}
|
|
2089
|
+
if (input.distributions !== undefined) {
|
|
2090
|
+
const distributions = assertObject(input.distributions, 'distributions');
|
|
2091
|
+
assertAllowedKeys(distributions, 'distributions', ['agentSkills']);
|
|
2092
|
+
if (distributions.agentSkills !== undefined) {
|
|
2093
|
+
const agentSkills = assertObject(distributions.agentSkills, 'distributions.agentSkills');
|
|
2094
|
+
assertAllowedKeys(agentSkills, 'distributions.agentSkills', ['skills']);
|
|
2095
|
+
manifest.distributions = {
|
|
2096
|
+
agentSkills: {
|
|
2097
|
+
skills: agentSkills.skills === undefined
|
|
2098
|
+
? undefined
|
|
2099
|
+
: normalizePluginRelativePath(pluginRoot, ensureString(agentSkills.skills, 'distributions.agentSkills.skills'), 'distributions.agentSkills.skills'),
|
|
2100
|
+
},
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
else {
|
|
2104
|
+
manifest.distributions = {};
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
if (input.interface !== undefined) {
|
|
2108
|
+
manifest.interface = normalizePluginInterface(input.interface, 'interface');
|
|
2109
|
+
}
|
|
2110
|
+
if (input.extensions !== undefined) {
|
|
2111
|
+
manifest.extensions = normalizeExtensions(input.extensions, 'extensions');
|
|
2112
|
+
}
|
|
2113
|
+
if (input.buildConfig !== undefined) {
|
|
2114
|
+
manifest.buildConfig = normalizeBuildConfig(pluginRoot, input.buildConfig, 'buildConfig', options.reservedImports);
|
|
2115
|
+
}
|
|
2116
|
+
if (input.scripts !== undefined) {
|
|
2117
|
+
manifest.scripts = normalizePluginScripts(pluginRoot, input.scripts, 'scripts');
|
|
2118
|
+
}
|
|
2119
|
+
if (manifest.buildConfig?.cacheable && !manifest.scripts?.node?.build) {
|
|
2120
|
+
throw new Error('Invalid buildConfig.cacheable: true requires scripts.node.build.');
|
|
2121
|
+
}
|
|
2122
|
+
if (input.config !== undefined) {
|
|
2123
|
+
manifest.config = normalizePluginConfigSchema(input.config, 'config');
|
|
2124
|
+
}
|
|
2125
|
+
if (input.artifacts !== undefined) {
|
|
2126
|
+
manifest.artifacts = normalizeArtifacts(pluginRoot, input.artifacts, 'artifacts', manifest.config, options.reservedImports);
|
|
2127
|
+
}
|
|
2128
|
+
if ((manifest.artifacts?.targets ?? []).some((target) => target.distribution === 'agent-skills')
|
|
2129
|
+
&& !manifest.distributions?.agentSkills) {
|
|
2130
|
+
throw new Error('Invalid artifacts.targets: agent-skills targets require distributions.agentSkills declaration.');
|
|
2131
|
+
}
|
|
2132
|
+
assertManifestPlatformSupport(manifest.name, manifest);
|
|
2133
|
+
return manifest;
|
|
2134
|
+
}
|
|
2135
|
+
async function validateDeclaredNativeAssets(pluginRoot, manifest) {
|
|
2136
|
+
const declaredAssets = [
|
|
2137
|
+
{ field: 'skills', defaultPath: 'skills' },
|
|
2138
|
+
{ field: 'slashCommands', defaultPath: stripDotSlash(DEFAULT_SLASH_COMMANDS_PATH) },
|
|
2139
|
+
{ field: 'mcpServers', defaultPath: '.mcp.json' },
|
|
2140
|
+
{ field: 'apps', defaultPath: '.app.json' },
|
|
2141
|
+
];
|
|
2142
|
+
await Promise.all(declaredAssets.map(async ({ field, defaultPath }) => {
|
|
2143
|
+
if (manifest[field]) {
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
if (await fs.pathExists(path.join(pluginRoot, defaultPath))) {
|
|
2147
|
+
if (field === 'skills') {
|
|
2148
|
+
const entries = await fs.readdir(path.join(pluginRoot, defaultPath), {
|
|
2149
|
+
withFileTypes: true,
|
|
2150
|
+
});
|
|
2151
|
+
const dynamicSkills = new Set((manifest.artifacts?.targets ?? [])
|
|
2152
|
+
.filter((target) => (target.target === 'codex.skill' || target.target === 'claude.skill') &&
|
|
2153
|
+
target.skillKey &&
|
|
2154
|
+
(target.sourceDirs?.length ?? 0) > 0 &&
|
|
2155
|
+
(target.sourceDirs ?? []).every((sourceDir) => stripDotSlash(sourceDir) === `skills/${target.skillKey}` ||
|
|
2156
|
+
stripDotSlash(sourceDir).startsWith(`skills/${target.skillKey}/`)))
|
|
2157
|
+
.map((target) => target.skillKey));
|
|
2158
|
+
if (entries.length > 0 &&
|
|
2159
|
+
entries.every((entry) => entry.isDirectory() && dynamicSkills.has(entry.name))) {
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
throw new Error(`Invalid plugin manifest: found ${defaultPath} but missing "${field}" declaration.`);
|
|
2164
|
+
}
|
|
2165
|
+
}));
|
|
2166
|
+
}
|
|
2167
|
+
async function validateDeclaredAssetPathsExist(pluginRoot, manifest) {
|
|
2168
|
+
const declaredAssets = ['skills', 'slashCommands', 'mcpServers', 'apps'];
|
|
2169
|
+
await Promise.all(declaredAssets.map(async (field) => {
|
|
2170
|
+
const relativePath = manifest[field];
|
|
2171
|
+
if (!relativePath) {
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const absolutePath = path.join(pluginRoot, stripDotSlash(relativePath));
|
|
2175
|
+
if (!(await fs.pathExists(absolutePath))) {
|
|
2176
|
+
throw new Error(`Invalid plugin manifest: ${field} points to "${relativePath}" but path does not exist.`);
|
|
2177
|
+
}
|
|
2178
|
+
}));
|
|
2179
|
+
const portableSkills = manifest.distributions?.agentSkills?.skills;
|
|
2180
|
+
if (portableSkills) {
|
|
2181
|
+
const absolutePath = path.join(pluginRoot, stripDotSlash(portableSkills));
|
|
2182
|
+
const stats = await fs.lstat(absolutePath).catch(() => undefined);
|
|
2183
|
+
if (!stats?.isDirectory() || stats.isSymbolicLink()) {
|
|
2184
|
+
throw new Error(`Invalid plugin manifest: distributions.agentSkills.skills points to "${portableSkills}" but it is not a regular directory.`);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
await Promise.all((manifest.artifacts?.targets ?? []).flatMap((target, targetIndex) => (target.sourceDirs ?? []).map(async (sourceDir, sourceIndex) => {
|
|
2188
|
+
const absolutePath = path.join(pluginRoot, stripDotSlash(sourceDir));
|
|
2189
|
+
const stats = await fs.lstat(absolutePath).catch(() => null);
|
|
2190
|
+
if (!stats?.isDirectory() || stats.isSymbolicLink()) {
|
|
2191
|
+
throw new Error(`Invalid artifacts.targets[${targetIndex}].sourceDirs[${sourceIndex}]: ` +
|
|
2192
|
+
`expected a directory at "${sourceDir}".`);
|
|
2193
|
+
}
|
|
2194
|
+
})));
|
|
2195
|
+
}
|
|
2196
|
+
async function validateSourceSkillInventory(pluginRoot, manifest) {
|
|
2197
|
+
const skillsRoot = manifest.skills ? path.join(pluginRoot, stripDotSlash(manifest.skills)) : null;
|
|
2198
|
+
const skillEntries = skillsRoot
|
|
2199
|
+
? await fs.readdir(skillsRoot, { withFileTypes: true }).catch(() => [])
|
|
2200
|
+
: [];
|
|
2201
|
+
const staticSkillNames = skillEntries
|
|
2202
|
+
.filter((entry) => entry.isDirectory())
|
|
2203
|
+
.map((entry) => entry.name)
|
|
2204
|
+
.sort();
|
|
2205
|
+
const staticSkillNameSet = new Set(staticSkillNames);
|
|
2206
|
+
const dynamicSkillNames = (manifest.artifacts?.targets ?? [])
|
|
2207
|
+
.filter((target) => target.target === 'codex.skill' || target.target === 'claude.skill')
|
|
2208
|
+
.map((target) => target.skillKey?.trim())
|
|
2209
|
+
.filter((skillKey) => Boolean(skillKey))
|
|
2210
|
+
.sort();
|
|
2211
|
+
const dynamicSkillNameSet = new Set(dynamicSkillNames);
|
|
2212
|
+
const portableSkillsRoot = manifest.distributions?.agentSkills?.skills
|
|
2213
|
+
? path.join(pluginRoot, stripDotSlash(manifest.distributions.agentSkills.skills))
|
|
2214
|
+
: null;
|
|
2215
|
+
const portableSkillEntries = portableSkillsRoot
|
|
2216
|
+
? await fs.readdir(portableSkillsRoot, { withFileTypes: true }).catch(() => [])
|
|
2217
|
+
: [];
|
|
2218
|
+
const portableSkillNameSet = new Set(portableSkillEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
|
|
2219
|
+
for (const [index, entry] of (manifest.buildConfig?.skillRuntimeFiles ?? []).entries()) {
|
|
2220
|
+
const nativeSelected = entry.distributions === undefined;
|
|
2221
|
+
const portableSelected = entry.distributions?.includes('agent-skills') ?? false;
|
|
2222
|
+
if (nativeSelected && (!manifest.skills || !staticSkillNameSet.has(entry.skill))) {
|
|
2223
|
+
throw new Error(`Invalid buildConfig.skillRuntimeFiles[${index}].skill: "${entry.skill}" is not a declared static Skill key.`);
|
|
2224
|
+
}
|
|
2225
|
+
if (portableSelected && !portableSkillNameSet.has(entry.skill)) {
|
|
2226
|
+
throw new Error(`Invalid buildConfig.skillRuntimeFiles[${index}].skill: "${entry.skill}" is not a declared portable Skill key.`);
|
|
2227
|
+
}
|
|
2228
|
+
if (nativeSelected && dynamicSkillNameSet.has(entry.skill)) {
|
|
2229
|
+
throw new Error(`Invalid buildConfig.skillRuntimeFiles[${index}].skill: dynamic Skill targets are not supported.`);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
for (const [index, entry] of (manifest.buildConfig?.generatedSkillRuntimeFiles ?? []).entries()) {
|
|
2233
|
+
const nativeSelected = entry.distributions === undefined;
|
|
2234
|
+
const portableSelected = entry.distributions?.includes('agent-skills') ?? false;
|
|
2235
|
+
if (nativeSelected && (!manifest.skills || !staticSkillNameSet.has(entry.skill))) {
|
|
2236
|
+
throw new Error(`Invalid buildConfig.generatedSkillRuntimeFiles[${index}].skill: "${entry.skill}" is not a declared static Skill key.`);
|
|
2237
|
+
}
|
|
2238
|
+
if (portableSelected && !portableSkillNameSet.has(entry.skill)) {
|
|
2239
|
+
throw new Error(`Invalid buildConfig.generatedSkillRuntimeFiles[${index}].skill: "${entry.skill}" is not a declared portable Skill key.`);
|
|
2240
|
+
}
|
|
2241
|
+
if (nativeSelected && dynamicSkillNameSet.has(entry.skill)) {
|
|
2242
|
+
throw new Error(`Invalid buildConfig.generatedSkillRuntimeFiles[${index}].skill: dynamic Skill targets are not supported.`);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (!manifest.slashCommands) {
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const slashCommands = await resolveSlashCommands(pluginRoot, manifest, {
|
|
2249
|
+
targetPlatforms: resolveSupportedPlatforms(manifest),
|
|
2250
|
+
});
|
|
2251
|
+
for (const command of slashCommands?.commands ?? []) {
|
|
2252
|
+
if (staticSkillNameSet.has(command.name) || dynamicSkillNameSet.has(command.name)) {
|
|
2253
|
+
throw new Error(`Invalid skill inventory: slash command "${command.name}" conflicts with a declared skill of the same name.`);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
function stripDotSlash(value) {
|
|
2258
|
+
return value.startsWith('./') ? value.slice(2) : value;
|
|
2259
|
+
}
|
|
2260
|
+
function isTemplateAsset(manifest, relativePath) {
|
|
2261
|
+
const normalized = stripDotSlash(relativePath).split(path.sep).join('/');
|
|
2262
|
+
const include = manifest.buildConfig?.templates?.include ?? [];
|
|
2263
|
+
return include.length > 0 && applyFileRules([normalized], include, []).length > 0;
|
|
2264
|
+
}
|
|
2265
|
+
async function validateSourceJsonNativeAsset(pluginRoot, manifest, field) {
|
|
2266
|
+
const relativePath = manifest[field];
|
|
2267
|
+
if (!relativePath || isTemplateAsset(manifest, relativePath)) {
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
const absolutePath = path.join(pluginRoot, stripDotSlash(relativePath));
|
|
2271
|
+
if (!(await fs.pathExists(absolutePath))) {
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
let content;
|
|
2275
|
+
try {
|
|
2276
|
+
content = await fs.readJson(absolutePath);
|
|
2277
|
+
}
|
|
2278
|
+
catch (error) {
|
|
2279
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2280
|
+
throw new Error(`Invalid ${field}: source JSON is invalid (${message}).`);
|
|
2281
|
+
}
|
|
2282
|
+
if (!content || typeof content !== 'object' || Array.isArray(content)) {
|
|
2283
|
+
throw new Error(`Invalid ${field}: expected a JSON object.`);
|
|
2284
|
+
}
|
|
2285
|
+
const record = content;
|
|
2286
|
+
if (field === 'mcpServers') {
|
|
2287
|
+
if (!record.mcpServers || typeof record.mcpServers !== 'object' || Array.isArray(record.mcpServers)) {
|
|
2288
|
+
throw new Error('Invalid mcpServers: expected top-level "mcpServers" object.');
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
else if (!record.apps || typeof record.apps !== 'object' || Array.isArray(record.apps)) {
|
|
2292
|
+
throw new Error('Invalid apps: expected top-level "apps" object.');
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
export async function validateSourcePluginManifest(pluginRoot, rawManifest, options = {}) {
|
|
2296
|
+
await assertPluginInputTree(pluginRoot);
|
|
2297
|
+
const distributionsInput = rawManifest !== null
|
|
2298
|
+
&& typeof rawManifest === 'object'
|
|
2299
|
+
&& !Array.isArray(rawManifest)
|
|
2300
|
+
? rawManifest.distributions
|
|
2301
|
+
: undefined;
|
|
2302
|
+
const requiresExplicitMinVersion = distributionsInput !== null
|
|
2303
|
+
&& typeof distributionsInput === 'object'
|
|
2304
|
+
&& !Array.isArray(distributionsInput)
|
|
2305
|
+
&& Object.hasOwn(distributionsInput, 'agentSkills');
|
|
2306
|
+
const manifest = validatePluginManifest(pluginRoot, rawManifest, {
|
|
2307
|
+
compatibilityMode: requiresExplicitMinVersion ? 'required' : 'defaulted',
|
|
2308
|
+
defaultMinVersion: DEFAULT_SOURCE_PLUGIN_AW_MIN_VERSION,
|
|
2309
|
+
reservedImports: options.reservedImports,
|
|
2310
|
+
});
|
|
2311
|
+
const parsedVersion = parseStrictSemVer(manifest.version, 'plugin manifest.version');
|
|
2312
|
+
if (parsedVersion.prerelease.length > 0 || parsedVersion.build.length > 0) {
|
|
2313
|
+
throw new Error('Invalid plugin manifest.version: expected a stable SemVer without prerelease or build metadata.');
|
|
2314
|
+
}
|
|
2315
|
+
if ((manifest.artifacts?.targets ?? []).some((target) => target.runtimeImports?.some((runtimeImport) => runtimeImport.resolved !== undefined))) {
|
|
2316
|
+
throw new Error('Invalid plugin manifest: artifact runtime import resolved metadata is build-owned.');
|
|
2317
|
+
}
|
|
2318
|
+
await validateDeclaredNativeAssets(pluginRoot, manifest);
|
|
2319
|
+
await validateDeclaredAssetPathsExist(pluginRoot, manifest);
|
|
2320
|
+
await Promise.all([
|
|
2321
|
+
validateSourceJsonNativeAsset(pluginRoot, manifest, 'mcpServers'),
|
|
2322
|
+
validateSourceJsonNativeAsset(pluginRoot, manifest, 'apps'),
|
|
2323
|
+
validateSourceSkillInventory(pluginRoot, manifest),
|
|
2324
|
+
...(manifest.distributions?.agentSkills?.skills
|
|
2325
|
+
? [validatePortableSkillDirectoriesRoot(path.join(pluginRoot, stripDotSlash(manifest.distributions.agentSkills.skills)))]
|
|
2326
|
+
: []),
|
|
2327
|
+
...(manifest.buildConfig?.runtimeExports ?? []).map((entry) => validateRuntimeExportSource({
|
|
2328
|
+
pluginRoot,
|
|
2329
|
+
name: entry.name,
|
|
2330
|
+
source: entry.source,
|
|
2331
|
+
})),
|
|
2332
|
+
]);
|
|
2333
|
+
await assertActualNativePluginCapability(pluginRoot, manifest);
|
|
2334
|
+
return manifest;
|
|
2335
|
+
}
|
|
2336
|
+
export async function readPluginManifest(pluginRoot, options = {}) {
|
|
2337
|
+
await assertPluginInputTree(pluginRoot);
|
|
2338
|
+
const manifestPath = path.join(pluginRoot, SOURCE_MANIFEST_PATH);
|
|
2339
|
+
const rawManifest = await readJsonFile(manifestPath);
|
|
2340
|
+
const mode = options.mode ?? 'source';
|
|
2341
|
+
const manifest = mode === 'source'
|
|
2342
|
+
? await validateSourcePluginManifest(pluginRoot, rawManifest, {
|
|
2343
|
+
reservedImports: options.reservedImports,
|
|
2344
|
+
})
|
|
2345
|
+
: validatePluginManifest(pluginRoot, rawManifest, {
|
|
2346
|
+
compatibilityMode: 'required',
|
|
2347
|
+
reservedImports: options.reservedImports,
|
|
2348
|
+
});
|
|
2349
|
+
if (mode === 'bundle')
|
|
2350
|
+
await assertActualNativePluginCapability(pluginRoot, manifest, 'bundle');
|
|
2351
|
+
return {
|
|
2352
|
+
rootDir: pluginRoot,
|
|
2353
|
+
manifestPath,
|
|
2354
|
+
manifest,
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
export async function discoverPluginsInDirectory(pluginsDir, options = {}) {
|
|
2358
|
+
if (!(await fs.pathExists(pluginsDir))) {
|
|
2359
|
+
return [];
|
|
2360
|
+
}
|
|
2361
|
+
const entries = await fs.readdir(pluginsDir);
|
|
2362
|
+
const plugins = await Promise.all(entries.map(async (entry) => {
|
|
2363
|
+
const pluginRoot = path.join(pluginsDir, entry);
|
|
2364
|
+
const manifestPath = path.join(pluginRoot, SOURCE_MANIFEST_PATH);
|
|
2365
|
+
if (!(await fs.pathExists(manifestPath))) {
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
return readPluginManifest(pluginRoot, options);
|
|
2369
|
+
}));
|
|
2370
|
+
return plugins
|
|
2371
|
+
.filter((plugin) => plugin !== null)
|
|
2372
|
+
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name));
|
|
2373
|
+
}
|
|
2374
|
+
export async function discoverPlugins(repoRoot, options = {}) {
|
|
2375
|
+
return discoverPluginsInDirectory(path.join(repoRoot, 'plugins'), options);
|
|
2376
|
+
}
|
|
2377
|
+
function selectPlugins(plugins, pluginNames) {
|
|
2378
|
+
if (!pluginNames || pluginNames.length === 0) {
|
|
2379
|
+
return plugins;
|
|
2380
|
+
}
|
|
2381
|
+
const pluginMap = new Map(plugins.map((plugin) => [plugin.manifest.name, plugin]));
|
|
2382
|
+
return pluginNames.map((pluginName) => {
|
|
2383
|
+
const plugin = pluginMap.get(pluginName);
|
|
2384
|
+
if (!plugin) {
|
|
2385
|
+
throw new Error(`Unknown plugin "${pluginName}".`);
|
|
2386
|
+
}
|
|
2387
|
+
return plugin;
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
export async function resolvePlugins(repoRoot, pluginNames, options = {}) {
|
|
2391
|
+
const plugins = await discoverPlugins(repoRoot, options);
|
|
2392
|
+
return selectPlugins(plugins, pluginNames);
|
|
2393
|
+
}
|
|
2394
|
+
export async function resolvePluginsFromCatalog(catalogRoot, pluginNames, options = {}) {
|
|
2395
|
+
const plugins = await discoverPluginsInDirectory(path.join(catalogRoot, 'plugins'), { ...options, mode: 'bundle' });
|
|
2396
|
+
return selectPlugins(plugins, pluginNames);
|
|
2397
|
+
}
|
|
2398
|
+
export function createMarketplaceManifestEntry(plugin) {
|
|
2399
|
+
return {
|
|
2400
|
+
name: plugin.manifest.name,
|
|
2401
|
+
source: `./plugins/${plugin.manifest.name}`,
|
|
2402
|
+
description: plugin.manifest.description,
|
|
2403
|
+
interface: plugin.manifest.interface,
|
|
2404
|
+
category: plugin.manifest.marketplace.category,
|
|
2405
|
+
tags: plugin.manifest.marketplace.tags,
|
|
2406
|
+
strict: plugin.manifest.marketplace.strict,
|
|
2407
|
+
policy: plugin.manifest.marketplace.policy,
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
export function createMarketplaceManifest(sourceMeta, plugins) {
|
|
2411
|
+
return {
|
|
2412
|
+
name: 'aw',
|
|
2413
|
+
owner: sourceMeta.owner,
|
|
2414
|
+
interface: sourceMeta.interface,
|
|
2415
|
+
plugins: plugins.map((plugin) => createMarketplaceManifestEntry(plugin)),
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
export function normalizeManifestDigestInput(value) {
|
|
2419
|
+
return stableStringify(value);
|
|
2420
|
+
}
|
|
2421
|
+
function normalizeCatalogRelativePath(value, fieldName) {
|
|
2422
|
+
const relativePath = ensureString(value, fieldName);
|
|
2423
|
+
if (path.posix.isAbsolute(relativePath)) {
|
|
2424
|
+
throw new Error(`Invalid ${fieldName}: absolute paths are not allowed.`);
|
|
2425
|
+
}
|
|
2426
|
+
if (!relativePath.startsWith('./')) {
|
|
2427
|
+
throw new Error(`Invalid ${fieldName}: path must start with "./".`);
|
|
2428
|
+
}
|
|
2429
|
+
const normalized = path.posix.normalize(relativePath);
|
|
2430
|
+
if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
|
|
2431
|
+
throw new Error(`Invalid ${fieldName}: path must stay inside the catalog package.`);
|
|
2432
|
+
}
|
|
2433
|
+
return normalized.startsWith('./') ? normalized : `./${normalized}`;
|
|
2434
|
+
}
|
|
2435
|
+
function assertCatalogChannelVersionContract(options) {
|
|
2436
|
+
const prerelease = isPrereleaseVersion(options.version);
|
|
2437
|
+
if (options.channel === 'latest' && prerelease) {
|
|
2438
|
+
throw new Error(`Invalid ${options.fieldName}: latest catalog rejects prerelease ${options.kind} versions.`);
|
|
2439
|
+
}
|
|
2440
|
+
if (options.channel === 'beta' && options.kind === 'catalog' && !prerelease) {
|
|
2441
|
+
throw new Error(`Invalid ${options.fieldName}: beta catalog requires a prerelease catalog version.`);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
function normalizeCatalogManifestEntry(value, fieldName) {
|
|
2445
|
+
const input = assertObject(value, fieldName);
|
|
2446
|
+
if (Object.hasOwn(input, 'dependencies')) {
|
|
2447
|
+
throw new Error(`Invalid ${fieldName}: "dependencies" is unsupported.`);
|
|
2448
|
+
}
|
|
2449
|
+
assertAllowedKeys(input, fieldName, [
|
|
2450
|
+
'name',
|
|
2451
|
+
'version',
|
|
2452
|
+
'description',
|
|
2453
|
+
'interface',
|
|
2454
|
+
'category',
|
|
2455
|
+
'tags',
|
|
2456
|
+
'strict',
|
|
2457
|
+
'policy',
|
|
2458
|
+
'compatibility',
|
|
2459
|
+
'source',
|
|
2460
|
+
'artifactDigest',
|
|
2461
|
+
'manifestDigest',
|
|
2462
|
+
]);
|
|
2463
|
+
const entry = {
|
|
2464
|
+
name: ensurePluginName(input.name, `${fieldName}.name`),
|
|
2465
|
+
version: assertStrictSemVer(input.version, `${fieldName}.version`),
|
|
2466
|
+
description: ensureString(input.description, `${fieldName}.description`),
|
|
2467
|
+
category: ensureString(input.category, `${fieldName}.category`),
|
|
2468
|
+
strict: ensureBoolean(input.strict, `${fieldName}.strict`),
|
|
2469
|
+
policy: normalizeMarketplacePolicy(input.policy, `${fieldName}.policy`),
|
|
2470
|
+
compatibility: normalizePluginCompatibility(input.compatibility, `${fieldName}.compatibility`, {
|
|
2471
|
+
allowMissing: false,
|
|
2472
|
+
}),
|
|
2473
|
+
source: normalizeCatalogRelativePath(input.source, `${fieldName}.source`),
|
|
2474
|
+
artifactDigest: ensureDigestString(input.artifactDigest, `${fieldName}.artifactDigest`),
|
|
2475
|
+
manifestDigest: ensureDigestString(input.manifestDigest, `${fieldName}.manifestDigest`),
|
|
2476
|
+
};
|
|
2477
|
+
if (input.tags !== undefined) {
|
|
2478
|
+
entry.tags = normalizeComparableStringArray(ensureStringArray(input.tags, `${fieldName}.tags`));
|
|
2479
|
+
}
|
|
2480
|
+
if (input.interface !== undefined) {
|
|
2481
|
+
entry.interface = normalizePluginInterface(input.interface, `${fieldName}.interface`);
|
|
2482
|
+
}
|
|
2483
|
+
return entry;
|
|
2484
|
+
}
|
|
2485
|
+
export function normalizeCatalogManifestBase(input, fieldName) {
|
|
2486
|
+
const catalogVersion = assertCatalogVersion(input.catalogVersion, `${fieldName}.catalogVersion`);
|
|
2487
|
+
const channel = assertCatalogChannel(input.channel, `${fieldName}.channel`);
|
|
2488
|
+
assertCatalogChannelVersionContract({
|
|
2489
|
+
channel,
|
|
2490
|
+
version: catalogVersion,
|
|
2491
|
+
fieldName: `${fieldName}.catalogVersion`,
|
|
2492
|
+
kind: 'catalog',
|
|
2493
|
+
});
|
|
2494
|
+
const compatibility = normalizePluginCompatibility(input.compatibility, `${fieldName}.compatibility`, { allowMissing: false });
|
|
2495
|
+
return { catalogVersion, channel, compatibility };
|
|
2496
|
+
}
|
|
2497
|
+
export function normalizeCatalogManifestEntries(rawEntries, base, fieldName) {
|
|
2498
|
+
if (!Array.isArray(rawEntries)) {
|
|
2499
|
+
throw new Error(`Invalid ${fieldName}.entries: expected an array.`);
|
|
2500
|
+
}
|
|
2501
|
+
const entries = rawEntries.map((entry, index) => normalizeCatalogManifestEntry(entry, `${fieldName}.entries[${index}]`));
|
|
2502
|
+
const entriesByName = new Map();
|
|
2503
|
+
for (const entry of entries) {
|
|
2504
|
+
if (entriesByName.has(entry.name)) {
|
|
2505
|
+
throw new Error(`Invalid ${fieldName}.entries: duplicate plugin "${entry.name}".`);
|
|
2506
|
+
}
|
|
2507
|
+
entriesByName.set(entry.name, entry);
|
|
2508
|
+
if (!(base.channel === 'beta' && !entry.version.includes('-'))) {
|
|
2509
|
+
assertCatalogChannelVersionContract({
|
|
2510
|
+
channel: base.channel,
|
|
2511
|
+
version: entry.version,
|
|
2512
|
+
fieldName: `${fieldName}.entries.${entry.name}.version`,
|
|
2513
|
+
kind: 'plugin',
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
assertPluginMinVersionWithinCatalogBounds({
|
|
2517
|
+
pluginName: entry.name,
|
|
2518
|
+
pluginMinVersion: entry.compatibility.aw.minVersion,
|
|
2519
|
+
catalogMinVersion: base.compatibility.aw.minVersion,
|
|
2520
|
+
fieldName: `${fieldName}.entries.${entry.name}.compatibility.aw.minVersion`,
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
return entries;
|
|
2524
|
+
}
|
|
2525
|
+
export function createBundleSourceManifest(plugin) {
|
|
2526
|
+
const scripts = plugin.manifest.scripts
|
|
2527
|
+
? structuredClone(plugin.manifest.scripts)
|
|
2528
|
+
: undefined;
|
|
2529
|
+
if (scripts?.tests) {
|
|
2530
|
+
delete scripts.tests;
|
|
2531
|
+
}
|
|
2532
|
+
if (scripts?.node?.build) {
|
|
2533
|
+
delete scripts.node.build;
|
|
2534
|
+
}
|
|
2535
|
+
let buildConfig = plugin.manifest.buildConfig
|
|
2536
|
+
? structuredClone(plugin.manifest.buildConfig)
|
|
2537
|
+
: undefined;
|
|
2538
|
+
if (buildConfig) {
|
|
2539
|
+
delete buildConfig.cacheable;
|
|
2540
|
+
delete buildConfig.runtimeExports;
|
|
2541
|
+
delete buildConfig.buildImports;
|
|
2542
|
+
delete buildConfig.skillRuntimeFiles;
|
|
2543
|
+
delete buildConfig.generatedSkillRuntimeFiles;
|
|
2544
|
+
if (Object.keys(buildConfig).length === 0) {
|
|
2545
|
+
buildConfig = undefined;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
return {
|
|
2549
|
+
...plugin.manifest,
|
|
2550
|
+
buildConfig,
|
|
2551
|
+
compatibility: plugin.manifest.compatibility
|
|
2552
|
+
? structuredClone(plugin.manifest.compatibility)
|
|
2553
|
+
: createPluginCompatibility(DEFAULT_SOURCE_PLUGIN_AW_MIN_VERSION),
|
|
2554
|
+
extensions: plugin.manifest.extensions
|
|
2555
|
+
? {
|
|
2556
|
+
instructions: plugin.manifest.extensions.instructions
|
|
2557
|
+
?.map((entry) => structuredClone(entry)),
|
|
2558
|
+
permissions: plugin.manifest.extensions.permissions
|
|
2559
|
+
?.map((entry) => structuredClone(entry)),
|
|
2560
|
+
hooks: plugin.manifest.extensions.hooks
|
|
2561
|
+
?.map((entry) => structuredClone(entry)),
|
|
2562
|
+
nativeHooks: plugin.manifest.extensions.nativeHooks
|
|
2563
|
+
? {
|
|
2564
|
+
claude: plugin.manifest.extensions.nativeHooks.claude
|
|
2565
|
+
?.map((entry) => structuredClone(entry)),
|
|
2566
|
+
codex: plugin.manifest.extensions.nativeHooks.codex
|
|
2567
|
+
?.map((entry) => structuredClone(entry)),
|
|
2568
|
+
}
|
|
2569
|
+
: undefined,
|
|
2570
|
+
agents: plugin.manifest.extensions.agents
|
|
2571
|
+
?.map((entry) => structuredClone(entry)),
|
|
2572
|
+
}
|
|
2573
|
+
: undefined,
|
|
2574
|
+
artifacts: plugin.manifest.artifacts
|
|
2575
|
+
? structuredClone(plugin.manifest.artifacts)
|
|
2576
|
+
: undefined,
|
|
2577
|
+
scripts,
|
|
2578
|
+
config: plugin.manifest.config
|
|
2579
|
+
? structuredClone(plugin.manifest.config)
|
|
2580
|
+
: undefined,
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
export function createPlatformManifestBase(plugin) {
|
|
2584
|
+
return {
|
|
2585
|
+
name: plugin.manifest.name,
|
|
2586
|
+
version: plugin.manifest.version,
|
|
2587
|
+
description: plugin.manifest.description,
|
|
2588
|
+
compatibility: structuredClone(plugin.manifest.compatibility ?? {
|
|
2589
|
+
aw: {
|
|
2590
|
+
minVersion: '0.0.0',
|
|
2591
|
+
},
|
|
2592
|
+
}),
|
|
2593
|
+
interface: plugin.manifest.interface,
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
function platformBundlePath(value, platform) {
|
|
2597
|
+
return `./${platform}/${stripDotSlash(value)}`;
|
|
2598
|
+
}
|
|
2599
|
+
function generatedPlatformEntryName(pluginName, itemName) {
|
|
2600
|
+
const slugify = (value) => value
|
|
2601
|
+
.trim()
|
|
2602
|
+
.toLowerCase()
|
|
2603
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
2604
|
+
.replace(/^-+|-+$/gu, '');
|
|
2605
|
+
return `aw-${slugify(pluginName)}-${slugify(itemName)}`;
|
|
2606
|
+
}
|
|
2607
|
+
function renderExpectedPlatformHooks(plugin, platform) {
|
|
2608
|
+
const groups = resolvePlatformHookDefinitions(plugin.manifest, platform)
|
|
2609
|
+
.map((definition) => renderHookDefinitionGroup(definition, platform))
|
|
2610
|
+
.sort((left, right) => JSON.stringify(normalizeRenderedHookGroupIdentity({
|
|
2611
|
+
platform,
|
|
2612
|
+
event: left.event,
|
|
2613
|
+
matcher: left.matcher,
|
|
2614
|
+
command: left.hooks[0].command,
|
|
2615
|
+
})).localeCompare(JSON.stringify(normalizeRenderedHookGroupIdentity({
|
|
2616
|
+
platform,
|
|
2617
|
+
event: right.event,
|
|
2618
|
+
matcher: right.matcher,
|
|
2619
|
+
command: right.hooks[0].command,
|
|
2620
|
+
}))));
|
|
2621
|
+
if (groups.length === 0)
|
|
2622
|
+
return undefined;
|
|
2623
|
+
if (platform === 'claude')
|
|
2624
|
+
return './claude/hooks/hooks.json';
|
|
2625
|
+
const hooks = {};
|
|
2626
|
+
for (const group of groups) {
|
|
2627
|
+
hooks[group.event] ??= [];
|
|
2628
|
+
hooks[group.event].push({
|
|
2629
|
+
...(group.matcher === undefined ? {} : { matcher: group.matcher }),
|
|
2630
|
+
hooks: group.hooks,
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
return { hooks };
|
|
2634
|
+
}
|
|
2635
|
+
async function isProjectedDirectory(plugin, relativePath) {
|
|
2636
|
+
const stats = await fs.lstat(path.join(plugin.rootDir, stripDotSlash(relativePath))).catch((error) => {
|
|
2637
|
+
if (error.code === 'ENOENT')
|
|
2638
|
+
return undefined;
|
|
2639
|
+
throw error;
|
|
2640
|
+
});
|
|
2641
|
+
if (!stats)
|
|
2642
|
+
return false;
|
|
2643
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
2644
|
+
throw new Error(`Invalid platform manifest projection path: expected a regular directory at ${relativePath}.`);
|
|
2645
|
+
}
|
|
2646
|
+
return true;
|
|
2647
|
+
}
|
|
2648
|
+
async function assertPlatformProjectionPath(plugin, relativePath) {
|
|
2649
|
+
if (!relativePath.startsWith('./')) {
|
|
2650
|
+
throw new Error(`Invalid platform manifest projection path ${JSON.stringify(relativePath)}.`);
|
|
2651
|
+
}
|
|
2652
|
+
const normalized = path.posix.normalize(relativePath);
|
|
2653
|
+
if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
|
|
2654
|
+
throw new Error(`Invalid platform manifest projection path ${JSON.stringify(relativePath)}.`);
|
|
2655
|
+
}
|
|
2656
|
+
let current = plugin.rootDir;
|
|
2657
|
+
for (const segment of stripDotSlash(normalized).split('/')) {
|
|
2658
|
+
current = path.join(current, segment);
|
|
2659
|
+
const stats = await fs.lstat(current).catch(() => undefined);
|
|
2660
|
+
if (!stats)
|
|
2661
|
+
throw new Error(`Invalid platform manifest projection: missing ${relativePath}.`);
|
|
2662
|
+
if (stats.isSymbolicLink()) {
|
|
2663
|
+
throw new Error(`Invalid platform manifest projection: symbolic links are not supported (${relativePath}).`);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
export async function validatePlatformManifestProjection(plugin, platform, rawManifest) {
|
|
2668
|
+
const expected = createPlatformManifestBase(plugin);
|
|
2669
|
+
const projectedPaths = [];
|
|
2670
|
+
if (plugin.manifest.skills) {
|
|
2671
|
+
const projected = platformBundlePath(plugin.manifest.skills, platform);
|
|
2672
|
+
if (await isProjectedDirectory(plugin, projected)) {
|
|
2673
|
+
expected.skills = projected;
|
|
2674
|
+
projectedPaths.push(projected);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
if (plugin.manifest.mcpServers) {
|
|
2678
|
+
expected.mcpServers = platformBundlePath(plugin.manifest.mcpServers, platform);
|
|
2679
|
+
projectedPaths.push(expected.mcpServers);
|
|
2680
|
+
}
|
|
2681
|
+
if (platform === 'codex' && plugin.manifest.apps) {
|
|
2682
|
+
expected.apps = platformBundlePath(plugin.manifest.apps, platform);
|
|
2683
|
+
projectedPaths.push(expected.apps);
|
|
2684
|
+
}
|
|
2685
|
+
expected.hooks = renderExpectedPlatformHooks(plugin, platform);
|
|
2686
|
+
if (typeof expected.hooks === 'string')
|
|
2687
|
+
projectedPaths.push(expected.hooks);
|
|
2688
|
+
if (platform === 'claude') {
|
|
2689
|
+
const overrides = plugin.manifest.buildConfig?.manifestOverrides?.claude;
|
|
2690
|
+
if (overrides?.outputStyles) {
|
|
2691
|
+
expected.outputStyles = platformBundlePath(overrides.outputStyles, platform);
|
|
2692
|
+
projectedPaths.push(expected.outputStyles);
|
|
2693
|
+
}
|
|
2694
|
+
if (overrides?.lspServers !== undefined) {
|
|
2695
|
+
const projectLspPath = (value) => {
|
|
2696
|
+
const projected = platformBundlePath(value, platform);
|
|
2697
|
+
projectedPaths.push(projected);
|
|
2698
|
+
return projected;
|
|
2699
|
+
};
|
|
2700
|
+
expected.lspServers = typeof overrides.lspServers === 'string'
|
|
2701
|
+
? projectLspPath(overrides.lspServers)
|
|
2702
|
+
: Array.isArray(overrides.lspServers)
|
|
2703
|
+
? overrides.lspServers.map((entry) => typeof entry === 'string' ? projectLspPath(entry) : entry)
|
|
2704
|
+
: overrides.lspServers;
|
|
2705
|
+
}
|
|
2706
|
+
expected.userConfig = overrides?.userConfig;
|
|
2707
|
+
expected.channels = overrides?.channels;
|
|
2708
|
+
const agents = resolvePlatformEntries(plugin.manifest.extensions?.agents, 'claude');
|
|
2709
|
+
if (agents.length > 0) {
|
|
2710
|
+
expected.agents = agents.map((agent) => `./claude/agents/${generatedPlatformEntryName(plugin.manifest.name, agent.name)}.md`);
|
|
2711
|
+
projectedPaths.push(...expected.agents);
|
|
2712
|
+
}
|
|
2713
|
+
if (plugin.manifest.slashCommands) {
|
|
2714
|
+
expected.commands = './claude/commands';
|
|
2715
|
+
projectedPaths.push(expected.commands);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
else if (plugin.manifest.slashCommands) {
|
|
2719
|
+
expected.skills ??= './codex/skills';
|
|
2720
|
+
if (!projectedPaths.includes(expected.skills))
|
|
2721
|
+
projectedPaths.push(expected.skills);
|
|
2722
|
+
}
|
|
2723
|
+
const canonicalExpected = JSON.parse(JSON.stringify(expected));
|
|
2724
|
+
if (!isDeepStrictEqual(rawManifest, canonicalExpected)) {
|
|
2725
|
+
throw new Error(`Invalid ${platform} platform manifest projection.`);
|
|
2726
|
+
}
|
|
2727
|
+
await Promise.all([...new Set(projectedPaths)].map((relativePath) => assertPlatformProjectionPath(plugin, relativePath)));
|
|
2728
|
+
return canonicalExpected;
|
|
2729
|
+
}
|
|
2730
|
+
//# sourceMappingURL=manifest-contract.js.map
|