@lesliechan721/aw-plugin-core 0.1.0-beta.3 → 0.1.0-beta.31

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 (43) hide show
  1. package/dist/aw-capabilities.d.ts +27 -0
  2. package/dist/aw-capabilities.js +108 -0
  3. package/dist/aw-capabilities.js.map +1 -0
  4. package/dist/catalog-workspace-resolution.d.ts +3 -0
  5. package/dist/catalog-workspace-resolution.js +34 -0
  6. package/dist/catalog-workspace-resolution.js.map +1 -0
  7. package/dist/generated/plugin-manifest-v3.d.ts +675 -0
  8. package/dist/generated/{plugin-manifest-v1.js → plugin-manifest-v3.js} +1 -1
  9. package/dist/generated/plugin-manifest-v3.js.map +1 -0
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/manifest-contract.d.ts +3 -6
  14. package/dist/manifest-contract.js +209 -406
  15. package/dist/manifest-contract.js.map +1 -1
  16. package/dist/manifest-type-contract.d.ts +2 -2
  17. package/dist/manifest.d.ts +10 -8
  18. package/dist/manifest.js +11 -7
  19. package/dist/manifest.js.map +1 -1
  20. package/dist/plugin-authoring-capabilities.d.ts +21 -0
  21. package/dist/plugin-authoring-capabilities.js +187 -0
  22. package/dist/plugin-authoring-capabilities.js.map +1 -0
  23. package/dist/plugin-config.d.ts +13 -0
  24. package/dist/plugin-config.js +645 -0
  25. package/dist/plugin-config.js.map +1 -0
  26. package/dist/plugin-requirements.d.ts +34 -0
  27. package/dist/plugin-requirements.js +250 -0
  28. package/dist/plugin-requirements.js.map +1 -0
  29. package/dist/plugin-script-contract.d.ts +10 -12
  30. package/dist/plugin-script-contract.js +34 -64
  31. package/dist/plugin-script-contract.js.map +1 -1
  32. package/dist/types.d.ts +142 -62
  33. package/dist/workspace.d.ts +54 -29
  34. package/dist/workspace.js +497 -269
  35. package/dist/workspace.js.map +1 -1
  36. package/package.json +8 -4
  37. package/schemas/plugin-manifest-v3.json +590 -0
  38. package/dist/generated/plugin-manifest-v1.d.ts +0 -368
  39. package/dist/generated/plugin-manifest-v1.js.map +0 -1
  40. package/dist/plugin-release.d.ts +0 -30
  41. package/dist/plugin-release.js +0 -187
  42. package/dist/plugin-release.js.map +0 -1
  43. package/schemas/plugin-manifest-v1.json +0 -329
package/dist/workspace.js CHANGED
@@ -2,16 +2,17 @@ import crypto from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import { isDeepStrictEqual } from 'node:util';
4
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';
5
+ import { assertCatalogVersion, assertStrictSemVer, compareStrictSemVer, parseStrictSemVer, MARKETPLACE_SOURCE_PATH, SOURCE_MANIFEST_PATH, createMarketplaceManifest, createMarketplaceManifestEntry, normalizeCatalogManifestBase, normalizeCatalogMarketplaceIdentity, normalizeCatalogManifestEntries, normalizeManifestDigestInput, readMarketplaceSourceMeta, readPluginManifest, validatePlatformManifestProjection, } from './manifest-contract.js';
6
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';
7
+ import { createAwPluginRequirementsReceipt, normalizeAwCapabilities, parseAwPluginRequirementsReceipt, } from './aw-capabilities.js';
8
+ export const DEFAULT_CATALOG_WORKSPACE_VERSION = '0.1.0';
9
9
  export const DEFAULT_CATALOG_OUTPUT_PATH = path.join('.aw', 'catalog');
10
10
  export const DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH = path.join('.aw', 'publish', 'catalog');
11
11
  export const DEFAULT_PLUGIN_OUTPUT_PATH = path.join('.aw', 'plugins');
12
12
  export const CATALOG_MANIFEST_FILENAME = 'catalog-manifest.json';
13
13
  export const GENERATED_OUTPUT_MARKER_FILENAME = '.aw-generated.json';
14
- export const WORKSPACE_MARKER_FILENAME = '.aw-workspace.json';
14
+ export const CATALOG_WORKSPACE_MARKER_FILENAME = '.aw-workspace.json';
15
+ const AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME = 'aw-plugin-requirements.json';
15
16
  function isInside(root, candidate) {
16
17
  const relative = path.relative(root, candidate);
17
18
  return relative === '' || (relative !== '..'
@@ -47,11 +48,11 @@ async function assertNoSymlinkPath(root, target) {
47
48
  }
48
49
  }
49
50
  }
