Skip to content
Open
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
7 changes: 5 additions & 2 deletions src/local-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ function getPortableLocalSource(source: string, lockDir: string): string {
/**
* Compute a SHA-256 hash from all files in a skill directory.
* Reads all files recursively, sorts them by relative path for determinism,
* and produces a single hash from their concatenated contents.
* and produces a single hash from their framed paths and contents.
*/
export async function computeSkillFolderHash(skillDir: string): Promise<string> {
const files: Array<{ relativePath: string; content: Buffer }> = [];
Expand All @@ -160,8 +160,11 @@ export async function computeSkillFolderHash(skillDir: string): Promise<string>

const hash = createHash('sha256');
for (const file of files) {
// Include the path in the hash so renames are detected
// Frame each file so a path change cannot be offset by a content change.
hash.update(file.relativePath);
hash.update('\0');
hash.update(String(file.content.length));
hash.update('\0');
hash.update(file.content);
}

Expand Down
18 changes: 18 additions & 0 deletions tests/local-lock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,24 @@ describe('local-lock', () => {
}
});

it('distinguishes a filename change from an offsetting content change', async () => {
const dir = await mkdtemp(join(tmpdir(), 'lock-test-'));
try {
const first = join(dir, 'first');
const second = join(dir, 'second');
await mkdir(first);
await mkdir(second);
await writeFile(join(first, 'SKILL.md'), 'same skill');
await writeFile(join(second, 'SKILL.md'), 'same skill');
await writeFile(join(first, 'a'), 'bc');
await writeFile(join(second, 'ab'), 'c');

expect(await computeSkillFolderHash(first)).not.toBe(await computeSkillFolderHash(second));
} finally {
await rm(dir, { recursive: true, force: true });
}
});

it('includes nested files in subdirectories', async () => {
const dir = await mkdtemp(join(tmpdir(), 'lock-test-'));
try {
Expand Down