@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.
Files changed (66) hide show
  1. package/dist/file-rules.d.ts +2 -0
  2. package/dist/file-rules.js +42 -0
  3. package/dist/file-rules.js.map +1 -0
  4. package/dist/generated/plugin-manifest-v1.d.ts +368 -0
  5. package/dist/generated/plugin-manifest-v1.js +3 -0
  6. package/dist/generated/plugin-manifest-v1.js.map +1 -0
  7. package/dist/hooks-contract.d.ts +189 -0
  8. package/dist/hooks-contract.js +967 -0
  9. package/dist/hooks-contract.js.map +1 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +4 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/manifest-contract.d.ts +96 -0
  14. package/dist/manifest-contract.js +2730 -0
  15. package/dist/manifest-contract.js.map +1 -0
  16. package/dist/manifest-type-contract.d.ts +7 -0
  17. package/dist/manifest-type-contract.js +2 -0
  18. package/dist/manifest-type-contract.js.map +1 -0
  19. package/dist/manifest.d.ts +27 -0
  20. package/dist/manifest.js +59 -0
  21. package/dist/manifest.js.map +1 -0
  22. package/dist/permission-command.d.ts +3 -0
  23. package/dist/permission-command.js +11 -0
  24. package/dist/permission-command.js.map +1 -0
  25. package/dist/platform-entries.d.ts +13 -0
  26. package/dist/platform-entries.js +20 -0
  27. package/dist/platform-entries.js.map +1 -0
  28. package/dist/platform-support.d.ts +6 -0
  29. package/dist/platform-support.js +38 -0
  30. package/dist/platform-support.js.map +1 -0
  31. package/dist/plugin-release.d.ts +30 -0
  32. package/dist/plugin-release.js +187 -0
  33. package/dist/plugin-release.js.map +1 -0
  34. package/dist/plugin-script-contract.d.ts +33 -0
  35. package/dist/plugin-script-contract.js +599 -0
  36. package/dist/plugin-script-contract.js.map +1 -0
  37. package/dist/portable-skill-validation.d.ts +7 -0
  38. package/dist/portable-skill-validation.js +111 -0
  39. package/dist/portable-skill-validation.js.map +1 -0
  40. package/dist/read-json.d.ts +1 -0
  41. package/dist/read-json.js +5 -0
  42. package/dist/read-json.js.map +1 -0
  43. package/dist/runtime-export-source.d.ts +14 -0
  44. package/dist/runtime-export-source.js +73 -0
  45. package/dist/runtime-export-source.js.map +1 -0
  46. package/dist/skill-authoring.d.ts +2 -0
  47. package/dist/skill-authoring.js +13 -0
  48. package/dist/skill-authoring.js.map +1 -0
  49. package/dist/skill-frontmatter-capabilities.d.ts +36 -0
  50. package/dist/skill-frontmatter-capabilities.js +120 -0
  51. package/dist/skill-frontmatter-capabilities.js.map +1 -0
  52. package/dist/skill-frontmatter-capabilities.json +200 -0
  53. package/dist/slash-commands.d.ts +43 -0
  54. package/dist/slash-commands.js +394 -0
  55. package/dist/slash-commands.js.map +1 -0
  56. package/dist/template-engine.d.ts +2 -0
  57. package/dist/template-engine.js +26 -0
  58. package/dist/template-engine.js.map +1 -0
  59. package/dist/types.d.ts +428 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/workspace.d.ts +105 -0
  63. package/dist/workspace.js +963 -0
  64. package/dist/workspace.js.map +1 -0
  65. package/package.json +46 -0
  66. package/schemas/plugin-manifest-v1.json +329 -0
