[READ-ONLY] Mirror of https://github.com/bombshell-dev/tools. Internal CLI to standardize tooling across all Bombshell projects
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(sync): macos symlink + multiple sync runs (#41)

* fix(sync): add tests

* fix(sync): symlink on macos + relink

* Update .changeset/sync-symlink-trailing-slash.md

Co-authored-by: paul valladares <85648028+dreyfus92@users.noreply.github.com>

---------

Co-authored-by: paul valladares <85648028+dreyfus92@users.noreply.github.com>

authored by

Nate Moore
paul valladares
and committed by
GitHub
(Jun 20, 2026, 3:44 PM -0500) dd6abfaf 22f87d0f

+69 -5
+56
src/commands/sync.test.ts
··· 1 + import { lstat, readlink } from 'node:fs/promises'; 2 + import { fileURLToPath } from 'node:url'; 3 + import { describe, it, expect } from 'vitest'; 4 + import { createFixture } from '../test-utils/index.ts'; 5 + import { copySkills } from './sync.ts'; 6 + 7 + describe('copySkills', () => { 8 + it('symlinks each skill into the destination', async () => { 9 + const fixture = await createFixture({ 10 + 'source-skills': { 11 + build: { 12 + 'SKILL.md': '---\nname: build\ndescription: Build the project.\n---\nbody', 13 + }, 14 + }, 15 + }); 16 + 17 + const source = new URL('source-skills/', fixture.root); 18 + const dest = new URL('project/skills/', fixture.root); 19 + 20 + const skills = await copySkills({ source, dest }); 21 + 22 + expect(skills).toEqual([{ name: 'build', description: 'Build the project.' }]); 23 + 24 + // The destination entry must be a symlink, not a copy. 25 + const linkPath = fileURLToPath(new URL('build', dest)); 26 + expect((await lstat(linkPath)).isSymbolicLink()).toBe(true); 27 + 28 + // And it must resolve back to the source skill. 29 + expect(await readlink(linkPath)).toBe('../../source-skills/build'); 30 + 31 + // Reading through the link reaches the real file. 32 + expect(await fixture.text('project/skills/build/SKILL.md')).toContain('name: build'); 33 + }); 34 + 35 + it('is idempotent and never touches the source on re-sync', async () => { 36 + const fixture = await createFixture({ 37 + 'source-skills': { 38 + build: { 39 + 'SKILL.md': '---\nname: build\ndescription: Build the project.\n---\nbody', 40 + }, 41 + }, 42 + }); 43 + 44 + const source = new URL('source-skills/', fixture.root); 45 + const dest = new URL('project/skills/', fixture.root); 46 + 47 + const first = await copySkills({ source, dest }); 48 + // Re-running must not throw and must yield the same result. 49 + const second = await copySkills({ source, dest }); 50 + 51 + expect(second).toEqual(first); 52 + // The source skill files must survive a re-sync. 53 + expect(await fixture.text('source-skills/build/SKILL.md')).toContain('name: build'); 54 + expect(await fixture.text('project/skills/build/SKILL.md')).toContain('name: build'); 55 + }); 56 + });
+8 -5
src/commands/sync.ts
··· 1 - import { readlink, symlink } from 'node:fs/promises'; 1 + import { readlink, rm, symlink } from 'node:fs/promises'; 2 2 import { findPackageJSON } from 'node:module'; 3 3 import { dirname, isAbsolute, relative, resolve } from 'node:path'; 4 4 import { platform } from 'node:process'; ··· 41 41 description: string; 42 42 } 43 43 44 - async function copySkills(options: { source: URL; dest: URL }): Promise<SkillInfo[]> { 44 + export async function copySkills(options: { source: URL; dest: URL }): Promise<SkillInfo[]> { 45 45 const { source, dest } = options; 46 46 const skills: SkillInfo[] = []; 47 47 ··· 60 60 61 61 for (const name of keep) { 62 62 const srcDir = new URL(`${name}/`, source); 63 - const destDir = new URL(`${name}/`, dest); 64 63 65 - await hfs.deleteAll(destDir); 64 + // Use a path without a trailing slash. macOS rejects a trailing-slash link 65 + // path with ENOENT, and `rm` on a trailing-slash directory symlink follows 66 + // the link and deletes the source rather than unlinking the symlink itself. 67 + const linkPath = resolve(destDirPath, name); 68 + await rm(linkPath, { recursive: true, force: true }); 66 69 67 70 const target = relative(destDirPath, fileURLToPath(srcDir)); 68 - await symlink(target, fileURLToPath(destDir), linkType); 71 + await symlink(target, linkPath, linkType); 69 72 70 73 const content = await hfs.text(new URL('SKILL.md', srcDir)); 71 74 if (content) {