diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index e8737eb..0046067 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -32,7 +32,6 @@ jobs: - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3 id: changes with: - predicate-quantifier: 'every' filters: | changed: - "packages/**" diff --git a/packages/pnpm-plugin-skills/README.md b/packages/pnpm-plugin-skills/README.md index 3a30e25..54e9626 100644 --- a/packages/pnpm-plugin-skills/README.md +++ b/packages/pnpm-plugin-skills/README.md @@ -26,12 +26,17 @@ Then create a `skills.json` in your project root: { "installDir": ".agents/skills", "linkTargets": [".claude/skills"], + "pnpmPlugin": { + "removePnpmfileChecksum": true + }, "skills": { "my-skill": "https://github.com/owner/repo.git#path:/skills/my-skill" } } ``` +`pnpmPlugin.removePnpmfileChecksum` is a temporary compatibility switch for repositories that need `pnpm-plugin-skills` to remove `pnpmfileChecksum` from `pnpm-lock.yaml` in `afterAllResolved`. + Now `pnpm install` will automatically install your skills. ## Architecture diff --git a/packages/pnpm-plugin-skills/pnpmfile.cjs b/packages/pnpm-plugin-skills/pnpmfile.cjs index 1e90c88..2c74ba3 100644 --- a/packages/pnpm-plugin-skills/pnpmfile.cjs +++ b/packages/pnpm-plugin-skills/pnpmfile.cjs @@ -1,8 +1,9 @@ // pnpm v10 -const { preResolution } = require('./dist/index.js') +const { afterAllResolved, preResolution } = require('./dist/index.js') module.exports = { hooks: { + afterAllResolved, preResolution, }, } diff --git a/packages/pnpm-plugin-skills/pnpmfile.mjs b/packages/pnpm-plugin-skills/pnpmfile.mjs index a61d3f6..7c67bac 100644 --- a/packages/pnpm-plugin-skills/pnpmfile.mjs +++ b/packages/pnpm-plugin-skills/pnpmfile.mjs @@ -1,4 +1,4 @@ // pnpm v11 -import { preResolution } from './dist/index.mjs' +import { afterAllResolved, preResolution } from './dist/index.mjs' -export const hooks = { preResolution } +export const hooks = { afterAllResolved, preResolution } diff --git a/packages/pnpm-plugin-skills/src/index.ts b/packages/pnpm-plugin-skills/src/index.ts index 5ac06b9..5de5316 100644 --- a/packages/pnpm-plugin-skills/src/index.ts +++ b/packages/pnpm-plugin-skills/src/index.ts @@ -1,4 +1,19 @@ -import { installCommand } from 'skills-package-manager' +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { installCommand, type SkillsManifest } from 'skills-package-manager' + +function readPluginManifest(rootDir: string): SkillsManifest | null { + const filePath = path.join(rootDir, 'skills.json') + + try { + const parsed = JSON.parse(readFileSync(filePath, 'utf8')) as { + pnpmPlugin?: SkillsManifest['pnpmPlugin'] + } + return parsed as SkillsManifest + } catch { + return null + } +} export async function preResolution( options: { lockfileDir?: string; workspaceRoot?: string } = {}, @@ -11,3 +26,28 @@ export async function preResolution( await installCommand({ cwd: lockfileDir }) return undefined } + +export function afterAllResolved( + lockfile: Record, + context: { lockfileDir?: string; workspaceDir?: string } = {}, +) { + const manifestRoot = context.lockfileDir ?? context.workspaceDir + if (!manifestRoot) { + return lockfile + } + + const manifest = readPluginManifest(manifestRoot) + if (!manifest) { + return lockfile + } + + if (manifest.pnpmPlugin?.removePnpmfileChecksum !== true) { + return lockfile + } + + if ('pnpmfileChecksum' in lockfile) { + delete lockfile.pnpmfileChecksum + } + + return lockfile +} diff --git a/packages/pnpm-plugin-skills/test/index.test.ts b/packages/pnpm-plugin-skills/test/index.test.ts index ebc822b..f59076b 100644 --- a/packages/pnpm-plugin-skills/test/index.test.ts +++ b/packages/pnpm-plugin-skills/test/index.test.ts @@ -65,3 +65,50 @@ describe('preResolution', () => { expect(existsSync(path.join(root, '.claude/skills/hello-skill'))).toBe(true) }) }) + +describe('afterAllResolved', () => { + it('removes pnpmfileChecksum when enabled in skills.json', async () => { + const { afterAllResolved } = await import('../src/index') + const root = mkdtempSync(path.join(tmpdir(), 'pnpm-plugin-skills-config-')) + + writeFileSync( + path.join(root, 'skills.json'), + JSON.stringify( + { + installDir: '.agents/skills', + linkTargets: [], + pnpmPlugin: { + removePnpmfileChecksum: true, + }, + skills: {}, + }, + null, + 2, + ), + ) + + const lockfile = { + lockfileVersion: '9.0', + pnpmfileChecksum: 'checksum-to-remove', + } + + const result = afterAllResolved(lockfile, { lockfileDir: root }) + + expect(result).toBe(lockfile) + expect(result).not.toHaveProperty('pnpmfileChecksum') + }) + + it('keeps pnpmfileChecksum by default', async () => { + const { afterAllResolved } = await import('../src/index') + + const lockfile = { + lockfileVersion: '9.0', + pnpmfileChecksum: 'checksum-to-keep', + } + + const result = afterAllResolved(lockfile, {}) + + expect(result).toBe(lockfile) + expect(result).toHaveProperty('pnpmfileChecksum', 'checksum-to-keep') + }) +}) diff --git a/packages/skills-package-manager/src/config/schema.ts b/packages/skills-package-manager/src/config/schema.ts index 76fbfca..c60a27f 100644 --- a/packages/skills-package-manager/src/config/schema.ts +++ b/packages/skills-package-manager/src/config/schema.ts @@ -16,6 +16,17 @@ export const skillsManifestSchema = z .default([]) .describe('Directories where skill symlinks will be created'), selfSkill: z.boolean().optional().describe('Whether this project is itself a skill'), + pnpmPlugin: z + .object({ + removePnpmfileChecksum: z + .boolean() + .optional() + .describe( + 'Temporarily remove pnpmfileChecksum from pnpm lockfiles in pnpm-plugin-skills afterAllResolved', + ), + }) + .optional() + .describe('pnpm-plugin-skills specific compatibility settings'), skills: z .record(z.string(), z.string()) .default({}) diff --git a/packages/skills-package-manager/src/config/skillsManifest.ts b/packages/skills-package-manager/src/config/skillsManifest.ts index 09616ac..3b1494f 100644 --- a/packages/skills-package-manager/src/config/skillsManifest.ts +++ b/packages/skills-package-manager/src/config/skillsManifest.ts @@ -49,6 +49,7 @@ export function normalizeSkillsManifest(manifest: Partial): Skil installDir: manifest.installDir ?? '.agents/skills', linkTargets: manifest.linkTargets ?? [], selfSkill: manifest.selfSkill ?? false, + pnpmPlugin: manifest.pnpmPlugin, skills: manifest.skills ?? {}, } } diff --git a/packages/skills-package-manager/src/config/writeSkillsManifest.ts b/packages/skills-package-manager/src/config/writeSkillsManifest.ts index 04934bd..d5ed14f 100644 --- a/packages/skills-package-manager/src/config/writeSkillsManifest.ts +++ b/packages/skills-package-manager/src/config/writeSkillsManifest.ts @@ -24,6 +24,10 @@ export async function writeSkillsManifest( nextManifest.selfSkill = manifest.selfSkill } + if (manifest.pnpmPlugin !== undefined) { + nextManifest.pnpmPlugin = manifest.pnpmPlugin + } + try { await writeFile(filePath, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8') } catch (error) { diff --git a/packages/skills-package-manager/test/manifest.test.ts b/packages/skills-package-manager/test/manifest.test.ts index d6266d7..633ce39 100644 --- a/packages/skills-package-manager/test/manifest.test.ts +++ b/packages/skills-package-manager/test/manifest.test.ts @@ -18,10 +18,36 @@ describe('manifest io', () => { $schema: DEFAULT_SCHEMA_URL, installDir: '.agents/skills', linkTargets: [], + pnpmPlugin: undefined, skills: { hello: 'link:./skills/hello' }, }) }) + it('reads pnpmPlugin config from skills.json', async () => { + const root = mkdtempSync(path.join(tmpdir(), 'skills-pm-manifest-pnpm-plugin-')) + writeFileSync( + path.join(root, 'skills.json'), + JSON.stringify( + { + installDir: '.agents/skills', + linkTargets: [], + pnpmPlugin: { + removePnpmfileChecksum: true, + }, + skills: {}, + }, + null, + 2, + ), + ) + + const manifest = await readSkillsManifest(root) + + expect(manifest?.pnpmPlugin).toEqual({ + removePnpmfileChecksum: true, + }) + }) + it('defaults selfSkill to undefined when omitted from skills.json', async () => { const root = mkdtempSync(path.join(tmpdir(), 'skills-pm-manifest-default-self-')) writeFileSync( @@ -104,6 +130,9 @@ describe('manifest validation', () => { installDir: '.custom/skills', linkTargets: ['.claude/skills'], selfSkill: true, + pnpmPlugin: { + removePnpmfileChecksum: true, + }, skills: { test: 'link:./test' }, } @@ -123,6 +152,7 @@ describe('manifest validation', () => { expect(result.data.installDir).toBe('.agents/skills') expect(result.data.linkTargets).toEqual([]) expect(result.data.selfSkill).toBeUndefined() + expect(result.data.pnpmPlugin).toBeUndefined() expect(result.data.skills).toEqual({}) } }) diff --git a/website/docs/architecture/pnpm-plugin.mdx b/website/docs/architecture/pnpm-plugin.mdx index cae9d46..ceea722 100644 --- a/website/docs/architecture/pnpm-plugin.mdx +++ b/website/docs/architecture/pnpm-plugin.mdx @@ -24,12 +24,17 @@ Then add the following to the workspace root: { "installDir": ".agents/skills", "linkTargets": [".claude/skills"], + "pnpmPlugin": { + "removePnpmfileChecksum": true + }, "skills": { "my-skill": "https://github.com/owner/repo.git#path:/skills/my-skill" } } ``` +`pnpmPlugin.removePnpmfileChecksum` is a temporary compatibility option for `pnpm-plugin-skills`. When enabled, the plugin removes `pnpmfileChecksum` from `pnpm-lock.yaml` in `afterAllResolved` to work around tools that run `pnpm install --ignore-pnpmfile`. + ## Suitable team scenarios - You want developers to run only `pnpm install` without remembering extra skill installation commands