@@ -0,0 +1,963 @@
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import fs from 'fs-extra';
5
+ import { assertCatalogVersion, assertStrictSemVer, compareStrictSemVer, MARKETPLACE_SOURCE_PATH, SOURCE_MANIFEST_PATH, createMarketplaceManifest, createMarketplaceManifestEntry, normalizeCatalogManifestBase, normalizeCatalogMarketplaceIdentity, normalizeCatalogManifestEntries, normalizeManifestDigestInput, readMarketplaceSourceMeta, readPluginManifest, validatePlatformManifestProjection, } from './manifest-contract.js';
6
+ import { resolveSupportedPlatforms } from './platform-support.js';
7
+ import { appendCatalogToPluginReleaseLedger, assertStablePluginVersion, PLUGIN_RELEASE_LEDGER_FILENAME, PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION, resolveBetaPluginVersionOverrides, validatePluginReleaseLedgerAgainstManifest, } from './plugin-release.js';
8
+ export const DEFAULT_WORKSPACE_CATALOG_VERSION = '0.1.0';
9
+ export const DEFAULT_CATALOG_OUTPUT_PATH = path.join('.aw', 'catalog');
10
+ export const DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH = path.join('.aw', 'publish', 'catalog');
11
+ export const DEFAULT_PLUGIN_OUTPUT_PATH = path.join('.aw', 'plugins');
12
+ export const CATALOG_MANIFEST_FILENAME = 'catalog-manifest.json';
13
+ export const GENERATED_OUTPUT_MARKER_FILENAME = '.aw-generated.json';
14
+ export const WORKSPACE_MARKER_FILENAME = '.aw-workspace.json';
15
+ function isInside(root, candidate) {
16
+ const relative = path.relative(root, candidate);
17
+ return relative === '' || (relative !== '..'
18
+ && !relative.startsWith(`..${path.sep}`)
19
+ && !path.isAbsolute(relative));
20
+ }
21
+ async function assertDirectoryWithoutSymlinks(directory, label) {
22
+ const requested = path.resolve(directory);
23
+ const stat = await fs.lstat(requested).catch((error) => {
24
+ if (error.code === 'ENOENT')
25
+ return null;
26
+ throw error;
27
+ });
28
+ if (!stat?.isDirectory() || stat.isSymbolicLink()) {
29
+ throw new Error(`${label} must be a regular non-symbolic-link directory: ${requested}`);
30
+ }
31
+ return fs.realpath(requested);
32
+ }
33
+ async function assertNoSymlinkPath(root, target) {
34
+ const relative = path.relative(root, target);
35
+ let current = root;
36
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
37
+ current = path.join(current, segment);
38
+ const stat = await fs.lstat(current).catch((error) => {
39
+ if (error.code === 'ENOENT')
40
+ return null;
41
+ throw error;
42
+ });
43
+ if (!stat)
44
+ return;
45
+ if (stat.isSymbolicLink()) {
46
+ throw new Error(`Output path must not contain symbolic links: ${current}`);
47
+ }
48
+ }
49
+ }
50
+ export async function resolveWorkspaceOutputPath(options) {
51
+ const workspaceRoot = await assertDirectoryWithoutSymlinks(options.workspaceRoot, 'Workspace root');
52
+ const outputRoot = path.resolve(workspaceRoot, options.outputPath);
53
+ if (!isInside(workspaceRoot, outputRoot) || outputRoot === workspaceRoot) {
54
+ throw new Error(`Output must be inside the workspace: ${outputRoot}`);
55
+ }
56
+ await assertNoSymlinkPath(workspaceRoot, outputRoot);
57
+ const protectedRoots = [
58
+ path.join(workspaceRoot, '.aw'),
59
+ path.join(workspaceRoot, '.git'),
60
+ path.join(workspaceRoot, 'plugins'),
61
+ ];
62
+ for (const protectedRoot of protectedRoots) {
63
+ if (protectedRoot.endsWith(`${path.sep}.aw`) && outputRoot !== protectedRoot)
64
+ continue;
65
+ if (isInside(protectedRoot, outputRoot) || isInside(outputRoot, protectedRoot)) {
66
+ throw new Error(`Output overlaps a protected workspace path: ${outputRoot}`);
67
+ }
68
+ }
69
+ const otherGeneratedRoot = path.join(workspaceRoot, options.kind === 'catalog' ? DEFAULT_PLUGIN_OUTPUT_PATH : DEFAULT_CATALOG_OUTPUT_PATH);
70
+ if (options.kind && (isInside(otherGeneratedRoot, outputRoot) || isInside(outputRoot, otherGeneratedRoot))) {
71
+ throw new Error(`Output overlaps a different generated output kind: ${outputRoot}`);
72
+ }
73
+ return { workspaceRoot, outputRoot };
74
+ }
75
+ function safeWorkspaceName(workspaceRoot) {
76
+ const normalized = path.basename(workspaceRoot).toLowerCase()
77
+ .replace(/[^a-z0-9]+/gu, '-')
78
+ .replace(/^-+|-+$/gu, '');
79
+ return normalized || 'aw-workspace';
80
+ }
81
+ function workspaceMetadata(options, workspaceRoot) {
82
+ const workspaceName = safeWorkspaceName(workspaceRoot);
83
+ return {
84
+ owner: {
85
+ name: options.ownerName?.trim() || workspaceName,
86
+ url: options.ownerUrl?.trim() || `https://example.invalid/aw-workspaces/${workspaceName}`,
87
+ },
88
+ interface: {
89
+ displayName: options.displayName?.trim() || workspaceName,
90
+ },
91
+ catalog: {
92
+ version: assertCatalogVersion(options.catalogVersion ?? DEFAULT_WORKSPACE_CATALOG_VERSION, 'catalog version'),
93
+ },
94
+ };
95
+ }
96
+ async function writeJsonAtomic(filePath, value) {
97
+ const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
98
+ await fs.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
99
+ try {
100
+ await fs.rename(temporary, filePath);
101
+ }
102
+ catch (error) {
103
+ await fs.remove(temporary);
104
+ throw error;
105
+ }
106
+ }
107
+ async function writeTextAtomic(filePath, value) {
108
+ const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
109
+ await fs.writeFile(temporary, value, { flag: 'wx' });
110
+ try {
111
+ await fs.rename(temporary, filePath);
112
+ }
113
+ catch (error) {
114
+ await fs.remove(temporary);
115
+ throw error;
116
+ }
117
+ }
118
+ function workspaceIdentity(workspaceRoot) {
119
+ return sha256(`aw-plugin-workspace-v1\0${path.resolve(workspaceRoot)}`);
120
+ }
121
+ function workspaceScaffoldMarker(metadata) {
122
+ return {
123
+ schemaVersion: 1,
124
+ kind: 'aw-plugin-workspace',
125
+ workspaceIdentity: sha256(crypto.randomUUID()),
126
+ sourceIdentity: sha256(stableStringify(metadata)),
127
+ };
128
+ }
129
+ function validateWorkspaceScaffoldMarker(value) {
130
+ const marker = value;
131
+ const digestPattern = /^sha256:[0-9a-f]{64}$/u;
132
+ if (!marker
133
+ || typeof marker !== 'object'
134
+ || Array.isArray(marker)
135
+ || !isDeepStrictEqual(Object.keys(marker).sort(), [
136
+ 'kind', 'schemaVersion', 'sourceIdentity', 'workspaceIdentity',
137
+ ])
138
+ || marker.schemaVersion !== 1
139
+ || marker.kind !== 'aw-plugin-workspace'
140
+ || typeof marker.workspaceIdentity !== 'string'
141
+ || !digestPattern.test(marker.workspaceIdentity)
142
+ || typeof marker.sourceIdentity !== 'string'
143
+ || !digestPattern.test(marker.sourceIdentity)) {
144
+ throw new Error('Invalid workspace scaffold marker.');
145
+ }
146
+ return { workspaceIdentity: marker.workspaceIdentity };
147
+ }
148
+ async function readWorkspaceOwnershipIdentity(workspaceRoot) {
149
+ const markerPath = path.join(workspaceRoot, WORKSPACE_MARKER_FILENAME);
150
+ if (!await fs.pathExists(markerPath))
151
+ return workspaceIdentity(workspaceRoot);
152
+ return validateWorkspaceScaffoldMarker(await readRegularJson(markerPath, 'Workspace marker')).workspaceIdentity;
153
+ }
154
+ async function readRegularJson(filePath, label) {
155
+ const stat = await fs.lstat(filePath).catch((error) => {
156
+ if (error.code === 'ENOENT')
157
+ return null;
158
+ throw error;
159
+ });
160
+ if (!stat?.isFile() || stat.isSymbolicLink()) {
161
+ throw new Error(`${label} must be a regular non-symbolic-link file: ${filePath}`);
162
+ }
163
+ return fs.readJson(filePath);
164
+ }
165
+ export async function initializePluginWorkspace(options) {
166
+ const workspaceRoot = await assertDirectoryWithoutSymlinks(options.workspaceRoot, 'Workspace root');
167
+ const pluginsRoot = path.join(workspaceRoot, 'plugins');
168
+ const metadataPath = path.join(workspaceRoot, MARKETPLACE_SOURCE_PATH);
169
+ const markerPath = path.join(workspaceRoot, WORKSPACE_MARKER_FILENAME);
170
+ const metadata = workspaceMetadata(options, workspaceRoot);
171
+ const existingMetadata = await fs.pathExists(metadataPath);
172
+ const existingMarker = await fs.pathExists(markerPath);
173
+ if ((existingMetadata || existingMarker) && !options.force) {
174
+ throw new Error(`Workspace catalog scaffold already exists: ${workspaceRoot}`);
175
+ }
176
+ if (await fs.pathExists(pluginsRoot)) {
177
+ const stat = await fs.lstat(pluginsRoot);
178
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
179
+ throw new Error(`Workspace plugins path must be a regular directory: ${pluginsRoot}`);
180
+ }
181
+ const entries = (await fs.readdir(pluginsRoot)).filter((entry) => entry !== path.basename(metadataPath));
182
+ if (entries.length > 0 && !existingMarker) {
183
+ throw new Error(`Refusing to initialize over a non-managed plugins directory: ${pluginsRoot}`);
184
+ }
185
+ }
186
+ if (existingMetadata) {
187
+ const current = await readMarketplaceSourceMeta(workspaceRoot);
188
+ if (!isDeepStrictEqual(current, metadata)) {
189
+ throw new Error(`Refusing to replace different workspace catalog metadata: ${metadataPath}`);
190
+ }
191
+ }
192
+ const expectedMarker = workspaceScaffoldMarker(metadata);
193
+ if (existingMarker) {
194
+ const currentMarker = await readRegularJson(markerPath, 'Workspace marker');
195
+ try {
196
+ validateWorkspaceScaffoldMarker(currentMarker);
197
+ }
198
+ catch {
199
+ throw new Error(`Refusing to replace a workspace scaffold with a different identity: ${workspaceRoot}`);
200
+ }
201
+ }
202
+ const gitignorePath = path.join(workspaceRoot, '.gitignore');
203
+ const gitignoreExists = await fs.pathExists(gitignorePath);
204
+ let previousGitignore = null;
205
+ if (gitignoreExists) {
206
+ const stat = await fs.lstat(gitignorePath);
207
+ if (!stat.isFile() || stat.isSymbolicLink()) {
208
+ throw new Error(`Workspace .gitignore must be a regular file: ${gitignorePath}`);
209
+ }
210
+ previousGitignore = await fs.readFile(gitignorePath, 'utf8');
211
+ }
212
+ await fs.ensureDir(pluginsRoot);
213
+ try {
214
+ if (previousGitignore === null) {
215
+ await fs.writeFile(gitignorePath, '.aw/\n', { flag: 'wx' });
216
+ }
217
+ else if (!previousGitignore.split(/\r?\n/u).includes('.aw/')) {
218
+ const separator = previousGitignore.length === 0 || previousGitignore.endsWith('\n') ? '' : '\n';
219
+ await writeTextAtomic(gitignorePath, `${previousGitignore}${separator}.aw/\n`);
220
+ }
221
+ if (!existingMetadata)
222
+ await writeJsonAtomic(metadataPath, metadata);
223
+ if (!existingMarker)
224
+ await writeJsonAtomic(markerPath, expectedMarker);
225
+ }
226
+ catch (error) {
227
+ if (!existingMetadata)
228
+ await fs.remove(metadataPath);
229
+ if (!existingMarker)
230
+ await fs.remove(markerPath);
231
+ if (previousGitignore === null)
232
+ await fs.remove(gitignorePath);
233
+ else
234
+ await writeTextAtomic(gitignorePath, previousGitignore);
235
+ if ((await fs.readdir(pluginsRoot)).length === 0)
236
+ await fs.remove(pluginsRoot);
237
+ throw error;
238
+ }
239
+ return { workspaceRoot, metadataPath, metadata };
240
+ }
241
+ export async function readPluginWorkspace(workspaceRoot, options = {}) {
242
+ const root = await assertDirectoryWithoutSymlinks(workspaceRoot, 'Workspace root');
243
+ const metadataPath = path.join(root, MARKETPLACE_SOURCE_PATH);
244
+ await readRegularJson(metadataPath, 'Workspace catalog metadata');
245
+ if (!isInside(root, await fs.realpath(metadataPath)))
246
+ throw new Error('Workspace catalog metadata resolves outside the workspace.');
247
+ const metadata = await readMarketplaceSourceMeta(root);
248
+ const pluginsRoot = await assertDirectoryWithoutSymlinks(path.join(root, 'plugins'), 'Workspace plugins directory');
249
+ if (!isInside(root, pluginsRoot))
250
+ throw new Error('Workspace plugins directory resolves outside the workspace.');
251
+ const plugins = [];
252
+ for (const entry of (await fs.readdir(pluginsRoot, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name))) {
253
+ if (entry.name === path.basename(metadataPath) || !entry.isDirectory()) {
254
+ if (entry.isSymbolicLink())
255
+ throw new Error(`Workspace plugin source must not be a symbolic link: ${entry.name}`);
256
+ continue;
257
+ }
258
+ const pluginRoot = path.join(pluginsRoot, entry.name);
259
+ const pluginStat = await fs.lstat(pluginRoot);
260
+ if (!pluginStat.isDirectory() || pluginStat.isSymbolicLink() || !isInside(root, await fs.realpath(pluginRoot))) {
261
+ throw new Error(`Workspace plugin source must be a regular in-workspace directory: ${pluginRoot}`);
262
+ }
263
+ const manifestDir = path.join(pluginRoot, path.dirname(SOURCE_MANIFEST_PATH));
264
+ const manifestPath = path.join(pluginRoot, SOURCE_MANIFEST_PATH);
265
+ await assertDirectoryWithoutSymlinks(manifestDir, 'Plugin manifest directory');
266
+ await readRegularJson(manifestPath, 'Plugin manifest');
267
+ if (!isInside(root, await fs.realpath(manifestPath)))
268
+ throw new Error(`Plugin manifest resolves outside the workspace: ${manifestPath}`);
269
+ const plugin = await readPluginManifest(pluginRoot, { mode: 'source', reservedImports: options.reservedImports });
270
+ if (plugin.manifest.name !== entry.name) {
271
+ throw new Error(`Plugin directory name ${JSON.stringify(entry.name)} must match manifest name ${JSON.stringify(plugin.manifest.name)}.`);
272
+ }
273
+ plugins.push(plugin);
274
+ }
275
+ return { workspaceRoot: root, metadataPath, metadata, plugins };
276
+ }
277
+ export async function setWorkspaceCatalogVersion(workspaceRoot, version, options = {}) {
278
+ const current = await readPluginWorkspace(workspaceRoot, options);
279
+ const metadata = {
280
+ ...current.metadata,
281
+ catalog: { version: assertCatalogVersion(version, 'catalog version') },
282
+ };
283
+ await writeJsonAtomic(current.metadataPath, metadata);
284
+ return metadata;
285
+ }
286
+ export function deriveCatalogChannel(version) {
287
+ const parsed = assertCatalogVersion(version, 'catalog version');
288
+ return parsed.includes('-') ? 'beta' : 'latest';
289
+ }
290
+ async function replaceGeneratedOutput(options) {
291
+ const parent = path.dirname(options.outputRoot);
292
+ await fs.ensureDir(parent);
293
+ const stagingRoot = path.join(parent, `.${path.basename(options.outputRoot)}.aw-staging-${crypto.randomUUID()}`);
294
+ const backupRoot = path.join(parent, `.${path.basename(options.outputRoot)}.aw-backup-${crypto.randomUUID()}`);
295
+ const exists = await fs.pathExists(options.outputRoot);
296
+ if (exists && !options.force)
297
+ throw new Error(`Output already exists: ${options.outputRoot}`);
298
+ if (exists && options.assertReplaceable)
299
+ await options.assertReplaceable(options.outputRoot);
300
+ await fs.ensureDir(stagingRoot);
301
+ try {
302
+ await options.build(stagingRoot);
303
+ await options.validate(stagingRoot);
304
+ if (exists)
305
+ await fs.rename(options.outputRoot, backupRoot);
306
+ try {
307
+ await fs.rename(stagingRoot, options.outputRoot);
308
+ }
309
+ catch (error) {
310
+ if (exists)
311
+ await fs.rename(backupRoot, options.outputRoot);
312
+ throw error;
313
+ }
314
+ if (exists)
315
+ await fs.remove(backupRoot);
316
+ }
317
+ catch (error) {
318
+ await fs.remove(stagingRoot);
319
+ throw error;
320
+ }
321
+ }
322
+ function assertDependencyGraph(plugins) {
323
+ const byName = new Map(plugins.map((plugin) => [plugin.manifest.name, plugin]));
324
+ const visiting = new Set();
325
+ const visited = new Set();
326
+ const visit = (name) => {
327
+ if (visiting.has(name))
328
+ throw new Error(`Plugin dependency cycle includes ${JSON.stringify(name)}.`);
329
+ if (visited.has(name))
330
+ return;
331
+ const plugin = byName.get(name);
332
+ if (!plugin)
333
+ throw new Error(`Missing same-source plugin dependency ${JSON.stringify(name)}.`);
334
+ visiting.add(name);
335
+ for (const dependency of plugin.manifest.dependencies ?? [])
336
+ visit(dependency);
337
+ visiting.delete(name);
338
+ visited.add(name);
339
+ };
340
+ for (const plugin of plugins)
341
+ visit(plugin.manifest.name);
342
+ }
343
+ function orderPlugins(plugins) {
344
+ assertDependencyGraph(plugins);
345
+ const result = [];
346
+ const seen = new Set();
347
+ const byName = new Map(plugins.map((plugin) => [plugin.manifest.name, plugin]));
348
+ const visit = (plugin) => {
349
+ if (seen.has(plugin.manifest.name))
350
+ return;
351
+ for (const name of [...(plugin.manifest.dependencies ?? [])].sort())
352
+ visit(byName.get(name));
353
+ seen.add(plugin.manifest.name);
354
+ result.push(plugin);
355
+ };
356
+ for (const plugin of [...plugins].sort((left, right) => left.manifest.name.localeCompare(right.manifest.name)))
357
+ visit(plugin);
358
+ return result;
359
+ }
360
+ function stableStringify(value) {
361
+ if (Array.isArray(value))
362
+ return `[${value.map(stableStringify).join(',')}]`;
363
+ if (value && typeof value === 'object') {
364
+ const record = value;
365
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(',')}}`;
366
+ }
367
+ return JSON.stringify(value);
368
+ }
369
+ function sha256(value) {
370
+ return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
371
+ }
372
+ function publishedContentDigest(manifest) {
373
+ return sha256(stableStringify({
374
+ schemaIdentity: { schemaVersion: manifest.schemaVersion },
375
+ marketplaceMetadata: {
376
+ name: 'aw',
377
+ owner: manifest.marketplace.owner,
378
+ interface: manifest.marketplace.interface,
379
+ },
380
+ compatibility: manifest.compatibility,
381
+ plugins: manifest.entries.map((entry) => ({
382
+ name: entry.name,
383
+ targetVersion: entry.version.replace(/-beta\.[1-9]\d*$/u, ''),
384
+ contentDigest: entry.contentDigest,
385
+ })).sort((left, right) => left.name.localeCompare(right.name)),
386
+ }));
387
+ }
388
+ function sourceIdentity(metadata, plugins) {
389
+ return sha256(stableStringify({
390
+ metadata,
391
+ plugins: [...plugins]
392
+ .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
393
+ .map((plugin) => plugin.manifest),
394
+ }));
395
+ }
396
+ function generatedOutputMarker(options) {
397
+ return {
398
+ schemaVersion: 1,
399
+ kind: options.kind,
400
+ workspaceIdentity: options.workspaceIdentity,
401
+ sourceIdentity: options.sourceIdentity,
402
+ ...(options.sourceName ? { sourceName: options.sourceName } : {}),
403
+ };
404
+ }
405
+ async function listBundleFiles(root, relative = '') {
406
+ const result = [];
407
+ for (const entry of await fs.readdir(path.join(root, relative), { withFileTypes: true })) {
408
+ const child = relative ? path.posix.join(relative, entry.name) : entry.name;
409
+ if (entry.isDirectory())
410
+ result.push(...await listBundleFiles(root, child));
411
+ else
412
+ result.push(child);
413
+ }
414
+ return result.sort();
415
+ }
416
+ async function catalogPathRecord(root, relative, versionNeutral) {
417
+ const filePath = path.join(root, ...relative.split('/'));
418
+ const stat = await fs.lstat(filePath);
419
+ if (stat.isSymbolicLink()) {
420
+ return { path: relative, kind: 'symlink', mode: '120000', digest: sha256(await fs.readlink(filePath)) };
421
+ }
422
+ if (!stat.isFile())
423
+ throw new Error(`Catalog digest path must be a regular file or symlink: ${filePath}.`);
424
+ const manifests = new Set(['.agents-plugin/plugin.json', '.claude-plugin/plugin.json', '.codex-plugin/plugin.json']);
425
+ const digest = versionNeutral && manifests.has(relative)
426
+ ? sha256(stableStringify({ ...await fs.readJson(filePath), version: '<plugin-version>' }))
427
+ : sha256(await fs.readFile(filePath));
428
+ return { path: relative, kind: 'file', mode: (stat.mode & 0o100) === 0 ? '100644' : '100755', digest };
429
+ }
430
+ async function calculateEntryDigest(root, versionNeutral) {
431
+ const files = await listBundleFiles(root);
432
+ return sha256(stableStringify(await Promise.all(files.map((file) => catalogPathRecord(root, file, versionNeutral)))));
433
+ }
434
+ async function expectedCatalogEntry(plugin, pluginRoot) {
435
+ return {
436
+ ...createMarketplaceManifestEntry(plugin),
437
+ version: plugin.manifest.version,
438
+ compatibility: plugin.manifest.compatibility,
439
+ artifactDigest: await calculateEntryDigest(pluginRoot, false),
440
+ manifestDigest: sha256(normalizeManifestDigestInput(plugin.manifest)),
441
+ contentDigest: await calculateEntryDigest(pluginRoot, true),
442
+ };
443
+ }
444
+ function normalizeCatalogRelease(raw, catalogVersion) {
445
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
446
+ throw new Error('Invalid catalog manifest release: expected an object.');
447
+ }
448
+ const release = raw;
449
+ if (release.state === 'development') {
450
+ if (Object.keys(release).join('\0') !== 'state') {
451
+ throw new Error('Invalid development catalog release: unknown field.');
452
+ }
453
+ return { state: 'development' };
454
+ }
455
+ const allowed = new Set(['state', 'targetVersion', 'planId', 'sourceHead', 'contentDigest']);
456
+ if (release.state !== 'published' || Object.keys(release).some((key) => !allowed.has(key))) {
457
+ throw new Error('Invalid published catalog release.');
458
+ }
459
+ const targetVersion = assertStrictSemVer(release.targetVersion, 'catalog release targetVersion');
460
+ if (targetVersion.includes('-') || targetVersion.includes('+')) {
461
+ throw new Error('Invalid catalog release targetVersion: expected a stable version.');
462
+ }
463
+ if (targetVersion !== catalogVersion.replace(/-beta\.[1-9]\d*$/u, '')) {
464
+ throw new Error('Invalid catalog release targetVersion: must match catalogVersion.');
465
+ }
466
+ const digestPattern = /^sha256:[0-9a-f]{64}$/u;
467
+ if (typeof release.planId !== 'string' || !digestPattern.test(release.planId)) {
468
+ throw new Error('Invalid catalog release planId.');
469
+ }
470
+ if (typeof release.contentDigest !== 'string' || !digestPattern.test(release.contentDigest)) {
471
+ throw new Error('Invalid catalog release contentDigest.');
472
+ }
473
+ if (typeof release.sourceHead !== 'string' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(release.sourceHead)) {
474
+ throw new Error('Invalid catalog release sourceHead.');
475
+ }
476
+ return {
477
+ state: 'published',
478
+ targetVersion,
479
+ planId: release.planId,
480
+ sourceHead: release.sourceHead,
481
+ contentDigest: release.contentDigest,
482
+ };
483
+ }
484
+ export async function buildWorkspaceCatalog(options) {
485
+ const workspace = await readPluginWorkspace(options.workspaceRoot, { reservedImports: options.reservedImports });
486
+ const plugins = orderPlugins(workspace.plugins);
487
+ const resolved = await resolveWorkspaceOutputPath({
488
+ workspaceRoot: workspace.workspaceRoot,
489
+ outputPath: options.outputPath ?? DEFAULT_CATALOG_OUTPUT_PATH,
490
+ kind: 'catalog',
491
+ });
492
+ const catalogVersion = workspace.metadata.catalog.version;
493
+ const channel = deriveCatalogChannel(catalogVersion);
494
+ const pluginNames = plugins.map((plugin) => plugin.manifest.name).sort();
495
+ const identity = sourceIdentity(workspace.metadata, plugins);
496
+ const marker = generatedOutputMarker({
497
+ kind: 'catalog',
498
+ workspaceIdentity: await readWorkspaceOwnershipIdentity(workspace.workspaceRoot),
499
+ sourceIdentity: identity,
500
+ });
501
+ await replaceGeneratedOutput({
502
+ outputRoot: resolved.outputRoot,
503
+ force: options.force === true,
504
+ build: async (stagingRoot) => {
505
+ const bundledPlugins = [];
506
+ for (const plugin of plugins) {
507
+ const closure = dependencyClosure(plugin, plugins);
508
+ const outputRoot = path.join(stagingRoot, 'plugins', plugin.manifest.name);
509
+ await fs.ensureDir(outputRoot);
510
+ await options.adapter.build({
511
+ workspaceRoot: workspace.workspaceRoot,
512
+ plugin,
513
+ plugins,
514
+ dependencies: closure.slice(0, -1),
515
+ outputRoot,
516
+ });
517
+ bundledPlugins.push(await readPluginManifest(outputRoot, { mode: 'bundle' }));
518
+ }
519
+ const byPlatform = (platform) => bundledPlugins
520
+ .filter((plugin) => resolveSupportedPlatforms(plugin.manifest).has(platform))
521
+ .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name));
522
+ await fs.outputJson(path.join(stagingRoot, '.agents', 'plugins', 'marketplace.json'), createMarketplaceManifest(workspace.metadata, byPlatform('codex')), { spaces: 2 });
523
+ await fs.outputJson(path.join(stagingRoot, '.claude-plugin', 'marketplace.json'), createMarketplaceManifest(workspace.metadata, byPlatform('claude')), { spaces: 2 });
524
+ let minVersion = '0.0.0';
525
+ for (const plugin of bundledPlugins) {
526
+ const candidate = plugin.manifest.compatibility.aw.minVersion;
527
+ if (compareStrictSemVer(candidate, minVersion) > 0)
528
+ minVersion = candidate;
529
+ }
530
+ const entries = await Promise.all([...bundledPlugins]
531
+ .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
532
+ .map((plugin) => expectedCatalogEntry(plugin, plugin.rootDir)));
533
+ const manifest = {
534
+ schemaVersion: 4,
535
+ catalogVersion,
536
+ channel,
537
+ compatibility: { aw: { minVersion } },
538
+ marketplace: {
539
+ owner: workspace.metadata.owner,
540
+ interface: workspace.metadata.interface,
541
+ },
542
+ release: { state: 'development' },
543
+ entries,
544
+ };
545
+ await fs.writeJson(path.join(stagingRoot, CATALOG_MANIFEST_FILENAME), manifest, { spaces: 2 });
546
+ await fs.writeJson(path.join(stagingRoot, GENERATED_OUTPUT_MARKER_FILENAME), marker, { spaces: 2 });
547
+ },
548
+ validate: async (stagingRoot) => { await validateCatalogSnapshot(stagingRoot); },
549
+ assertReplaceable: async (existingRoot) => {
550
+ await validateCatalogSnapshot(existingRoot);
551
+ await assertGeneratedOutput(existingRoot, marker);
552
+ },
553
+ });
554
+ return { outputRoot: resolved.outputRoot, catalogVersion, channel, pluginNames };
555
+ }
556
+ async function rewritePublishedPluginVersions(snapshotRoot, overrides) {
557
+ for (const [pluginName, version] of Object.entries(overrides)) {
558
+ assertStrictSemVer(version, `${pluginName} published version`);
559
+ for (const relative of [
560
+ '.agents-plugin/plugin.json',
561
+ '.claude-plugin/plugin.json',
562
+ '.codex-plugin/plugin.json',
563
+ ]) {
564
+ const manifestPath = path.join(snapshotRoot, 'plugins', pluginName, relative);
565
+ if (!(await fs.pathExists(manifestPath)))
566
+ continue;
567
+ const manifest = await fs.readJson(manifestPath);
568
+ await fs.writeFile(manifestPath, `${JSON.stringify({ ...manifest, version }, null, 2)}\n`);
569
+ }
570
+ }
571
+ }
572
+ export async function validatePublishedCatalogSnapshot(snapshotDirectory) {
573
+ const validation = await validateCatalogSnapshot(snapshotDirectory);
574
+ if (validation.manifest.release.state !== 'published') {
575
+ throw new Error('Published Catalog snapshot must have release.state published.');
576
+ }
577
+ if (!validation.ledger)
578
+ throw new Error('Published Catalog snapshot must include a plugin release ledger.');
579
+ return { ...validation, ledger: validation.ledger };
580
+ }
581
+ export async function preparePublishedWorkspaceCatalog(options) {
582
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(options.sourceHead)) {
583
+ throw new Error('Published Catalog sourceHead must be a committed Git object ID.');
584
+ }
585
+ const workspace = await readPluginWorkspace(options.workspaceRoot, { reservedImports: options.reservedImports });
586
+ for (const plugin of workspace.plugins) {
587
+ assertStablePluginVersion(plugin.manifest.version, `${plugin.manifest.name} source version`);
588
+ }
589
+ const catalogVersion = workspace.metadata.catalog.version;
590
+ const channel = deriveCatalogChannel(catalogVersion);
591
+ const previous = options.previousSnapshotRoot
592
+ ? await validatePublishedCatalogSnapshot(options.previousSnapshotRoot)
593
+ : null;
594
+ if (previous && compareStrictSemVer(catalogVersion, previous.manifest.catalogVersion) <= 0) {
595
+ throw new Error(`Catalog version ${catalogVersion} must be higher than published ${previous.manifest.catalogVersion}.`);
596
+ }
597
+ const previousLedger = previous?.ledger ?? {
598
+ schemaVersion: PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION,
599
+ records: [],
600
+ };
601
+ const temporaryRelative = path.join('.aw', 'publish', `.catalog-preparation-${crypto.randomUUID()}`);
602
+ const finalRelative = options.outputPath
603
+ ?? `${DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH}-v${catalogVersion}`;
604
+ let temporaryRoot = path.resolve(workspace.workspaceRoot, temporaryRelative);
605
+ try {
606
+ const built = await buildWorkspaceCatalog({
607
+ workspaceRoot: workspace.workspaceRoot,
608
+ outputPath: temporaryRelative,
609
+ adapter: options.adapter,
610
+ reservedImports: options.reservedImports,
611
+ });
612
+ temporaryRoot = built.outputRoot;
613
+ const development = await validateCatalogSnapshot(temporaryRoot);
614
+ const contentDigests = new Map(development.manifest.entries.map((entry) => [entry.name, entry.contentDigest]));
615
+ const overrides = channel === 'beta'
616
+ ? resolveBetaPluginVersionOverrides({
617
+ plugins: workspace.plugins,
618
+ contentDigests,
619
+ ledger: previousLedger,
620
+ })
621
+ : Object.fromEntries(workspace.plugins.map((plugin) => [plugin.manifest.name, plugin.manifest.version]));
622
+ await rewritePublishedPluginVersions(temporaryRoot, overrides);
623
+ const bundledPlugins = await Promise.all(workspace.plugins.map((plugin) => readPluginManifest(path.join(temporaryRoot, 'plugins', plugin.manifest.name), { mode: 'bundle' })));
624
+ const entries = await Promise.all(bundledPlugins.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
625
+ .map((plugin) => expectedCatalogEntry(plugin, plugin.rootDir)));
626
+ const manifestWithoutRelease = {
627
+ ...development.manifest,
628
+ entries,
629
+ };
630
+ const contentDigest = publishedContentDigest(manifestWithoutRelease);
631
+ const manifest = {
632
+ ...manifestWithoutRelease,
633
+ release: {
634
+ state: 'published',
635
+ targetVersion: catalogVersion.replace(/-beta\.[1-9]\d*$/u, ''),
636
+ planId: contentDigest,
637
+ sourceHead: options.sourceHead,
638
+ contentDigest,
639
+ },
640
+ };
641
+ const ledger = appendCatalogToPluginReleaseLedger({
642
+ previous: previousLedger,
643
+ manifest,
644
+ contentDigests,
645
+ });
646
+ await Promise.all([
647
+ fs.writeFile(path.join(temporaryRoot, CATALOG_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`),
648
+ fs.writeFile(path.join(temporaryRoot, PLUGIN_RELEASE_LEDGER_FILENAME), `${JSON.stringify(ledger, null, 2)}\n`),
649
+ ]);
650
+ const prepared = await validatePublishedCatalogSnapshot(temporaryRoot);
651
+ const resolved = await resolveWorkspaceOutputPath({
652
+ workspaceRoot: workspace.workspaceRoot,
653
+ outputPath: finalRelative,
654
+ kind: 'catalog',
655
+ });
656
+ const marker = await fs.readJson(path.join(temporaryRoot, GENERATED_OUTPUT_MARKER_FILENAME));
657
+ await replaceGeneratedOutput({
658
+ outputRoot: resolved.outputRoot,
659
+ force: true,
660
+ build: (stagingRoot) => fs.copy(temporaryRoot, stagingRoot, { dereference: false }),
661
+ validate: async (stagingRoot) => { await validatePublishedCatalogSnapshot(stagingRoot); },
662
+ assertReplaceable: async (existingRoot) => {
663
+ await validatePublishedCatalogSnapshot(existingRoot);
664
+ await assertGeneratedOutput(existingRoot, marker);
665
+ },
666
+ });
667
+ return {
668
+ outputRoot: resolved.outputRoot,
669
+ catalogVersion,
670
+ channel,
671
+ pluginNames: built.pluginNames,
672
+ snapshotDigest: prepared.snapshotDigest,
673
+ contentDigest,
674
+ planId: contentDigest,
675
+ };
676
+ }
677
+ finally {
678
+ await fs.remove(temporaryRoot);
679
+ }
680
+ }
681
+ function dependencyClosure(plugin, all) {
682
+ const byName = new Map(all.map((entry) => [entry.manifest.name, entry]));
683
+ const result = [];
684
+ const visited = new Set();
685
+ const visit = (entry) => {
686
+ if (visited.has(entry.manifest.name))
687
+ return;
688
+ for (const name of [...(entry.manifest.dependencies ?? [])].sort()) {
689
+ const dependency = byName.get(name);
690
+ if (!dependency)
691
+ throw new Error(`Missing same-source plugin dependency ${JSON.stringify(name)}.`);
692
+ visit(dependency);
693
+ }
694
+ visited.add(entry.manifest.name);
695
+ result.push(entry);
696
+ };
697
+ visit(plugin);
698
+ return result;
699
+ }
700
+ export async function buildWorkspacePlugin(options) {
701
+ const workspace = await readPluginWorkspace(options.workspaceRoot, { reservedImports: options.reservedImports });
702
+ assertDependencyGraph(workspace.plugins);
703
+ const requestedPluginRoot = path.resolve(workspace.workspaceRoot, options.pluginDirectory);
704
+ const pluginRoot = await fs.realpath(requestedPluginRoot);
705
+ if (!isInside(workspace.workspaceRoot, pluginRoot))
706
+ throw new Error('Plugin directory must stay inside the workspace.');
707
+ const plugin = workspace.plugins.find((entry) => entry.rootDir === pluginRoot);
708
+ if (!plugin) {
709
+ throw new Error(`Plugin directory is not part of this workspace source: ${pluginRoot}`);
710
+ }
711
+ const closure = dependencyClosure(plugin, workspace.plugins);
712
+ const identity = sha256(normalizeManifestDigestInput(plugin.manifest));
713
+ const marker = generatedOutputMarker({
714
+ kind: 'plugin',
715
+ workspaceIdentity: await readWorkspaceOwnershipIdentity(workspace.workspaceRoot),
716
+ sourceIdentity: identity,
717
+ sourceName: plugin.manifest.name,
718
+ });
719
+ const resolved = await resolveWorkspaceOutputPath({
720
+ workspaceRoot: workspace.workspaceRoot,
721
+ outputPath: options.outputPath ?? path.join(DEFAULT_PLUGIN_OUTPUT_PATH, plugin.manifest.name),
722
+ kind: 'plugin',
723
+ });
724
+ await replaceGeneratedOutput({
725
+ outputRoot: resolved.outputRoot,
726
+ force: options.force === true,
727
+ build: (stagingRoot) => options.adapter.build({
728
+ workspaceRoot: workspace.workspaceRoot,
729
+ plugin,
730
+ plugins: workspace.plugins,
731
+ dependencies: closure.slice(0, -1),
732
+ outputRoot: stagingRoot,
733
+ }).then(() => fs.writeJson(path.join(stagingRoot, GENERATED_OUTPUT_MARKER_FILENAME), marker, { spaces: 2 })),
734
+ validate: async (stagingRoot) => {
735
+ await readPluginManifest(stagingRoot, { mode: 'bundle' });
736
+ await assertGeneratedOutput(stagingRoot, marker);
737
+ },
738
+ assertReplaceable: async (existingRoot) => {
739
+ await assertGeneratedOutput(existingRoot, marker);
740
+ },
741
+ });
742
+ const digest = await digestDirectory(resolved.outputRoot);
743
+ return {
744
+ outputRoot: resolved.outputRoot,
745
+ pluginName: plugin.manifest.name,
746
+ dependencies: closure.slice(0, -1).map((entry) => entry.manifest.name),
747
+ digest,
748
+ };
749
+ }
750
+ async function assertGeneratedOutput(root, expected) {
751
+ const markerPath = path.join(root, GENERATED_OUTPUT_MARKER_FILENAME);
752
+ const stat = await fs.lstat(markerPath).catch((error) => {
753
+ if (error.code === 'ENOENT')
754
+ return null;
755
+ throw error;
756
+ });
757
+ if (!stat?.isFile() || stat.isSymbolicLink()) {
758
+ throw new Error(`Refusing to replace foreign plugin output: ${root}`);
759
+ }
760
+ const marker = await fs.readJson(markerPath);
761
+ const expectedKeys = expected.sourceName === undefined
762
+ ? ['kind', 'schemaVersion', 'sourceIdentity', 'workspaceIdentity']
763
+ : ['kind', 'schemaVersion', 'sourceIdentity', 'sourceName', 'workspaceIdentity'];
764
+ const digestPattern = /^sha256:[0-9a-f]{64}$/u;
765
+ if (!isDeepStrictEqual(Object.keys(marker).sort(), expectedKeys)
766
+ || marker.schemaVersion !== 1
767
+ || typeof marker.workspaceIdentity !== 'string'
768
+ || !digestPattern.test(marker.workspaceIdentity)
769
+ || typeof marker.sourceIdentity !== 'string'
770
+ || !digestPattern.test(marker.sourceIdentity)) {
771
+ throw new Error(`Refusing to replace foreign plugin output: ${root}`);
772
+ }
773
+ const ownershipFields = ['schemaVersion', 'kind', 'workspaceIdentity', 'sourceName'];
774
+ if (ownershipFields.some((field) => marker[field] !== expected[field])) {
775
+ throw new Error(`Refusing to replace plugin output with a different identity: ${root}`);
776
+ }
777
+ }
778
+ async function digestDirectory(root) {
779
+ const entries = [];
780
+ const walk = async (directory) => {
781
+ for (const name of (await fs.readdir(directory)).sort()) {
782
+ const filePath = path.join(directory, name);
783
+ const relative = path.relative(root, filePath).split(path.sep).join('/');
784
+ if (relative === GENERATED_OUTPUT_MARKER_FILENAME)
785
+ continue;
786
+ const stat = await fs.lstat(filePath);
787
+ if (stat.isSymbolicLink())
788
+ throw new Error(`Generated output contains a symbolic link: ${relative}`);
789
+ if (stat.isDirectory())
790
+ await walk(filePath);
791
+ else if (stat.isFile())
792
+ entries.push(`${relative}\0${crypto.createHash('sha256').update(await fs.readFile(filePath)).digest('hex')}`);
793
+ else
794
+ throw new Error(`Generated output contains an unsupported entry: ${relative}`);
795
+ }
796
+ };
797
+ await walk(root);
798
+ return `sha256:${crypto.createHash('sha256').update(entries.join('\n')).digest('hex')}`;
799
+ }
800
+ export async function validateCatalogSnapshot(snapshotDirectory) {
801
+ const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot');
802
+ const allowed = new Set([
803
+ CATALOG_MANIFEST_FILENAME,
804
+ GENERATED_OUTPUT_MARKER_FILENAME,
805
+ '.agents',
806
+ '.claude-plugin',
807
+ 'plugins',
808
+ 'plugin-release-ledger.json',
809
+ ]);
810
+ const entries = await fs.readdir(rootDir);
811
+ const unexpected = entries.filter((entry) => !allowed.has(entry));
812
+ const required = [CATALOG_MANIFEST_FILENAME, '.agents', '.claude-plugin', 'plugins'];
813
+ const missing = required.filter((entry) => !entries.includes(entry));
814
+ if (unexpected.length > 0 || missing.length > 0) {
815
+ throw new Error(`Invalid catalog snapshot root layout: missing [${missing.join(', ')}], unexpected [${unexpected.join(', ')}].`);
816
+ }
817
+ if (entries.includes(GENERATED_OUTPUT_MARKER_FILENAME)) {
818
+ const marker = await readRegularJson(path.join(rootDir, GENERATED_OUTPUT_MARKER_FILENAME), 'Catalog generated output marker');
819
+ const digestPattern = /^sha256:[0-9a-f]{64}$/u;
820
+ if (!isDeepStrictEqual(Object.keys(marker).sort(), [
821
+ 'kind', 'schemaVersion', 'sourceIdentity', 'workspaceIdentity',
822
+ ])
823
+ || marker.schemaVersion !== 1
824
+ || marker.kind !== 'catalog'
825
+ || typeof marker.workspaceIdentity !== 'string'
826
+ || !digestPattern.test(marker.workspaceIdentity)
827
+ || typeof marker.sourceIdentity !== 'string'
828
+ || !digestPattern.test(marker.sourceIdentity)) {
829
+ throw new Error('Invalid catalog generated output marker.');
830
+ }
831
+ }
832
+ const assertLayout = async (directory, expected) => {
833
+ const real = await assertDirectoryWithoutSymlinks(directory, 'Catalog snapshot directory');
834
+ if (!isInside(rootDir, real))
835
+ throw new Error(`Catalog snapshot directory resolves outside the snapshot: ${directory}`);
836
+ const actual = (await fs.readdir(directory)).sort();
837
+ const wanted = [...expected].sort();
838
+ if (!isDeepStrictEqual(actual, wanted)) {
839
+ throw new Error(`Invalid catalog snapshot directory layout at ${directory}.`);
840
+ }
841
+ };
842
+ await assertLayout(path.join(rootDir, '.agents'), ['plugins']);
843
+ await assertLayout(path.join(rootDir, '.agents', 'plugins'), ['marketplace.json']);
844
+ await assertLayout(path.join(rootDir, '.claude-plugin'), ['marketplace.json']);
845
+ await readRegularJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'), 'Codex marketplace');
846
+ await readRegularJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'), 'Claude marketplace');
847
+ const snapshotDigest = await digestDirectory(rootDir);
848
+ const raw = await fs.readJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME));
849
+ const allowedManifestKeys = new Set([
850
+ 'schemaVersion', 'catalogVersion', 'channel', 'compatibility', 'marketplace', 'release', 'entries',
851
+ ]);
852
+ for (const key of Object.keys(raw))
853
+ if (!allowedManifestKeys.has(key))
854
+ throw new Error(`Invalid catalog manifest: unknown field ${JSON.stringify(key)}.`);
855
+ if (raw.schemaVersion !== 4)
856
+ throw new Error('Invalid catalog manifest schemaVersion: expected 4.');
857
+ const base = normalizeCatalogManifestBase(raw, 'catalog manifest v4');
858
+ const marketplaceIdentity = normalizeCatalogMarketplaceIdentity(raw.marketplace);
859
+ if (!Array.isArray(raw.entries))
860
+ throw new Error('Invalid catalog manifest entries: expected an array.');
861
+ const rawEntries = raw.entries;
862
+ const contentDigests = rawEntries.map((entry, index) => {
863
+ if (typeof entry.contentDigest !== 'string' || !/^sha256:[0-9a-f]{64}$/u.test(entry.contentDigest)) {
864
+ throw new Error(`Invalid catalog manifest entries[${index}].contentDigest.`);
865
+ }
866
+ return entry.contentDigest;
867
+ });
868
+ const entriesWithoutContentDigest = rawEntries.map((entry) => {
869
+ const copy = { ...entry };
870
+ delete copy.contentDigest;
871
+ return copy;
872
+ });
873
+ const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest v4')
874
+ .map((entry, index) => ({ ...entry, contentDigest: contentDigests[index] }));
875
+ const pluginNames = normalizedEntries.map((entry, index) => {
876
+ if (typeof entry.name !== 'string')
877
+ throw new Error(`Invalid catalog manifest entries[${index}].name.`);
878
+ return entry.name;
879
+ });
880
+ if (new Set(pluginNames).size !== pluginNames.length)
881
+ throw new Error('Invalid catalog manifest entries: duplicate plugin name.');
882
+ if (pluginNames.join('\0') !== [...pluginNames].sort().join('\0'))
883
+ throw new Error('Invalid catalog manifest entries: expected stable name order.');
884
+ const diskNames = (await fs.readdir(path.join(rootDir, 'plugins'))).sort();
885
+ if (diskNames.join('\0') !== [...pluginNames].sort().join('\0'))
886
+ throw new Error('Invalid catalog snapshot plugin inventory.');
887
+ const plugins = [];
888
+ for (const [index, name] of pluginNames.entries()) {
889
+ const pluginRoot = path.join(rootDir, 'plugins', name);
890
+ const plugin = await readPluginManifest(pluginRoot, { mode: 'bundle' });
891
+ for (const platform of ['claude', 'codex']) {
892
+ const platformManifest = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
893
+ const exists = await fs.pathExists(platformManifest);
894
+ if (exists !== resolveSupportedPlatforms(plugin.manifest).has(platform)) {
895
+ throw new Error(`Catalog plugin ${JSON.stringify(name)} has an inconsistent ${platform} bundle projection.`);
896
+ }
897
+ if (exists) {
898
+ const rawPlatformManifest = await readRegularJson(platformManifest, `${platform} plugin manifest`);
899
+ await validatePlatformManifestProjection(plugin, platform, rawPlatformManifest);
900
+ }
901
+ }
902
+ const expected = await expectedCatalogEntry(plugin, pluginRoot);
903
+ for (const field of Object.keys(expected)) {
904
+ if (!isDeepStrictEqual(normalizedEntries[index][field], expected[field])) {
905
+ throw new Error(`Catalog entry ${field} for ${JSON.stringify(name)} does not match its plugin bundle.`);
906
+ }
907
+ }
908
+ plugins.push(plugin);
909
+ }
910
+ assertDependencyGraph(plugins);
911
+ const agentsMarketplace = await fs.readJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'));
912
+ const claudeMarketplace = await fs.readJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'));
913
+ const marketplaceMetadata = {
914
+ owner: marketplaceIdentity.owner,
915
+ interface: marketplaceIdentity.interface,
916
+ catalog: { version: base.catalogVersion },
917
+ };
918
+ const expectedAgents = createMarketplaceManifest(marketplaceMetadata, plugins.filter((plugin) => resolveSupportedPlatforms(plugin.manifest).has('codex')));
919
+ const expectedClaude = createMarketplaceManifest(marketplaceMetadata, plugins.filter((plugin) => resolveSupportedPlatforms(plugin.manifest).has('claude')));
920
+ const normalizeMarketplaceOrder = (marketplace) => {
921
+ const canonical = JSON.parse(JSON.stringify(marketplace));
922
+ return {
923
+ ...canonical,
924
+ plugins: Array.isArray(canonical.plugins)
925
+ ? [...canonical.plugins].sort((left, right) => String(left?.name).localeCompare(String(right?.name)))
926
+ : canonical.plugins,
927
+ };
928
+ };
929
+ if (!isDeepStrictEqual(normalizeMarketplaceOrder(agentsMarketplace), normalizeMarketplaceOrder(expectedAgents))) {
930
+ throw new Error('Codex marketplace metadata or plugin inventory does not match the snapshot bundles.');
931
+ }
932
+ if (!isDeepStrictEqual(normalizeMarketplaceOrder(claudeMarketplace), normalizeMarketplaceOrder(expectedClaude))) {
933
+ throw new Error('Claude marketplace metadata or plugin inventory does not match the snapshot bundles.');
934
+ }
935
+ const release = normalizeCatalogRelease(raw.release, base.catalogVersion);
936
+ const manifest = {
937
+ schemaVersion: 4,
938
+ catalogVersion: base.catalogVersion,
939
+ channel: base.channel,
940
+ compatibility: base.compatibility,
941
+ marketplace: marketplaceIdentity,
942
+ release,
943
+ entries: normalizedEntries,
944
+ };
945
+ if (release.state === 'published') {
946
+ const contentDigest = publishedContentDigest(manifest);
947
+ if (release.contentDigest !== contentDigest) {
948
+ throw new Error('Published catalog release contentDigest does not match the snapshot.');
949
+ }
950
+ }
951
+ const hasLedger = entries.includes(PLUGIN_RELEASE_LEDGER_FILENAME);
952
+ if (release.state === 'published' && !hasLedger) {
953
+ throw new Error('Published Catalog snapshot must include plugin-release-ledger.json.');
954
+ }
955
+ if (release.state === 'development' && hasLedger) {
956
+ throw new Error('Development Catalog snapshot must not include plugin-release-ledger.json.');
957
+ }
958
+ const ledger = hasLedger
959
+ ? validatePluginReleaseLedgerAgainstManifest(await readRegularJson(path.join(rootDir, PLUGIN_RELEASE_LEDGER_FILENAME), 'Plugin release ledger'), manifest)
960
+ : undefined;
961
+ return { rootDir, manifest, ...(ledger ? { ledger } : {}), snapshotDigest };
962
+ }
963
+ //# sourceMappingURL=workspace.js.map