50
- export async function resolveWorkspaceOutputPath(options) {
51
- const workspaceRoot = await assertDirectoryWithoutSymlinks(options.workspaceRoot, 'Workspace root');
51
+ export async function resolveCatalogWorkspaceOutputPath(options) {
52
+ const workspaceRoot = await assertDirectoryWithoutSymlinks(options.workspaceRoot, 'Catalog Workspace root');
52
53
  const outputRoot = path.resolve(workspaceRoot, options.outputPath);
53
54
  if (!isInside(workspaceRoot, outputRoot) || outputRoot === workspaceRoot) {
54
- throw new Error(`Output must be inside the workspace: ${outputRoot}`);
55
+ throw new Error(`Output must be inside the Catalog Workspace: ${outputRoot}`);
55
56
  }
56
57
  await assertNoSymlinkPath(workspaceRoot, outputRoot);
57
58
  const protectedRoots = [
@@ -63,7 +64,7 @@ export async function resolveWorkspaceOutputPath(options) {
63
64
  if (protectedRoot.endsWith(`${path.sep}.aw`) && outputRoot !== protectedRoot)
64
65
  continue;
65
66
  if (isInside(protectedRoot, outputRoot) || isInside(outputRoot, protectedRoot)) {
66
- throw new Error(`Output overlaps a protected workspace path: ${outputRoot}`);
67
+ throw new Error(`Output overlaps a protected Catalog Workspace path: ${outputRoot}`);
67
68
  }
68
69
  }
69
70
  const otherGeneratedRoot = path.join(workspaceRoot, options.kind === 'catalog' ? DEFAULT_PLUGIN_OUTPUT_PATH : DEFAULT_CATALOG_OUTPUT_PATH);
@@ -72,24 +73,24 @@ export async function resolveWorkspaceOutputPath(options) {
72
73
  }
73
74
  return { workspaceRoot, outputRoot };
74
75
  }
75
- function safeWorkspaceName(workspaceRoot) {
76
+ function safeCatalogWorkspaceName(workspaceRoot) {
76
77
  const normalized = path.basename(workspaceRoot).toLowerCase()
77
78
  .replace(/[^a-z0-9]+/gu, '-')
78
79
  .replace(/^-+|-+$/gu, '');
79
80
  return normalized || 'aw-workspace';
80
81
  }
81
- function workspaceMetadata(options, workspaceRoot) {
82
- const workspaceName = safeWorkspaceName(workspaceRoot);
82
+ function catalogWorkspaceMetadata(options, workspaceRoot) {
83
+ const catalogWorkspaceName = safeCatalogWorkspaceName(workspaceRoot);
83
84
  return {
84
85
  owner: {
85
- name: options.ownerName?.trim() || workspaceName,
86
- url: options.ownerUrl?.trim() || `https://example.invalid/aw-workspaces/${workspaceName}`,
86
+ name: options.ownerName?.trim() || catalogWorkspaceName,
87
+ url: options.ownerUrl?.trim() || `https://example.invalid/aw-workspaces/${catalogWorkspaceName}`,
87
88
  },
88
89
  interface: {
89
- displayName: options.displayName?.trim() || workspaceName,
90
+ displayName: options.displayName?.trim() || catalogWorkspaceName,
90
91
  },
91
92
  catalog: {
92
- version: assertCatalogVersion(options.catalogVersion ?? DEFAULT_WORKSPACE_CATALOG_VERSION, 'catalog version'),
93
+ version: assertCatalogVersion(options.catalogVersion ?? DEFAULT_CATALOG_WORKSPACE_VERSION, 'catalog version'),
93
94
  },
94
95
  };
95
96
  }
@@ -115,41 +116,42 @@ async function writeTextAtomic(filePath, value) {
115
116
  throw error;
116
117
  }
117
118
  }
118
- function workspaceIdentity(workspaceRoot) {
119
- return sha256(`aw-plugin-workspace-v1\0${path.resolve(workspaceRoot)}`);
120
- }
121
- function workspaceScaffoldMarker(metadata) {
119
+ function catalogWorkspaceMarker() {
122
120
  return {
123
121
  schemaVersion: 1,
124
- kind: 'aw-plugin-workspace',
122
+ kind: 'aw-catalog-workspace',
125
123
  workspaceIdentity: sha256(crypto.randomUUID()),
126
- sourceIdentity: sha256(stableStringify(metadata)),
127
124
  };
128
125
  }
129
- function validateWorkspaceScaffoldMarker(value) {
126
+ function validateCatalogWorkspaceMarker(value) {
130
127
  const marker = value;
131
- const digestPattern = /^sha256:[0-9a-f]{64}$/u;
132
128
  if (!marker
133
129
  || typeof marker !== 'object'
134
130
  || Array.isArray(marker)
135
131
  || !isDeepStrictEqual(Object.keys(marker).sort(), [
136
- 'kind', 'schemaVersion', 'sourceIdentity', 'workspaceIdentity',
132
+ 'kind', 'schemaVersion', 'workspaceIdentity',
137
133
  ])
138
134
  || marker.schemaVersion !== 1
139
- || marker.kind !== 'aw-plugin-workspace'
135
+ || marker.kind !== 'aw-catalog-workspace'
140
136
  || 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.');
137
+ || marker.workspaceIdentity.trim().length === 0) {
138
+ throw new Error('Invalid Catalog Workspace marker.');
145
139
  }
146
140
  return { workspaceIdentity: marker.workspaceIdentity };
147
141
  }
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;
142
+ export async function readCatalogWorkspaceMarker(workspaceRoot) {
143
+ const markerPath = path.join(workspaceRoot, CATALOG_WORKSPACE_MARKER_FILENAME);
144
+ let value;
145
+ try {
146
+ value = await readRegularJson(markerPath, 'Catalog Workspace marker');
147
+ }
148
+ catch (error) {
149
+ if (error instanceof SyntaxError)
150
+ throw new Error('Invalid Catalog Workspace marker.');
151
+ throw error;
152
+ }
153
+ const marker = validateCatalogWorkspaceMarker(value);
154
+ return { markerPath, workspaceIdentity: marker.workspaceIdentity };
153
155
  }
154
156
  async function readRegularJson(filePath, label) {
155
157
  const stat = await fs.lstat(filePath).catch((error) => {
@@ -162,21 +164,24 @@ async function readRegularJson(filePath, label) {
162
164
  }
163
165
  return fs.readJson(filePath);
164
166
  }
165
- export async function initializePluginWorkspace(options) {
166
- const workspaceRoot = await assertDirectoryWithoutSymlinks(options.workspaceRoot, 'Workspace root');
167
+ export async function initializeCatalogWorkspace(options) {
168
+ const workspaceRoot = await assertDirectoryWithoutSymlinks(options.workspaceRoot, 'Catalog Workspace root');
167
169
  const pluginsRoot = path.join(workspaceRoot, 'plugins');
168
170
  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 markerPath = path.join(workspaceRoot, CATALOG_WORKSPACE_MARKER_FILENAME);
172
+ const metadata = catalogWorkspaceMetadata(options, workspaceRoot);
171
173
  const existingMetadata = await fs.pathExists(metadataPath);
172
174
  const existingMarker = await fs.pathExists(markerPath);
173
175
  if ((existingMetadata || existingMarker) && !options.force) {
174
- throw new Error(`Workspace catalog scaffold already exists: ${workspaceRoot}`);
176
+ throw new Error(`Catalog Workspace scaffold already exists: ${workspaceRoot}`);
177
+ }
178
+ if (existingMetadata && !existingMarker) {
179
+ throw new Error(`Refusing to adopt Catalog Workspace metadata without a valid marker: ${metadataPath}`);
175
180
  }
176
181
  if (await fs.pathExists(pluginsRoot)) {
177
182
  const stat = await fs.lstat(pluginsRoot);
178
183
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
179
- throw new Error(`Workspace plugins path must be a regular directory: ${pluginsRoot}`);
184
+ throw new Error(`Catalog Workspace plugins path must be a regular directory: ${pluginsRoot}`);
180
185
  }
181
186
  const entries = (await fs.readdir(pluginsRoot)).filter((entry) => entry !== path.basename(metadataPath));
182
187
  if (entries.length > 0 && !existingMarker) {
@@ -186,17 +191,18 @@ export async function initializePluginWorkspace(options) {
186
191
  if (existingMetadata) {
187
192
  const current = await readMarketplaceSourceMeta(workspaceRoot);
188
193
  if (!isDeepStrictEqual(current, metadata)) {
189
- throw new Error(`Refusing to replace different workspace catalog metadata: ${metadataPath}`);
194
+ throw new Error(`Refusing to replace different Catalog Workspace metadata: ${metadataPath}`);
190
195
  }
191
196
  }
192
- const expectedMarker = workspaceScaffoldMarker(metadata);
197
+ const expectedMarker = catalogWorkspaceMarker();
198
+ let workspaceIdentity = expectedMarker.workspaceIdentity;
193
199
  if (existingMarker) {
194
- const currentMarker = await readRegularJson(markerPath, 'Workspace marker');
200
+ const currentMarker = await readRegularJson(markerPath, 'Catalog Workspace marker');
195
201
  try {
196
- validateWorkspaceScaffoldMarker(currentMarker);
202
+ workspaceIdentity = validateCatalogWorkspaceMarker(currentMarker).workspaceIdentity;
197
203
  }
198
204
  catch {
199
- throw new Error(`Refusing to replace a workspace scaffold with a different identity: ${workspaceRoot}`);
205
+ throw new Error(`Refusing to replace a Catalog Workspace marker with a different identity: ${workspaceRoot}`);
200
206
  }
201
207
  }
202
208
  const gitignorePath = path.join(workspaceRoot, '.gitignore');
@@ -205,7 +211,7 @@ export async function initializePluginWorkspace(options) {
205
211
  if (gitignoreExists) {
206
212
  const stat = await fs.lstat(gitignorePath);
207
213
  if (!stat.isFile() || stat.isSymbolicLink()) {
208
- throw new Error(`Workspace .gitignore must be a regular file: ${gitignorePath}`);
214
+ throw new Error(`Catalog Workspace .gitignore must be a regular file: ${gitignorePath}`);
209
215
  }
210
216
  previousGitignore = await fs.readFile(gitignorePath, 'utf8');
211
217
  }
@@ -236,49 +242,82 @@ export async function initializePluginWorkspace(options) {
236
242
  await fs.remove(pluginsRoot);
237
243
  throw error;
238
244
  }
239
- return { workspaceRoot, metadataPath, metadata };
245
+ return { workspaceRoot, markerPath, workspaceIdentity, metadataPath, metadata };
240
246
  }
241
- export async function readPluginWorkspace(workspaceRoot, options = {}) {
242
- const root = await assertDirectoryWithoutSymlinks(workspaceRoot, 'Workspace root');
247
+ export async function readCatalogWorkspace(workspaceRoot, options = {}) {
248
+ const root = await assertDirectoryWithoutSymlinks(workspaceRoot, 'Catalog Workspace root');
249
+ const marker = await readCatalogWorkspaceMarker(root);
243
250
  const metadataPath = path.join(root, MARKETPLACE_SOURCE_PATH);
244
- await readRegularJson(metadataPath, 'Workspace catalog metadata');
251
+ await readRegularJson(metadataPath, 'Catalog Workspace metadata');
245
252
  if (!isInside(root, await fs.realpath(metadataPath)))
246
- throw new Error('Workspace catalog metadata resolves outside the workspace.');
253
+ throw new Error('Catalog Workspace metadata resolves outside the workspace.');
247
254
  const metadata = await readMarketplaceSourceMeta(root);
248
- const pluginsRoot = await assertDirectoryWithoutSymlinks(path.join(root, 'plugins'), 'Workspace plugins directory');
255
+ const pluginsRoot = await assertDirectoryWithoutSymlinks(path.join(root, 'plugins'), 'Catalog Workspace plugins directory');
249
256
  if (!isInside(root, pluginsRoot))
250
- throw new Error('Workspace plugins directory resolves outside the workspace.');
257
+ throw new Error('Catalog Workspace plugins directory resolves outside the workspace.');
251
258
  const plugins = [];
252
259
  for (const entry of (await fs.readdir(pluginsRoot, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name))) {
253
260
  if (entry.name === path.basename(metadataPath) || !entry.isDirectory()) {
254
261
  if (entry.isSymbolicLink())
255
- throw new Error(`Workspace plugin source must not be a symbolic link: ${entry.name}`);
262
+ throw new Error(`Catalog Workspace plugin source must not be a symbolic link: ${entry.name}`);
256
263
  continue;
257
264
  }
258
265
  const pluginRoot = path.join(pluginsRoot, entry.name);
259
266
  const pluginStat = await fs.lstat(pluginRoot);
260
267
  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}`);
268
+ throw new Error(`Catalog Workspace plugin source must be a regular in-workspace directory: ${pluginRoot}`);
262
269
  }
263
270
  const manifestDir = path.join(pluginRoot, path.dirname(SOURCE_MANIFEST_PATH));
264
271
  const manifestPath = path.join(pluginRoot, SOURCE_MANIFEST_PATH);
265
272
  await assertDirectoryWithoutSymlinks(manifestDir, 'Plugin manifest directory');
266
273
  await readRegularJson(manifestPath, 'Plugin manifest');
267
274
  if (!isInside(root, await fs.realpath(manifestPath)))
268
- throw new Error(`Plugin manifest resolves outside the workspace: ${manifestPath}`);
275
+ throw new Error(`Plugin manifest resolves outside the Catalog Workspace: ${manifestPath}`);
269
276
  const plugin = await readPluginManifest(pluginRoot, { mode: 'source', reservedImports: options.reservedImports });
270
277
  if (plugin.manifest.name !== entry.name) {
271
278
  throw new Error(`Plugin directory name ${JSON.stringify(entry.name)} must match manifest name ${JSON.stringify(plugin.manifest.name)}.`);
272
279
  }
273
280
  plugins.push(plugin);
274
281
  }
275
- return { workspaceRoot: root, metadataPath, metadata, plugins };
282
+ return {
283
+ workspaceRoot: root,
284
+ markerPath: marker.markerPath,
285
+ workspaceIdentity: marker.workspaceIdentity,
286
+ metadataPath,
287
+ metadata,
288
+ plugins,
289
+ };
290
+ }
291
+ export function resolveCatalogVersionMutation(currentVersion, mutation) {
292
+ const current = assertCatalogVersion(currentVersion, 'current catalog version');
293
+ const hasVersion = mutation.version !== undefined;
294
+ const hasBump = mutation.bump !== undefined;
295
+ if (hasVersion === hasBump) {
296
+ throw new Error('Catalog version mutation requires exactly one of version or bump.');
297
+ }
298
+ if (mutation.version !== undefined) {
299
+ return assertCatalogVersion(mutation.version, 'catalog version');
300
+ }
301
+ if (!['major', 'minor', 'patch'].includes(mutation.bump)) {
302
+ throw new Error('Catalog version bump must be major, minor, or patch.');
303
+ }
304
+ const [major, minor, patch] = current.split('-', 1)[0].split('.').map(BigInt);
305
+ if (mutation.bump === 'major')
306
+ return `${major + 1n}.0.0`;
307
+ if (mutation.bump === 'minor')
308
+ return `${major}.${minor + 1n}.0`;
309
+ return `${major}.${minor}.${patch + 1n}`;
276
310
  }
277
- export async function setWorkspaceCatalogVersion(workspaceRoot, version, options = {}) {
278
- const current = await readPluginWorkspace(workspaceRoot, options);
311
+ export async function mutateCatalogWorkspaceVersion(workspaceRoot, mutation, options = {}) {
312
+ const current = await readCatalogWorkspace(workspaceRoot, options);
313
+ const version = resolveCatalogVersionMutation(current.metadata.catalog.version, mutation);
314
+ if (version === current.metadata.catalog.version)
315
+ return current.metadata;
279
316
  const metadata = {
280
317
  ...current.metadata,
281
- catalog: { version: assertCatalogVersion(version, 'catalog version') },
318
+ catalog: {
319
+ version,
320
+ },
282
321
  };
283
322
  await writeJsonAtomic(current.metadataPath, metadata);
284
323
  return metadata;
@@ -369,29 +408,54 @@ function stableStringify(value) {
369
408
  function sha256(value) {
370
409
  return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
371
410
  }
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
- }));
411
+ const SOURCE_IDENTITY_EXCLUDED_DIRECTORIES = new Set([
412
+ '.git', '.aw', 'node_modules', 'evals', 'reports',
413
+ ]);
414
+ const SOURCE_IDENTITY_EXCLUDED_FILES = new Set([
415
+ '.DS_Store', 'AGENTS.md', 'CLAUDE.md',
416
+ ]);
417
+ async function sourceIdentityRecords(root, relative = '') {
418
+ const records = [];
419
+ for (const entry of (await fs.readdir(path.join(root, relative), { withFileTypes: true }))
420
+ .sort((left, right) => left.name.localeCompare(right.name))) {
421
+ if (SOURCE_IDENTITY_EXCLUDED_DIRECTORIES.has(entry.name) || SOURCE_IDENTITY_EXCLUDED_FILES.has(entry.name)) {
422
+ continue;
423
+ }
424
+ const child = relative ? path.posix.join(relative, entry.name) : entry.name;
425
+ const filePath = path.join(root, child);
426
+ const stat = await fs.lstat(filePath);
427
+ if (stat.isSymbolicLink())
428
+ throw new Error(`Catalog source identity does not accept symbolic links: ${filePath}`);
429
+ if (stat.isDirectory())
430
+ records.push(...await sourceIdentityRecords(root, child));
431
+ else if (stat.isFile()) {
432
+ const mode = (stat.mode & 0o111) === 0 ? '100644' : '100755';
433
+ records.push(`${child}\0${mode}\0${sha256(await fs.readFile(filePath))}`);
434
+ }
435
+ else {
436
+ throw new Error(`Catalog source identity only accepts regular files and directories: ${filePath}`);
437
+ }
438
+ }
439
+ return records;
440
+ }
441
+ export async function calculateCatalogWorkspaceSourceIdentity(workspace) {
442
+ const roots = [workspace.metadataPath, ...workspace.plugins.map((plugin) => plugin.rootDir)];
443
+ const records = [];
444
+ for (const sourceRoot of roots.sort((left, right) => left.localeCompare(right))) {
445
+ const relativeRoot = path.relative(workspace.workspaceRoot, sourceRoot).split(path.sep).join('/');
446
+ const stat = await fs.lstat(sourceRoot);
447
+ if (stat.isSymbolicLink())
448
+ throw new Error(`Catalog source identity does not accept symbolic links: ${sourceRoot}`);
449
+ if (stat.isFile()) {
450
+ const mode = (stat.mode & 0o111) === 0 ? '100644' : '100755';
451
+ records.push(`${relativeRoot}\0${mode}\0${sha256(await fs.readFile(sourceRoot))}`);
452
+ }
453
+ else if (stat.isDirectory()) {
454
+ for (const record of await sourceIdentityRecords(sourceRoot))
455
+ records.push(`${relativeRoot}/${record}`);
456
+ }
457
+ }
458
+ return sha256(records.sort().join('\n'));
395
459
  }
396
460
  function generatedOutputMarker(options) {
397
461
  return {
@@ -422,9 +486,26 @@ async function catalogPathRecord(root, relative, versionNeutral) {
422
486
  if (!stat.isFile())
423
487
  throw new Error(`Catalog digest path must be a regular file or symlink: ${filePath}.`);
424
488
  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));
489
+ let digest;
490
+ if (versionNeutral && manifests.has(relative)) {
491
+ digest = sha256(stableStringify({
492
+ ...await fs.readJson(filePath),
493
+ version: '<plugin-version>',
494
+ }));
495
+ }
496
+ else if (versionNeutral && path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME) {
497
+ const receipt = await fs.readJson(filePath);
498
+ const plugin = receipt.plugin && typeof receipt.plugin === 'object' && !Array.isArray(receipt.plugin)
499
+ ? receipt.plugin
500
+ : {};
501
+ digest = sha256(stableStringify({
502
+ ...receipt,
503
+ plugin: { ...plugin, version: '<plugin-version>' },
504
+ }));
505
+ }
506
+ else {
507
+ digest = sha256(await fs.readFile(filePath));
508
+ }
428
509
  return { path: relative, kind: 'file', mode: (stat.mode & 0o100) === 0 ? '100644' : '100755', digest };
429
510
  }
430
511
  async function calculateEntryDigest(root, versionNeutral) {
@@ -432,16 +513,44 @@ async function calculateEntryDigest(root, versionNeutral) {
432
513
  return sha256(stableStringify(await Promise.all(files.map((file) => catalogPathRecord(root, file, versionNeutral)))));
433
514
  }
434
515
  async function expectedCatalogEntry(plugin, pluginRoot) {
516
+ const bundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} bundle manifest`);
435
517
  return {
436
518
  ...createMarketplaceManifestEntry(plugin),
437
519
  version: plugin.manifest.version,
438
- compatibility: plugin.manifest.compatibility,
520
+ dependencies: [...(plugin.manifest.dependencies ?? [])].sort(),
521
+ requires: {
522
+ aw: {
523
+ capabilities: normalizeAwCapabilities(plugin.manifest.requires?.aw.capabilities ?? []),
524
+ },
525
+ },
439
526
  artifactDigest: await calculateEntryDigest(pluginRoot, false),
440
- manifestDigest: sha256(normalizeManifestDigestInput(plugin.manifest)),
527
+ manifestDigest: sha256(normalizeManifestDigestInput(bundleManifest)),
441
528
  contentDigest: await calculateEntryDigest(pluginRoot, true),
442
529
  };
443
530
  }
444
- function normalizeCatalogRelease(raw, catalogVersion) {
531
+ async function validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot) {
532
+ const files = await listBundleFiles(pluginRoot);
533
+ const expectedReceiptPaths = new Set(files.flatMap((relative) => /^(?:claude|codex|agent-skills)\/skills\/[^/]+\/SKILL\.md$/u.test(relative)
534
+ ? [path.posix.join(path.posix.dirname(relative), AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)]
535
+ : []));
536
+ const actualReceiptPaths = files.filter((relative) => path.posix.basename(relative) === AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME);
537
+ for (const relative of expectedReceiptPaths) {
538
+ if (!actualReceiptPaths.includes(relative)) {
539
+ throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} Skill root is missing ${AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME}: ${relative}.`);
540
+ }
541
+ }
542
+ const expected = createAwPluginRequirementsReceipt({ name: plugin.manifest.name, version: plugin.manifest.version }, plugin.manifest.requires?.aw.capabilities ?? []);
543
+ for (const relative of actualReceiptPaths) {
544
+ if (!expectedReceiptPaths.has(relative)) {
545
+ throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a reserved runtime requirements receipt outside a Skill root: ${relative}.`);
546
+ }
547
+ const receipt = parseAwPluginRequirementsReceipt(await readRegularJson(path.join(pluginRoot, ...relative.split('/')), `Catalog plugin ${JSON.stringify(plugin.manifest.name)} runtime requirements receipt`));
548
+ if (!isDeepStrictEqual(receipt, expected)) {
549
+ throw new Error(`Catalog plugin ${JSON.stringify(plugin.manifest.name)} has a mismatched runtime requirements receipt: ${relative}.`);
550
+ }
551
+ }
552
+ }
553
+ function normalizeCatalogRelease(raw) {
445
554
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
446
555
  throw new Error('Invalid catalog manifest release: expected an object.');
447
556
  }
@@ -452,50 +561,33 @@ function normalizeCatalogRelease(raw, catalogVersion) {
452
561
  }
453
562
  return { state: 'development' };
454
563
  }
455
- const allowed = new Set(['state', 'targetVersion', 'planId', 'sourceHead', 'contentDigest']);
564
+ const allowed = new Set(['state', 'sourceHead']);
456
565
  if (release.state !== 'published' || Object.keys(release).some((key) => !allowed.has(key))) {
457
566
  throw new Error('Invalid published catalog release.');
458
567
  }
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
568
  if (typeof release.sourceHead !== 'string' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(release.sourceHead)) {
474
569
  throw new Error('Invalid catalog release sourceHead.');
475
570
  }
476
571
  return {
477
572
  state: 'published',
478
- targetVersion,
479
- planId: release.planId,
480
573
  sourceHead: release.sourceHead,
481
- contentDigest: release.contentDigest,
482
574
  };
483
575
  }
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,
576
+ export async function buildCatalogWorkspace(options) {
577
+ const catalogWorkspace = await readCatalogWorkspace(options.workspaceRoot, { reservedImports: options.reservedImports });
578
+ const plugins = orderPlugins(catalogWorkspace.plugins);
579
+ const resolved = await resolveCatalogWorkspaceOutputPath({
580
+ workspaceRoot: catalogWorkspace.workspaceRoot,
489
581
  outputPath: options.outputPath ?? DEFAULT_CATALOG_OUTPUT_PATH,
490
582
  kind: 'catalog',
491
583
  });
492
- const catalogVersion = workspace.metadata.catalog.version;
584
+ const catalogVersion = catalogWorkspace.metadata.catalog.version;
493
585
  const channel = deriveCatalogChannel(catalogVersion);
494
586
  const pluginNames = plugins.map((plugin) => plugin.manifest.name).sort();
495
- const identity = sourceIdentity(workspace.metadata, plugins);
587
+ const identity = await calculateCatalogWorkspaceSourceIdentity(catalogWorkspace);
496
588
  const marker = generatedOutputMarker({
497
589
  kind: 'catalog',
498
- workspaceIdentity: await readWorkspaceOwnershipIdentity(workspace.workspaceRoot),
590
+ workspaceIdentity: catalogWorkspace.workspaceIdentity,
499
591
  sourceIdentity: identity,
500
592
  });
501
593
  await replaceGeneratedOutput({
@@ -503,12 +595,13 @@ export async function buildWorkspaceCatalog(options) {
503
595
  force: options.force === true,
504
596
  build: async (stagingRoot) => {
505
597
  const bundledPlugins = [];
598
+ await fs.ensureDir(path.join(stagingRoot, 'plugins'));
506
599
  for (const plugin of plugins) {
507
600
  const closure = dependencyClosure(plugin, plugins);
508
601
  const outputRoot = path.join(stagingRoot, 'plugins', plugin.manifest.name);
509
602
  await fs.ensureDir(outputRoot);
510
603
  await options.adapter.build({
511
- workspaceRoot: workspace.workspaceRoot,
604
+ workspaceRoot: catalogWorkspace.workspaceRoot,
512
605
  plugin,
513
606
  plugins,
514
607
  dependencies: closure.slice(0, -1),
@@ -519,25 +612,19 @@ export async function buildWorkspaceCatalog(options) {
519
612
  const byPlatform = (platform) => bundledPlugins
520
613
  .filter((plugin) => resolveSupportedPlatforms(plugin.manifest).has(platform))
521
614
  .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
- }
615
+ await fs.outputJson(path.join(stagingRoot, '.agents', 'plugins', 'marketplace.json'), createMarketplaceManifest(catalogWorkspace.metadata, byPlatform('codex')), { spaces: 2 });
616
+ await fs.outputJson(path.join(stagingRoot, '.claude-plugin', 'marketplace.json'), createMarketplaceManifest(catalogWorkspace.metadata, byPlatform('claude')), { spaces: 2 });
530
617
  const entries = await Promise.all([...bundledPlugins]
531
618
  .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
532
619
  .map((plugin) => expectedCatalogEntry(plugin, plugin.rootDir)));
533
620
  const manifest = {
534
- schemaVersion: 4,
621
+ schemaVersion: 7,
535
622
  catalogVersion,
536
623
  channel,
537
- compatibility: { aw: { minVersion } },
624
+ requires: { aw: { capabilities: ['aw.catalog-snapshot.v7'] } },
538
625
  marketplace: {
539
- owner: workspace.metadata.owner,
540
- interface: workspace.metadata.interface,
626
+ owner: catalogWorkspace.metadata.owner,
627
+ interface: catalogWorkspace.metadata.interface,
541
628
  },
542
629
  release: { state: 'development' },
543
630
  entries,
@@ -545,9 +632,9 @@ export async function buildWorkspaceCatalog(options) {
545
632
  await fs.writeJson(path.join(stagingRoot, CATALOG_MANIFEST_FILENAME), manifest, { spaces: 2 });
546
633
  await fs.writeJson(path.join(stagingRoot, GENERATED_OUTPUT_MARKER_FILENAME), marker, { spaces: 2 });
547
634
  },
548
- validate: async (stagingRoot) => { await validateCatalogSnapshot(stagingRoot); },
635
+ validate: async (stagingRoot) => { await validateCatalogSnapshotComplete(stagingRoot); },
549
636
  assertReplaceable: async (existingRoot) => {
550
- await validateCatalogSnapshot(existingRoot);
637
+ await validateCatalogSnapshotComplete(existingRoot);
551
638
  await assertGeneratedOutput(existingRoot, marker);
552
639
  },
553
640
  });
@@ -567,89 +654,154 @@ async function rewritePublishedPluginVersions(snapshotRoot, overrides) {
567
654
  const manifest = await fs.readJson(manifestPath);
568
655
  await fs.writeFile(manifestPath, `${JSON.stringify({ ...manifest, version }, null, 2)}\n`);
569
656
  }
657
+ const pluginRoot = path.join(snapshotRoot, 'plugins', pluginName);
658
+ for (const relative of await listBundleFiles(pluginRoot)) {
659
+ if (path.posix.basename(relative) !== AW_PLUGIN_REQUIREMENTS_RECEIPT_FILENAME)
660
+ continue;
661
+ const receiptPath = path.join(pluginRoot, ...relative.split('/'));
662
+ const receipt = parseAwPluginRequirementsReceipt(await fs.readJson(receiptPath));
663
+ if (receipt.plugin.name !== pluginName) {
664
+ throw new Error(`Catalog plugin ${JSON.stringify(pluginName)} has a mismatched runtime requirements receipt.`);
665
+ }
666
+ await fs.writeFile(receiptPath, `${JSON.stringify(createAwPluginRequirementsReceipt({ name: pluginName, version }, receipt.requiredCapabilities), null, 2)}\n`);
667
+ }
668
+ }
669
+ }
670
+ function assertStablePluginVersion(version, fieldName) {
671
+ const parsed = parseStrictSemVer(version, fieldName);
672
+ if (parsed.prerelease.length > 0 || parsed.build.length > 0) {
673
+ throw new Error(`Invalid ${fieldName}: expected a stable SemVer without prerelease or build metadata.`);
674
+ }
675
+ return version;
676
+ }
677
+ function stableVersionBase(version, fieldName) {
678
+ const parsed = parseStrictSemVer(version, fieldName);
679
+ return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
680
+ }
681
+ function resolvePublishedPluginVersions(options) {
682
+ const previousEntries = new Map(options.previousPluginEntries.map((entry) => [entry.name, entry]));
683
+ for (const entry of options.previous?.entries ?? [])
684
+ previousEntries.set(entry.name, entry);
685
+ const versions = {};
686
+ for (const plugin of options.plugins) {
687
+ const name = plugin.manifest.name;
688
+ const sourceVersion = assertStablePluginVersion(plugin.manifest.version, `${name} source version`);
689
+ const contentDigest = options.contentDigests.get(name);
690
+ if (!contentDigest)
691
+ throw new Error(`Missing release content digest for plugin ${JSON.stringify(name)}.`);
692
+ const previous = previousEntries.get(name);
693
+ if (!previous) {
694
+ versions[name] = options.channel === 'beta' ? `${sourceVersion}-beta.1` : sourceVersion;
695
+ continue;
696
+ }
697
+ const previousVersion = assertStrictSemVer(previous.version, `${name} previous published version`);
698
+ if (options.channel === 'latest') {
699
+ const comparison = compareStrictSemVer(sourceVersion, previousVersion);
700
+ if (comparison < 0) {
701
+ throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous ${previousVersion}.`);
702
+ }
703
+ if (comparison === 0 && previous.contentDigest !== contentDigest) {
704
+ throw new Error(`Plugin ${name} version ${sourceVersion} maps to different content. Increase the source plugin version.`);
705
+ }
706
+ versions[name] = sourceVersion;
707
+ continue;
708
+ }
709
+ const previousBase = stableVersionBase(previousVersion, `${name} previous published version`);
710
+ const baseComparison = compareStrictSemVer(sourceVersion, previousBase);
711
+ if (baseComparison < 0) {
712
+ throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous base ${previousBase}.`);
713
+ }
714
+ if (baseComparison > 0) {
715
+ versions[name] = `${sourceVersion}-beta.1`;
716
+ continue;
717
+ }
718
+ const parsedPrevious = parseStrictSemVer(previousVersion, `${name} previous published version`);
719
+ if (parsedPrevious.prerelease.length === 0) {
720
+ if (previous.contentDigest !== contentDigest) {
721
+ throw new Error(`Plugin ${name} stable version ${sourceVersion} maps to different content. Increase the source plugin version.`);
722
+ }
723
+ versions[name] = previousVersion;
724
+ continue;
725
+ }
726
+ if (parsedPrevious.prerelease.length !== 2
727
+ || parsedPrevious.prerelease[0] !== 'beta'
728
+ || !/^[1-9]\d*$/u.test(parsedPrevious.prerelease[1])) {
729
+ throw new Error(`Plugin ${name} previous version ${previousVersion} is not a stable or beta publication version.`);
730
+ }
731
+ versions[name] = previous.contentDigest === contentDigest
732
+ ? previousVersion
733
+ : `${sourceVersion}-beta.${Number(parsedPrevious.prerelease[1]) + 1}`;
570
734
  }
735
+ return versions;
571
736
  }
572
737
  export async function validatePublishedCatalogSnapshot(snapshotDirectory) {
573
- const validation = await validateCatalogSnapshot(snapshotDirectory);
738
+ const validation = await validateCatalogSnapshotComplete(snapshotDirectory);
574
739
  if (validation.manifest.release.state !== 'published') {
575
740
  throw new Error('Published Catalog snapshot must have release.state published.');
576
741
  }
577
- if (!validation.ledger)
578
- throw new Error('Published Catalog snapshot must include a plugin release ledger.');
579
- return { ...validation, ledger: validation.ledger };
742
+ return validation;
580
743
  }
581
- export async function preparePublishedWorkspaceCatalog(options) {
744
+ export async function preparePublishedCatalogWorkspace(options) {
582
745
  if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(options.sourceHead)) {
583
746
  throw new Error('Published Catalog sourceHead must be a committed Git object ID.');
584
747
  }
585
- const workspace = await readPluginWorkspace(options.workspaceRoot, { reservedImports: options.reservedImports });
586
- for (const plugin of workspace.plugins) {
748
+ const catalogWorkspace = await readCatalogWorkspace(options.workspaceRoot, { reservedImports: options.reservedImports });
749
+ for (const plugin of catalogWorkspace.plugins) {
587
750
  assertStablePluginVersion(plugin.manifest.version, `${plugin.manifest.name} source version`);
588
751
  }
589
- const catalogVersion = workspace.metadata.catalog.version;
752
+ const catalogVersion = catalogWorkspace.metadata.catalog.version;
590
753
  const channel = deriveCatalogChannel(catalogVersion);
591
754
  const previous = options.previousSnapshotRoot
592
755
  ? await validatePublishedCatalogSnapshot(options.previousSnapshotRoot)
593
756
  : null;
757
+ if (!previous && (options.historicalPluginEntries?.length ?? 0) > 0) {
758
+ throw new Error('Published Plugin history requires a previous Catalog snapshot.');
759
+ }
594
760
  if (previous && compareStrictSemVer(catalogVersion, previous.manifest.catalogVersion) <= 0) {
595
761
  throw new Error(`Catalog version ${catalogVersion} must be higher than published ${previous.manifest.catalogVersion}.`);
596
762
  }
597
- const previousLedger = previous?.ledger ?? {
598
- schemaVersion: PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION,
599
- records: [],
600
- };
601
763
  const temporaryRelative = path.join('.aw', 'publish', `.catalog-preparation-${crypto.randomUUID()}`);
602
764
  const finalRelative = options.outputPath
603
765
  ?? `${DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH}-v${catalogVersion}`;
604
- let temporaryRoot = path.resolve(workspace.workspaceRoot, temporaryRelative);
766
+ let temporaryRoot = path.resolve(catalogWorkspace.workspaceRoot, temporaryRelative);
605
767
  try {
606
- const built = await buildWorkspaceCatalog({
607
- workspaceRoot: workspace.workspaceRoot,
768
+ const built = await buildCatalogWorkspace({
769
+ workspaceRoot: catalogWorkspace.workspaceRoot,
608
770
  outputPath: temporaryRelative,
609
771
  adapter: options.adapter,
610
772
  reservedImports: options.reservedImports,
611
773
  });
612
774
  temporaryRoot = built.outputRoot;
613
- const development = await validateCatalogSnapshot(temporaryRoot);
775
+ const development = await validateCatalogSnapshotComplete(temporaryRoot);
614
776
  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]));
777
+ const overrides = resolvePublishedPluginVersions({
778
+ channel,
779
+ plugins: catalogWorkspace.plugins,
780
+ contentDigests,
781
+ previous: previous?.manifest ?? null,
782
+ previousPluginEntries: options.historicalPluginEntries ?? [],
783
+ });
622
784
  await rewritePublishedPluginVersions(temporaryRoot, overrides);
623
- const bundledPlugins = await Promise.all(workspace.plugins.map((plugin) => readPluginManifest(path.join(temporaryRoot, 'plugins', plugin.manifest.name), { mode: 'bundle' })));
785
+ const bundledPlugins = await Promise.all(catalogWorkspace.plugins.map((plugin) => readPluginManifest(path.join(temporaryRoot, 'plugins', plugin.manifest.name), { mode: 'bundle' })));
624
786
  const entries = await Promise.all(bundledPlugins.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
625
787
  .map((plugin) => expectedCatalogEntry(plugin, plugin.rootDir)));
626
788
  const manifestWithoutRelease = {
627
789
  ...development.manifest,
790
+ catalogVersion,
791
+ channel,
628
792
  entries,
629
793
  };
630
- const contentDigest = publishedContentDigest(manifestWithoutRelease);
631
794
  const manifest = {
632
795
  ...manifestWithoutRelease,
633
796
  release: {
634
797
  state: 'published',
635
- targetVersion: catalogVersion.replace(/-beta\.[1-9]\d*$/u, ''),
636
- planId: contentDigest,
637
798
  sourceHead: options.sourceHead,
638
- contentDigest,
639
799
  },
640
800
  };
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
- ]);
801
+ await fs.writeFile(path.join(temporaryRoot, CATALOG_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`);
650
802
  const prepared = await validatePublishedCatalogSnapshot(temporaryRoot);
651
- const resolved = await resolveWorkspaceOutputPath({
652
- workspaceRoot: workspace.workspaceRoot,
803
+ const resolved = await resolveCatalogWorkspaceOutputPath({
804
+ workspaceRoot: catalogWorkspace.workspaceRoot,
653
805
  outputPath: finalRelative,
654
806
  kind: 'catalog',
655
807
  });
@@ -670,8 +822,6 @@ export async function preparePublishedWorkspaceCatalog(options) {
670
822
  channel,
671
823
  pluginNames: built.pluginNames,
672
824
  snapshotDigest: prepared.snapshotDigest,
673
- contentDigest,
674
- planId: contentDigest,
675
825
  };
676
826
  }
677
827
  finally {
@@ -697,27 +847,27 @@ function dependencyClosure(plugin, all) {
697
847
  visit(plugin);
698
848
  return result;
699
849
  }
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);
850
+ export async function buildCatalogWorkspacePlugin(options) {
851
+ const catalogWorkspace = await readCatalogWorkspace(options.workspaceRoot, { reservedImports: options.reservedImports });
852
+ assertDependencyGraph(catalogWorkspace.plugins);
853
+ const requestedPluginRoot = path.resolve(catalogWorkspace.workspaceRoot, options.pluginDirectory);
704
854
  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);
855
+ if (!isInside(catalogWorkspace.workspaceRoot, pluginRoot))
856
+ throw new Error('Plugin directory must stay inside the Catalog Workspace.');
857
+ const plugin = catalogWorkspace.plugins.find((entry) => entry.rootDir === pluginRoot);
708
858
  if (!plugin) {
709
- throw new Error(`Plugin directory is not part of this workspace source: ${pluginRoot}`);
859
+ throw new Error(`Plugin directory is not part of this Catalog Workspace source: ${pluginRoot}`);
710
860
  }
711
- const closure = dependencyClosure(plugin, workspace.plugins);
861
+ const closure = dependencyClosure(plugin, catalogWorkspace.plugins);
712
862
  const identity = sha256(normalizeManifestDigestInput(plugin.manifest));
713
863
  const marker = generatedOutputMarker({
714
864
  kind: 'plugin',
715
- workspaceIdentity: await readWorkspaceOwnershipIdentity(workspace.workspaceRoot),
865
+ workspaceIdentity: catalogWorkspace.workspaceIdentity,
716
866
  sourceIdentity: identity,
717
867
  sourceName: plugin.manifest.name,
718
868
  });
719
- const resolved = await resolveWorkspaceOutputPath({
720
- workspaceRoot: workspace.workspaceRoot,
869
+ const resolved = await resolveCatalogWorkspaceOutputPath({
870
+ workspaceRoot: catalogWorkspace.workspaceRoot,
721
871
  outputPath: options.outputPath ?? path.join(DEFAULT_PLUGIN_OUTPUT_PATH, plugin.manifest.name),
722
872
  kind: 'plugin',
723
873
  });
@@ -725,9 +875,9 @@ export async function buildWorkspacePlugin(options) {
725
875
  outputRoot: resolved.outputRoot,
726
876
  force: options.force === true,
727
877
  build: (stagingRoot) => options.adapter.build({
728
- workspaceRoot: workspace.workspaceRoot,
878
+ workspaceRoot: catalogWorkspace.workspaceRoot,
729
879
  plugin,
730
- plugins: workspace.plugins,
880
+ plugins: catalogWorkspace.plugins,
731
881
  dependencies: closure.slice(0, -1),
732
882
  outputRoot: stagingRoot,
733
883
  }).then(() => fs.writeJson(path.join(stagingRoot, GENERATED_OUTPUT_MARKER_FILENAME), marker, { spaces: 2 })),
@@ -765,7 +915,7 @@ async function assertGeneratedOutput(root, expected) {
765
915
  if (!isDeepStrictEqual(Object.keys(marker).sort(), expectedKeys)
766
916
  || marker.schemaVersion !== 1
767
917
  || typeof marker.workspaceIdentity !== 'string'
768
- || !digestPattern.test(marker.workspaceIdentity)
918
+ || marker.workspaceIdentity.trim().length === 0
769
919
  || typeof marker.sourceIdentity !== 'string'
770
920
  || !digestPattern.test(marker.sourceIdentity)) {
771
921
  throw new Error(`Refusing to replace foreign plugin output: ${root}`);
@@ -797,7 +947,7 @@ async function digestDirectory(root) {
797
947
  await walk(root);
798
948
  return `sha256:${crypto.createHash('sha256').update(entries.join('\n')).digest('hex')}`;
799
949
  }
800
- export async function validateCatalogSnapshot(snapshotDirectory) {
950
+ async function validateCatalogSnapshotTransportInternal(snapshotDirectory) {
801
951
  const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot');
802
952
  const allowed = new Set([
803
953
  CATALOG_MANIFEST_FILENAME,
@@ -805,7 +955,6 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
805
955
  '.agents',
806
956
  '.claude-plugin',
807
957
  'plugins',
808
- 'plugin-release-ledger.json',
809
958
  ]);
810
959
  const entries = await fs.readdir(rootDir);
811
960
  const unexpected = entries.filter((entry) => !allowed.has(entry));
@@ -815,19 +964,7 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
815
964
  throw new Error(`Invalid catalog snapshot root layout: missing [${missing.join(', ')}], unexpected [${unexpected.join(', ')}].`);
816
965
  }
817
966
  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
- }
967
+ validateCatalogGeneratedOutputMarker(await readRegularJson(path.join(rootDir, GENERATED_OUTPUT_MARKER_FILENAME), 'Catalog generated output marker'));
831
968
  }
832
969
  const assertLayout = async (directory, expected) => {
833
970
  const real = await assertDirectoryWithoutSymlinks(directory, 'Catalog snapshot directory');
@@ -845,16 +982,117 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
845
982
  await readRegularJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'), 'Codex marketplace');
846
983
  await readRegularJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'), 'Claude marketplace');
847
984
  const snapshotDigest = await digestDirectory(rootDir);
848
- const raw = await fs.readJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME));
985
+ const manifest = normalizeCatalogManifestV7(await readRegularJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME), 'Catalog manifest v7'));
986
+ const normalizedEntries = manifest.entries;
987
+ const marketplaceIdentity = manifest.marketplace;
988
+ const pluginNames = normalizedEntries.map((entry) => entry.name);
989
+ const diskNames = (await fs.readdir(path.join(rootDir, 'plugins'))).sort();
990
+ if (diskNames.join('\0') !== [...pluginNames].sort().join('\0'))
991
+ throw new Error('Invalid catalog snapshot plugin inventory.');
992
+ const platformEntries = {
993
+ claude: [],
994
+ codex: [],
995
+ };
996
+ for (const [index, name] of pluginNames.entries()) {
997
+ const pluginRoot = path.join(rootDir, 'plugins', name);
998
+ await assertDirectoryWithoutSymlinks(pluginRoot, `Catalog plugin ${JSON.stringify(name)}`);
999
+ const entry = normalizedEntries[index];
1000
+ const rawBundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(name)} bundle manifest`);
1001
+ const expectedDigests = {
1002
+ artifactDigest: await calculateEntryDigest(pluginRoot, false),
1003
+ manifestDigest: sha256(normalizeManifestDigestInput(rawBundleManifest)),
1004
+ contentDigest: await calculateEntryDigest(pluginRoot, true),
1005
+ };
1006
+ for (const field of Object.keys(expectedDigests)) {
1007
+ if (entry[field] !== expectedDigests[field]) {
1008
+ throw new Error(`Catalog entry ${field} for ${JSON.stringify(name)} does not match its plugin bundle.`);
1009
+ }
1010
+ }
1011
+ for (const platform of ['claude', 'codex']) {
1012
+ const platformManifest = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
1013
+ const exists = await fs.pathExists(platformManifest);
1014
+ if (exists) {
1015
+ await readRegularJson(platformManifest, `${platform} plugin manifest`);
1016
+ platformEntries[platform].push(entry);
1017
+ }
1018
+ }
1019
+ }
1020
+ const agentsMarketplace = await fs.readJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'));
1021
+ const claudeMarketplace = await fs.readJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'));
1022
+ const marketplaceEntry = (entry) => ({
1023
+ name: entry.name,
1024
+ source: entry.source,
1025
+ description: entry.description,
1026
+ interface: entry.interface,
1027
+ category: entry.category,
1028
+ tags: entry.tags,
1029
+ strict: entry.strict,
1030
+ policy: entry.policy,
1031
+ });
1032
+ const expectedMarketplace = (platform) => ({
1033
+ name: 'aw',
1034
+ owner: marketplaceIdentity.owner,
1035
+ interface: marketplaceIdentity.interface,
1036
+ plugins: platformEntries[platform].map(marketplaceEntry),
1037
+ });
1038
+ const expectedAgents = expectedMarketplace('codex');
1039
+ const expectedClaude = expectedMarketplace('claude');
1040
+ const normalizeMarketplaceOrder = (marketplace) => {
1041
+ const canonical = JSON.parse(JSON.stringify(marketplace));
1042
+ return {
1043
+ ...canonical,
1044
+ plugins: Array.isArray(canonical.plugins)
1045
+ ? [...canonical.plugins].sort((left, right) => String(left?.name).localeCompare(String(right?.name)))
1046
+ : canonical.plugins,
1047
+ };
1048
+ };
1049
+ if (!isDeepStrictEqual(normalizeMarketplaceOrder(agentsMarketplace), normalizeMarketplaceOrder(expectedAgents))) {
1050
+ throw new Error('Codex marketplace metadata or plugin inventory does not match the snapshot bundles.');
1051
+ }
1052
+ if (!isDeepStrictEqual(normalizeMarketplaceOrder(claudeMarketplace), normalizeMarketplaceOrder(expectedClaude))) {
1053
+ throw new Error('Claude marketplace metadata or plugin inventory does not match the snapshot bundles.');
1054
+ }
1055
+ return { rootDir, manifest, snapshotDigest };
1056
+ }
1057
+ function validateCatalogGeneratedOutputMarker(raw) {
1058
+ const marker = raw;
1059
+ const digestPattern = /^sha256:[0-9a-f]{64}$/u;
1060
+ if (!marker
1061
+ || typeof marker !== 'object'
1062
+ || Array.isArray(marker)
1063
+ || !isDeepStrictEqual(Object.keys(marker).sort(), [
1064
+ 'kind', 'schemaVersion', 'sourceIdentity', 'workspaceIdentity',
1065
+ ])
1066
+ || marker.schemaVersion !== 1
1067
+ || marker.kind !== 'catalog'
1068
+ || typeof marker.workspaceIdentity !== 'string'
1069
+ || marker.workspaceIdentity.trim().length === 0
1070
+ || typeof marker.sourceIdentity !== 'string'
1071
+ || !digestPattern.test(marker.sourceIdentity)) {
1072
+ throw new Error('Invalid catalog generated output marker.');
1073
+ }
1074
+ return {
1075
+ workspaceIdentity: marker.workspaceIdentity,
1076
+ sourceIdentity: marker.sourceIdentity,
1077
+ };
1078
+ }
1079
+ function normalizeCatalogManifestV7(rawValue) {
1080
+ if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
1081
+ throw new Error('Invalid catalog manifest v7: expected an object.');
1082
+ }
1083
+ const raw = rawValue;
849
1084
  const allowedManifestKeys = new Set([
850
- 'schemaVersion', 'catalogVersion', 'channel', 'compatibility', 'marketplace', 'release', 'entries',
1085
+ 'schemaVersion', 'catalogVersion', 'channel', 'requires', 'marketplace', 'release', 'entries',
851
1086
  ]);
852
1087
  for (const key of Object.keys(raw))
853
1088
  if (!allowedManifestKeys.has(key))
854
1089
  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');
1090
+ if (raw.schemaVersion !== 7)
1091
+ throw new Error('Invalid catalog manifest schemaVersion: expected 7.');
1092
+ const base = normalizeCatalogManifestBase(raw, 'catalog manifest v7');
1093
+ if (!(base.requires.aw.capabilities ?? []).includes('aw.catalog-snapshot.v7')) {
1094
+ throw new Error('Invalid catalog manifest requires.aw.capabilities: expected aw.catalog-snapshot.v7.');
1095
+ }
858
1096
  const marketplaceIdentity = normalizeCatalogMarketplaceIdentity(raw.marketplace);
859
1097
  if (!Array.isArray(raw.entries))
860
1098
  throw new Error('Invalid catalog manifest entries: expected an array.');
@@ -870,94 +1108,84 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
870
1108
  delete copy.contentDigest;
871
1109
  return copy;
872
1110
  });
873
- const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest v4')
1111
+ const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest v7')
874
1112
  .map((entry, index) => ({ ...entry, contentDigest: contentDigests[index] }));
875
1113
  const pluginNames = normalizedEntries.map((entry, index) => {
876
1114
  if (typeof entry.name !== 'string')
877
1115
  throw new Error(`Invalid catalog manifest entries[${index}].name.`);
1116
+ if (entry.source !== `./plugins/${entry.name}`) {
1117
+ throw new Error(`Catalog entry source for ${JSON.stringify(entry.name)} does not match its plugin bundle.`);
1118
+ }
878
1119
  return entry.name;
879
1120
  });
880
1121
  if (new Set(pluginNames).size !== pluginNames.length)
881
1122
  throw new Error('Invalid catalog manifest entries: duplicate plugin name.');
882
1123
  if (pluginNames.join('\0') !== [...pluginNames].sort().join('\0'))
883
1124
  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.');
1125
+ const release = normalizeCatalogRelease(raw.release);
1126
+ return {
1127
+ schemaVersion: 7,
1128
+ catalogVersion: base.catalogVersion,
1129
+ channel: base.channel,
1130
+ requires: base.requires,
1131
+ marketplace: marketplaceIdentity,
1132
+ release,
1133
+ entries: normalizedEntries,
1134
+ };
1135
+ }
1136
+ export async function validatePublishedCatalogSnapshotMetadata(snapshotDirectory) {
1137
+ const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot metadata');
1138
+ const [marker, manifest] = await Promise.all([
1139
+ readRegularJson(path.join(rootDir, GENERATED_OUTPUT_MARKER_FILENAME), 'Catalog generated output marker').then(validateCatalogGeneratedOutputMarker),
1140
+ readRegularJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME), 'Catalog manifest v7').then(normalizeCatalogManifestV7),
1141
+ ]);
1142
+ if (manifest.release.state !== 'published') {
1143
+ throw new Error('Published Catalog snapshot must have release.state published.');
1144
+ }
1145
+ return { rootDir, workspaceIdentity: marker.workspaceIdentity, manifest };
1146
+ }
1147
+ export async function validateCatalogSnapshotTransport(snapshotDirectory) {
1148
+ return validateCatalogSnapshotTransportInternal(snapshotDirectory);
1149
+ }
1150
+ async function validateCatalogPluginEntriesComplete(validation, pluginNames) {
1151
+ const entriesByName = new Map(validation.manifest.entries.map((entry) => [entry.name, entry]));
887
1152
  const plugins = [];
888
- for (const [index, name] of pluginNames.entries()) {
889
- const pluginRoot = path.join(rootDir, 'plugins', name);
1153
+ for (const name of pluginNames) {
1154
+ const entry = entriesByName.get(name);
1155
+ if (!entry)
1156
+ throw new Error(`Catalog plugin ${JSON.stringify(name)} does not exist.`);
1157
+ const pluginRoot = path.join(validation.rootDir, 'plugins', entry.name);
890
1158
  const plugin = await readPluginManifest(pluginRoot, { mode: 'bundle' });
1159
+ await validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot);
891
1160
  for (const platform of ['claude', 'codex']) {
892
- const platformManifest = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
893
- const exists = await fs.pathExists(platformManifest);
1161
+ const platformManifestPath = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
1162
+ const exists = await fs.pathExists(platformManifestPath);
894
1163
  if (exists !== resolveSupportedPlatforms(plugin.manifest).has(platform)) {
895
- throw new Error(`Catalog plugin ${JSON.stringify(name)} has an inconsistent ${platform} bundle projection.`);
1164
+ throw new Error(`Catalog plugin ${JSON.stringify(entry.name)} has an inconsistent ${platform} bundle projection.`);
896
1165
  }
897
1166
  if (exists) {
898
- const rawPlatformManifest = await readRegularJson(platformManifest, `${platform} plugin manifest`);
1167
+ const rawPlatformManifest = await readRegularJson(platformManifestPath, `${platform} plugin manifest`);
899
1168
  await validatePlatformManifestProjection(plugin, platform, rawPlatformManifest);
900
1169
  }
901
1170
  }
902
1171
  const expected = await expectedCatalogEntry(plugin, pluginRoot);
903
1172
  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.`);
1173
+ if (!isDeepStrictEqual(entry[field], expected[field])) {
1174
+ throw new Error(`Catalog entry ${field} for ${JSON.stringify(entry.name)} does not match its plugin bundle.`);
906
1175
  }
907
1176
  }
908
1177
  plugins.push(plugin);
909
1178
  }
910
1179
  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 };
1180
+ }
1181
+ export async function validateCatalogSnapshotPluginEntriesComplete(snapshotDirectory, pluginNames) {
1182
+ const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
1183
+ await validateCatalogPluginEntriesComplete(validation, pluginNames);
1184
+ return validation;
1185
+ }
1186
+ export async function validateCatalogSnapshotComplete(snapshotDirectory) {
1187
+ const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
1188
+ await validateCatalogPluginEntriesComplete(validation, validation.manifest.entries.map((entry) => entry.name));
1189
+ return validation;
962
1190
  }
963
1191
  //# sourceMappingURL=workspace.js.map