Skip to content

Commit b1d66c8

Browse files
committed
feat(sync): link skills into .agents/skills
Also removes links left in skills/ by earlier syncs, matched by name since their targets may no longer exist.
1 parent 6f994ae commit b1d66c8

3 files changed

Lines changed: 36 additions & 5 deletions

File tree

‎.changeset/sync-agents-skills.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@bomb.sh/tools': minor
3+
---
4+
5+
Changes `bsh sync` to link skills into `.agents/skills/` instead of `skills/`. Re-run `bsh sync` to move existing links; it removes the old `skills/` links and updates the `.gitignore` and `AGENTS.md` entries.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ If you'd like to use this package for your own projects, please consider forking
1616

1717
## Agent Skills
1818

19-
If you use an AI coding agent, run `pnpm bsh sync` to symlink this package's skill files into your project's `skills/` directory. Synced skills are automatically added to your `.gitignore`, and an index of them is maintained in your `AGENTS.md`. Claude Code users: add `@AGENTS.md` to your project's `CLAUDE.md`.
19+
If you use an AI coding agent, run `pnpm bsh sync` to symlink this package's skill files into your project's `.agents/skills/` directory. Synced skills are automatically added to your `.gitignore`, and an index of them is maintained in your `AGENTS.md`.

‎src/commands/sync.ts‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const SENTINEL_START = '<!-- bsh:skills -->';
1313
const SENTINEL_END = '<!-- /bsh:skills -->';
1414
const GITIGNORE_START = '# bsh:skills';
1515
const GITIGNORE_END = '# /bsh:skills';
16+
const SKILLS_DIR = '.agents/skills/';
17+
// Where syncs before `.agents/skills/` linked skills.
18+
const LEGACY_SKILLS_DIR = 'skills/';
1619

1720
export async function sync(_ctx: CommandContext): Promise<void> {
1821
const parentPkg = await findParentPackage();
@@ -29,11 +32,12 @@ export async function sync(_ctx: CommandContext): Promise<void> {
2932
return;
3033
}
3134

32-
const skills = await copySkills({ source, dest: new URL('skills/', root) });
35+
const skills = await copySkills({ source, dest: new URL(SKILLS_DIR, root) });
36+
await removeLegacyLinks({ dir: new URL(LEGACY_SKILLS_DIR, root), skills });
3337
await updateGitignore({ root, skills });
3438
await updateAgentsMd({ root, skills });
3539

36-
console.info(`Synced ${skills.length} skills to skills/`);
40+
console.info(`Synced ${skills.length} skills to ${SKILLS_DIR}`);
3741
}
3842

3943
interface SkillInfo {
@@ -105,12 +109,33 @@ async function pruneStaleLinks(options: {
105109
}
106110
}
107111

112+
/**
113+
* Remove links from syncs that predate `.agents/skills/`. Match by name, not
114+
* target: after an upgrade those links point into a store path that may no
115+
* longer exist. Anything that isn't a symlink is left alone.
116+
*/
117+
async function removeLegacyLinks(options: { dir: URL; skills: SkillInfo[] }): Promise<void> {
118+
const { dir, skills } = options;
119+
if (!(await hfs.isDirectory(dir))) return;
120+
121+
const names = new Set(skills.map((s) => s.name));
122+
let remaining = 0;
123+
for await (const entry of hfs.list(dir)) {
124+
if (entry.isSymlink && names.has(entry.name)) {
125+
await rm(fileURLToPath(new URL(entry.name, dir)));
126+
} else {
127+
remaining++;
128+
}
129+
}
130+
if (remaining === 0) await hfs.delete(dir);
131+
}
132+
108133
async function updateGitignore(options: { root: URL; skills: SkillInfo[] }): Promise<void> {
109134
const { root, skills } = options;
110135
const gitignorePath = new URL('.gitignore', root);
111136
let content = (await hfs.text(gitignorePath)) ?? '';
112137

113-
const lines = skills.map((s) => `skills/${s.name}/`);
138+
const lines = skills.map((s) => `${SKILLS_DIR}${s.name}/`);
114139
const section = [GITIGNORE_START, ...lines, GITIGNORE_END].join('\n');
115140

116141
const startIdx = content.indexOf(GITIGNORE_START);
@@ -133,7 +158,8 @@ async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Prom
133158

134159
const lines = skills.map((s) => {
135160
const desc = s.description.split('.')[0]?.trim();
136-
return `- **${s.name}** — [skills/${s.name}/SKILL.md](skills/${s.name}/SKILL.md)${desc ? ` - ${desc}` : ''}`;
161+
const path = `${SKILLS_DIR}${s.name}/SKILL.md`;
162+
return `- **${s.name}** — [${path}](${path})${desc ? ` - ${desc}` : ''}`;
137163
});
138164

139165
const section = [

0 commit comments

Comments
 (0)