Skip to content

Commit 340e898

Browse files
committed
fix(sync): refactor sync command
1 parent 1ac89a1 commit 340e898

2 files changed

Lines changed: 49 additions & 46 deletions

File tree

.changeset/easy-doodles-make.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bomb.sh/tools": patch
3+
---
4+
5+
Fixes errors that caused bsh sync to crash

src/commands/sync.ts

Lines changed: 44 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
1-
import { existsSync, type Dirent } from 'node:fs';
2-
import { mkdir, readdir, readFile, readlink, rm, symlink, writeFile } from 'node:fs/promises';
3-
import { isAbsolute, relative, resolve } from 'node:path';
4-
import { cwd, platform } from 'node:process';
1+
import { readlink, symlink } from 'node:fs/promises';
2+
import { findPackageJSON } from 'node:module';
3+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
4+
import { platform } from 'node:process';
55
import { fileURLToPath, pathToFileURL } from 'node:url';
6+
import { NodeHfs } from '@humanfs/node';
67
import { parse } from 'ultramatter';
78
import type { CommandContext } from '../context.ts';
89

10+
const hfs = new NodeHfs();
11+
912
const SENTINEL_START = '<!-- bsh:skills -->';
1013
const SENTINEL_END = '<!-- /bsh:skills -->';
1114
const GITIGNORE_START = '# bsh:skills';
1215
const GITIGNORE_END = '# /bsh:skills';
1316

