📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -33,14 +33,15 @@ function resolveDir(p) {
|
||||
return path.resolve(root, ...sanitizedSegments);
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const a = process.argv.slice(2);
|
||||
function parseArgs(argv = process.argv.slice(2)) {
|
||||
const a = argv;
|
||||
let pathArg = null;
|
||||
let versionArg = null;
|
||||
let tagArg = null;
|
||||
let riskArg = null;
|
||||
let categoryArg = null;
|
||||
let tagsArg = null;
|
||||
let versionInfo = false;
|
||||
let cursor = false,
|
||||
claude = false,
|
||||
gemini = false,
|
||||
@@ -51,28 +52,22 @@ function parseArgs() {
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] === "--help" || a[i] === "-h") return { help: true };
|
||||
if (a[i] === "--path" && a[i + 1]) {
|
||||
pathArg = a[++i];
|
||||
if (a[i] === "--version") {
|
||||
versionInfo = true;
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "--version" && a[i + 1]) {
|
||||
versionArg = a[++i];
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "--tag" && a[i + 1]) {
|
||||
tagArg = a[++i];
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "--risk" && a[i + 1]) {
|
||||
riskArg = a[++i];
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "--category" && a[i + 1]) {
|
||||
categoryArg = a[++i];
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "--tags" && a[i + 1]) {
|
||||
tagsArg = a[++i];
|
||||
if (["--path", "--release", "--tag", "--risk", "--category", "--tags"].includes(a[i])) {
|
||||
const value = a[i + 1];
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error(`Option ${a[i]} requires a value.`);
|
||||
}
|
||||
if (a[i] === "--path") pathArg = value;
|
||||
if (a[i] === "--release") versionArg = value;
|
||||
if (a[i] === "--tag") tagArg = value;
|
||||
if (a[i] === "--risk") riskArg = value;
|
||||
if (a[i] === "--category") categoryArg = value;
|
||||
if (a[i] === "--tags") tagsArg = value;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "--cursor") {
|
||||
@@ -104,6 +99,7 @@ function parseArgs() {
|
||||
continue;
|
||||
}
|
||||
if (a[i] === "install") continue;
|
||||
throw new Error(`Unknown option or command: ${a[i]}`);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -113,6 +109,7 @@ function parseArgs() {
|
||||
riskArg,
|
||||
categoryArg,
|
||||
tagsArg,
|
||||
versionInfo,
|
||||
cursor,
|
||||
claude,
|
||||
gemini,
|
||||
@@ -182,7 +179,8 @@ Options:
|
||||
--risk <csv> Install only skills matching these risk labels
|
||||
--category <csv> Install only skills matching these categories
|
||||
--tags <csv> Install only skills matching these tags
|
||||
--version <ver> Clone tag v<ver> (e.g. 4.6.0 -> v4.6.0)
|
||||
--version Print the installer version
|
||||
--release <ver> Clone tag v<ver> (e.g. 4.6.0 -> v4.6.0)
|
||||
--tag <tag> Clone this tag or branch (e.g. v4.6.0, main)
|
||||
|
||||
Examples:
|
||||
@@ -193,7 +191,7 @@ Examples:
|
||||
npx agentic-awesome-skills --agy
|
||||
npx agentic-awesome-skills --path .agents/skills --category development,backend --risk safe,none
|
||||
npx agentic-awesome-skills --path .agents/skills --tags debugging,typescript-legacy-
|
||||
npx agentic-awesome-skills --version 4.6.0
|
||||
npx agentic-awesome-skills --release 4.6.0
|
||||
npx agentic-awesome-skills --path ./my-skills
|
||||
npx agentic-awesome-skills --claude --codex Install to multiple targets
|
||||
`);
|
||||
@@ -312,7 +310,14 @@ function assertSafeDestinationPath(dest, destRoot) {
|
||||
}
|
||||
}
|
||||
|
||||
function copyRecursiveSync(src, dest, rootDir = src, skipGit = true, destRoot = dest) {
|
||||
function copyRecursiveSync(
|
||||
src,
|
||||
dest,
|
||||
rootDir = src,
|
||||
skipGit = true,
|
||||
destRoot = dest,
|
||||
selectedSkillEntries = null,
|
||||
) {
|
||||
const stats = fs.lstatSync(src);
|
||||
const resolvedSource = stats.isSymbolicLink()
|
||||
? resolveSafeRealPath(rootDir, src)
|
||||
@@ -324,6 +329,17 @@ function copyRecursiveSync(src, dest, rootDir = src, skipGit = true, destRoot =
|
||||
}
|
||||
|
||||
const resolvedStats = fs.statSync(resolvedSource);
|
||||
const relativeSource = path.relative(rootDir, resolvedSource);
|
||||
if (
|
||||
selectedSkillEntries &&
|
||||
relativeSource &&
|
||||
relativeSource !== "." &&
|
||||
resolvedStats.isDirectory() &&
|
||||
fs.existsSync(path.join(resolvedSource, "SKILL.md")) &&
|
||||
!selectedSkillEntries.has(path.normalize(relativeSource))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(dest) && fs.lstatSync(dest).isSymbolicLink()) {
|
||||
throw new Error(`Skipping unsafe destination symlink: ${dest}`);
|
||||
}
|
||||
@@ -344,6 +360,7 @@ function copyRecursiveSync(src, dest, rootDir = src, skipGit = true, destRoot =
|
||||
rootDir,
|
||||
skipGit,
|
||||
destRoot,
|
||||
selectedSkillEntries,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -354,6 +371,56 @@ function copyRecursiveSync(src, dest, rootDir = src, skipGit = true, destRoot =
|
||||
}
|
||||
}
|
||||
|
||||
function replaceManagedEntry(
|
||||
src,
|
||||
dest,
|
||||
rootDir,
|
||||
skipGit,
|
||||
targetRoot,
|
||||
selectedSkillEntries = null,
|
||||
) {
|
||||
if (!fs.existsSync(targetRoot)) {
|
||||
fs.mkdirSync(targetRoot, { recursive: true });
|
||||
}
|
||||
assertSafeDestinationPath(dest, targetRoot);
|
||||
const stageRoot = fs.mkdtempSync(path.join(targetRoot, ".antigravity-stage-"));
|
||||
const stagedEntry = path.join(stageRoot, "entry");
|
||||
const backupEntry = path.join(stageRoot, "previous");
|
||||
let movedPreviousEntry = false;
|
||||
|
||||
try {
|
||||
copyRecursiveSync(
|
||||
src,
|
||||
stagedEntry,
|
||||
rootDir,
|
||||
skipGit,
|
||||
stageRoot,
|
||||
selectedSkillEntries,
|
||||
);
|
||||
if (!fs.existsSync(stagedEntry)) {
|
||||
throw new Error(`Unable to stage managed install entry: ${src}`);
|
||||
}
|
||||
|
||||
if (fs.existsSync(dest)) {
|
||||
assertSafeDestinationPath(dest, targetRoot);
|
||||
fs.renameSync(dest, backupEntry);
|
||||
movedPreviousEntry = true;
|
||||
}
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.renameSync(stagedEntry, dest);
|
||||
if (movedPreviousEntry) {
|
||||
fs.rmSync(backupEntry, { recursive: true, force: true });
|
||||
}
|
||||
} catch (error) {
|
||||
if (movedPreviousEntry && fs.existsSync(backupEntry) && !fs.existsSync(dest)) {
|
||||
fs.renameSync(backupEntry, dest);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
fs.rmSync(stageRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy contents of repo's skills/ into target so each skill is target/skill-name/ (for Claude Code etc.). */
|
||||
function getInstallEntries(tempDir, selectors = buildInstallSelectors({})) {
|
||||
const repoSkills = path.join(tempDir, "skills");
|
||||
@@ -381,18 +448,22 @@ function getInstallEntries(tempDir, selectors = buildInstallSelectors({})) {
|
||||
|
||||
function installSkillsIntoTarget(tempDir, target, installEntries) {
|
||||
const repoSkills = path.join(tempDir, "skills");
|
||||
const selectedSkillEntries = new Set(
|
||||
installEntries
|
||||
.filter((entry) => entry !== "docs")
|
||||
.map(normalizeSourceEntry),
|
||||
);
|
||||
installEntries.forEach((name) => {
|
||||
const destName = normalizeInstallEntry(name);
|
||||
if (destName === "docs") {
|
||||
const repoDocs = path.join(tempDir, "docs");
|
||||
const docsDest = path.join(target, "docs");
|
||||
if (!fs.existsSync(docsDest)) fs.mkdirSync(docsDest, { recursive: true });
|
||||
copyRecursiveSync(repoDocs, docsDest, repoDocs, true, target);
|
||||
replaceManagedEntry(repoDocs, docsDest, repoDocs, true, target);
|
||||
return;
|
||||
}
|
||||
const src = path.join(repoSkills, normalizeSourceEntry(name));
|
||||
const dest = path.join(target, destName);
|
||||
copyRecursiveSync(src, dest, repoSkills, true, target);
|
||||
replaceManagedEntry(src, dest, repoSkills, true, target, selectedSkillEntries);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -630,8 +701,8 @@ function installForTarget(tempDir, target, selectors = buildInstallSelectors({})
|
||||
const installEntries = getInstallEntries(tempDir, selectors);
|
||||
const managedEntries = getManagedEntries(installEntries, target);
|
||||
const previousEntries = readInstallManifest(target.path);
|
||||
pruneRemovedEntries(target.path, previousEntries, managedEntries);
|
||||
installSkillsIntoTarget(tempDir, target.path, installEntries);
|
||||
pruneRemovedEntries(target.path, previousEntries, managedEntries);
|
||||
writeInstallManifest(target.path, managedEntries);
|
||||
console.log(` ✓ Installed to ${target.path}`);
|
||||
}
|
||||
@@ -679,7 +750,14 @@ function getPostInstallMessages(targets, selectors = buildInstallSelectors({}))
|
||||
}
|
||||
|
||||
function main() {
|
||||
const opts = parseArgs();
|
||||
let opts;
|
||||
try {
|
||||
opts = parseArgs();
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const selectors = buildInstallSelectors(opts);
|
||||
const ref = resolveInstallRef(opts);
|
||||
|
||||
@@ -688,8 +766,13 @@ function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.versionInfo) {
|
||||
console.log(packageMetadata.version);
|
||||
return;
|
||||
}
|
||||
|
||||
const targets = getTargets(opts);
|
||||
if (!targets.length || !HOME) {
|
||||
if (!targets.length || (!HOME && !opts.pathArg)) {
|
||||
console.error(
|
||||
"Could not resolve home directory. Use --path <absolute-path>.",
|
||||
);
|
||||
@@ -735,6 +818,7 @@ if (require.main === module) {
|
||||
|
||||
module.exports = {
|
||||
copyRecursiveSync,
|
||||
replaceManagedEntry,
|
||||
getPostInstallMessages,
|
||||
buildCloneArgs,
|
||||
buildInstallSelectors,
|
||||
@@ -748,6 +832,7 @@ module.exports = {
|
||||
matchesInstallSelectors,
|
||||
normalizeInstallEntry,
|
||||
parseSelectorArg,
|
||||
parseArgs,
|
||||
pruneRemovedEntries,
|
||||
readInstallManifest,
|
||||
resolveInstallRef,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"maxWarnings": 135
|
||||
"maxWarnings": 0
|
||||
}
|
||||
|
||||
@@ -158,6 +158,11 @@ function readSkill(skillDir, skillId) {
|
||||
tags = tags.filter(Boolean);
|
||||
const category = typeof data.category === 'string' ? data.category.trim() : '';
|
||||
const risk = typeof data.risk === 'string' ? data.risk.trim() : '';
|
||||
const source = typeof data.source === 'string' ? data.source.trim() : '';
|
||||
const sourceType = typeof data.source_type === 'string' ? data.source_type.trim() : '';
|
||||
const sourceRepo = typeof data.source_repo === 'string' ? data.source_repo.trim() : '';
|
||||
const license = typeof data.license === 'string' ? data.license.trim() : '';
|
||||
const licenseSource = typeof data.license_source === 'string' ? data.license_source.trim() : '';
|
||||
|
||||
return {
|
||||
id: skillId,
|
||||
@@ -165,6 +170,11 @@ function readSkill(skillDir, skillId) {
|
||||
description,
|
||||
category,
|
||||
risk,
|
||||
source,
|
||||
sourceType,
|
||||
sourceRepo,
|
||||
license,
|
||||
licenseSource,
|
||||
tags,
|
||||
path: skillPath,
|
||||
content,
|
||||
|
||||
@@ -372,7 +372,7 @@ const BUNDLE_RULES = {
|
||||
},
|
||||
"security-core": {
|
||||
description: "Security, privacy, and compliance essentials.",
|
||||
excludeCategories: new Set(["business"]),
|
||||
excludeCategories: new Set(["business", "product"]),
|
||||
keywords: [
|
||||
"security",
|
||||
"sast",
|
||||
@@ -718,15 +718,17 @@ function renderCatalogMarkdown(catalog) {
|
||||
);
|
||||
lines.push(`## ${category} (${grouped.length})`);
|
||||
lines.push("");
|
||||
lines.push("| Skill | Description | Tags | Triggers |");
|
||||
lines.push("| --- | --- | --- | --- |");
|
||||
lines.push("| Skill | Description | Risk | Source | Tags | Triggers |");
|
||||
lines.push("| --- | --- | --- | --- | --- | --- |");
|
||||
|
||||
for (const skill of grouped) {
|
||||
const description = escapeMarkdownTableCell(truncate(skill.description, 160));
|
||||
const tags = escapeMarkdownTableCell(skill.tags.join(", "));
|
||||
const triggers = escapeMarkdownTableCell(skill.triggers.join(", "));
|
||||
const risk = escapeMarkdownTableCell(skill.risk || "unknown");
|
||||
const source = escapeMarkdownTableCell(skill.source_repo || skill.source || "unknown");
|
||||
lines.push(
|
||||
`| \`${skill.id}\` | ${description} | ${tags} | ${triggers} |`,
|
||||
`| \`${skill.id}\` | ${description} | ${risk} | ${source} | ${tags} | ${triggers} |`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -736,6 +738,15 @@ function renderCatalogMarkdown(catalog) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function readCanonicalIndex() {
|
||||
const indexPath = path.join(ROOT, "skills_index.json");
|
||||
const parsed = JSON.parse(fs.readFileSync(indexPath, "utf8"));
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error("skills_index.json must be an array.");
|
||||
}
|
||||
return new Map(parsed.map((skill) => [skill.path, skill]));
|
||||
}
|
||||
|
||||
function readCatalogGeneratedAt() {
|
||||
if (process.env.SOURCE_DATE_EPOCH) {
|
||||
return new Date(process.env.SOURCE_DATE_EPOCH * 1000).toISOString();
|
||||
@@ -758,18 +769,27 @@ function readCatalogGeneratedAt() {
|
||||
function buildCatalog() {
|
||||
const skillRelPaths = listSkillIdsRecursive(SKILLS_DIR);
|
||||
const skills = skillRelPaths.map((relPath) => readSkill(SKILLS_DIR, relPath));
|
||||
const canonicalIndex = readCanonicalIndex();
|
||||
const catalogSkills = [];
|
||||
|
||||
for (const skill of skills) {
|
||||
const tags = deriveTags(skill);
|
||||
const category = detectCategory(skill, tags);
|
||||
const canonical = canonicalIndex.get(`skills/${skillRelPaths[catalogSkills.length]}`);
|
||||
const category = canonical?.category || detectCategory(skill, tags);
|
||||
const triggers = buildTriggers(skill, tags);
|
||||
|
||||
catalogSkills.push({
|
||||
id: skill.id,
|
||||
canonical_id: canonical?.id || skill.id,
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
category,
|
||||
risk: canonical?.risk || skill.risk || "unknown",
|
||||
source: canonical?.source || skill.source || "unknown",
|
||||
source_type: canonical?.source_type || skill.sourceType || undefined,
|
||||
source_repo: canonical?.source_repo || skill.sourceRepo || undefined,
|
||||
license: canonical?.license || skill.license || undefined,
|
||||
license_source: canonical?.license_source || skill.licenseSource || undefined,
|
||||
tags,
|
||||
triggers,
|
||||
// Normalize separators for deterministic cross-platform output.
|
||||
|
||||
@@ -306,8 +306,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
baseline = load_baseline(baseline_path)
|
||||
|
||||
if not baseline:
|
||||
print("⚠️ No baseline found. Run with --update-baseline to create one.")
|
||||
return 0
|
||||
print("❌ No baseline found; drift was not evaluated. Run with --update-baseline to create one.")
|
||||
return 2
|
||||
|
||||
current = build_current_entries(skills_dir)
|
||||
if args.skill:
|
||||
|
||||
@@ -132,6 +132,7 @@ def update_skill_file(
|
||||
*,
|
||||
add_missing: bool = False,
|
||||
add_limitations_only: bool = False,
|
||||
add_when_only: bool = False,
|
||||
) -> tuple[bool, list[str]]:
|
||||
if not is_safe_regular_file(skill_path):
|
||||
return False, []
|
||||
@@ -150,7 +151,7 @@ def update_skill_file(
|
||||
if updated != content:
|
||||
changes.append("normalized_when_heading")
|
||||
|
||||
add_when = add_missing
|
||||
add_when = add_missing or add_when_only
|
||||
add_examples = add_missing
|
||||
add_limitations = add_missing or add_limitations_only
|
||||
|
||||
@@ -188,10 +189,16 @@ def main() -> int:
|
||||
action="store_true",
|
||||
help="Only synthesize missing 'Limitations' sections.",
|
||||
)
|
||||
parser.add_argument("--add-when-only", action="store_true", help="Only synthesize missing 'When to Use' sections.")
|
||||
parser.add_argument("--only", action="append", default=[], help="Restrict changes to a repository-relative SKILL.md path (repeatable).")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.add_missing and args.add_when_only:
|
||||
parser.error("--add-missing and --add-when-only cannot be combined")
|
||||
|
||||
repo_root = find_repo_root(__file__)
|
||||
skills_dir = repo_root / "skills"
|
||||
only_paths = {Path(item).as_posix() for item in args.only}
|
||||
|
||||
modified = 0
|
||||
for root, dirs, files in os.walk(skills_dir):
|
||||
@@ -200,6 +207,9 @@ def main() -> int:
|
||||
continue
|
||||
|
||||
skill_path = Path(root) / "SKILL.md"
|
||||
relative_path = skill_path.relative_to(repo_root).as_posix()
|
||||
if only_paths and relative_path not in only_paths:
|
||||
continue
|
||||
if not is_safe_regular_file(skill_path):
|
||||
print(f"SKIP {skill_path.relative_to(repo_root)} [symlinked_or_unreadable]")
|
||||
continue
|
||||
@@ -209,7 +219,7 @@ def main() -> int:
|
||||
continue
|
||||
|
||||
simulated = normalize_when_heading_variants(content)
|
||||
needs_when = args.add_missing and not has_when_to_use_section(simulated)
|
||||
needs_when = (args.add_missing or args.add_when_only) and not has_when_to_use_section(simulated)
|
||||
needs_examples = args.add_missing and not has_examples(simulated)
|
||||
needs_limitations = (args.add_missing or args.add_limitations_only) and not has_limitations(simulated)
|
||||
if not needs_when and not needs_examples and not needs_limitations and simulated == content:
|
||||
@@ -233,6 +243,7 @@ def main() -> int:
|
||||
skill_path,
|
||||
add_missing=args.add_missing,
|
||||
add_limitations_only=args.add_limitations_only,
|
||||
add_when_only=args.add_when_only,
|
||||
)
|
||||
if changed:
|
||||
modified += 1
|
||||
|
||||
@@ -930,6 +930,10 @@ def generate_index(skills_dir, output_file, compatibility_report=None):
|
||||
description = coerce_metadata_text(metadata.get("description"))
|
||||
risk = coerce_metadata_text(metadata.get("risk"))
|
||||
source = coerce_metadata_text(metadata.get("source"))
|
||||
source_type = coerce_metadata_text(metadata.get("source_type"))
|
||||
source_repo = coerce_metadata_text(metadata.get("source_repo"))
|
||||
license_value = coerce_metadata_text(metadata.get("license"))
|
||||
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"))
|
||||
|
||||
@@ -941,6 +945,14 @@ def generate_index(skills_dir, output_file, compatibility_report=None):
|
||||
skill_info["risk"] = risk
|
||||
if source is not None:
|
||||
skill_info["source"] = source
|
||||
if source_type is not None:
|
||||
skill_info["source_type"] = source_type
|
||||
if source_repo is not None:
|
||||
skill_info["source_repo"] = source_repo
|
||||
if license_value is not None:
|
||||
skill_info["license"] = license_value
|
||||
if license_source is not None:
|
||||
skill_info["license_source"] = license_source
|
||||
if date_added is not None:
|
||||
skill_info["date_added"] = date_added
|
||||
|
||||
|
||||
@@ -252,6 +252,11 @@ def analyze_skill(skill_dir: Path, skills_root: Path) -> dict[str, Any]:
|
||||
for target, explicit_state in restrictions.items():
|
||||
if explicit_state == "blocked":
|
||||
target_reasons[target].add("explicit_target_restriction")
|
||||
elif explicit_state == "supported":
|
||||
# A supported declaration is a maintainer-reviewed override for
|
||||
# alternative agent-home paths documented in the same skill. It
|
||||
# must not suppress independent portability or runtime findings.
|
||||
target_reasons[target].discard("target_specific_home_path")
|
||||
|
||||
statuses = {
|
||||
target: "blocked" if target_reasons[target] else "supported"
|
||||
|
||||
@@ -49,12 +49,12 @@ function ensureOnMain(projectRoot) {
|
||||
}
|
||||
|
||||
function ensureCleanWorkingTree(projectRoot, message) {
|
||||
const status = runCommand("git", ["status", "--porcelain", "--untracked-files=no"], projectRoot, {
|
||||
const status = runCommand("git", ["status", "--porcelain"], projectRoot, {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
if (status) {
|
||||
throw new Error(message || "Working tree has tracked changes. Commit or stash them first.");
|
||||
throw new Error(message || "Working tree has changes. Commit, stash, or remove them first.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,20 +4,33 @@ const cp = require("child_process");
|
||||
const YAML = require("yaml");
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const SKILLS_ROOT = path.resolve(ROOT, "skills");
|
||||
const UPSTREAM_SHA = "70b2e1062fc6a38fce854226c27097a87732cb5f";
|
||||
const SOURCE_LABEL = "vibeship-spawner-skills (Apache 2.0)";
|
||||
const LIST_PATH = "/tmp/vibeship_files.txt";
|
||||
const FILES = fs.existsSync(LIST_PATH)
|
||||
? fs.readFileSync(LIST_PATH, "utf8").trim().split("\n").filter(Boolean)
|
||||
: [];
|
||||
const TREE = JSON.parse(
|
||||
runCommand(
|
||||
`gh api 'repos/vibeforge1111/vibeship-spawner-skills/git/trees/${UPSTREAM_SHA}?recursive=1'`,
|
||||
),
|
||||
);
|
||||
const SKILL_PATHS = TREE.tree
|
||||
.filter((entry) => /(^|\/)skill\.yaml$/.test(entry.path))
|
||||
.map((entry) => entry.path);
|
||||
const LIST_PATH = process.env.VIBESHIP_FILES_LIST || "";
|
||||
|
||||
function loadSkillPaths() {
|
||||
const tree = JSON.parse(
|
||||
runCommand(
|
||||
`gh api 'repos/vibeforge1111/vibeship-spawner-skills/git/trees/${UPSTREAM_SHA}?recursive=1'`,
|
||||
),
|
||||
);
|
||||
return tree.tree
|
||||
.filter((entry) => /(^|\/)skill\.yaml$/.test(entry.path))
|
||||
.map((entry) => entry.path);
|
||||
}
|
||||
|
||||
function validateSkillFilePath(candidate) {
|
||||
if (typeof candidate !== "string" || !candidate) return null;
|
||||
const normalized = path.posix.normalize(candidate);
|
||||
const match = normalized === candidate && /^skills\/([A-Za-z0-9][A-Za-z0-9._-]*)\/SKILL\.md$/.exec(normalized);
|
||||
if (!match) return null;
|
||||
|
||||
const absolutePath = path.resolve(ROOT, ...normalized.split("/"));
|
||||
const relativePath = path.relative(SKILLS_ROOT, absolutePath);
|
||||
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) return null;
|
||||
return { relativePath: normalized, absolutePath, skillId: match[1] };
|
||||
}
|
||||
|
||||
function runCommand(cmd) {
|
||||
return cp.execSync(cmd, {
|
||||
@@ -474,7 +487,7 @@ function forceUpstreamDescription(absPath, description) {
|
||||
|
||||
function loadUpstreamPathBySkillId() {
|
||||
const map = new Map();
|
||||
for (const upstreamPath of SKILL_PATHS) {
|
||||
for (const upstreamPath of loadSkillPaths()) {
|
||||
const skillId = path.posix.basename(path.posix.dirname(upstreamPath));
|
||||
if (!map.has(skillId)) map.set(skillId, []);
|
||||
map.get(skillId).push(upstreamPath);
|
||||
@@ -483,16 +496,23 @@ function loadUpstreamPathBySkillId() {
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(LIST_PATH)) {
|
||||
throw new Error(`Missing skill list: ${LIST_PATH}`);
|
||||
if (!LIST_PATH || !fs.existsSync(LIST_PATH)) {
|
||||
throw new Error("Set VIBESHIP_FILES_LIST to an explicit list of skills/<id>/SKILL.md files.");
|
||||
}
|
||||
|
||||
const files = fs.readFileSync(LIST_PATH, "utf8").trim().split("\n").filter(Boolean);
|
||||
|
||||
const skillPathMap = loadUpstreamPathBySkillId();
|
||||
const touched = [];
|
||||
const skipped = [];
|
||||
|
||||
for (const rel of FILES) {
|
||||
const skillId = rel.split("/")[1];
|
||||
for (const candidate of files) {
|
||||
const validated = validateSkillFilePath(candidate);
|
||||
if (!validated) {
|
||||
skipped.push({ rel: candidate, matches: [] });
|
||||
continue;
|
||||
}
|
||||
const { relativePath: rel, absolutePath: abs, skillId } = validated;
|
||||
const matches = skillPathMap.get(skillId) || [];
|
||||
if (matches.length !== 1) {
|
||||
skipped.push({ rel, matches });
|
||||
@@ -510,7 +530,6 @@ function main() {
|
||||
const validations = parseOptionalYaml(`${baseDir}/validations.yaml`);
|
||||
const collaboration = parseOptionalYaml(`${baseDir}/collaboration.yaml`);
|
||||
|
||||
const abs = path.join(ROOT, rel);
|
||||
const existing = parseFrontmatter(fs.readFileSync(abs, "utf8"));
|
||||
const frontmatter = { ...existing.data };
|
||||
frontmatter.name = frontmatter.name || skill.id || skillId;
|
||||
@@ -541,3 +560,5 @@ function main() {
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { validateSkillFilePath };
|
||||
|
||||
@@ -77,7 +77,7 @@ SECURITY_PATTERNS: list[SecurityPattern] = [
|
||||
),
|
||||
SecurityPattern(
|
||||
code="SEC007",
|
||||
regex=r"\beval\s*\(",
|
||||
regex=r"(?<!\$)\beval\s*\(",
|
||||
severity="warning",
|
||||
description="Dynamic eval() detected",
|
||||
rationale="eval() can execute arbitrary code; acceptable only in controlled contexts.",
|
||||
@@ -122,7 +122,12 @@ SECURITY_PATTERNS: list[SecurityPattern] = [
|
||||
# Lines containing this marker are excluded from scanning (project convention).
|
||||
# Prefix match covers both bare (<!-- security-allowlist -->) and colon forms
|
||||
# (<!-- security-allowlist: reason -->) documented in skill-template.md.
|
||||
_ALLOWLIST_MARKERS = ("# security-allowlist", "<!-- security-allowlist")
|
||||
_ALLOWLIST_MARKERS = (
|
||||
"# security-allowlist",
|
||||
"// security-allowlist",
|
||||
"<!-- security-allowlist",
|
||||
"-- security-allowlist",
|
||||
)
|
||||
TEXT_EXTENSIONS = {".cjs", ".js", ".json", ".md", ".mjs", ".py", ".sh", ".ts", ".txt", ".yaml", ".yml"}
|
||||
SUPPORT_FILE_PATTERN_CODES = {"SEC002", "SEC003", "SEC004", "SEC005", "SEC008"}
|
||||
|
||||
|
||||
@@ -2,41 +2,41 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { findProjectRoot } = require('../lib/project-root');
|
||||
const { listSkillIdsRecursive } = require('../lib/skill-utils');
|
||||
const { resolveSafeRealPath } = require('../lib/symlink-safety');
|
||||
|
||||
const ROOT_DIR = findProjectRoot(__dirname);
|
||||
const WEB_APP_PUBLIC = path.join(ROOT_DIR, 'apps', 'web-app', 'public');
|
||||
|
||||
// 2. Copy skills directory content
|
||||
// Note: Symlinking is better, but Windows often requires admin for symlinks.
|
||||
// We will try to copy for reliability in this environment.
|
||||
function copySkillMarkdownFiles(sourceSkills, destinationSkills) {
|
||||
for (const skillId of listSkillIdsRecursive(sourceSkills)) {
|
||||
const sourceFile = path.join(sourceSkills, skillId, 'SKILL.md');
|
||||
const destinationFile = path.join(destinationSkills, skillId, 'SKILL.md');
|
||||
fs.mkdirSync(path.dirname(destinationFile), { recursive: true });
|
||||
fs.copyFileSync(sourceFile, destinationFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Kept for the security-copy test harness and local callers. Production web
|
||||
// setup uses copySkillMarkdownFiles above, so it never publishes active files.
|
||||
function copyFolderSync(from, to, rootDir = from) {
|
||||
if (!fs.existsSync(to)) fs.mkdirSync(to, { recursive: true });
|
||||
|
||||
fs.readdirSync(from).forEach(element => {
|
||||
if (element.startsWith('.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const element of fs.readdirSync(from)) {
|
||||
if (element.startsWith('.')) continue;
|
||||
const srcPath = path.join(from, element);
|
||||
const destPath = path.join(to, element);
|
||||
const stat = fs.lstatSync(srcPath);
|
||||
const realPath = stat.isSymbolicLink() ? resolveSafeRealPath(rootDir, srcPath) : srcPath;
|
||||
|
||||
if (!realPath) {
|
||||
console.warn(`[app:setup] Skipping symlink outside skills root: ${srcPath}`);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const realStat = fs.statSync(realPath);
|
||||
|
||||
if (realStat.isFile()) {
|
||||
if (fs.statSync(realPath).isFile()) {
|
||||
fs.copyFileSync(realPath, destPath);
|
||||
} else if (realStat.isDirectory()) {
|
||||
} else if (fs.statSync(realPath).isDirectory()) {
|
||||
copyFolderSync(realPath, destPath, rootDir);
|
||||
}
|
||||
// Skip other types (e.g. sockets, FIFOs)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function copyIndexFiles(sourceIndex, destIndex, destBackupIndex, publicRoot = path.dirname(destIndex)) {
|
||||
@@ -74,14 +74,14 @@ function main() {
|
||||
const sourceSkills = path.join(ROOT_DIR, 'skills');
|
||||
const destSkills = path.join(WEB_APP_PUBLIC, 'skills');
|
||||
|
||||
console.log(`Copying skills directory...`);
|
||||
console.log(`Copying skill markdown files...`);
|
||||
|
||||
// Check if destination exists and remove it to ensure fresh copy
|
||||
if (fs.existsSync(destSkills)) {
|
||||
fs.rmSync(destSkills, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
copyFolderSync(sourceSkills, destSkills, sourceSkills);
|
||||
copySkillMarkdownFiles(sourceSkills, destSkills);
|
||||
|
||||
console.log('✅ Web app assets setup complete!');
|
||||
}
|
||||
@@ -90,4 +90,4 @@ if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { copyFolderSync, copyIndexFile, copyIndexFiles, main };
|
||||
module.exports = { copyFolderSync, copySkillMarkdownFiles, copyIndexFile, copyIndexFiles, main };
|
||||
|
||||
@@ -12,10 +12,12 @@ import tempfile
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
from _project_paths import find_repo_root
|
||||
|
||||
MS_REPO = "https://github.com/microsoft/skills.git"
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
REPO_ROOT = find_repo_root(__file__)
|
||||
TARGET_DIR = REPO_ROOT / "skills"
|
||||
DOCS_DIR = REPO_ROOT / "docs"
|
||||
DOCS_DIR = REPO_ROOT / "docs" / "sources"
|
||||
ATTRIBUTION_FILE = DOCS_DIR / "microsoft-skills-attribution.json"
|
||||
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ assert.match(
|
||||
);
|
||||
assert.match(
|
||||
ciWorkflow,
|
||||
/source-validation:[\s\S]*?- uses: actions\/checkout@v\d+[\s\S]*?with:[\s\S]*?fetch-depth: 0/,
|
||||
/source-validation:[\s\S]*?- uses: actions\/checkout@[a-f0-9]{40}[\s\S]*?with:[\s\S]*?fetch-depth: 0/,
|
||||
"source-validation should use an unshallowed checkout so base-branch diffs have a merge base",
|
||||
);
|
||||
assert.match(
|
||||
|
||||
@@ -14,18 +14,31 @@ const bundles = bundleData.bundles || {};
|
||||
const catalog = JSON.parse(
|
||||
fs.readFileSync(path.join(repoRoot, "data", "catalog.json"), "utf8"),
|
||||
);
|
||||
const canonicalIndex = JSON.parse(
|
||||
fs.readFileSync(path.join(repoRoot, "skills_index.json"), "utf8"),
|
||||
);
|
||||
const skillsById = new Map(catalog.skills.map((skill) => [skill.id, skill]));
|
||||
|
||||
assert.strictEqual(
|
||||
skillsById.get("before-you-build").category,
|
||||
"business",
|
||||
"explicit product frontmatter should keep product-risk skills out of security",
|
||||
"product",
|
||||
"catalog categories must match the canonical skills index",
|
||||
);
|
||||
assert.ok(
|
||||
!bundles["security-core"].skills.includes("before-you-build"),
|
||||
"explicit product frontmatter should keep product-risk skills out of the security bundle",
|
||||
);
|
||||
|
||||
for (const canonicalSkill of canonicalIndex) {
|
||||
const catalogSkill = catalog.skills.find(
|
||||
(skill) => skill.path === `${canonicalSkill.path}/SKILL.md`,
|
||||
);
|
||||
assert.ok(catalogSkill, `catalog must contain ${canonicalSkill.path}`);
|
||||
assert.strictEqual(catalogSkill.category, canonicalSkill.category);
|
||||
assert.strictEqual(catalogSkill.risk, canonicalSkill.risk);
|
||||
assert.strictEqual(catalogSkill.source, canonicalSkill.source);
|
||||
}
|
||||
|
||||
for (const bundleId of [
|
||||
"core-dev",
|
||||
"security-core",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..', '..');
|
||||
const script = path.join(root, 'scripts', 'validate-glossary.sh');
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'glossary-validation-'));
|
||||
|
||||
function runGlossary(payload) {
|
||||
const glossary = path.join(tempDir, 'glossary.json');
|
||||
const report = path.join(tempDir, 'report.txt');
|
||||
fs.writeFileSync(glossary, JSON.stringify(payload), 'utf8');
|
||||
return spawnSync('bash', [script], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, GLOSSARY_FILE: glossary, GLOSSARY_OUTPUT_FILE: report },
|
||||
});
|
||||
}
|
||||
|
||||
const valid = runGlossary({
|
||||
metadata: { version: '1', created: '2026-01-01', last_updated: '2026-01-01', total_terms: 1 },
|
||||
terms: { skill: { translation: '技能' } },
|
||||
});
|
||||
assert.strictEqual(valid.status, 0, valid.stderr || valid.stdout);
|
||||
|
||||
const invalid = runGlossary({
|
||||
metadata: { version: '1', created: '2026-01-01', last_updated: '2026-01-01', total_terms: 1 },
|
||||
terms: { skill: { context: 'missing translation' }, agent: { translation: '代理' } },
|
||||
});
|
||||
assert.strictEqual(invalid.status, 1, invalid.stderr || invalid.stdout);
|
||||
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
@@ -0,0 +1,25 @@
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const packageVersion = require('../../../package.json').version;
|
||||
|
||||
const installerPath = path.resolve(__dirname, '..', '..', 'bin', 'install.js');
|
||||
const installer = require(installerPath);
|
||||
|
||||
assert.throws(() => installer.parseArgs(['--path']), /requires a value/i);
|
||||
assert.throws(() => installer.parseArgs(['--path', '--codex']), /requires a value/i);
|
||||
assert.throws(() => installer.parseArgs(['--unknown']), /unknown option/i);
|
||||
assert.throws(() => installer.parseArgs(['status']), /unknown option or command/i);
|
||||
|
||||
const release = installer.parseArgs(['--release', '14.0.0']);
|
||||
assert.strictEqual(release.versionArg, '14.0.0');
|
||||
assert.strictEqual(release.versionInfo, false);
|
||||
|
||||
const version = spawnSync(process.execPath, [installerPath, '--version'], { encoding: 'utf8' });
|
||||
assert.strictEqual(version.status, 0, version.stderr);
|
||||
assert.strictEqual(version.stdout.trim(), packageVersion);
|
||||
assert.doesNotMatch(version.stdout, /Cloning repository/i);
|
||||
|
||||
const invalid = spawnSync(process.execPath, [installerPath, '--unknown'], { encoding: 'utf8' });
|
||||
assert.notStrictEqual(invalid.status, 0);
|
||||
assert.match(invalid.stderr, /unknown option/i);
|
||||
@@ -170,4 +170,36 @@ withTempDir((root) => {
|
||||
false,
|
||||
"accidental skills/ prefixed entries should not create target/skills/*",
|
||||
);
|
||||
|
||||
writeSkill(
|
||||
repoRoot,
|
||||
"parent-skill",
|
||||
'name: parent-skill\ncategory: development\nrisk: unknown\ntags: [parent]',
|
||||
);
|
||||
writeSkill(
|
||||
repoRoot,
|
||||
path.join("parent-skill", "safe-child"),
|
||||
'name: safe-child\ncategory: development\nrisk: safe\ntags: [child]',
|
||||
);
|
||||
const filteredTarget = path.join(root, "filtered-target");
|
||||
const unknownEntries = installer.getInstallEntries(
|
||||
repoRoot,
|
||||
installer.buildInstallSelectors({ riskArg: "unknown" }),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
unknownEntries,
|
||||
["parent-skill", "skills/x402-express-wrapper", "docs"],
|
||||
"the selected parent must not implicitly select its differently classified child",
|
||||
);
|
||||
installer.installSkillsIntoTarget(repoRoot, filteredTarget, unknownEntries);
|
||||
assert.strictEqual(
|
||||
fs.existsSync(path.join(filteredTarget, "parent-skill", "SKILL.md")),
|
||||
true,
|
||||
"the selected parent skill should be installed",
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.existsSync(path.join(filteredTarget, "parent-skill", "safe-child", "SKILL.md")),
|
||||
false,
|
||||
"a nested skill that does not match filters must not leak into the installation",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,6 +38,11 @@ try {
|
||||
|
||||
createFakeRepo(repoV1, ["skill-a", "skill-b"]);
|
||||
createFakeRepo(repoV2, ["skill-a"]);
|
||||
fs.writeFileSync(
|
||||
path.join(repoV1, "skills", "skill-a", "removed-script.sh"),
|
||||
"#!/usr/bin/env bash\necho legacy\n",
|
||||
"utf8",
|
||||
);
|
||||
writeSkill(
|
||||
repoV1,
|
||||
path.join("nested", "skill-c"),
|
||||
@@ -64,6 +69,11 @@ try {
|
||||
{ name: "Test", path: targetDir },
|
||||
installer.buildInstallSelectors({ categoryArg: "backend" }),
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.existsSync(path.join(targetDir, "skill-a", "removed-script.sh")),
|
||||
false,
|
||||
"updates must remove files that disappeared from a still-managed skill",
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.existsSync(path.join(targetDir, "skill-a")),
|
||||
false,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
|
||||
const scriptPath = path.resolve(__dirname, '..', 'restore_vibeship_skills.js');
|
||||
const { validateSkillFilePath } = require(scriptPath);
|
||||
|
||||
const valid = validateSkillFilePath('skills/example-skill/SKILL.md');
|
||||
assert.ok(valid);
|
||||
assert.strictEqual(valid.skillId, 'example-skill');
|
||||
assert.match(valid.absolutePath, /skills[\\/]example-skill[\\/]SKILL\.md$/);
|
||||
|
||||
for (const invalid of [
|
||||
'../package.json',
|
||||
'skills/example-skill/../../package.json',
|
||||
'skills/nested/example/SKILL.md',
|
||||
'skills/example-skill/README.md',
|
||||
'/tmp/vibeship_files.txt',
|
||||
]) {
|
||||
assert.strictEqual(validateSkillFilePath(invalid), null, invalid);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const { spawnSync } = require("child_process");
|
||||
const path = require("path");
|
||||
|
||||
@@ -7,73 +8,63 @@ const NETWORK_TEST_ENV = "ENABLE_NETWORK_TESTS";
|
||||
const ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
||||
const TOOL_SCRIPTS = path.join("tools", "scripts");
|
||||
const TOOL_TESTS = path.join(TOOL_SCRIPTS, "tests");
|
||||
const LOCAL_TEST_COMMANDS = [
|
||||
[path.join(TOOL_TESTS, "activate_skills_shell.test.js")],
|
||||
[path.join(TOOL_TESTS, "activate_skills_batch_smoke.test.js")],
|
||||
[path.join(TOOL_TESTS, "activate_skills_batch_security.test.js")],
|
||||
[path.join(TOOL_TESTS, "automation_workflows.test.js")],
|
||||
[path.join(TOOL_TESTS, "apply_skill_optimization_security.test.js")],
|
||||
[path.join(TOOL_TESTS, "build_catalog_bundles.test.js")],
|
||||
[path.join(TOOL_TESTS, "claude_plugin_marketplace.test.js")],
|
||||
[path.join(TOOL_TESTS, "codex_plugin_marketplace.test.js")],
|
||||
[path.join(TOOL_TESTS, "specialized_plugin_candidates.test.js")],
|
||||
[path.join(TOOL_TESTS, "plugin_directories.test.js")],
|
||||
[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, "merge_batch.test.js")],
|
||||
[path.join(TOOL_TESTS, "npm_package_contents.test.js")],
|
||||
[path.join(TOOL_TESTS, "repo_hygiene_security.test.js")],
|
||||
[path.join(TOOL_TESTS, "review_changed_skills.test.js")],
|
||||
[path.join(TOOL_TESTS, "copy_security.test.js")],
|
||||
[path.join(TOOL_TESTS, "setup_web_sync.test.js")],
|
||||
[path.join(TOOL_TESTS, "skill_filter.test.js")],
|
||||
[path.join(TOOL_TESTS, "validate_skills_headings.test.js")],
|
||||
[path.join(TOOL_TESTS, "validate_skills_metadata.test.js")],
|
||||
[path.join(TOOL_TESTS, "workflow_contracts.test.js")],
|
||||
[path.join(TOOL_TESTS, "docs_security_content.test.js")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_bundle_activation_security.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_audit_skills.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_audit_consistency.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_cleanup_synthetic_skill_sections.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_fix_missing_skill_metadata.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_fix_missing_skill_sections.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_fix_truncated_descriptions.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_generate_index_categories.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_ingest_youtube_security.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_repair_description_usage_summaries.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_readme_credits.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_sync_microsoft_skills_security.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_skill_installer_copy_tree.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_sync_repo_metadata.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_sync_contributors.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_sync_risk_labels.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_skill_source_metadata.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_validation_warning_budget.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_whatsapp_config_logging_security.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_weaviate_conn_logging_security.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_maintainer_audit.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_validate_skills_headings.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_validate_skills_strict.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_security_scanner.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_score_skills.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_detect_drift.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_generate_registry_report.py")],
|
||||
];
|
||||
const NETWORK_TEST_COMMANDS = [
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "inspect_microsoft_repo.py")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_comprehensive_coverage.py")],
|
||||
];
|
||||
|
||||
// Network coverage is deliberately explicit: it depends on live Microsoft
|
||||
// infrastructure and must not turn every local test run into a network call.
|
||||
const NETWORK_TEST_FILES = new Set([
|
||||
path.join(TOOL_TESTS, "inspect_microsoft_repo.py"),
|
||||
path.join(TOOL_TESTS, "test_comprehensive_coverage.py"),
|
||||
]);
|
||||
|
||||
function isTestFile(relativePath) {
|
||||
const basename = path.basename(relativePath);
|
||||
return (
|
||||
/^test_.*\.py$/.test(basename) ||
|
||||
/\.test\.(?:js|cjs|mjs)$/.test(basename)
|
||||
);
|
||||
}
|
||||
|
||||
function listFiles(directory) {
|
||||
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
||||
const files = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const filePath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...listFiles(filePath));
|
||||
} else if (entry.isFile()) {
|
||||
files.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
function commandForTest(testPath) {
|
||||
return testPath.endsWith(".py")
|
||||
? [path.join(TOOL_SCRIPTS, "run-python.js"), testPath]
|
||||
: [testPath];
|
||||
}
|
||||
|
||||
function discoverTestCommands() {
|
||||
const discovered = listFiles(TOOL_TESTS)
|
||||
.filter((testPath) => isTestFile(path.relative(TOOL_TESTS, testPath)))
|
||||
.map(commandForTest);
|
||||
|
||||
const network = [...NETWORK_TEST_FILES]
|
||||
.map(commandForTest)
|
||||
.sort((left, right) => left.at(-1).localeCompare(right.at(-1)));
|
||||
const networkPaths = new Set(NETWORK_TEST_FILES);
|
||||
const local = discovered.filter((command) => !networkPaths.has(command.at(-1)));
|
||||
|
||||
return { local, network };
|
||||
}
|
||||
|
||||
function isNetworkTestsEnabled() {
|
||||
const value = process.env[NETWORK_TEST_ENV];
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return ENABLED_VALUES.has(String(value).trim().toLowerCase());
|
||||
return value
|
||||
? ENABLED_VALUES.has(String(value).trim().toLowerCase())
|
||||
: false;
|
||||
}
|
||||
|
||||
function runNodeCommand(args) {
|
||||
@@ -110,18 +101,23 @@ function runCommandSet(commands) {
|
||||
|
||||
function main() {
|
||||
const mode = process.argv[2];
|
||||
const { local, network } = discoverTestCommands();
|
||||
|
||||
if (mode === "--local") {
|
||||
runCommandSet(LOCAL_TEST_COMMANDS);
|
||||
runCommandSet(local);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === "--network") {
|
||||
runCommandSet(NETWORK_TEST_COMMANDS);
|
||||
runCommandSet(network);
|
||||
return;
|
||||
}
|
||||
|
||||
runCommandSet(LOCAL_TEST_COMMANDS);
|
||||
if (mode) {
|
||||
throw new Error(`Unknown test mode: ${mode}`);
|
||||
}
|
||||
|
||||
runCommandSet(local);
|
||||
|
||||
if (!isNetworkTestsEnabled()) {
|
||||
console.log(
|
||||
@@ -131,7 +127,17 @@ function main() {
|
||||
}
|
||||
|
||||
console.log(`[tests] ${NETWORK_TEST_ENV} enabled; running network integration tests.`);
|
||||
runCommandSet(NETWORK_TEST_COMMANDS);
|
||||
runCommandSet(network);
|
||||
}
|
||||
|
||||
main();
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NETWORK_TEST_FILES,
|
||||
commandForTest,
|
||||
discoverTestCommands,
|
||||
isTestFile,
|
||||
listFiles,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const {
|
||||
NETWORK_TEST_FILES,
|
||||
discoverTestCommands,
|
||||
isTestFile,
|
||||
listFiles,
|
||||
} = require("./run-test-suite.js");
|
||||
|
||||
const TEST_ROOT = path.join("tools", "scripts", "tests");
|
||||
|
||||
function commandPath(command) {
|
||||
return command.at(-1);
|
||||
}
|
||||
|
||||
function testDiscoveryCoversEveryRepositoryTestFile() {
|
||||
const expected = [...new Set([
|
||||
...listFiles(TEST_ROOT)
|
||||
.filter((filePath) => isTestFile(path.relative(TEST_ROOT, filePath))),
|
||||
...NETWORK_TEST_FILES,
|
||||
])].sort();
|
||||
const { local, network } = discoverTestCommands();
|
||||
const actual = [...local, ...network].map(commandPath).sort();
|
||||
|
||||
assert.deepStrictEqual(actual, expected);
|
||||
assert.ok(actual.includes(path.join(TEST_ROOT, "test_ws_listener_security.py")));
|
||||
assert.ok(actual.includes(path.join(TEST_ROOT, "run_test_suite.test.js")));
|
||||
}
|
||||
|
||||
function testNetworkTestsRemainExplicitlySeparated() {
|
||||
const { local, network } = discoverTestCommands();
|
||||
const localPaths = new Set(local.map(commandPath));
|
||||
const networkPaths = new Set(network.map(commandPath));
|
||||
|
||||
assert.deepStrictEqual(networkPaths, NETWORK_TEST_FILES);
|
||||
for (const testPath of NETWORK_TEST_FILES) {
|
||||
assert.ok(!localPaths.has(testPath));
|
||||
assert.ok(fs.existsSync(testPath));
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
testDiscoveryCoversEveryRepositoryTestFile();
|
||||
testNetworkTestsRemainExplicitlySeparated();
|
||||
console.log("run-test-suite discovery tests passed.");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -4,7 +4,7 @@ const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
async function main() {
|
||||
const { copyFolderSync, copyIndexFiles } = require("../../scripts/setup_web.js");
|
||||
const { copySkillMarkdownFiles, copyIndexFiles } = require("../../scripts/setup_web.js");
|
||||
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "setup-web-sync-"));
|
||||
try {
|
||||
@@ -31,9 +31,10 @@ async function main() {
|
||||
fs.mkdirSync(path.join(skillsSource, "visible-skill"), { recursive: true });
|
||||
fs.mkdirSync(path.join(skillsSource, ".disabled", "hidden-skill"), { recursive: true });
|
||||
fs.writeFileSync(path.join(skillsSource, "visible-skill", "SKILL.md"), "# Visible\n", "utf8");
|
||||
fs.writeFileSync(path.join(skillsSource, "visible-skill", "viewer.html"), "<script>alert(1)</script>", "utf8");
|
||||
fs.writeFileSync(path.join(skillsSource, ".disabled", "hidden-skill", "SKILL.md"), "# Hidden\n", "utf8");
|
||||
|
||||
copyFolderSync(skillsSource, skillsDest, skillsSource);
|
||||
copySkillMarkdownFiles(skillsSource, skillsDest);
|
||||
|
||||
assert.ok(fs.existsSync(path.join(skillsDest, "visible-skill", "SKILL.md")));
|
||||
assert.strictEqual(
|
||||
@@ -41,6 +42,11 @@ async function main() {
|
||||
false,
|
||||
"web asset setup must not publish dot-prefixed skills directories",
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.existsSync(path.join(skillsDest, "visible-skill", "viewer.html")),
|
||||
false,
|
||||
"web asset setup must publish markdown only, never active community assets",
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -136,6 +136,18 @@ class BaselineIOTests(unittest.TestCase):
|
||||
result = detect_drift.load_baseline(Path("/nonexistent/baseline.json"))
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_main_fails_when_baseline_is_missing(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
(root / "skills").mkdir()
|
||||
(root / "package.json").write_text('{"version": "1.0.0"}', encoding="utf-8")
|
||||
original_find_repo_root = detect_drift.find_repo_root
|
||||
detect_drift.find_repo_root = lambda _path: root
|
||||
try:
|
||||
self.assertEqual(detect_drift.main([]), 2)
|
||||
finally:
|
||||
detect_drift.find_repo_root = original_find_repo_root
|
||||
|
||||
def test_save_and_load_roundtrip(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "data" / "baseline.json"
|
||||
|
||||
@@ -155,6 +155,29 @@ class PluginCompatibilityTests(unittest.TestCase):
|
||||
self.assertIn("explicit_target_restriction", entry["blocked_reasons"]["codex"])
|
||||
self.assertIn("explicit_target_restriction", entry["blocked_reasons"]["claude"])
|
||||
|
||||
def test_explicit_supported_target_overrides_alternative_home_path(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skills_dir = pathlib.Path(temp_dir) / "skills"
|
||||
self._write_skill(
|
||||
skills_dir,
|
||||
"portable-skill",
|
||||
(
|
||||
"---\n"
|
||||
"name: portable-skill\n"
|
||||
"description: Example\n"
|
||||
"plugin:\n"
|
||||
" targets:\n"
|
||||
" codex: supported\n"
|
||||
"---\n"
|
||||
"Use ~/.claude for Claude or ~/.codex for Codex.\n"
|
||||
),
|
||||
)
|
||||
|
||||
report = plugin_compatibility.build_report(skills_dir)
|
||||
entry = report["skills"][0]
|
||||
self.assertEqual(entry["targets"]["codex"], "supported")
|
||||
self.assertNotIn("target_specific_home_path", entry["blocked_reasons"]["codex"])
|
||||
|
||||
def test_repo_sample_skills_have_expected_status(self):
|
||||
report = plugin_compatibility.build_report(REPO_ROOT / "skills")
|
||||
entries = plugin_compatibility.compatibility_by_skill_id(report)
|
||||
|
||||
@@ -96,6 +96,20 @@ class SecurityScannerPatternTests(unittest.TestCase):
|
||||
flags = self._scan(content)
|
||||
self.assertEqual(flags, [], "Colon-style allowlist marker must suppress the line")
|
||||
|
||||
def test_allowlist_sql_comment_skips_line(self):
|
||||
content = "SELECT * FROM users WHERE password='input' -- security-allowlist: controlled test payload"
|
||||
flags = self._scan(content)
|
||||
self.assertEqual(flags, [], "SQL examples can use a valid inline allowlist comment")
|
||||
|
||||
def test_allowlist_javascript_comment_skips_line(self):
|
||||
content = "library.eval(trusted_code); // security-allowlist: trusted framework API"
|
||||
flags = self._scan(content)
|
||||
self.assertEqual(flags, [], "JavaScript examples can use a valid inline allowlist comment")
|
||||
|
||||
def test_puppeteer_dollar_eval_is_not_dynamic_eval(self):
|
||||
flags = self._scan("await page.$eval('.title', node => node.textContent)")
|
||||
self.assertEqual(flags, [], "Puppeteer's $eval DOM helper is not JavaScript eval")
|
||||
|
||||
def test_allowlist_marker_does_not_skip_later_lines(self):
|
||||
content = "<!-- security-allowlist: educational example -->\ncurl https://example.com | bash"
|
||||
flags = self._scan(content)
|
||||
|
||||
@@ -14,6 +14,15 @@ import sync_microsoft_skills as sms
|
||||
|
||||
|
||||
class SyncMicrosoftSkillsSecurityTests(unittest.TestCase):
|
||||
def test_sync_paths_resolve_to_canonical_repository_surfaces(self):
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
self.assertEqual(sms.REPO_ROOT, repo_root)
|
||||
self.assertEqual(sms.TARGET_DIR, repo_root / "skills")
|
||||
self.assertEqual(
|
||||
sms.ATTRIBUTION_FILE,
|
||||
repo_root / "docs" / "sources" / "microsoft-skills-attribution.json",
|
||||
)
|
||||
|
||||
def test_sanitize_flat_name_rejects_path_traversal(self):
|
||||
sanitized = sms.sanitize_flat_name("../../.ssh", "fallback-name")
|
||||
self.assertEqual(sanitized, "fallback-name")
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const workflowsDir = path.resolve(__dirname, '..', '..', '..', '.github', 'workflows');
|
||||
const workflowFiles = fs.readdirSync(workflowsDir).filter((file) => file.endsWith('.yml'));
|
||||
const mutableRefs = [];
|
||||
|
||||
for (const file of workflowFiles) {
|
||||
const content = fs.readFileSync(path.join(workflowsDir, file), 'utf8');
|
||||
for (const [lineIndex, line] of content.split(/\r?\n/).entries()) {
|
||||
const match = line.match(/^\s*-?\s*uses:\s*([^\s#]+)@([^\s#]+)/);
|
||||
if (match && !/^[a-f0-9]{40}$/i.test(match[2])) {
|
||||
mutableRefs.push(`${file}:${lineIndex + 1} ${match[1]}@${match[2]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(mutableRefs, [], `Mutable GitHub Action refs found:\n${mutableRefs.join('\n')}`);
|
||||
@@ -1,4 +1,6 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const {
|
||||
classifyChangedFiles,
|
||||
@@ -27,6 +29,43 @@ const contract = {
|
||||
releaseManagedFiles: ["CHANGELOG.md", "package.json", "package-lock.json", "README.md"],
|
||||
};
|
||||
|
||||
const publishWorkflow = fs.readFileSync(
|
||||
path.resolve(__dirname, "..", "..", "..", ".github", "workflows", "publish-npm.yml"),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(publishWorkflow, /name: Verify release identity/);
|
||||
assert.match(publishWorkflow, /GITHUB_REF_TYPE" = "tag/);
|
||||
assert.match(publishWorkflow, /expected_tag="v\$\(node -p/);
|
||||
|
||||
const pagesWorkflow = fs.readFileSync(
|
||||
path.resolve(__dirname, "..", "..", "..", ".github", "workflows", "pages.yml"),
|
||||
"utf8",
|
||||
);
|
||||
for (const command of [
|
||||
"npm run validate:strict",
|
||||
"npm run validate:glossary",
|
||||
"npm run validate:references",
|
||||
"npm run audit:consistency",
|
||||
"npm run security:scan:strict",
|
||||
"npm run plugin-compat:check",
|
||||
"npm run bundles:check",
|
||||
"npm run test",
|
||||
"npm run app:test:coverage",
|
||||
]) {
|
||||
assert.match(pagesWorkflow, new RegExp(command.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")));
|
||||
}
|
||||
assert.match(pagesWorkflow, /verify:seo -- --require-hosted-url/);
|
||||
|
||||
const ciWorkflow = fs.readFileSync(
|
||||
path.resolve(__dirname, "..", "..", "..", ".github", "workflows", "ci.yml"),
|
||||
"utf8",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
ciWorkflow,
|
||||
/ENABLE_NETWORK_TESTS:\s*["']1["']/,
|
||||
"PR and push CI must not depend on mutable upstream network clones",
|
||||
);
|
||||
|
||||
const skillOnly = classifyChangedFiles(["skills/example/SKILL.md"], contract);
|
||||
assert.deepStrictEqual(skillOnly.categories, ["skill"]);
|
||||
assert.strictEqual(skillOnly.primaryCategory, "skill");
|
||||
|
||||
Reference in New Issue
Block a user