feat(installer): Add selective install filters and new skill

Add installer filters for risk, category, and tags so maintainers and
users can ship smaller skill surfaces to context-sensitive runtimes.
Document the reduced-install flow for OpenCode-style hosts, add the
humanize-chinese community skill, and sync the generated catalog and
plugin-safe artifacts that now reflect the release batch.

Refs #437
Refs #440
Refs #443
This commit is contained in:
sickn33
2026-04-03 17:08:33 +02:00
parent db36188c78
commit bb2304a34f
36 changed files with 4076 additions and 158 deletions

View File

@@ -0,0 +1,146 @@
const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const installer = require(path.resolve(__dirname, "..", "..", "bin", "install.js"));
function withTempDir(fn) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "installer-filters-"));
try {
fn(dir);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
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",
);
}
assert.deepStrictEqual(
installer.parseSelectorArg("safe,critical,offensive-"),
{ include: ["safe", "critical"], exclude: ["offensive"] },
"parseSelectorArg should split CSV values and treat suffix - as exclude",
);
assert.deepStrictEqual(
installer.parseSelectorArg("safe,safe-,none"),
{ include: ["none"], exclude: ["safe"] },
"parseSelectorArg should let excludes win over duplicate includes",
);
assert.strictEqual(
installer.isOpenCodeStylePath(path.join("/tmp", ".agents", "skills")),
true,
"OpenCode-style paths should be detected from .agents/skills",
);
assert.strictEqual(
installer.isOpenCodeStylePath(path.join("/tmp", ".codex", "skills")),
false,
"non-OpenCode paths should not trigger the .agents/skills guidance",
);
withTempDir((root) => {
const repoRoot = path.join(root, "repo");
fs.mkdirSync(path.join(repoRoot, "skills"), { recursive: true });
fs.mkdirSync(path.join(repoRoot, "docs"), { recursive: true });
writeSkill(
repoRoot,
"safe-debugger",
'name: safe-debugger\ncategory: development\nrisk: safe\ntags: [debugging, typescript]',
);
writeSkill(
repoRoot,
"offensive-tool",
'name: offensive-tool\ncategory: security\nrisk: offensive\ntags: [pentest, red-team]',
);
writeSkill(
repoRoot,
path.join("nested", "metadata-tags"),
'name: metadata-tags\ncategory: backend\nrisk: none\nmetadata:\n tags: "api,saas"',
);
assert.deepStrictEqual(
installer.getInstallEntries(repoRoot, installer.buildInstallSelectors({})),
["nested/metadata-tags", "offensive-tool", "safe-debugger", "docs"],
"full installs should return recursive skill paths plus docs",
);
assert.deepStrictEqual(
installer.getInstallEntries(
repoRoot,
installer.buildInstallSelectors({
riskArg: "safe,none",
categoryArg: "development,backend",
tagsArg: "typescript,saas",
}),
),
["nested/metadata-tags", "safe-debugger", "docs"],
"filters should AND across flags and keep docs when skills match",
);
assert.deepStrictEqual(
installer.getInstallEntries(
repoRoot,
installer.buildInstallSelectors({
tagsArg: "pentest-",
}),
),
["nested/metadata-tags", "safe-debugger", "docs"],
"exclude-only tag filters should remove matching skills",
);
assert.strictEqual(
installer.matchesInstallSelectors(
{ risk: "safe", category: "development", tags: ["debugging", "typescript"] },
installer.buildInstallSelectors({
riskArg: "safe",
categoryArg: "development",
tagsArg: "typescript",
}),
),
true,
"skills should match when all selector dimensions pass",
);
assert.strictEqual(
installer.matchesInstallSelectors(
{ risk: "", category: "development", tags: ["debugging"] },
installer.buildInstallSelectors({
riskArg: "safe",
}),
),
false,
"missing scalar metadata should not satisfy positive selectors",
);
const openCodeMessages = installer.getPostInstallMessages(
[{ name: "Custom", path: path.join(root, ".agents", "skills") }],
installer.buildInstallSelectors({}),
);
assert.ok(
openCodeMessages.some((message) => message.includes("reduced install")),
"OpenCode-style paths should get reduced-install guidance",
);
const filteredOpenCodeMessages = installer.getPostInstallMessages(
[{ name: "Custom", path: path.join(root, ".agents", "skills") }],
installer.buildInstallSelectors({ categoryArg: "development" }),
);
assert.strictEqual(
filteredOpenCodeMessages.some((message) => message.includes("Example:")),
false,
"OpenCode guidance should skip the example once selectors are already active",
);
});

