Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/cli/src/utils/skillsManifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,33 @@ describe("hashSkillBundle", () => {
expect(hashSkillBundle(dir).files).toBe(3);
});

it("ignores Python bytecode a skill script leaves behind, but not a real edit", () => {
const dir = writeSkill("a", {
"SKILL.md": "hello",
"scripts/lib/auth.py": "x = 1\n",
});
const clean = hashSkillBundle(dir);
mkdirSync(join(dir, "scripts/lib/__pycache__"));
writeFileSync(join(dir, "scripts/lib/__pycache__/auth.cpython-312.pyc"), "\x00bytecode");
writeFileSync(join(dir, "scripts/lib/stray.pyc"), "\x00bytecode");
expect(hashSkillBundle(dir)).toEqual(clean);

writeFileSync(join(dir, "scripts/lib/auth.py"), "x = 2\n");
expect(hashSkillBundle(dir).hash).not.toBe(clean.hash);
});

it("ignores dependency installs, OS files, and editor swap files", () => {
const dir = writeSkill("a", { "SKILL.md": "hello", "scripts/run.mjs": "run();" });
const clean = hashSkillBundle(dir);
writeSkill("a", {
"node_modules/dep/index.js": "module.exports = 1;",
"scripts/.DS_Store": "",
"scripts/.run.mjs.swp": "swap",
"SKILL.md~": "backup",
});
expect(hashSkillBundle(dir)).toEqual(clean);
});

it("normalises CRLF so a Windows checkout is not flagged as different", () => {
const lf = writeSkill("lf", { "SKILL.md": "line1\nline2\n" });
const crlf = writeSkill("crlf", { "SKILL.md": "line1\r\nline2\r\n" });
Expand Down
25 changes: 22 additions & 3 deletions packages/cli/src/utils/skillsManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,33 @@ export const FALLBACK_CORE_SKILLS: readonly string[] = [

// ── Hashing ────────────────────────────────────────────────────────────────

// By-products of using a skill (bytecode from running its scripts, installs, OS
// and editor files), skipped on both the source and the installed side so a
// tree that ran a script hashes the same as a clean checkout.
const RUNTIME_DIRS = new Set([
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
"node_modules",
]);
const RUNTIME_FILES = new Set([".DS_Store", "Thumbs.db"]);
const RUNTIME_FILE_SUFFIXES = [".pyc", ".pyo", ".swp", ".swo", "~"];

function isRuntimeFile(name: string): boolean {
return RUNTIME_FILES.has(name) || RUNTIME_FILE_SUFFIXES.some((s) => name.endsWith(s));
}

function listFilesSorted(dir: string): string[] {
const out: string[] = [];
const walk = (d: string): void => {
for (const name of readdirSync(d)) {
if (name === ".DS_Store") continue;
const p = join(d, name);
if (statSync(p).isDirectory()) walk(p);
else out.push(p);
if (statSync(p).isDirectory()) {
if (!RUNTIME_DIRS.has(name)) walk(p);
} else if (!isRuntimeFile(name)) {
out.push(p);
}
}
};
walk(dir);
Expand Down
Loading