@lesliechan721/aw-plugin-core 0.1.0-beta.1 → 0.1.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aw-capabilities.d.ts +27 -0
- package/dist/aw-capabilities.js +106 -0
- package/dist/aw-capabilities.js.map +1 -0
- package/dist/catalog-workspace-resolution.d.ts +3 -0
- package/dist/catalog-workspace-resolution.js +34 -0
- package/dist/catalog-workspace-resolution.js.map +1 -0
- package/dist/generated/plugin-manifest-v3.d.ts +658 -0
- package/dist/generated/{plugin-manifest-v1.js → plugin-manifest-v3.js} +1 -1
- package/dist/generated/plugin-manifest-v3.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest-contract.d.ts +3 -6
- package/dist/manifest-contract.js +135 -405
- package/dist/manifest-contract.js.map +1 -1
- package/dist/manifest-type-contract.d.ts +2 -2
- package/dist/manifest.d.ts +10 -8
- package/dist/manifest.js +11 -7
- package/dist/manifest.js.map +1 -1
- package/dist/plugin-authoring-capabilities.d.ts +21 -0
- package/dist/plugin-authoring-capabilities.js +183 -0
- package/dist/plugin-authoring-capabilities.js.map +1 -0
- package/dist/plugin-config.d.ts +13 -0
- package/dist/plugin-config.js +619 -0
- package/dist/plugin-config.js.map +1 -0
- package/dist/plugin-requirements.d.ts +34 -0
- package/dist/plugin-requirements.js +248 -0
- package/dist/plugin-requirements.js.map +1 -0
- package/dist/plugin-script-contract.d.ts +8 -10
- package/dist/plugin-script-contract.js +32 -62
- package/dist/plugin-script-contract.js.map +1 -1
- package/dist/types.d.ts +126 -61
- package/dist/workspace.d.ts +54 -29
- package/dist/workspace.js +497 -269
- package/dist/workspace.js.map +1 -1
- package/package.json +5 -4
- package/schemas/plugin-manifest-v3.json +574 -0
- package/dist/generated/plugin-manifest-v1.d.ts +0 -368
- package/dist/generated/plugin-manifest-v1.js.map +0 -1
- package/dist/plugin-release.d.ts +0 -30
- package/dist/plugin-release.js +0 -187
- package/dist/plugin-release.js.map +0 -1
- 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 {
|
|
8
|
-
export const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
82
|
-
const
|
|
82
|
+
function catalogWorkspaceMetadata(options, workspaceRoot) {
|
|
83
|
+
const catalogWorkspaceName = safeCatalogWorkspaceName(workspaceRoot);
|
|
83
84
|
return {
|
|
84
85
|
owner: {
|
|
85
|
-
name: options.ownerName?.trim() ||
|
|
86
|
-
url: options.ownerUrl?.trim() || `https://example.invalid/aw-workspaces/${
|
|
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() ||
|
|
90
|
+
displayName: options.displayName?.trim() || catalogWorkspaceName,
|
|
90
91
|
},
|
|
91
92
|
catalog: {
|
|
92
|
-
version: assertCatalogVersion(options.catalogVersion ??
|
|
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
|
|
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-
|
|
122
|
+
kind: 'aw-catalog-workspace',
|
|
125
123
|
workspaceIdentity: sha256(crypto.randomUUID()),
|
|
126
|
-
sourceIdentity: sha256(stableStringify(metadata)),
|
|
127
124
|
};
|
|
128
125
|
}
|
|
129
|
-
function
|
|
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', '
|
|
132
|
+
'kind', 'schemaVersion', 'workspaceIdentity',
|
|
137
133
|
])
|
|
138
134
|
|| marker.schemaVersion !== 1
|
|
139
|
-
|| marker.kind !== 'aw-
|
|
135
|
+
|| marker.kind !== 'aw-catalog-workspace'
|
|
140
136
|
|| typeof marker.workspaceIdentity !== 'string'
|
|
141
|
-
||
|
|
142
|
-
|
|
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
|
|
149
|
-
const markerPath = path.join(workspaceRoot,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
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,
|
|
170
|
-
const metadata =
|
|
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
|
|
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
|
|
194
|
+
throw new Error(`Refusing to replace different Catalog Workspace metadata: ${metadataPath}`);
|
|
190
195
|
}
|
|
191
196
|
}
|
|
192
|
-
const expectedMarker =
|
|
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
|
-
|
|
202
|
+
workspaceIdentity = validateCatalogWorkspaceMarker(currentMarker).workspaceIdentity;
|
|
197
203
|
}
|
|
198
204
|
catch {
|
|
199
|
-
throw new Error(`Refusing to replace a
|
|
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
|
|
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
|
|
251
|
+
await readRegularJson(metadataPath, 'Catalog Workspace metadata');
|
|
245
252
|
if (!isInside(root, await fs.realpath(metadataPath)))
|
|
246
|
-
throw new Error('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
|
|
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 {
|
|
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
|
|
278
|
-
const current = await
|
|
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: {
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
.
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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(
|
|
527
|
+
manifestDigest: sha256(normalizeManifestDigestInput(bundleManifest)),
|
|
441
528
|
contentDigest: await calculateEntryDigest(pluginRoot, true),
|
|
442
529
|
};
|
|
443
530
|
}
|
|
444
|
-
function
|
|
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', '
|
|
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
|
|
485
|
-
const
|
|
486
|
-
const plugins = orderPlugins(
|
|
487
|
-
const resolved = await
|
|
488
|
-
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 =
|
|
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 =
|
|
587
|
+
const identity = await calculateCatalogWorkspaceSourceIdentity(catalogWorkspace);
|
|
496
588
|
const marker = generatedOutputMarker({
|
|
497
589
|
kind: 'catalog',
|
|
498
|
-
workspaceIdentity:
|
|
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:
|
|
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(
|
|
523
|
-
await fs.outputJson(path.join(stagingRoot, '.claude-plugin', 'marketplace.json'), createMarketplaceManifest(
|
|
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:
|
|
621
|
+
schemaVersion: 7,
|
|
535
622
|
catalogVersion,
|
|
536
623
|
channel,
|
|
537
|
-
|
|
624
|
+
requires: { aw: { capabilities: ['aw.catalog-snapshot.v7'] } },
|
|
538
625
|
marketplace: {
|
|
539
|
-
owner:
|
|
540
|
-
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
|
|
635
|
+
validate: async (stagingRoot) => { await validateCatalogSnapshotComplete(stagingRoot); },
|
|
549
636
|
assertReplaceable: async (existingRoot) => {
|
|
550
|
-
await
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
586
|
-
for (const plugin of
|
|
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 =
|
|
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(
|
|
766
|
+
let temporaryRoot = path.resolve(catalogWorkspace.workspaceRoot, temporaryRelative);
|
|
605
767
|
try {
|
|
606
|
-
const built = await
|
|
607
|
-
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
|
|
775
|
+
const development = await validateCatalogSnapshotComplete(temporaryRoot);
|
|
614
776
|
const contentDigests = new Map(development.manifest.entries.map((entry) => [entry.name, entry.contentDigest]));
|
|
615
|
-
const overrides =
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
652
|
-
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
|
|
701
|
-
const
|
|
702
|
-
assertDependencyGraph(
|
|
703
|
-
const requestedPluginRoot = path.resolve(
|
|
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(
|
|
706
|
-
throw new Error('Plugin directory must stay inside the
|
|
707
|
-
const plugin =
|
|
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
|
|
859
|
+
throw new Error(`Plugin directory is not part of this Catalog Workspace source: ${pluginRoot}`);
|
|
710
860
|
}
|
|
711
|
-
const closure = dependencyClosure(plugin,
|
|
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:
|
|
865
|
+
workspaceIdentity: catalogWorkspace.workspaceIdentity,
|
|
716
866
|
sourceIdentity: identity,
|
|
717
867
|
sourceName: plugin.manifest.name,
|
|
718
868
|
});
|
|
719
|
-
const resolved = await
|
|
720
|
-
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:
|
|
878
|
+
workspaceRoot: catalogWorkspace.workspaceRoot,
|
|
729
879
|
plugin,
|
|
730
|
-
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
|
-
||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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', '
|
|
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 !==
|
|
856
|
-
throw new Error('Invalid catalog manifest schemaVersion: expected
|
|
857
|
-
const base = normalizeCatalogManifestBase(raw, 'catalog manifest
|
|
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
|
|
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
|
|
885
|
-
|
|
886
|
-
|
|
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
|
|
889
|
-
const
|
|
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
|
|
893
|
-
const exists = await fs.pathExists(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
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
|