View File

@@ -14,6 +14,8 @@ function writeSkill(repoRoot, skillName, content = "# Skill\n") {
function createFakeRepo(rootDir, skills) {
fs.mkdirSync(path.join(rootDir, "skills"), { recursive: true });
fs.mkdirSync(path.join(rootDir, "docs"), { recursive: true });
fs.writeFileSync(path.join(rootDir, "docs", "README.md"), "# Docs\n", "utf8");
for (const skillName of skills) {
writeSkill(rootDir, skillName, `# ${skillName}\n`);
}
@@ -35,22 +37,47 @@ try {
createFakeRepo(repoV1, ["skill-a", "skill-b"]);
createFakeRepo(repoV2, ["skill-a"]);
writeSkill(
repoV1,
path.join("nested", "skill-c"),
"---\nname: nested-skill-c\ncategory: backend\nrisk: safe\ntags: [api]\n---\n",
);
writeSkill(
repoV2,
"skill-a",
"---\nname: skill-a\ncategory: development\nrisk: safe\ntags: [debugging]\n---\n",
);
writeSkill(
repoV2,
path.join("nested", "skill-c"),
"---\nname: nested-skill-c\ncategory: backend\nrisk: safe\ntags: [api]\n---\n",
);
installer.installForTarget(repoV1, { name: "Test", path: targetDir });
assert.ok(fs.existsSync(path.join(targetDir, "skill-a", "SKILL.md")));
assert.ok(fs.existsSync(path.join(targetDir, "skill-b", "SKILL.md")));
assert.ok(fs.existsSync(path.join(targetDir, "nested", "skill-c", "SKILL.md")));
installer.installForTarget(repoV2, { name: "Test", path: targetDir });
assert.ok(fs.existsSync(path.join(targetDir, "skill-a", "SKILL.md")));
installer.installForTarget(
repoV2,
{ name: "Test", path: targetDir },
installer.buildInstallSelectors({ categoryArg: "backend" }),
);
assert.strictEqual(
fs.existsSync(path.join(targetDir, "skill-a")),
false,
"non-matching top-level skills should be pruned during filtered updates",
);
assert.strictEqual(
fs.existsSync(path.join(targetDir, "skill-b")),
false,
"stale managed skill should be pruned during updates",
"stale managed top-level skills should be pruned during updates",
);
assert.ok(fs.existsSync(path.join(targetDir, "nested", "skill-c", "SKILL.md")));
assert.deepStrictEqual(
readManifestEntries(targetDir),
["skill-a"],
"install manifest should mirror the latest installed entries",
["docs", "nested/skill-c"],
"install manifest should mirror the latest filtered install entries",
);
const badTargetPath = path.join(tmpRoot, "bad-target");

View File

@@ -19,6 +19,7 @@ const LOCAL_TEST_COMMANDS = [
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_editorial_bundles.py")],
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_plugin_compatibility.py")],
[path.join(TOOL_TESTS, "installer_antigravity_guidance.test.js")],
[path.join(TOOL_TESTS, "installer_filters.test.js")],
[path.join(TOOL_TESTS, "installer_update_sync.test.js")],
[path.join(TOOL_TESTS, "jetski_gemini_loader.test.cjs")],
[path.join(TOOL_TESTS, "npm_package_contents.test.js")],

View File

@@ -3,7 +3,7 @@ const fs = require("fs");
const os = require("os");
const path = require("path");
const { listSkillIds, listSkillIdsRecursive } = require("../../lib/skill-utils");
const { listSkillIds, listSkillIdsRecursive, readSkill } = require("../../lib/skill-utils");
function withTempDir(fn) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skill-utils-security-"));
@@ -110,3 +110,33 @@ withTempDir((root) => {
fs.lstatSync = originalLstatSync;
}
});
withTempDir((root) => {
const skillsDir = path.join(root, "skills");
const skillDir = path.join(skillsDir, "metadata-skill");
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(
path.join(skillDir, "SKILL.md"),
`---
name: metadata-skill
category: backend
risk: safe
metadata:
tags: "[api, saas]"
---
# metadata-skill
`,
"utf8",
);
const skill = readSkill(skillsDir, "metadata-skill");
assert.strictEqual(skill.category, "backend", "readSkill should expose category metadata");
assert.strictEqual(skill.risk, "safe", "readSkill should expose risk metadata");
assert.deepStrictEqual(
skill.tags,
["api", "saas"],
"readSkill should normalize inline tag lists from metadata.tags",
);
});