@lesliechan721/aw-plugin-core 0.1.0-beta.7 → 0.1.0-beta.9
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-v1.d.ts → plugin-manifest-v2.d.ts} +44 -31
- package/dist/generated/{plugin-manifest-v1.js → plugin-manifest-v2.js} +1 -1
- package/dist/generated/plugin-manifest-v2.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 +54 -63
- package/dist/manifest-contract.js.map +1 -1
- package/dist/manifest-type-contract.d.ts +2 -2
- package/dist/manifest.d.ts +8 -8
- package/dist/manifest.js +9 -7
- package/dist/manifest.js.map +1 -1
- package/dist/plugin-authoring-capabilities.js +4 -3
- package/dist/plugin-authoring-capabilities.js.map +1 -1
- 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 -0
- package/dist/plugin-script-contract.js +38 -1
- package/dist/plugin-script-contract.js.map +1 -1
- package/dist/types.d.ts +25 -22
- package/dist/workspace.d.ts +17 -9
- package/dist/workspace.js +299 -138
- package/dist/workspace.js.map +1 -1
- package/package.json +2 -2
- package/schemas/{plugin-manifest-v1.json → plugin-manifest-v2.json} +41 -11
- 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/dist/workspace.js
CHANGED
|
@@ -2,9 +2,9 @@ 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 {
|
|
7
|
+
import { createAwPluginRequirementsReceipt, normalizeAwCapabilities, parseAwPluginRequirementsReceipt, } from './aw-capabilities.js';
|
|
8
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');
|
|
@@ -12,6 +12,7 @@ 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
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 !== '..'
|
|
@@ -138,9 +139,18 @@ function validateCatalogWorkspaceMarker(value) {
|
|
|
138
139
|
}
|
|
139
140
|
return { workspaceIdentity: marker.workspaceIdentity };
|
|
140
141
|
}
|
|
141
|
-
async function readCatalogWorkspaceMarker(workspaceRoot) {
|
|
142
|
+
export async function readCatalogWorkspaceMarker(workspaceRoot) {
|
|
142
143
|
const markerPath = path.join(workspaceRoot, CATALOG_WORKSPACE_MARKER_FILENAME);
|
|
143
|
-
|
|
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);
|
|
144
154
|
return { markerPath, workspaceIdentity: marker.workspaceIdentity };
|
|
145
155
|
}
|
|
146
156
|
async function readRegularJson(filePath, label) {
|
|
@@ -398,29 +408,54 @@ function stableStringify(value) {
|
|
|
398
408
|
function sha256(value) {
|
|
399
409
|
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
|
400
410
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
.
|
|
423
|
-
|
|
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'));
|
|
424
459
|
}
|
|
425
460
|
function generatedOutputMarker(options) {
|
|
426
461
|
return {
|
|
@@ -451,9 +486,26 @@ async function catalogPathRecord(root, relative, versionNeutral) {
|
|
|
451
486
|
if (!stat.isFile())
|
|
452
487
|
throw new Error(`Catalog digest path must be a regular file or symlink: ${filePath}.`);
|
|
453
488
|
const manifests = new Set(['.agents-plugin/plugin.json', '.claude-plugin/plugin.json', '.codex-plugin/plugin.json']);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
}
|
|
457
509
|
return { path: relative, kind: 'file', mode: (stat.mode & 0o100) === 0 ? '100644' : '100755', digest };
|
|
458
510
|
}
|
|
459
511
|
async function calculateEntryDigest(root, versionNeutral) {
|
|
@@ -461,16 +513,44 @@ async function calculateEntryDigest(root, versionNeutral) {
|
|
|
461
513
|
return sha256(stableStringify(await Promise.all(files.map((file) => catalogPathRecord(root, file, versionNeutral)))));
|
|
462
514
|
}
|
|
463
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`);
|
|
464
517
|
return {
|
|
465
518
|
...createMarketplaceManifestEntry(plugin),
|
|
466
519
|
version: plugin.manifest.version,
|
|
467
|
-
|
|
520
|
+
dependencies: [...(plugin.manifest.dependencies ?? [])].sort(),
|
|
521
|
+
requires: {
|
|
522
|
+
aw: {
|
|
523
|
+
capabilities: normalizeAwCapabilities(plugin.manifest.requires?.aw.capabilities ?? []),
|
|
524
|
+
},
|
|
525
|
+
},
|
|
468
526
|
artifactDigest: await calculateEntryDigest(pluginRoot, false),
|
|
469
|
-
manifestDigest: sha256(normalizeManifestDigestInput(
|
|
527
|
+
manifestDigest: sha256(normalizeManifestDigestInput(bundleManifest)),
|
|
470
528
|
contentDigest: await calculateEntryDigest(pluginRoot, true),
|
|
471
529
|
};
|
|
472
530
|
}
|
|
473
|
-
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) {
|
|
474
554
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
475
555
|
throw new Error('Invalid catalog manifest release: expected an object.');
|
|
476
556
|
}
|
|
@@ -481,33 +561,16 @@ function normalizeCatalogRelease(raw, catalogVersion) {
|
|
|
481
561
|
}
|
|
482
562
|
return { state: 'development' };
|
|
483
563
|
}
|
|
484
|
-
const allowed = new Set(['state', '
|
|
564
|
+
const allowed = new Set(['state', 'sourceHead']);
|
|
485
565
|
if (release.state !== 'published' || Object.keys(release).some((key) => !allowed.has(key))) {
|
|
486
566
|
throw new Error('Invalid published catalog release.');
|
|
487
567
|
}
|
|
488
|
-
const targetVersion = assertStrictSemVer(release.targetVersion, 'catalog release targetVersion');
|
|
489
|
-
if (targetVersion.includes('-') || targetVersion.includes('+')) {
|
|
490
|
-
throw new Error('Invalid catalog release targetVersion: expected a stable version.');
|
|
491
|
-
}
|
|
492
|
-
if (targetVersion !== catalogVersion.replace(/-beta\.[1-9]\d*$/u, '')) {
|
|
493
|
-
throw new Error('Invalid catalog release targetVersion: must match catalogVersion.');
|
|
494
|
-
}
|
|
495
|
-
const digestPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
496
|
-
if (typeof release.planId !== 'string' || !digestPattern.test(release.planId)) {
|
|
497
|
-
throw new Error('Invalid catalog release planId.');
|
|
498
|
-
}
|
|
499
|
-
if (typeof release.contentDigest !== 'string' || !digestPattern.test(release.contentDigest)) {
|
|
500
|
-
throw new Error('Invalid catalog release contentDigest.');
|
|
501
|
-
}
|
|
502
568
|
if (typeof release.sourceHead !== 'string' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(release.sourceHead)) {
|
|
503
569
|
throw new Error('Invalid catalog release sourceHead.');
|
|
504
570
|
}
|
|
505
571
|
return {
|
|
506
572
|
state: 'published',
|
|
507
|
-
targetVersion,
|
|
508
|
-
planId: release.planId,
|
|
509
573
|
sourceHead: release.sourceHead,
|
|
510
|
-
contentDigest: release.contentDigest,
|
|
511
574
|
};
|
|
512
575
|
}
|
|
513
576
|
export async function buildCatalogWorkspace(options) {
|
|
@@ -521,7 +584,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
521
584
|
const catalogVersion = catalogWorkspace.metadata.catalog.version;
|
|
522
585
|
const channel = deriveCatalogChannel(catalogVersion);
|
|
523
586
|
const pluginNames = plugins.map((plugin) => plugin.manifest.name).sort();
|
|
524
|
-
const identity =
|
|
587
|
+
const identity = await calculateCatalogWorkspaceSourceIdentity(catalogWorkspace);
|
|
525
588
|
const marker = generatedOutputMarker({
|
|
526
589
|
kind: 'catalog',
|
|
527
590
|
workspaceIdentity: catalogWorkspace.workspaceIdentity,
|
|
@@ -532,6 +595,7 @@ export async function buildCatalogWorkspace(options) {
|
|
|
532
595
|
force: options.force === true,
|
|
533
596
|
build: async (stagingRoot) => {
|
|
534
597
|
const bundledPlugins = [];
|
|
598
|
+
await fs.ensureDir(path.join(stagingRoot, 'plugins'));
|
|
535
599
|
for (const plugin of plugins) {
|
|
536
600
|
const closure = dependencyClosure(plugin, plugins);
|
|
537
601
|
const outputRoot = path.join(stagingRoot, 'plugins', plugin.manifest.name);
|
|
@@ -550,20 +614,14 @@ export async function buildCatalogWorkspace(options) {
|
|
|
550
614
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name));
|
|
551
615
|
await fs.outputJson(path.join(stagingRoot, '.agents', 'plugins', 'marketplace.json'), createMarketplaceManifest(catalogWorkspace.metadata, byPlatform('codex')), { spaces: 2 });
|
|
552
616
|
await fs.outputJson(path.join(stagingRoot, '.claude-plugin', 'marketplace.json'), createMarketplaceManifest(catalogWorkspace.metadata, byPlatform('claude')), { spaces: 2 });
|
|
553
|
-
let minVersion = '0.0.0';
|
|
554
|
-
for (const plugin of bundledPlugins) {
|
|
555
|
-
const candidate = plugin.manifest.compatibility.aw.minVersion;
|
|
556
|
-
if (compareStrictSemVer(candidate, minVersion) > 0)
|
|
557
|
-
minVersion = candidate;
|
|
558
|
-
}
|
|
559
617
|
const entries = await Promise.all([...bundledPlugins]
|
|
560
618
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
|
|
561
619
|
.map((plugin) => expectedCatalogEntry(plugin, plugin.rootDir)));
|
|
562
620
|
const manifest = {
|
|
563
|
-
schemaVersion:
|
|
621
|
+
schemaVersion: 6,
|
|
564
622
|
catalogVersion,
|
|
565
623
|
channel,
|
|
566
|
-
|
|
624
|
+
requires: { aw: { capabilities: ['aw.catalog-snapshot.v6'] } },
|
|
567
625
|
marketplace: {
|
|
568
626
|
owner: catalogWorkspace.metadata.owner,
|
|
569
627
|
interface: catalogWorkspace.metadata.interface,
|
|
@@ -574,9 +632,9 @@ export async function buildCatalogWorkspace(options) {
|
|
|
574
632
|
await fs.writeJson(path.join(stagingRoot, CATALOG_MANIFEST_FILENAME), manifest, { spaces: 2 });
|
|
575
633
|
await fs.writeJson(path.join(stagingRoot, GENERATED_OUTPUT_MARKER_FILENAME), marker, { spaces: 2 });
|
|
576
634
|
},
|
|
577
|
-
validate: async (stagingRoot) => { await
|
|
635
|
+
validate: async (stagingRoot) => { await validateCatalogSnapshotComplete(stagingRoot); },
|
|
578
636
|
assertReplaceable: async (existingRoot) => {
|
|
579
|
-
await
|
|
637
|
+
await validateCatalogSnapshotComplete(existingRoot);
|
|
580
638
|
await assertGeneratedOutput(existingRoot, marker);
|
|
581
639
|
},
|
|
582
640
|
});
|
|
@@ -596,16 +654,90 @@ async function rewritePublishedPluginVersions(snapshotRoot, overrides) {
|
|
|
596
654
|
const manifest = await fs.readJson(manifestPath);
|
|
597
655
|
await fs.writeFile(manifestPath, `${JSON.stringify({ ...manifest, version }, null, 2)}\n`);
|
|
598
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.previous?.entries.map((entry) => [entry.name, entry]) ?? []);
|
|
683
|
+
const versions = {};
|
|
684
|
+
for (const plugin of options.plugins) {
|
|
685
|
+
const name = plugin.manifest.name;
|
|
686
|
+
const sourceVersion = assertStablePluginVersion(plugin.manifest.version, `${name} source version`);
|
|
687
|
+
const contentDigest = options.contentDigests.get(name);
|
|
688
|
+
if (!contentDigest)
|
|
689
|
+
throw new Error(`Missing release content digest for plugin ${JSON.stringify(name)}.`);
|
|
690
|
+
const previous = previousEntries.get(name);
|
|
691
|
+
if (!previous) {
|
|
692
|
+
versions[name] = options.channel === 'beta' ? `${sourceVersion}-beta.1` : sourceVersion;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
const previousVersion = assertStrictSemVer(previous.version, `${name} previous published version`);
|
|
696
|
+
if (options.channel === 'latest') {
|
|
697
|
+
const comparison = compareStrictSemVer(sourceVersion, previousVersion);
|
|
698
|
+
if (comparison < 0) {
|
|
699
|
+
throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous ${previousVersion}.`);
|
|
700
|
+
}
|
|
701
|
+
if (comparison === 0 && previous.contentDigest !== contentDigest) {
|
|
702
|
+
throw new Error(`Plugin ${name} version ${sourceVersion} maps to different content. Increase the source plugin version.`);
|
|
703
|
+
}
|
|
704
|
+
versions[name] = sourceVersion;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const previousBase = stableVersionBase(previousVersion, `${name} previous published version`);
|
|
708
|
+
const baseComparison = compareStrictSemVer(sourceVersion, previousBase);
|
|
709
|
+
if (baseComparison < 0) {
|
|
710
|
+
throw new Error(`Plugin ${name} source version ${sourceVersion} must not be lower than previous base ${previousBase}.`);
|
|
711
|
+
}
|
|
712
|
+
if (baseComparison > 0) {
|
|
713
|
+
versions[name] = `${sourceVersion}-beta.1`;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const parsedPrevious = parseStrictSemVer(previousVersion, `${name} previous published version`);
|
|
717
|
+
if (parsedPrevious.prerelease.length === 0) {
|
|
718
|
+
if (previous.contentDigest !== contentDigest) {
|
|
719
|
+
throw new Error(`Plugin ${name} stable version ${sourceVersion} maps to different content. Increase the source plugin version.`);
|
|
720
|
+
}
|
|
721
|
+
versions[name] = previousVersion;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (parsedPrevious.prerelease.length !== 2
|
|
725
|
+
|| parsedPrevious.prerelease[0] !== 'beta'
|
|
726
|
+
|| !/^[1-9]\d*$/u.test(parsedPrevious.prerelease[1])) {
|
|
727
|
+
throw new Error(`Plugin ${name} previous version ${previousVersion} is not a stable or beta publication version.`);
|
|
728
|
+
}
|
|
729
|
+
versions[name] = previous.contentDigest === contentDigest
|
|
730
|
+
? previousVersion
|
|
731
|
+
: `${sourceVersion}-beta.${Number(parsedPrevious.prerelease[1]) + 1}`;
|
|
599
732
|
}
|
|
733
|
+
return versions;
|
|
600
734
|
}
|
|
601
735
|
export async function validatePublishedCatalogSnapshot(snapshotDirectory) {
|
|
602
|
-
const validation = await
|
|
736
|
+
const validation = await validateCatalogSnapshotComplete(snapshotDirectory);
|
|
603
737
|
if (validation.manifest.release.state !== 'published') {
|
|
604
738
|
throw new Error('Published Catalog snapshot must have release.state published.');
|
|
605
739
|
}
|
|
606
|
-
|
|
607
|
-
throw new Error('Published Catalog snapshot must include a plugin release ledger.');
|
|
608
|
-
return { ...validation, ledger: validation.ledger };
|
|
740
|
+
return validation;
|
|
609
741
|
}
|
|
610
742
|
export async function preparePublishedCatalogWorkspace(options) {
|
|
611
743
|
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(options.sourceHead)) {
|
|
@@ -623,10 +755,6 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
623
755
|
if (previous && compareStrictSemVer(catalogVersion, previous.manifest.catalogVersion) <= 0) {
|
|
624
756
|
throw new Error(`Catalog version ${catalogVersion} must be higher than published ${previous.manifest.catalogVersion}.`);
|
|
625
757
|
}
|
|
626
|
-
const previousLedger = previous?.ledger ?? {
|
|
627
|
-
schemaVersion: PLUGIN_RELEASE_LEDGER_SCHEMA_VERSION,
|
|
628
|
-
records: [],
|
|
629
|
-
};
|
|
630
758
|
const temporaryRelative = path.join('.aw', 'publish', `.catalog-preparation-${crypto.randomUUID()}`);
|
|
631
759
|
const finalRelative = options.outputPath
|
|
632
760
|
?? `${DEFAULT_PUBLISHED_CATALOG_OUTPUT_PATH}-v${catalogVersion}`;
|
|
@@ -639,15 +767,14 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
639
767
|
reservedImports: options.reservedImports,
|
|
640
768
|
});
|
|
641
769
|
temporaryRoot = built.outputRoot;
|
|
642
|
-
const development = await
|
|
770
|
+
const development = await validateCatalogSnapshotComplete(temporaryRoot);
|
|
643
771
|
const contentDigests = new Map(development.manifest.entries.map((entry) => [entry.name, entry.contentDigest]));
|
|
644
|
-
const overrides =
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
: Object.fromEntries(catalogWorkspace.plugins.map((plugin) => [plugin.manifest.name, plugin.manifest.version]));
|
|
772
|
+
const overrides = resolvePublishedPluginVersions({
|
|
773
|
+
channel,
|
|
774
|
+
plugins: catalogWorkspace.plugins,
|
|
775
|
+
contentDigests,
|
|
776
|
+
previous: previous?.manifest ?? null,
|
|
777
|
+
});
|
|
651
778
|
await rewritePublishedPluginVersions(temporaryRoot, overrides);
|
|
652
779
|
const bundledPlugins = await Promise.all(catalogWorkspace.plugins.map((plugin) => readPluginManifest(path.join(temporaryRoot, 'plugins', plugin.manifest.name), { mode: 'bundle' })));
|
|
653
780
|
const entries = await Promise.all(bundledPlugins.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))
|
|
@@ -658,26 +785,14 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
658
785
|
channel,
|
|
659
786
|
entries,
|
|
660
787
|
};
|
|
661
|
-
const contentDigest = publishedContentDigest(manifestWithoutRelease);
|
|
662
788
|
const manifest = {
|
|
663
789
|
...manifestWithoutRelease,
|
|
664
790
|
release: {
|
|
665
791
|
state: 'published',
|
|
666
|
-
targetVersion: catalogVersion.replace(/-beta\.[1-9]\d*$/u, ''),
|
|
667
|
-
planId: contentDigest,
|
|
668
792
|
sourceHead: options.sourceHead,
|
|
669
|
-
contentDigest,
|
|
670
793
|
},
|
|
671
794
|
};
|
|
672
|
-
|
|
673
|
-
previous: previousLedger,
|
|
674
|
-
manifest,
|
|
675
|
-
contentDigests,
|
|
676
|
-
});
|
|
677
|
-
await Promise.all([
|
|
678
|
-
fs.writeFile(path.join(temporaryRoot, CATALOG_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`),
|
|
679
|
-
fs.writeFile(path.join(temporaryRoot, PLUGIN_RELEASE_LEDGER_FILENAME), `${JSON.stringify(ledger, null, 2)}\n`),
|
|
680
|
-
]);
|
|
795
|
+
await fs.writeFile(path.join(temporaryRoot, CATALOG_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
681
796
|
const prepared = await validatePublishedCatalogSnapshot(temporaryRoot);
|
|
682
797
|
const resolved = await resolveCatalogWorkspaceOutputPath({
|
|
683
798
|
workspaceRoot: catalogWorkspace.workspaceRoot,
|
|
@@ -701,8 +816,6 @@ export async function preparePublishedCatalogWorkspace(options) {
|
|
|
701
816
|
channel,
|
|
702
817
|
pluginNames: built.pluginNames,
|
|
703
818
|
snapshotDigest: prepared.snapshotDigest,
|
|
704
|
-
contentDigest,
|
|
705
|
-
planId: contentDigest,
|
|
706
819
|
};
|
|
707
820
|
}
|
|
708
821
|
finally {
|
|
@@ -828,7 +941,7 @@ async function digestDirectory(root) {
|
|
|
828
941
|
await walk(root);
|
|
829
942
|
return `sha256:${crypto.createHash('sha256').update(entries.join('\n')).digest('hex')}`;
|
|
830
943
|
}
|
|
831
|
-
|
|
944
|
+
async function validateCatalogSnapshotTransportInternal(snapshotDirectory) {
|
|
832
945
|
const rootDir = await assertDirectoryWithoutSymlinks(snapshotDirectory, 'Catalog snapshot');
|
|
833
946
|
const allowed = new Set([
|
|
834
947
|
CATALOG_MANIFEST_FILENAME,
|
|
@@ -836,7 +949,6 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
836
949
|
'.agents',
|
|
837
950
|
'.claude-plugin',
|
|
838
951
|
'plugins',
|
|
839
|
-
'plugin-release-ledger.json',
|
|
840
952
|
]);
|
|
841
953
|
const entries = await fs.readdir(rootDir);
|
|
842
954
|
const unexpected = entries.filter((entry) => !allowed.has(entry));
|
|
@@ -878,14 +990,17 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
878
990
|
const snapshotDigest = await digestDirectory(rootDir);
|
|
879
991
|
const raw = await fs.readJson(path.join(rootDir, CATALOG_MANIFEST_FILENAME));
|
|
880
992
|
const allowedManifestKeys = new Set([
|
|
881
|
-
'schemaVersion', 'catalogVersion', 'channel', '
|
|
993
|
+
'schemaVersion', 'catalogVersion', 'channel', 'requires', 'marketplace', 'release', 'entries',
|
|
882
994
|
]);
|
|
883
995
|
for (const key of Object.keys(raw))
|
|
884
996
|
if (!allowedManifestKeys.has(key))
|
|
885
997
|
throw new Error(`Invalid catalog manifest: unknown field ${JSON.stringify(key)}.`);
|
|
886
|
-
if (raw.schemaVersion !==
|
|
887
|
-
throw new Error('Invalid catalog manifest schemaVersion: expected
|
|
888
|
-
const base = normalizeCatalogManifestBase(raw, 'catalog manifest
|
|
998
|
+
if (raw.schemaVersion !== 6)
|
|
999
|
+
throw new Error('Invalid catalog manifest schemaVersion: expected 6.');
|
|
1000
|
+
const base = normalizeCatalogManifestBase(raw, 'catalog manifest v6');
|
|
1001
|
+
if (!(base.requires.aw.capabilities ?? []).includes('aw.catalog-snapshot.v6')) {
|
|
1002
|
+
throw new Error('Invalid catalog manifest requires.aw.capabilities: expected aw.catalog-snapshot.v6.');
|
|
1003
|
+
}
|
|
889
1004
|
const marketplaceIdentity = normalizeCatalogMarketplaceIdentity(raw.marketplace);
|
|
890
1005
|
if (!Array.isArray(raw.entries))
|
|
891
1006
|
throw new Error('Invalid catalog manifest entries: expected an array.');
|
|
@@ -901,7 +1016,7 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
901
1016
|
delete copy.contentDigest;
|
|
902
1017
|
return copy;
|
|
903
1018
|
});
|
|
904
|
-
const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest
|
|
1019
|
+
const normalizedEntries = normalizeCatalogManifestEntries(entriesWithoutContentDigest, base, 'catalog manifest v6')
|
|
905
1020
|
.map((entry, index) => ({ ...entry, contentDigest: contentDigests[index] }));
|
|
906
1021
|
const pluginNames = normalizedEntries.map((entry, index) => {
|
|
907
1022
|
if (typeof entry.name !== 'string')
|
|
@@ -915,39 +1030,57 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
915
1030
|
const diskNames = (await fs.readdir(path.join(rootDir, 'plugins'))).sort();
|
|
916
1031
|
if (diskNames.join('\0') !== [...pluginNames].sort().join('\0'))
|
|
917
1032
|
throw new Error('Invalid catalog snapshot plugin inventory.');
|
|
918
|
-
const
|
|
1033
|
+
const platformEntries = {
|
|
1034
|
+
claude: [],
|
|
1035
|
+
codex: [],
|
|
1036
|
+
};
|
|
919
1037
|
for (const [index, name] of pluginNames.entries()) {
|
|
920
1038
|
const pluginRoot = path.join(rootDir, 'plugins', name);
|
|
921
|
-
|
|
1039
|
+
await assertDirectoryWithoutSymlinks(pluginRoot, `Catalog plugin ${JSON.stringify(name)}`);
|
|
1040
|
+
const entry = normalizedEntries[index];
|
|
1041
|
+
if (entry.source !== `./plugins/${name}`) {
|
|
1042
|
+
throw new Error(`Catalog entry source for ${JSON.stringify(name)} does not match its plugin bundle.`);
|
|
1043
|
+
}
|
|
1044
|
+
const rawBundleManifest = await readRegularJson(path.join(pluginRoot, SOURCE_MANIFEST_PATH), `Catalog plugin ${JSON.stringify(name)} bundle manifest`);
|
|
1045
|
+
const expectedDigests = {
|
|
1046
|
+
artifactDigest: await calculateEntryDigest(pluginRoot, false),
|
|
1047
|
+
manifestDigest: sha256(normalizeManifestDigestInput(rawBundleManifest)),
|
|
1048
|
+
contentDigest: await calculateEntryDigest(pluginRoot, true),
|
|
1049
|
+
};
|
|
1050
|
+
for (const field of Object.keys(expectedDigests)) {
|
|
1051
|
+
if (entry[field] !== expectedDigests[field]) {
|
|
1052
|
+
throw new Error(`Catalog entry ${field} for ${JSON.stringify(name)} does not match its plugin bundle.`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
922
1055
|
for (const platform of ['claude', 'codex']) {
|
|
923
1056
|
const platformManifest = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
|
|
924
1057
|
const exists = await fs.pathExists(platformManifest);
|
|
925
|
-
if (exists !== resolveSupportedPlatforms(plugin.manifest).has(platform)) {
|
|
926
|
-
throw new Error(`Catalog plugin ${JSON.stringify(name)} has an inconsistent ${platform} bundle projection.`);
|
|
927
|
-
}
|
|
928
1058
|
if (exists) {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
const expected = await expectedCatalogEntry(plugin, pluginRoot);
|
|
934
|
-
for (const field of Object.keys(expected)) {
|
|
935
|
-
if (!isDeepStrictEqual(normalizedEntries[index][field], expected[field])) {
|
|
936
|
-
throw new Error(`Catalog entry ${field} for ${JSON.stringify(name)} does not match its plugin bundle.`);
|
|
1059
|
+
await readRegularJson(platformManifest, `${platform} plugin manifest`);
|
|
1060
|
+
platformEntries[platform].push(entry);
|
|
937
1061
|
}
|
|
938
1062
|
}
|
|
939
|
-
plugins.push(plugin);
|
|
940
1063
|
}
|
|
941
|
-
assertDependencyGraph(plugins);
|
|
942
1064
|
const agentsMarketplace = await fs.readJson(path.join(rootDir, '.agents', 'plugins', 'marketplace.json'));
|
|
943
1065
|
const claudeMarketplace = await fs.readJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'));
|
|
944
|
-
const
|
|
1066
|
+
const marketplaceEntry = (entry) => ({
|
|
1067
|
+
name: entry.name,
|
|
1068
|
+
source: entry.source,
|
|
1069
|
+
description: entry.description,
|
|
1070
|
+
interface: entry.interface,
|
|
1071
|
+
category: entry.category,
|
|
1072
|
+
tags: entry.tags,
|
|
1073
|
+
strict: entry.strict,
|
|
1074
|
+
policy: entry.policy,
|
|
1075
|
+
});
|
|
1076
|
+
const expectedMarketplace = (platform) => ({
|
|
1077
|
+
name: 'aw',
|
|
945
1078
|
owner: marketplaceIdentity.owner,
|
|
946
1079
|
interface: marketplaceIdentity.interface,
|
|
947
|
-
|
|
948
|
-
};
|
|
949
|
-
const expectedAgents =
|
|
950
|
-
const expectedClaude =
|
|
1080
|
+
plugins: platformEntries[platform].map(marketplaceEntry),
|
|
1081
|
+
});
|
|
1082
|
+
const expectedAgents = expectedMarketplace('codex');
|
|
1083
|
+
const expectedClaude = expectedMarketplace('claude');
|
|
951
1084
|
const normalizeMarketplaceOrder = (marketplace) => {
|
|
952
1085
|
const canonical = JSON.parse(JSON.stringify(marketplace));
|
|
953
1086
|
return {
|
|
@@ -963,32 +1096,60 @@ export async function validateCatalogSnapshot(snapshotDirectory) {
|
|
|
963
1096
|
if (!isDeepStrictEqual(normalizeMarketplaceOrder(claudeMarketplace), normalizeMarketplaceOrder(expectedClaude))) {
|
|
964
1097
|
throw new Error('Claude marketplace metadata or plugin inventory does not match the snapshot bundles.');
|
|
965
1098
|
}
|
|
966
|
-
const release = normalizeCatalogRelease(raw.release
|
|
1099
|
+
const release = normalizeCatalogRelease(raw.release);
|
|
967
1100
|
const manifest = {
|
|
968
|
-
schemaVersion:
|
|
1101
|
+
schemaVersion: 6,
|
|
969
1102
|
catalogVersion: base.catalogVersion,
|
|
970
1103
|
channel: base.channel,
|
|
971
|
-
|
|
1104
|
+
requires: base.requires,
|
|
972
1105
|
marketplace: marketplaceIdentity,
|
|
973
1106
|
release,
|
|
974
1107
|
entries: normalizedEntries,
|
|
975
1108
|
};
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1109
|
+
return { rootDir, manifest, snapshotDigest };
|
|
1110
|
+
}
|
|
1111
|
+
export async function validateCatalogSnapshotTransport(snapshotDirectory) {
|
|
1112
|
+
return validateCatalogSnapshotTransportInternal(snapshotDirectory);
|
|
1113
|
+
}
|
|
1114
|
+
async function validateCatalogPluginEntriesComplete(validation, pluginNames) {
|
|
1115
|
+
const entriesByName = new Map(validation.manifest.entries.map((entry) => [entry.name, entry]));
|
|
1116
|
+
const plugins = [];
|
|
1117
|
+
for (const name of pluginNames) {
|
|
1118
|
+
const entry = entriesByName.get(name);
|
|
1119
|
+
if (!entry)
|
|
1120
|
+
throw new Error(`Catalog plugin ${JSON.stringify(name)} does not exist.`);
|
|
1121
|
+
const pluginRoot = path.join(validation.rootDir, 'plugins', entry.name);
|
|
1122
|
+
const plugin = await readPluginManifest(pluginRoot, { mode: 'bundle' });
|
|
1123
|
+
await validateBundleRuntimeRequirementsReceipts(plugin, pluginRoot);
|
|
1124
|
+
for (const platform of ['claude', 'codex']) {
|
|
1125
|
+
const platformManifestPath = path.join(pluginRoot, `.${platform}-plugin`, 'plugin.json');
|
|
1126
|
+
const exists = await fs.pathExists(platformManifestPath);
|
|
1127
|
+
if (exists !== resolveSupportedPlatforms(plugin.manifest).has(platform)) {
|
|
1128
|
+
throw new Error(`Catalog plugin ${JSON.stringify(entry.name)} has an inconsistent ${platform} bundle projection.`);
|
|
1129
|
+
}
|
|
1130
|
+
if (exists) {
|
|
1131
|
+
const rawPlatformManifest = await readRegularJson(platformManifestPath, `${platform} plugin manifest`);
|
|
1132
|
+
await validatePlatformManifestProjection(plugin, platform, rawPlatformManifest);
|
|
1133
|
+
}
|
|
980
1134
|
}
|
|
1135
|
+
const expected = await expectedCatalogEntry(plugin, pluginRoot);
|
|
1136
|
+
for (const field of Object.keys(expected)) {
|
|
1137
|
+
if (!isDeepStrictEqual(entry[field], expected[field])) {
|
|
1138
|
+
throw new Error(`Catalog entry ${field} for ${JSON.stringify(entry.name)} does not match its plugin bundle.`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
plugins.push(plugin);
|
|
981
1142
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
return
|
|
1143
|
+
assertDependencyGraph(plugins);
|
|
1144
|
+
}
|
|
1145
|
+
export async function validateCatalogSnapshotPluginEntriesComplete(snapshotDirectory, pluginNames) {
|
|
1146
|
+
const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
|
|
1147
|
+
await validateCatalogPluginEntriesComplete(validation, pluginNames);
|
|
1148
|
+
return validation;
|
|
1149
|
+
}
|
|
1150
|
+
export async function validateCatalogSnapshotComplete(snapshotDirectory) {
|
|
1151
|
+
const validation = await validateCatalogSnapshotTransport(snapshotDirectory);
|
|
1152
|
+
await validateCatalogPluginEntriesComplete(validation, validation.manifest.entries.map((entry) => entry.name));
|
|
1153
|
+
return validation;
|
|
993
1154
|
}
|
|
994
1155
|
//# sourceMappingURL=workspace.js.map
|