📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -831,6 +831,18 @@ def coerce_metadata_text(value):
|
||||
return value
|
||||
return str(value)
|
||||
|
||||
|
||||
def coerce_metadata_list(value):
|
||||
if isinstance(value, set):
|
||||
values = [coerce_metadata_text(item) for item in sorted(value, key=str)]
|
||||
elif isinstance(value, (list, tuple)):
|
||||
values = [coerce_metadata_text(item) for item in value]
|
||||
elif isinstance(value, str):
|
||||
values = value.split(",") if "," in value else value.split()
|
||||
else:
|
||||
return []
|
||||
return list(dict.fromkeys(item.strip() for item in values if item and item.strip()))
|
||||
|
||||
def parse_frontmatter(content):
|
||||
"""
|
||||
Parses YAML frontmatter, sanitizing unquoted values containing @.
|
||||
@@ -936,6 +948,11 @@ def generate_index(skills_dir, output_file, compatibility_report=None):
|
||||
license_source = coerce_metadata_text(metadata.get("license_source"))
|
||||
date_added = coerce_metadata_text(metadata.get("date_added"))
|
||||
category = coerce_metadata_text(metadata.get("category"))
|
||||
tags_value = metadata.get("tags")
|
||||
nested_metadata = metadata.get("metadata")
|
||||
if tags_value is None and isinstance(nested_metadata, Mapping):
|
||||
tags_value = nested_metadata.get("tags")
|
||||
tags = coerce_metadata_list(tags_value)
|
||||
|
||||
if name is not None:
|
||||
skill_info["name"] = name
|
||||
@@ -955,6 +972,8 @@ def generate_index(skills_dir, output_file, compatibility_report=None):
|
||||
skill_info["license_source"] = license_source
|
||||
if date_added is not None:
|
||||
skill_info["date_added"] = date_added
|
||||
if tags:
|
||||
skill_info["tags"] = tags
|
||||
|
||||
# Category: prefer frontmatter, then folder structure, then conservative inference
|
||||
if category is not None:
|
||||
|
||||
@@ -174,12 +174,15 @@ function prepareRelease(projectRoot, version) {
|
||||
console.log(`[release] package.json already set to ${version}; keeping current version.`);
|
||||
}
|
||||
|
||||
runReleaseSuite(projectRoot);
|
||||
runCommand(
|
||||
"npm",
|
||||
["run", "sync:metadata", "--", "--refresh-volatile"],
|
||||
projectRoot,
|
||||
);
|
||||
// Volatile metadata is an input to catalog timestamps. Refresh it before the
|
||||
// canonical release sync so the tagged tree is exactly what publish CI will
|
||||
// regenerate and verify.
|
||||
runReleaseSuite(projectRoot);
|
||||
|
||||
const refreshedReleaseNotes = ensureChangelogSection(projectRoot, version);
|
||||
const notesPath = writeReleaseNotes(projectRoot, version, refreshedReleaseNotes);
|
||||
|
||||
@@ -655,6 +655,238 @@ def _materialize_plugin_skills(root: Path, destination_root: Path, skill_ids: li
|
||||
_copy_skill_directory(root, skill_id, destination_root)
|
||||
|
||||
|
||||
def _skill_tree_files(root: Path, skill_ids: list[str]) -> dict[str, Path]:
|
||||
files: dict[str, Path] = {}
|
||||
skills_root = root / "skills"
|
||||
resolved_skills_root = skills_root.resolve()
|
||||
|
||||
def collect(source_path: Path, relative_path: Path) -> None:
|
||||
resolved_source = source_path.resolve(strict=True)
|
||||
resolved_source.relative_to(resolved_skills_root)
|
||||
if resolved_source.is_dir():
|
||||
for child in resolved_source.iterdir():
|
||||
collect(child, relative_path / child.name)
|
||||
return
|
||||
files[relative_path.as_posix()] = resolved_source
|
||||
|
||||
for skill_id in skill_ids:
|
||||
source_root = skills_root / skill_id
|
||||
if not source_root.is_dir():
|
||||
raise ValueError(f"Expected canonical skill directory is missing: {skill_id}")
|
||||
for child in source_root.iterdir():
|
||||
collect(child, Path(skill_id) / child.name)
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def _assert_skill_mirror_matches(
|
||||
root: Path,
|
||||
destination_root: Path,
|
||||
skill_ids: list[str],
|
||||
label: str,
|
||||
) -> None:
|
||||
if destination_root.is_symlink():
|
||||
raise ValueError(f"{label} skills directory must not be a symlink: {destination_root}")
|
||||
if not destination_root.is_dir():
|
||||
raise ValueError(f"{label} skills directory is missing: {destination_root}")
|
||||
|
||||
expected_files = _skill_tree_files(root, skill_ids)
|
||||
actual_files = {
|
||||
path.relative_to(destination_root).as_posix(): path
|
||||
for path in destination_root.rglob("*")
|
||||
if path.is_file()
|
||||
}
|
||||
mirrored_symlinks = sorted(
|
||||
path.relative_to(destination_root).as_posix()
|
||||
for path in destination_root.rglob("*")
|
||||
if path.is_symlink()
|
||||
)
|
||||
if mirrored_symlinks:
|
||||
raise ValueError(f"{label} contains unexpected symlink: {mirrored_symlinks[0]}")
|
||||
expected_paths = set(expected_files)
|
||||
actual_paths = set(actual_files)
|
||||
|
||||
missing = sorted(expected_paths - actual_paths)
|
||||
if missing:
|
||||
raise ValueError(f"{label} is missing mirrored file: {missing[0]}")
|
||||
|
||||
unexpected = sorted(actual_paths - expected_paths)
|
||||
if unexpected:
|
||||
raise ValueError(f"{label} contains unexpected mirrored file: {unexpected[0]}")
|
||||
|
||||
for relative_path in sorted(expected_paths):
|
||||
if expected_files[relative_path].read_bytes() != actual_files[relative_path].read_bytes():
|
||||
raise ValueError(f"{label} contains stale mirrored file: {relative_path}")
|
||||
|
||||
|
||||
def _assert_json_matches(
|
||||
path: Path,
|
||||
expected: dict[str, Any],
|
||||
label: str,
|
||||
allowed_root: Path,
|
||||
) -> None:
|
||||
relative_path = path.relative_to(allowed_root)
|
||||
current_path = allowed_root
|
||||
for part in relative_path.parts:
|
||||
current_path /= part
|
||||
if current_path.is_symlink():
|
||||
raise ValueError(f"{label} path must not contain a symlink: {current_path}")
|
||||
if not path.is_file():
|
||||
raise ValueError(f"{label} is missing: {path}")
|
||||
expected_content = (json.dumps(expected, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
|
||||
if path.read_bytes() != expected_content:
|
||||
raise ValueError(f"{label} is out of sync: {path}")
|
||||
|
||||
|
||||
def _assert_plugin_metadata_layout(
|
||||
plugin_root: Path,
|
||||
expected_relative_paths: set[str],
|
||||
label: str,
|
||||
) -> None:
|
||||
if plugin_root.is_symlink():
|
||||
raise ValueError(f"{label} root must not be a symlink: {plugin_root}")
|
||||
if not plugin_root.is_dir():
|
||||
raise ValueError(f"{label} root is missing: {plugin_root}")
|
||||
actual_relative_paths = {
|
||||
path.relative_to(plugin_root).as_posix()
|
||||
for path in plugin_root.rglob("*")
|
||||
if path.is_file() and "skills" not in path.relative_to(plugin_root).parts[:1]
|
||||
}
|
||||
unexpected_symlinks = sorted(
|
||||
path.relative_to(plugin_root).as_posix()
|
||||
for path in plugin_root.rglob("*")
|
||||
if path.is_symlink() and "skills" not in path.relative_to(plugin_root).parts[:1]
|
||||
)
|
||||
if unexpected_symlinks:
|
||||
raise ValueError(f"{label} contains unexpected metadata symlink: {unexpected_symlinks[0]}")
|
||||
if actual_relative_paths != expected_relative_paths:
|
||||
missing = sorted(expected_relative_paths - actual_relative_paths)
|
||||
unexpected = sorted(actual_relative_paths - expected_relative_paths)
|
||||
detail = f"missing {missing[0]}" if missing else f"unexpected {unexpected[0]}"
|
||||
raise ValueError(f"{label} metadata layout is out of sync: {detail}")
|
||||
|
||||
|
||||
def check_editorial_bundle_plugins(
|
||||
root: Path,
|
||||
metadata: dict[str, Any],
|
||||
bundles: list[dict[str, Any]],
|
||||
compatibility: dict[str, dict[str, Any]],
|
||||
) -> None:
|
||||
bundle_support = {
|
||||
bundle["id"]: _bundle_target_status(bundle, compatibility)
|
||||
for bundle in bundles
|
||||
}
|
||||
codex_skill_ids = _supported_skill_ids(compatibility, "codex")
|
||||
claude_skill_ids = _supported_skill_ids(compatibility, "claude")
|
||||
|
||||
_assert_json_matches(
|
||||
root / CODEX_MARKETPLACE_PATH,
|
||||
_render_codex_marketplace(bundles, bundle_support),
|
||||
"Codex marketplace",
|
||||
root,
|
||||
)
|
||||
_assert_json_matches(
|
||||
root / CLAUDE_MARKETPLACE_PATH,
|
||||
_render_claude_marketplace(metadata, bundles, bundle_support),
|
||||
"Claude marketplace",
|
||||
root,
|
||||
)
|
||||
_assert_json_matches(
|
||||
root / CLAUDE_PLUGIN_PATH,
|
||||
_root_claude_plugin_manifest(metadata, len(claude_skill_ids)),
|
||||
"root Claude plugin manifest",
|
||||
root,
|
||||
)
|
||||
|
||||
codex_root = root / "plugins" / ROOT_CODEX_PLUGIN_NAME
|
||||
claude_root = root / "plugins" / ROOT_CLAUDE_PLUGIN_DIRNAME
|
||||
_assert_json_matches(
|
||||
codex_root / ".codex-plugin" / "plugin.json",
|
||||
_root_codex_plugin_manifest(metadata, len(codex_skill_ids)),
|
||||
"root Codex plugin manifest",
|
||||
root,
|
||||
)
|
||||
_assert_json_matches(
|
||||
claude_root / ".claude-plugin" / "plugin.json",
|
||||
_root_claude_plugin_manifest(metadata, len(claude_skill_ids)),
|
||||
"root Claude plugin manifest",
|
||||
root,
|
||||
)
|
||||
_assert_plugin_metadata_layout(
|
||||
codex_root,
|
||||
{".codex-plugin/plugin.json"},
|
||||
"root Codex plugin",
|
||||
)
|
||||
_assert_plugin_metadata_layout(
|
||||
claude_root,
|
||||
{".claude-plugin/plugin.json"},
|
||||
"root Claude plugin",
|
||||
)
|
||||
_assert_skill_mirror_matches(root, codex_root / "skills", codex_skill_ids, "root Codex plugin")
|
||||
_assert_skill_mirror_matches(root, claude_root / "skills", claude_skill_ids, "root Claude plugin")
|
||||
|
||||
expected_bundle_names = {
|
||||
_bundle_plugin_name(bundle["id"])
|
||||
for bundle in bundles
|
||||
if bundle_support[bundle["id"]]["codex"] or bundle_support[bundle["id"]]["claude"]
|
||||
}
|
||||
actual_bundle_names = {
|
||||
path.name
|
||||
for path in (root / "plugins").glob("agentic-bundle-*")
|
||||
if path.is_dir()
|
||||
}
|
||||
if actual_bundle_names != expected_bundle_names:
|
||||
missing = sorted(expected_bundle_names - actual_bundle_names)
|
||||
unexpected = sorted(actual_bundle_names - expected_bundle_names)
|
||||
detail = f"missing {missing[0]}" if missing else f"unexpected {unexpected[0]}"
|
||||
raise ValueError(f"Generated bundle plugin directories are out of sync: {detail}")
|
||||
|
||||
for bundle in bundles:
|
||||
support = bundle_support[bundle["id"]]
|
||||
if not support["codex"] and not support["claude"]:
|
||||
continue
|
||||
plugin_root = root / "plugins" / _bundle_plugin_name(bundle["id"])
|
||||
skill_ids = [skill["id"] for skill in bundle["skills"]]
|
||||
_assert_skill_mirror_matches(
|
||||
root,
|
||||
plugin_root / "skills",
|
||||
skill_ids,
|
||||
f'bundle plugin {bundle["id"]}',
|
||||
)
|
||||
|
||||
manifest_specs = (
|
||||
(
|
||||
"codex",
|
||||
plugin_root / ".codex-plugin" / "plugin.json",
|
||||
_bundle_codex_plugin_manifest(metadata, bundle),
|
||||
),
|
||||
(
|
||||
"claude",
|
||||
plugin_root / ".claude-plugin" / "plugin.json",
|
||||
_bundle_claude_plugin_manifest(metadata, bundle),
|
||||
),
|
||||
)
|
||||
expected_manifest_paths: set[str] = set()
|
||||
for target, manifest_path, expected_manifest in manifest_specs:
|
||||
if support[target]:
|
||||
expected_manifest_paths.add(manifest_path.relative_to(plugin_root).as_posix())
|
||||
_assert_json_matches(
|
||||
manifest_path,
|
||||
expected_manifest,
|
||||
f'bundle {bundle["id"]} {target} manifest',
|
||||
root,
|
||||
)
|
||||
elif manifest_path.exists():
|
||||
raise ValueError(
|
||||
f'Bundle {bundle["id"]} contains an unsupported {target} manifest: {manifest_path}'
|
||||
)
|
||||
_assert_plugin_metadata_layout(
|
||||
plugin_root,
|
||||
expected_manifest_paths,
|
||||
f'bundle plugin {bundle["id"]}',
|
||||
)
|
||||
|
||||
|
||||
def _remove_path(path: Path) -> None:
|
||||
if path.is_symlink() or path.is_file():
|
||||
path.unlink()
|
||||
@@ -860,7 +1092,11 @@ def main() -> int:
|
||||
current_doc = (root / "docs" / "users" / "bundles.md").read_text(encoding="utf-8")
|
||||
if current_doc != expected_doc:
|
||||
raise SystemExit("docs/users/bundles.md is out of sync with data/editorial-bundles.json")
|
||||
print("✅ Editorial bundles manifest and generated doc are in sync.")
|
||||
try:
|
||||
check_editorial_bundle_plugins(root, metadata, bundles, compatibility)
|
||||
except ValueError as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
print("✅ Editorial bundles, marketplaces, manifests, and plugin mirrors are in sync.")
|
||||
return 0
|
||||
sync_editorial_bundles(root)
|
||||
print("✅ Editorial bundles synced.")
|
||||
|
||||
@@ -191,6 +191,30 @@ def sync_getting_started(content: str, metadata: dict) -> str:
|
||||
return content
|
||||
|
||||
|
||||
def sync_web_index_shell(content: str, metadata: dict) -> str:
|
||||
skill_label = metadata["total_skills_label"]
|
||||
return sync_regex_text(
|
||||
content,
|
||||
[
|
||||
(r"\d[\d,]*\+ installable agentic skills", f"{skill_label} installable agentic skills"),
|
||||
(r"\d[\d,]*\+ AI coding skills", f"{skill_label} AI coding skills"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def sync_llms_text(content: str, metadata: dict) -> str:
|
||||
skill_label = metadata["total_skills_label"]
|
||||
return sync_regex_text(
|
||||
content,
|
||||
[
|
||||
(r"Current release: V[\d.]+\.", f"Current release: V{metadata['version']}."),
|
||||
(r"\d[\d,]*\+ agentic SKILL\.md playbooks", f"{skill_label} agentic SKILL.md playbooks"),
|
||||
(r"Skill count: \d[\d,]*\+\.", f"Skill count: {skill_label}."),
|
||||
(r"\d[\d,]*\+ reusable SKILL\.md playbooks", f"{skill_label} reusable SKILL.md playbooks"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def sync_bundles_doc(content: str, metadata: dict, base_dir: str | Path | None = None) -> str:
|
||||
root = Path(base_dir) if base_dir is not None else Path(find_repo_root(__file__))
|
||||
manifest_path = root / "data" / "editorial-bundles.json"
|
||||
@@ -316,6 +340,8 @@ def sync_curated_docs(base_dir: str, metadata: dict, dry_run: bool) -> int:
|
||||
updated_files = 0
|
||||
updated_files += int(update_text_file(root / "README.md", sync_readme_copy, metadata, dry_run))
|
||||
updated_files += int(update_text_file(root / "docs" / "users" / "getting-started.md", sync_getting_started, metadata, dry_run))
|
||||
updated_files += int(update_text_file(root / "apps" / "web-app" / "index.html", sync_web_index_shell, metadata, dry_run))
|
||||
updated_files += int(update_text_file(root / "apps" / "web-app" / "public" / "llms.txt", sync_llms_text, metadata, dry_run))
|
||||
updated_files += int(
|
||||
update_text_file(
|
||||
root / "docs" / "users" / "bundles.md",
|
||||
|
||||
@@ -15,6 +15,11 @@ const publishWorkflow = readText(".github/workflows/publish-npm.yml");
|
||||
const releaseWorkflowScript = readText("tools/scripts/release_workflow.js");
|
||||
const hygieneWorkflowPath = path.join(repoRoot, ".github", "workflows", "repo-hygiene.yml");
|
||||
|
||||
const prepareReleaseBlock = releaseWorkflowScript.slice(
|
||||
releaseWorkflowScript.indexOf("function prepareRelease"),
|
||||
releaseWorkflowScript.indexOf("function publishRelease"),
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
packageJson.scripts["sync:release-state"],
|
||||
"package.json should expose a deterministic release-state sync command",
|
||||
@@ -90,6 +95,8 @@ assert.match(
|
||||
for (const filePath of [
|
||||
"README.md",
|
||||
"package.json",
|
||||
"apps/web-app/index.html",
|
||||
"apps/web-app/public/llms.txt",
|
||||
"docs/users/getting-started.md",
|
||||
"docs/users/bundles.md",
|
||||
"docs/users/claude-code-skills.md",
|
||||
@@ -277,6 +284,11 @@ assert.match(
|
||||
/runCommand\("npm", \["run", "app:install"\], projectRoot\);[\s\S]*runCommand\("npm", \["run", "app:build"\], projectRoot\);/,
|
||||
"release workflow should install web-app dependencies before building the app",
|
||||
);
|
||||
assert.ok(
|
||||
prepareReleaseBlock.indexOf('["run", "sync:metadata", "--", "--refresh-volatile"]') <
|
||||
prepareReleaseBlock.indexOf("runReleaseSuite(projectRoot)"),
|
||||
"release preparation should refresh volatile metadata before generating canonical release artifacts",
|
||||
);
|
||||
assert.match(
|
||||
publishWorkflow,
|
||||
/npm pack --dry-run --json/,
|
||||
|
||||
@@ -15,6 +15,23 @@ const release = installer.parseArgs(['--release', '14.0.0']);
|
||||
assert.strictEqual(release.versionArg, '14.0.0');
|
||||
assert.strictEqual(release.versionInfo, false);
|
||||
|
||||
const exactPreview = installer.parseArgs([
|
||||
'--codex',
|
||||
'--skills',
|
||||
'frontend-design,game-development/2d-games',
|
||||
'--dry-run',
|
||||
]);
|
||||
assert.strictEqual(exactPreview.skillsArg, 'frontend-design,game-development/2d-games');
|
||||
assert.strictEqual(exactPreview.dryRun, true);
|
||||
assert.deepStrictEqual(
|
||||
installer.parseExactSkillArg(exactPreview.skillsArg),
|
||||
['frontend-design', 'game-development/2d-games'],
|
||||
);
|
||||
assert.throws(
|
||||
() => installer.parseExactSkillArg('frontend-design,,backend-dev-guidelines'),
|
||||
/non-empty exact skill/i,
|
||||
);
|
||||
|
||||
const version = spawnSync(process.execPath, [installerPath, '--version'], { encoding: 'utf8' });
|
||||
assert.strictEqual(version.status, 0, version.stderr);
|
||||
assert.strictEqual(version.stdout.trim(), packageVersion);
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const installer = require(path.resolve(__dirname, "..", "..", "bin", "install.js"));
|
||||
const packageVersion = require(path.resolve(__dirname, "..", "..", "..", "package.json")).version;
|
||||
const pinnedRef = `v${packageVersion}`;
|
||||
|
||||
function writeSkill(repoRoot, skillPath, frontmatter) {
|
||||
const skillDir = path.join(repoRoot, "skills", skillPath);
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---\n${frontmatter}\n---\n\n# ${skillPath}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "installer-exact-selection-"));
|
||||
|
||||
try {
|
||||
const repoRoot = path.join(tmpRoot, "repo");
|
||||
fs.mkdirSync(path.join(repoRoot, "skills"), { recursive: true });
|
||||
fs.mkdirSync(path.join(repoRoot, "docs"), { recursive: true });
|
||||
fs.writeFileSync(path.join(repoRoot, "docs", "README.md"), "# Docs\n", "utf8");
|
||||
|
||||
writeSkill(
|
||||
repoRoot,
|
||||
"frontend-design",
|
||||
"name: frontend-design\ncategory: development\nrisk: safe\ntags: [frontend]",
|
||||
);
|
||||
writeSkill(
|
||||
repoRoot,
|
||||
path.join("game-development", "2d-games"),
|
||||
"name: 2D Games\ncategory: games\nrisk: safe\ntags: [game]",
|
||||
);
|
||||
writeSkill(
|
||||
repoRoot,
|
||||
path.join("legacy-games", "2d-games"),
|
||||
"name: Legacy 2D Games\ncategory: games\nrisk: safe\ntags: [game]",
|
||||
);
|
||||
writeSkill(
|
||||
repoRoot,
|
||||
"shared-one",
|
||||
"name: Shared Skill\ncategory: development\nrisk: safe\ntags: [shared]",
|
||||
);
|
||||
writeSkill(
|
||||
repoRoot,
|
||||
"shared-two",
|
||||
"name: Shared Skill\ncategory: development\nrisk: safe\ntags: [shared]",
|
||||
);
|
||||
|
||||
const requested = installer.parseExactSkillArg("frontend-design,game-development/2d-games");
|
||||
assert.deepStrictEqual(
|
||||
installer.getInstallEntries(repoRoot, installer.buildInstallSelectors({}), requested),
|
||||
["frontend-design", "game-development/2d-games", "docs"],
|
||||
"--skills should resolve exact root ids and full nested paths",
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
installer.getInstallEntries(
|
||||
repoRoot,
|
||||
installer.buildInstallSelectors({ categoryArg: "development" }),
|
||||
requested,
|
||||
),
|
||||
["frontend-design", "docs"],
|
||||
"--skills must combine with metadata filters using AND",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => installer.getInstallEntries(
|
||||
repoRoot,
|
||||
installer.buildInstallSelectors({ categoryArg: "games" }),
|
||||
installer.parseExactSkillArg("frontend-design"),
|
||||
),
|
||||
/No skills matched/i,
|
||||
"an empty exact-selection/filter intersection must fail instead of installing docs or a broad fallback",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => installer.getInstallEntries(
|
||||
repoRoot,
|
||||
installer.buildInstallSelectors({}),
|
||||
installer.parseExactSkillArg("does-not-exist"),
|
||||
),
|
||||
/unknown skill requested/i,
|
||||
"unknown exact skill selections must fail",
|
||||
);
|
||||
assert.throws(
|
||||
() => installer.getInstallEntries(
|
||||
repoRoot,
|
||||
installer.buildInstallSelectors({}),
|
||||
installer.parseExactSkillArg("2d-games"),
|
||||
),
|
||||
/ambiguous skill requested/i,
|
||||
"ambiguous basename selections must require a full nested path",
|
||||
);
|
||||
assert.throws(
|
||||
() => installer.getInstallEntries(
|
||||
repoRoot,
|
||||
installer.buildInstallSelectors({}),
|
||||
installer.parseExactSkillArg("Shared Skill"),
|
||||
),
|
||||
/ambiguous skill requested/i,
|
||||
"ambiguous canonical names must fail",
|
||||
);
|
||||
|
||||
const absentTarget = path.join(tmpRoot, "absent-target");
|
||||
assert.throws(
|
||||
() => installer.installForTarget(
|
||||
repoRoot,
|
||||
{ name: "Absent", path: absentTarget },
|
||||
installer.buildInstallSelectors({}),
|
||||
null,
|
||||
installer.parseExactSkillArg("does-not-exist"),
|
||||
),
|
||||
/unknown skill requested/i,
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.existsSync(absentTarget),
|
||||
false,
|
||||
"invalid --skills selection must fail before an absent target is created",
|
||||
);
|
||||
|
||||
const existingTarget = path.join(tmpRoot, "existing-target");
|
||||
fs.mkdirSync(existingTarget, { recursive: true });
|
||||
const sentinelPath = path.join(existingTarget, "sentinel.txt");
|
||||
fs.writeFileSync(sentinelPath, "keep", "utf8");
|
||||
const manifestPath = path.join(existingTarget, ".antigravity-install-manifest.json");
|
||||
fs.writeFileSync(
|
||||
manifestPath,
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
entries: ["frontend-design", "legacy-skill", "docs", "../outside"],
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const plan = installer.buildDryRunPlan(
|
||||
pinnedRef,
|
||||
[
|
||||
{ name: "Existing", path: existingTarget },
|
||||
{ name: "Absent", path: absentTarget },
|
||||
],
|
||||
["game-development/2d-games", "frontend-design", "docs"],
|
||||
);
|
||||
assert.deepStrictEqual(plan.targets[0].remove, ["legacy-skill"]);
|
||||
assert.deepStrictEqual(plan.targets[0].ignoredUnsafeManifestEntries, ["../outside"]);
|
||||
assert.strictEqual(plan.targets[1].targetExists, false);
|
||||
const output = [];
|
||||
const originalLog = console.log;
|
||||
console.log = (message) => output.push(String(message));
|
||||
try {
|
||||
installer.printDryRunPlan(plan);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
assert.strictEqual(fs.readFileSync(sentinelPath, "utf8"), "keep");
|
||||
assert.strictEqual(fs.existsSync(absentTarget), false);
|
||||
assert.match(output.join("\n"), new RegExp(`Ref: ${pinnedRef.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
|
||||
assert.match(output.join("\n"), new RegExp(existingTarget.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
assert.match(output.join("\n"), /Exact skill set \(2\):/);
|
||||
assert.match(output.join("\n"), /frontend-design/);
|
||||
assert.match(output.join("\n"), /remove stale managed entries \(1\)/i);
|
||||
assert.match(output.join("\n"), /legacy-skill/);
|
||||
assert.match(output.join("\n"), /ignored unsafe manifest entry: \.\.\/outside/i);
|
||||
|
||||
// Exercise the real CLI boundary with a deterministic fake git clone. This
|
||||
// proves --dry-run returns before installForTarget can mutate either an
|
||||
// existing target or a missing one.
|
||||
const fakeBin = path.join(tmpRoot, "fake-bin");
|
||||
fs.mkdirSync(fakeBin, { recursive: true });
|
||||
const fakeGit = path.join(fakeBin, "git");
|
||||
fs.writeFileSync(
|
||||
fakeGit,
|
||||
`#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const destination = process.argv[process.argv.length - 1];
|
||||
fs.appendFileSync(process.env.FAKE_GIT_LOG, JSON.stringify(process.argv.slice(2)) + "\\n");
|
||||
fs.cpSync(process.env.FAKE_GIT_SOURCE, destination, { recursive: true, force: true });
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
fs.chmodSync(fakeGit, 0o755);
|
||||
|
||||
const fakeGitLog = path.join(tmpRoot, "fake-git-log.jsonl");
|
||||
const cliEnv = {
|
||||
...process.env,
|
||||
FAKE_GIT_SOURCE: repoRoot,
|
||||
FAKE_GIT_LOG: fakeGitLog,
|
||||
PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ""}`,
|
||||
};
|
||||
const installerPath = path.resolve(__dirname, "..", "..", "bin", "install.js");
|
||||
const beforeDryRunEntries = fs.readdirSync(existingTarget).sort();
|
||||
const dryRun = spawnSync(
|
||||
process.execPath,
|
||||
[installerPath, "--path", existingTarget, "--release", packageVersion, "--skills", "frontend-design", "--dry-run"],
|
||||
{ encoding: "utf8", env: cliEnv },
|
||||
);
|
||||
assert.strictEqual(dryRun.status, 0, dryRun.stderr);
|
||||
assert.match(dryRun.stdout, new RegExp(`Ref: ${pinnedRef.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
|
||||
assert.match(dryRun.stdout, /Exact skill set \(1\):/);
|
||||
assert.match(dryRun.stdout, /remove stale managed entries \(1\)/i);
|
||||
assert.match(dryRun.stdout, /legacy-skill/);
|
||||
assert.deepStrictEqual(fs.readdirSync(existingTarget).sort(), beforeDryRunEntries);
|
||||
assert.strictEqual(fs.readFileSync(sentinelPath, "utf8"), "keep");
|
||||
const cloneArgs = JSON.parse(fs.readFileSync(fakeGitLog, "utf8").trim().split("\n")[0]);
|
||||
assert.deepStrictEqual(
|
||||
cloneArgs.slice(0, 5),
|
||||
["clone", "--depth", "1", "--branch", pinnedRef],
|
||||
"the real CLI dry run must clone the explicitly pinned release",
|
||||
);
|
||||
|
||||
const outsideSentinel = path.join(tmpRoot, "outside");
|
||||
fs.writeFileSync(outsideSentinel, "outside stays", "utf8");
|
||||
assert.doesNotThrow(() => installer.pruneRemovedEntries(existingTarget, ["../outside"], []));
|
||||
assert.strictEqual(fs.readFileSync(outsideSentinel, "utf8"), "outside stays");
|
||||
|
||||
const unknownTarget = path.join(tmpRoot, "unknown-target");
|
||||
const unknown = spawnSync(
|
||||
process.execPath,
|
||||
[installerPath, "--path", unknownTarget, "--skills", "does-not-exist", "--dry-run"],
|
||||
{ encoding: "utf8", env: cliEnv },
|
||||
);
|
||||
assert.strictEqual(unknown.status, 1);
|
||||
assert.match(unknown.stderr, /Unknown skill requested/);
|
||||
assert.strictEqual(fs.existsSync(unknownTarget), false);
|
||||
|
||||
const multiHome = path.join(tmpRoot, "multi-home");
|
||||
const firstTarget = path.join(multiHome, ".claude", "skills");
|
||||
const codexHome = path.join(tmpRoot, "codex-home");
|
||||
const unsafeTarget = path.join(codexHome, "skills");
|
||||
const unsafeRealTarget = path.join(tmpRoot, "unsafe-real-target");
|
||||
fs.mkdirSync(firstTarget, { recursive: true });
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
fs.mkdirSync(unsafeRealTarget, { recursive: true });
|
||||
const firstSentinel = path.join(firstTarget, "first-sentinel.txt");
|
||||
fs.writeFileSync(firstSentinel, "first stays", "utf8");
|
||||
let createdUnsafeSymlink = false;
|
||||
try {
|
||||
fs.symlinkSync(unsafeRealTarget, unsafeTarget, "dir");
|
||||
createdUnsafeSymlink = true;
|
||||
} catch (error) {
|
||||
// Some platforms disallow unprivileged directory symlinks.
|
||||
}
|
||||
if (createdUnsafeSymlink) {
|
||||
const multiTarget = spawnSync(
|
||||
process.execPath,
|
||||
[installerPath, "--claude", "--codex", "--skills", "frontend-design"],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: { ...cliEnv, HOME: multiHome, CODEX_HOME: codexHome },
|
||||
},
|
||||
);
|
||||
assert.strictEqual(multiTarget.status, 1);
|
||||
assert.match(multiTarget.stderr, /symlinked target/i);
|
||||
assert.deepStrictEqual(fs.readdirSync(firstTarget), ["first-sentinel.txt"]);
|
||||
assert.strictEqual(fs.readFileSync(firstSentinel, "utf8"), "first stays");
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -65,7 +65,13 @@ class EditorialBundlesTests(unittest.TestCase):
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_get_bundle_skills_reads_json_manifest_by_name_and_id(self):
|
||||
expected = ["concise-planning", "git-pushing", "kaizen", "lint-and-validate", "systematic-debugging"]
|
||||
expected = [
|
||||
"concise-planning",
|
||||
"git-pushing",
|
||||
"lint-and-validate",
|
||||
"systematic-debugging",
|
||||
"test-driven-development",
|
||||
]
|
||||
self.assertEqual(get_bundle_skills.get_bundle_skills(["Essentials"]), expected)
|
||||
self.assertEqual(get_bundle_skills.get_bundle_skills(["essentials"]), expected)
|
||||
web_wizard_skills = get_bundle_skills.get_bundle_skills(["web-wizard"])
|
||||
@@ -91,13 +97,13 @@ class EditorialBundlesTests(unittest.TestCase):
|
||||
sample_skill_dir = essentials_plugin / "concise-planning"
|
||||
self.assertTrue((sample_skill_dir / "SKILL.md").is_file())
|
||||
|
||||
def test_generated_plugin_count_matches_manifest(self):
|
||||
generated_plugins = sorted(
|
||||
def test_generated_plugins_cover_manifest_during_source_only_prs(self):
|
||||
generated_plugins = {
|
||||
path.name
|
||||
for path in (REPO_ROOT / "plugins").iterdir()
|
||||
if path.is_dir() and path.name.startswith("agentic-bundle-")
|
||||
)
|
||||
expected_plugins = sorted(
|
||||
}
|
||||
expected_plugins = {
|
||||
f'agentic-bundle-{bundle["id"]}'
|
||||
for bundle in self.manifest_bundles
|
||||
if any(
|
||||
@@ -107,8 +113,22 @@ class EditorialBundlesTests(unittest.TestCase):
|
||||
)
|
||||
for target in ("codex", "claude")
|
||||
)
|
||||
}
|
||||
self.assertFalse(
|
||||
expected_plugins - generated_plugins,
|
||||
f"generated bundle plugins are missing: {sorted(expected_plugins - generated_plugins)}",
|
||||
)
|
||||
self.assertEqual(generated_plugins, expected_plugins)
|
||||
|
||||
def test_plugin_sync_prunes_stale_bundle_directories(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = pathlib.Path(temp_dir)
|
||||
stale_plugin = root / "plugins" / "agentic-bundle-retired"
|
||||
stale_plugin.mkdir(parents=True)
|
||||
(stale_plugin / "marker.txt").write_text("stale", encoding="utf-8")
|
||||
|
||||
editorial_bundles.sync_editorial_bundle_plugins(root, {}, [], {})
|
||||
|
||||
self.assertFalse(stale_plugin.exists())
|
||||
|
||||
def test_codex_bundle_plugin_names_keep_qualified_skill_names_valid(self):
|
||||
max_name_length = 64
|
||||
@@ -207,6 +227,113 @@ class EditorialBundlesTests(unittest.TestCase):
|
||||
f"Claude root plugin inclusion mismatch for {skill_id}",
|
||||
)
|
||||
|
||||
def test_skill_mirror_check_rejects_stale_and_unexpected_files(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = pathlib.Path(temp_dir)
|
||||
canonical = root / "skills" / "sample"
|
||||
mirror = root / "plugin" / "skills" / "sample"
|
||||
canonical.mkdir(parents=True)
|
||||
mirror.mkdir(parents=True)
|
||||
(canonical / "SKILL.md").write_text("canonical\n", encoding="utf-8")
|
||||
(mirror / "SKILL.md").write_text("stale\n", encoding="utf-8")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "stale mirrored file: sample/SKILL.md"):
|
||||
editorial_bundles._assert_skill_mirror_matches(
|
||||
root,
|
||||
root / "plugin" / "skills",
|
||||
["sample"],
|
||||
"sample plugin",
|
||||
)
|
||||
|
||||
(mirror / "SKILL.md").write_text("canonical\n", encoding="utf-8")
|
||||
(mirror / "unexpected.txt").write_text("extra\n", encoding="utf-8")
|
||||
with self.assertRaisesRegex(ValueError, "unexpected mirrored file: sample/unexpected.txt"):
|
||||
editorial_bundles._assert_skill_mirror_matches(
|
||||
root,
|
||||
root / "plugin" / "skills",
|
||||
["sample"],
|
||||
"sample plugin",
|
||||
)
|
||||
|
||||
def test_skill_mirror_check_rejects_symlinks(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = pathlib.Path(temp_dir)
|
||||
canonical = root / "skills" / "sample"
|
||||
mirror = root / "plugin" / "skills" / "sample"
|
||||
canonical.mkdir(parents=True)
|
||||
mirror.mkdir(parents=True)
|
||||
(canonical / "SKILL.md").write_text("canonical\n", encoding="utf-8")
|
||||
(mirror / "SKILL.md").symlink_to(canonical / "SKILL.md")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "unexpected symlink: sample/SKILL.md"):
|
||||
editorial_bundles._assert_skill_mirror_matches(
|
||||
root,
|
||||
root / "plugin" / "skills",
|
||||
["sample"],
|
||||
"sample plugin",
|
||||
)
|
||||
|
||||
mirror_root_link = root / "linked-skills"
|
||||
mirror_root_link.symlink_to(root / "plugin" / "skills", target_is_directory=True)
|
||||
with self.assertRaisesRegex(ValueError, "skills directory must not be a symlink"):
|
||||
editorial_bundles._assert_skill_mirror_matches(
|
||||
root,
|
||||
mirror_root_link,
|
||||
["sample"],
|
||||
"sample plugin",
|
||||
)
|
||||
|
||||
def test_plugin_metadata_layout_rejects_unexpected_files(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
plugin_root = pathlib.Path(temp_dir) / "plugin"
|
||||
manifest = plugin_root / ".codex-plugin" / "plugin.json"
|
||||
manifest.parent.mkdir(parents=True)
|
||||
manifest.write_text("{}\n", encoding="utf-8")
|
||||
(plugin_root / "README.md").write_text("stale\n", encoding="utf-8")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "metadata layout is out of sync: unexpected README.md"):
|
||||
editorial_bundles._assert_plugin_metadata_layout(
|
||||
plugin_root,
|
||||
{".codex-plugin/plugin.json"},
|
||||
"sample plugin",
|
||||
)
|
||||
|
||||
def test_json_check_requires_canonical_serialization_and_regular_file(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = pathlib.Path(temp_dir)
|
||||
manifest = root / "plugin.json"
|
||||
manifest.write_text('{"name":"sample"}\n', encoding="utf-8")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "is out of sync"):
|
||||
editorial_bundles._assert_json_matches(
|
||||
manifest,
|
||||
{"name": "sample"},
|
||||
"sample manifest",
|
||||
root,
|
||||
)
|
||||
|
||||
manifest.write_bytes(b'{\r\n "name": "sample"\r\n}\r\n')
|
||||
with self.assertRaisesRegex(ValueError, "is out of sync"):
|
||||
editorial_bundles._assert_json_matches(
|
||||
manifest,
|
||||
{"name": "sample"},
|
||||
"sample manifest",
|
||||
root,
|
||||
)
|
||||
|
||||
external = root / "external"
|
||||
external.mkdir()
|
||||
(external / "plugin.json").write_text('{\n "name": "sample"\n}\n', encoding="utf-8")
|
||||
linked_parent = root / "linked"
|
||||
linked_parent.symlink_to(external, target_is_directory=True)
|
||||
with self.assertRaisesRegex(ValueError, "path must not contain a symlink"):
|
||||
editorial_bundles._assert_json_matches(
|
||||
linked_parent / "plugin.json",
|
||||
{"name": "sample"},
|
||||
"sample manifest",
|
||||
root,
|
||||
)
|
||||
|
||||
def test_remove_tree_retries_on_enotempty(self):
|
||||
target = REPO_ROOT / "plugins" / "agentic-awesome-skills" / "skills"
|
||||
calls = {"count": 0}
|
||||
|
||||
@@ -100,6 +100,38 @@ class GenerateIndexCategoryTests(unittest.TestCase):
|
||||
self.assertEqual(categories["nested-skill"], "bundles")
|
||||
self.assertEqual(categories["playwright-skill"], "test-automation")
|
||||
|
||||
def test_generate_index_preserves_top_level_and_nested_tags(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base = pathlib.Path(temp_dir)
|
||||
skills_dir = base / "skills"
|
||||
output_file = base / "skills_index.json"
|
||||
|
||||
top_level = skills_dir / "top-level"
|
||||
top_level.mkdir(parents=True)
|
||||
(top_level / "SKILL.md").write_text(
|
||||
"---\nname: top-level\ndescription: Top level tags\ntags: [api, typescript, api]\n---\nbody\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
nested_tags = skills_dir / "nested-tags"
|
||||
nested_tags.mkdir(parents=True)
|
||||
(nested_tags / "SKILL.md").write_text(
|
||||
"---\nname: nested-tags\ndescription: Nested tags\nmetadata:\n tags: data, python\n---\nbody\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
skills = generate_index.generate_index(str(skills_dir), str(output_file))
|
||||
by_id = {skill["id"]: skill for skill in skills}
|
||||
|
||||
self.assertEqual(by_id["top-level"]["tags"], ["api", "typescript"])
|
||||
self.assertEqual(by_id["nested-tags"]["tags"], ["data", "python"])
|
||||
|
||||
def test_tag_coercion_is_deterministic_for_yaml_sets(self):
|
||||
self.assertEqual(
|
||||
generate_index.coerce_metadata_list({"typescript", "api"}),
|
||||
["api", "typescript"],
|
||||
)
|
||||
|
||||
def test_generate_index_rejects_duplicate_route_ids(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base = pathlib.Path(temp_dir)
|
||||
|
||||
@@ -59,6 +59,20 @@ class SyncRepoMetadataTests(unittest.TestCase):
|
||||
(root / "docs" / "users").mkdir(parents=True)
|
||||
(root / "docs" / "maintainers").mkdir(parents=True)
|
||||
(root / "docs" / "integrations" / "jetski-gemini-loader").mkdir(parents=True)
|
||||
(root / "apps" / "web-app" / "public").mkdir(parents=True)
|
||||
|
||||
(root / "apps" / "web-app" / "index.html").write_text(
|
||||
'<meta name="description" content="Explore 1,273+ installable agentic skills">\n'
|
||||
'<title>Agentic Awesome Skills GitHub | 1,273+ AI coding skills</title>\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "apps" / "web-app" / "public" / "llms.txt").write_text(
|
||||
"> Installable GitHub library of 1,273+ agentic SKILL.md playbooks.\n"
|
||||
"- Current release: V8.3.0.\n"
|
||||
"- Skill count: 1,273+.\n"
|
||||
"Agentic Awesome Skills is an installable library of 1,273+ reusable SKILL.md playbooks.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
(root / "docs" / "users" / "getting-started.md").write_text(
|
||||
"# Getting Started with Agentic Awesome Skills (V8.3.0)\n",
|
||||
@@ -107,7 +121,7 @@ class SyncRepoMetadataTests(unittest.TestCase):
|
||||
|
||||
updated_files = sync_repo_metadata.sync_curated_docs(str(root), metadata, dry_run=False)
|
||||
|
||||
self.assertGreaterEqual(updated_files, 10)
|
||||
self.assertGreaterEqual(updated_files, 12)
|
||||
readme = (root / "README.md").read_text(encoding="utf-8")
|
||||
self.assertIn("1,304+ agentic skills", readme)
|
||||
self.assertIn("[📚 Browse 1,304+ Skills](#browse-1304-skills)", readme)
|
||||
@@ -117,6 +131,13 @@ class SyncRepoMetadataTests(unittest.TestCase):
|
||||
self.assertIn("1,304+ files", (root / "docs" / "users" / "gemini-cli-skills.md").read_text(encoding="utf-8"))
|
||||
self.assertIn("1,304+ specialized areas", (root / "docs" / "users" / "kiro-integration.md").read_text(encoding="utf-8"))
|
||||
self.assertIn("Total Bundles: 2", (root / "docs" / "users" / "bundles.md").read_text(encoding="utf-8"))
|
||||
web_index = (root / "apps" / "web-app" / "index.html").read_text(encoding="utf-8")
|
||||
self.assertIn("1,304+ installable agentic skills", web_index)
|
||||
self.assertIn("1,304+ AI coding skills", web_index)
|
||||
llms_text = (root / "apps" / "web-app" / "public" / "llms.txt").read_text(encoding="utf-8")
|
||||
self.assertIn("Current release: V8.4.0.", llms_text)
|
||||
self.assertIn("Skill count: 1,304+.", llms_text)
|
||||
self.assertIn("1,304+ reusable SKILL.md playbooks", llms_text)
|
||||
jetski_cortex = (root / "docs" / "integrations" / "jetski-cortex.md").read_text(encoding="utf-8")
|
||||
self.assertIn("1,304+ skill", jetski_cortex)
|
||||
self.assertNotIn("1,1", jetski_cortex)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { resolveExactSkillSelections } = require('../../bin/install');
|
||||
const { listSkillIdsRecursive } = require('../../lib/skill-utils');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..', '..');
|
||||
const repoSkills = path.join(repoRoot, 'skills');
|
||||
const catalog = JSON.parse(fs.readFileSync(path.join(repoRoot, 'skills_index.json'), 'utf8'));
|
||||
const installerEntries = listSkillIdsRecursive(repoSkills);
|
||||
|
||||
assert.strictEqual(
|
||||
installerEntries.length,
|
||||
catalog.length,
|
||||
'the Workbench catalog and installer must see the same number of canonical skills',
|
||||
);
|
||||
|
||||
const resolvedEntries = resolveExactSkillSelections(
|
||||
repoSkills,
|
||||
installerEntries,
|
||||
catalog.map((skill) => skill.id),
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
resolvedEntries.size,
|
||||
catalog.length,
|
||||
'every Workbench id must resolve to exactly one installer entry',
|
||||
);
|
||||
|
||||
for (const skill of catalog) {
|
||||
const expectedEntry = skill.path.replace(/^skills\//, '');
|
||||
assert.ok(
|
||||
resolvedEntries.has(expectedEntry),
|
||||
`Workbench id ${skill.id} must resolve to ${expectedEntry}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Workbench installer contract passed for ${catalog.length} canonical skills.`);
|
||||
Reference in New Issue
Block a user