diff --git a/.changeset/sync-skill-descriptions.md b/.changeset/sync-skill-descriptions.md new file mode 100644 index 0000000..cbfa5b2 --- /dev/null +++ b/.changeset/sync-skill-descriptions.md @@ -0,0 +1,5 @@ +--- +'@bomb.sh/tools': patch +--- + +Fixes `bsh sync` writing garbled or truncated skill descriptions to `AGENTS.md` diff --git a/package.json b/package.json index f129fc2..250938d 100644 --- a/package.json +++ b/package.json @@ -70,9 +70,9 @@ "publint": "^0.3.18", "tinyexec": "^1.1.1", "tsdown": "^0.22.3", - "ultramatter": "^0.0.4", "vitest": "^4.1.2", - "vitest-ansi-serializer": "^0.2.1" + "vitest-ansi-serializer": "^0.2.1", + "yaml": "^2.9.1" }, "devDependencies": { "@changesets/cli": "^2.30.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32d0df1..1c9fd6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,15 +38,15 @@ importers: tsdown: specifier: ^0.22.3 version: 0.22.3(@typescript/native-preview@7.0.0-dev.20260623.1)(oxc-resolver@11.19.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1))(publint@0.3.18)(typescript@5.9.3)(unrun@0.2.37) - ultramatter: - specifier: ^0.0.4 - version: 0.0.4 vitest: specifier: ^4.1.2 - version: 4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3)) + version: 4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1)) vitest-ansi-serializer: specifier: ^0.2.1 - version: 0.2.1(vitest@4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3))) + version: 0.2.1(vitest@4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1))) + yaml: + specifier: ^2.9.1 + version: 2.9.1 devDependencies: '@changesets/cli': specifier: ^2.30.0 @@ -1896,9 +1896,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ultramatter@0.0.4: - resolution: {integrity: sha512-1f/hO3mR+/Hgue4eInOF/Qm/wzDqwhYha4DxM0hre9YIUyso3fE2XtrAU6B4njLqTC8CM49EZaYgsVSa+dXHGw==} - unbash@3.0.0: resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} engines: {node: '>=14'} @@ -2017,8 +2014,8 @@ packages: engines: {node: '>=8'} hasBin: true - yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} engines: {node: '>= 14.6'} hasBin: true @@ -2368,6 +2365,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -2719,7 +2723,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@rolldown/binding-wasm32-wasi@1.1.2': @@ -2890,13 +2894,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.2(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3))': + '@vitest/mocker@4.1.2(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1))': dependencies: '@vitest/spy': 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1) '@vitest/pretty-format@4.1.2': dependencies: @@ -3158,7 +3162,7 @@ snapshots: strip-json-comments: 5.0.3 tinyglobby: 0.2.16 unbash: 3.0.0 - yaml: 2.8.3 + yaml: 2.9.1 zod: 4.3.6 transitivePeerDependencies: - '@emnapi/core' @@ -3563,8 +3567,6 @@ snapshots: typescript@5.9.3: optional: true - ultramatter@0.0.4: {} - unbash@3.0.0: {} unconfig-core@7.5.0: @@ -3581,28 +3583,28 @@ snapshots: rolldown: 1.0.0-rc.17 optional: true - vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3): + vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.8 rollup: 4.60.1 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 22.19.15 fsevents: 2.3.3 jiti: 2.6.1 - yaml: 2.8.3 + yaml: 2.9.1 - vitest-ansi-serializer@0.2.1(vitest@4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3))): + vitest-ansi-serializer@0.2.1(vitest@4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1))): dependencies: - vitest: 4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3)) + vitest: 4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1)) - vitest@4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3)): + vitest@4.1.2(@types/node@22.19.15)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1)): dependencies: '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3)) + '@vitest/mocker': 4.1.2(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1)) '@vitest/pretty-format': 4.1.2 '@vitest/runner': 4.1.2 '@vitest/snapshot': 4.1.2 @@ -3619,7 +3621,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(yaml@2.9.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.15 @@ -3637,6 +3639,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - yaml@2.8.3: {} + yaml@2.9.1: {} zod@4.3.6: {} diff --git a/src/commands/sync.test.ts b/src/commands/sync.test.ts index 130eb42..c233642 100644 --- a/src/commands/sync.test.ts +++ b/src/commands/sync.test.ts @@ -2,7 +2,7 @@ import { lstat, readlink } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; import { createFixture, createMocks } from '../test-utils/index.ts'; -import { copySkills, findParentPackage } from './sync.ts'; +import { copySkills, findParentPackage, updateAgentsMd } from './sync.ts'; describe('copySkills', () => { it('symlinks each skill into the destination', async () => { @@ -53,6 +53,55 @@ describe('copySkills', () => { expect(await fixture.text('source-skills/build/SKILL.md')).toContain('name: build'); expect(await fixture.text('project/skills/build/SKILL.md')).toContain('name: build'); }); + + it('reads block scalar descriptions without the indicator', async () => { + const fixture = await createFixture({ + 'source-skills': { + test: { + 'SKILL.md': + '---\nname: test\ndescription: >\n Vitest test runner with colocated .test.ts files.\n Use when writing tests.\nmetadata:\n type: core\n---\nbody', + }, + lint: { + 'SKILL.md': '---\nname: lint\ndescription: |-\n Lint the project.\n---\nbody', + }, + }, + }); + + const skills = await copySkills({ + source: new URL('source-skills/', fixture.root), + dest: new URL('project/skills/', fixture.root), + }); + + expect(skills).toEqual( + expect.arrayContaining([ + { + name: 'test', + description: 'Vitest test runner with colocated .test.ts files. Use when writing tests.', + }, + { name: 'lint', description: 'Lint the project.' }, + ]), + ); + }); +}); + +describe('updateAgentsMd', () => { + it('summarizes each skill with its first full sentence', async () => { + const fixture = await createFixture({ 'AGENTS.md': '# Project\n' }); + + await updateAgentsMd({ + root: fixture.root, + skills: [ + { + name: 'test', + description: 'Vitest test runner with colocated .test.ts files. Use when writing tests.', + }, + ], + }); + + expect(await fixture.text('AGENTS.md')).toContain( + '- **test** — [skills/test/SKILL.md](skills/test/SKILL.md) - Vitest test runner with colocated .test.ts files\n', + ); + }); }); describe('findParentPackage', () => { diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 67c1504..f641a74 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -3,7 +3,7 @@ import { findPackageJSON } from 'node:module'; import { cwd, env, platform } from 'node:process'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { NodeHfs } from '@humanfs/node'; -import { parse } from 'ultramatter'; +import { parse } from 'yaml'; import type { CommandContext } from '../context.ts'; import { relativeUrlPath, resolveLinkTarget } from '../utils.ts'; @@ -126,13 +126,13 @@ async function updateGitignore(options: { root: URL; skills: SkillInfo[] }): Pro await hfs.write(gitignorePath, content); } -async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise { +export async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Promise { const { root, skills } = options; const agentsPath = new URL('AGENTS.md', root); let content = (await hfs.text(agentsPath)) ?? ''; const lines = skills.map((s) => { - const desc = s.description.split('.')[0]?.trim(); + const desc = s.description.split(/\.(?:\s|$)/)[0]?.trim(); return `- **${s.name}** — [skills/${s.name}/SKILL.md](skills/${s.name}/SKILL.md)${desc ? ` - ${desc}` : ''}`; }); @@ -160,7 +160,9 @@ async function updateAgentsMd(options: { root: URL; skills: SkillInfo[] }): Prom } function parseFrontmatter(content: string): SkillInfo | undefined { - const { frontmatter } = parse(content); + const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content); + if (!match) return undefined; + const frontmatter = parse(match[1]!) as Record | null; if (!frontmatter) return undefined; const name = frontmatter.name as string | undefined; const description =