1417
export async function sync(_ctx: CommandContext): Promise<void> {
15-
const root = pathToFileURL(`${cwd()}/`);
16-
17-
if (await isSelf(root)) {
18-
console.info('Skipping sync — running inside @bomb.sh/tools');
18+
const parentPkg = findParentPackage();
19+
if (!parentPkg) {
20+
console.info('Skipping sync — no parent project found (running inside @bomb.sh/tools?)');
1921
return;
2022
}
2123

22-
const source = new URL('node_modules/@bomb.sh/tools/skills/', root);
23-
if (!existsSync(source)) {
24-
console.error('@bomb.sh/tools is not installed. Run `pnpm add -D @bomb.sh/tools` first.');
24+
const root = pathToFileURL(`${dirname(parentPkg)}/`);
25+
const source = new URL('../../skills/', import.meta.url);
26+
27+
if (!(await hfs.isDirectory(source))) {
28+
console.error('Could not locate bundled skills directory.');
2529
return;
2630
}
2731

@@ -40,12 +44,15 @@ interface SkillInfo {
4044
async function copySkills(options: { source: URL; dest: URL }): Promise<SkillInfo[]> {
4145
const { source, dest } = options;
4246
const skills: SkillInfo[] = [];
43-
const entries = await readdir(source, { withFileTypes: true });
44-
const keep = new Set<string>(
45-
entries.filter((e) => e.isDirectory() && !e.name.startsWith('_')).map((e) => e.name),
46-
);
4747

48-
await mkdir(dest, { recursive: true });
48+
const keep = new Set<string>();
49+
for await (const entry of hfs.list(source)) {
50+
if (entry.isDirectory && !entry.name.startsWith('_')) {
51+
keep.add(entry.name);
52+
}
53+
}
54+
55+
await hfs.createDirectory(dest);
4956
await pruneStaleLinks({ dest, source, keep });
5057

5158
const destDirPath = fileURLToPath(dest);
@@ -55,14 +62,12 @@ async function copySkills(options: { source: URL; dest: URL }): Promise<SkillInf
5562
const srcDir = new URL(`${name}/`, source);
5663
const destDir = new URL(`${name}/`, dest);
5764

58-
await rm(destDir, { recursive: true, force: true });
65+
await hfs.deleteAll(destDir);
5966

6067
const target = relative(destDirPath, fileURLToPath(srcDir));
6168
await symlink(target, fileURLToPath(destDir), linkType);
6269

63-
const skillMd = new URL('SKILL.md', srcDir);
64-
65-
const content = await safeRead(skillMd);
70+
const content = await hfs.text(new URL('SKILL.md', srcDir));
6671
if (content) {
6772
const frontmatter = parseFrontmatter(content);
6873
if (frontmatter) {
@@ -80,26 +85,21 @@ async function pruneStaleLinks(options: {
8085
keep: Set<string>;
8186
}): Promise<void> {
8287
const { dest, source, keep } = options;
88+
if (!(await hfs.isDirectory(dest))) return;
89+
8390
const destPath = fileURLToPath(dest);
8491
const sourcePath = fileURLToPath(source);
8592

86-
let entries: Dirent[];
87-
try {
88-
entries = await readdir(dest, { withFileTypes: true });
89-
} catch {
90-
return;
91-
}
92-
93-
for (const entry of entries) {
94-
if (!entry.isSymbolicLink()) continue;
93+
for await (const entry of hfs.list(dest)) {
94+
if (!entry.isSymlink) continue;
9595
if (keep.has(entry.name)) continue;
9696

9797
const linkPath = fileURLToPath(new URL(entry.name, dest));
9898
try {
9999
const target = await readlink(linkPath);
100100
const absTarget = isAbsolute(target) ? target : resolve(destPath, target);
101101
if (absTarget.startsWith(sourcePath)) {
102-
await rm(linkPath, { recursive: true, force: true });
102+
await hfs.deleteAll(linkPath);
103103
}
104104
} catch {
105105
// ignore unreadable links
@@ -110,7 +110,7 @@ async function pruneStaleLinks(options: {
110110
async function updateGitignore(options: { root: URL; skills: SkillInfo[] }): Promise<void> {
111111
const { root, skills } = options;
112112
const gitignorePath = new URL('.gitignore', root);
113-
let content = (await safeRead(gitignorePath)) ?? '';
113+
let content = (await hfs.text(gitignorePath)) ?? '';
114114

115115
const lines = skills.map((s) => `skills/${s.name}/`);
116116
const section = [GITIGNORE_START, ...lines, GITIGNORE_END].join('\n');
@@ -125,13 +125,13 @@ async function updateGitignore(options: { root: URL; skills: SkillInfo[] }): Pro
125125
content = content + suffix + '\n' + section + '\n';
126126
}
127127

128-
await writeFile(gitignorePath, content, 'utf8');
128+
await hfs.write(gitignorePath, content);
129129
}
130130

131131
async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise<void> {
132132
const { root, skills } = options;
133133
const agentsPath = new URL('AGENTS.md', root);
134-
let content = (await safeRead(agentsPath)) ?? '';
134+
let content = (await hfs.text(agentsPath)) ?? '';
135135

136136
const lines = skills.map((s) => {
137137
const desc = s.description.split('.')[0]?.trim();
@@ -158,7 +158,7 @@ async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Prom
158158
content = content + suffix + '\n' + section + '\n';
159159
}
160160

161-
await writeFile(agentsPath, content, 'utf8');
161+
await hfs.write(agentsPath, content);
162162
}
163163

164164
function parseFrontmatter(content: string): SkillInfo | undefined {
@@ -171,18 +171,16 @@ function parseFrontmatter(content: string): SkillInfo | undefined {
171171
return { name, description };
172172
}
173173

174-
async function isSelf(root: URL): Promise<boolean> {
175-
const content = await safeRead(new URL('package.json', root));
176-
if (!content) return false;
177-
const pkg = JSON.parse(content) as { name?: string };
178-
return pkg.name === '@bomb.sh/tools';
179-
}
174+
function findParentPackage(): string | null {
175+
const ownPkg = findPackageJSON(import.meta.url);
176+
if (!ownPkg) return null;
180177

181-
async function safeRead(url: URL): Promise<string | null> {
182-
try {
183-
return await readFile(url, 'utf8');
184-
} catch (err) {
185-
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') return null;
186-
throw err;
178+
let cursor = dirname(dirname(ownPkg));
179+
while (cursor !== dirname(cursor)) {
180+
const candidate = findPackageJSON(pathToFileURL(`${cursor}/`));
181+
if (!candidate) return null;
182+
if (candidate !== ownPkg) return candidate;
183+
cursor = dirname(dirname(candidate));
187184
}
185+
return null;
188186
}

0 commit comments

Comments
 (0)