Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24]
# 22.12.0 verifies package.json's stated `engines.node` floor; 24 is
# the version this repo is otherwise developed/tested against (see
# mise.toml). Keep both — the floor is meaningless if only the newer
# version is ever tested.
node-version: ["22.12.0", 24]
steps:
- uses: actions/checkout@v7

Expand All @@ -23,11 +27,20 @@ jobs:
- name: Install dependencies
run: npm ci

# Installs the real `claude` CLI so `claude plugin validate --strict`
# (the strongest available conformance oracle for the claude target,
# per CONFORMANCE.md) actually runs in CI instead of silently skipping.
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code

- name: Check
run: npm run check

- name: Audit production dependencies
run: npm run audit

- name: Verify package contents
run: npm pack --dry-run
run: node scripts/verify-pack-contents.mjs

- name: Verify packed install runs
run: node scripts/verify-packed-install.mjs
22 changes: 22 additions & 0 deletions .github/workflows/pr-label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: PR label

on:
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled]

jobs:
labels:
runs-on: ubuntu-latest
steps:
- name: Require a lerna-changelog label
uses: actions/github-script@v8
with:
script: |
const required = ["breaking", "enhancement", "bug", "documentation", "internal"];
const labels = context.payload.pull_request.labels.map((label) => label.name);
const matched = labels.filter((label) => required.includes(label));
if (matched.length === 0) {
core.setFailed(
`This PR needs one of the following labels for the changelog: ${required.join(", ")}.`,
);
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ examples/glean/dist/
examples/basic/dist/
coverage/
.DS_Store
EVAL-CHECKLIST.md
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ manager or publisher.
- After changing CLI commands/options, regenerate the README CLI reference with
`node dist/cli.js docs` (the gate's `docs --check` fails if it is stale).

Node >= 24, ESM, `moduleResolution: nodenext`, `strict: true`.
Node >= 22.12.0 (the `engines` floor in `package.json`, verified in CI against
that exact version), ESM, `moduleResolution: nodenext`, `strict: true`. Local
dev tooling (`mise.toml`) pins Node 24, but that's a dev-environment choice,
not the supported floor.

## Architecture

Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pluginpack

[![Experimental](https://img.shields.io/badge/-Experimental-D8FD49?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMzIgMzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNC4zMDA2IDIuOTU0MjdMMjAuNzY1NiAwLjE5OTk1MUwxNy45MDI4IDMuOTk1MjdDMTMuNTY1MyAxLjkzNDk1IDguMjMwMTkgMy4wODQzOSA1LjE5Mzk0IDcuMDA5ODNDMS42NTg4OCAxMS41NjQyIDIuNDgzIDE4LjExMzggNy4wMzczOCAyMS42NDg5QzguNzcyMzggMjIuOTkzNSAxMC43ODkzIDIzLjcwOTIgMTIuODI3OSAyMy44MTc3QzE2LjE0NjEgMjQuMDEyOCAxOS41MDc3IDIyLjYyNDggMjEuNjc2NSAxOS44MDU1QzI0LjczNDQgMTUuODggMjQuNTE3NSAxMC40MTQ4IDIxLjQ1OTYgNi43Mjc4OUwyNC4zMDA2IDIuOTU0MjdaTTE4LjExOTcgMTcuMDUxMkMxNi4xMDI4IDE5LjYzMiAxMi4zNzI1IDIwLjEwOTEgOS43NzAwMSAxOC4wOTIyQzcuMTg5MTkgMTYuMDc1MiA2LjcxMjA3IDEyLjMyMzMgOC43MjkwMSA5Ljc0MjQ2QzkuNzA0OTQgOC40ODQ1OCAxMS4xMTQ2IDcuNjgyMTQgMTIuNjc2MSA3LjQ4Njk2QzEzLjA0NDggNy40NDM1OCAxMy40MTM1IDcuNDIxOSAxMy43ODIyIDcuNDQzNThDMTQuOTc1IDcuNTA4NjUgMTYuMTI0NCA3Ljk0MjM5IDE3LjA3ODcgOC42Nzk3N0MxOS42NTk1IDEwLjcxODQgMjAuMTM2NiAxNC40NzAzIDE4LjExOTcgMTcuMDUxMloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yNC41MTc2IDIxLjY5MjJDMjMuOTMyIDIyLjQ1MTMgMjMuMjgxNCAyMy4xMjM2IDIyLjU2NTcgMjMuNzUyNUMyMS44NzE3IDI0LjMzODEgMjEuMTEyNyAyNC44ODAzIDIwLjMxMDIgMjUuMzM1N0MxOS41Mjk1IDI1Ljc2OTUgMTguNjgzNyAyNi4xMzgyIDE3LjgzNzggMjYuNDIwMUMxNi45OTIgMjYuNzAyIDE2LjEwMjggMjYuODk3MiAxNS4yMTM3IDI3LjAwNTdDMTQuMzI0NSAyNy4xMTQxIDEzLjQzNTMgMjcuMTU3NSAxMi41MjQ0IDI3LjA5MjRDMTEuNjEzNSAyNy4wMjczIDEwLjcyNDMgMjYuODc1NSA5Ljg1Njg0IDI2LjY1ODdMOS42NjE2NSAyNy4zNzQzTDguNzcyNDYgMzAuOTk2MkM5LjkwMDIxIDMxLjI5OTggMTEuMDQ5NyAzMS40NzMzIDEyLjIyMDggMzEuNTZDMTIuMjY0MiAzMS41NiAxMi4zMjkyIDMxLjU2IDEyLjM3MjYgMzEuNTZDMTMuNTAwMyAzMS42MjUxIDE0LjY0OTggMzEuNTgxNyAxNS43NTU4IDMxLjQ1MTZDMTYuOTI3IDMxLjI5OTggMTguMDk4MSAzMS4wMzk1IDE5LjIyNTggMzAuNjcwOEMyMC4zNTM2IDMwLjMwMjIgMjEuNDU5NyAyOS44MjUgMjIuNTAwNyAyOS4yMzk1QzIzLjU2MzQgMjguNjUzOSAyNC41NjEgMjcuOTM4MiAyNS40OTM1IDI3LjE1NzVDMjYuNDQ3OCAyNi4zNTUgMjcuMzE1MyAyNS40NDQyIDI4LjA3NDQgMjQuNDQ2NUMyOC4xODI4IDI0LjMxNjQgMjguMjY5NSAyNC4xNjQ2IDI4LjM3OCAyNC4wMTI4TDI0Ljc3NzkgMjEuMzQ1MkMyNC42Njk0IDIxLjQ1MzcgMjQuNjA0NCAyMS41ODM4IDI0LjUxNzYgMjEuNjkyMloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg==&labelColor=343CED)](https://github.com/gleanwork/.github/blob/main/docs/repository-stability.md#experimental)
[![Prerelease](https://img.shields.io/badge/-Prerelease-F6F3EB?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMzIgMzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yNC4zMDA2IDIuOTU0MjdMMjAuNzY1NiAwLjE5OTk1MUwxNy45MDI4IDMuOTk1MjdDMTMuNTY1MyAxLjkzNDk1IDguMjMwMTkgMy4wODQzOSA1LjE5Mzk0IDcuMDA5ODNDMS42NTg4OCAxMS41NjQyIDIuNDgzIDE4LjExMzggNy4wMzczOCAyMS42NDg5QzguNzcyMzggMjIuOTkzNSAxMC43ODkzIDIzLjcwOTIgMTIuODI3OSAyMy44MTc3QzE2LjE0NjEgMjQuMDEyOCAxOS41MDc3IDIyLjYyNDggMjEuNjc2NSAxOS44MDU1QzI0LjczNDQgMTUuODggMjQuNTE3NSAxMC40MTQ4IDIxLjQ1OTYgNi43Mjc4OUwyNC4zMDA2IDIuOTU0MjdaTTE4LjExOTcgMTcuMDUxMkMxNi4xMDI4IDE5LjYzMiAxMi4zNzI1IDIwLjEwOTEgOS43NzAwMSAxOC4wOTIyQzcuMTg5MTkgMTYuMDc1MiA2LjcxMjA3IDEyLjMyMzMgOC43MjkwMSA5Ljc0MjQ2QzkuNzA0OTQgOC40ODQ1OCAxMS4xMTQ2IDcuNjgyMTQgMTIuNjc2MSA3LjQ4Njk2QzEzLjA0NDggNy40NDM1OCAxMy40MTM1IDcuNDIxOSAxMy43ODIyIDcuNDQzNThDMTQuOTc1IDcuNTA4NjUgMTYuMTI0NCA3Ljk0MjM5IDE3LjA3ODcgOC42Nzk3N0MxOS42NTk1IDEwLjcxODQgMjAuMTM2NiAxNC40NzAzIDE4LjExOTcgMTcuMDUxMloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yNC41MTc2IDIxLjY5MjJDMjMuOTMyIDIyLjQ1MTMgMjMuMjgxNCAyMy4xMjM2IDIyLjU2NTcgMjMuNzUyNUMyMS44NzE3IDI0LjMzODEgMjEuMTEyNyAyNC44ODAzIDIwLjMxMDIgMjUuMzM1N0MxOS41Mjk1IDI1Ljc2OTUgMTguNjgzNyAyNi4xMzgyIDE3LjgzNzggMjYuNDIwMUMxNi45OTIgMjYuNzAyIDE2LjEwMjggMjYuODk3MiAxNS4yMTM3IDI3LjAwNTdDMTQuMzI0NSAyNy4xMTQxIDEzLjQzNTMgMjcuMTU3NSAxMi41MjQ0IDI3LjA5MjRDMTEuNjEzNSAyNy4wMjczIDEwLjcyNDMgMjYuODc1NSA5Ljg1Njg0IDI2LjY1ODdMOS42NjE2NSAyNy4zNzQzTDguNzcyNDYgMzAuOTk2MkM5LjkwMDIxIDMxLjI5OTggMTEuMDQ5NyAzMS40NzMzIDEyLjIyMDggMzEuNTZDMTIuMjY0MiAzMS41NiAxMi4zMjkyIDMxLjU2IDEyLjM3MjYgMzEuNTZDMTMuNTAwMyAzMS42MjUxIDE0LjY0OTggMzEuNTgxNyAxNS43NTU4IDMxLjQ1MTZDMTYuOTI3IDMxLjI5OTggMTguMDk4MSAzMS4wMzk1IDE5LjIyNTggMzAuNjcwOEMyMC4zNTM2IDMwLjMwMjIgMjEuNDU5NyAyOS44MjUgMjIuNTAwNyAyOS4yMzk1QzIzLjU2MzQgMjguNjUzOSAyNC41NjEgMjcuOTM4MiAyNS40OTM1IDI3LjE1NzVDMjYuNDQ3OCAyNi4zNTUgMjcuMzE1MyAyNS40NDQyIDI4LjA3NDQgMjQuNDQ2NUMyOC4xODI4IDI0LjMxNjQgMjguMjY5NSAyNC4xNjQ2IDI4LjM3OCAyNC4wMTI4TDI0Ljc3NzkgMjEuMzQ1MkMyNC42Njk0IDIxLjQ1MzcgMjQuNjA0NCAyMS41ODM4IDI0LjUxNzYgMjEuNjkyMloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg==&labelColor=343CED)](https://github.com/gleanwork/.github/blob/main/docs/repository-stability.md#prerelease)
[![npm version](https://img.shields.io/npm/v/@gleanwork/pluginpack.svg)](https://www.npmjs.com/package/@gleanwork/pluginpack)
[![CI](https://github.com/gleanwork/pluginpack/actions/workflows/ci.yml/badge.svg)](https://github.com/gleanwork/pluginpack/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Expand Down Expand Up @@ -89,6 +89,12 @@ export default defineConfig({
acme: { from: ["core"] },
},
},
codex: {
outDir: "plugins/codex",
plugins: {
acme: { from: ["core"] },
},
},
},
});
```
Expand All @@ -100,7 +106,7 @@ npx pluginpack build
npx pluginpack validate --target cursor
```

Users who only want portable skills install from the `skills/` subpath, for example `npx skills add owner/repo/skills --skill '*'`. Claude, Cursor, Antigravity, and Copilot users install from the generated native layout that can include skills, agents, rules, hooks, assets, MCP config, and target-specific manifests.
Users who only want portable skills install from the `skills/` subpath, for example `npx skills add owner/repo/skills --skill '*'`. Claude, Cursor, Antigravity, Copilot, and Codex users install from the generated native layout that can include skills, agents, rules, hooks, assets, MCP config, and target-specific manifests.

## Mental Model

Expand Down Expand Up @@ -491,6 +497,7 @@ import {
prune,
clean,
buildInstallSnippet,
getInstallSnippetCitation,
getSupportedInstallTargets,
getUnsupportedInstallTargets,
} from "@gleanwork/pluginpack";
Expand All @@ -506,6 +513,7 @@ import {
| `prune(options?)` | `Promise<CleanupResult[]>` | Remove stale managed files no longer emitted by the config. |
| `clean(options?)` | `Promise<CleanupResult[]>` | Remove all managed files for configured targets. |
| `buildInstallSnippet(target, params)` | `InstallSnippet` | The install command/URL for one target (see **Install Snippet**). |
| `getInstallSnippetCitation(target)` | `Citation` | The documentation source backing that target's install snippet. |
| `getSupportedInstallTargets()` | `TargetName[]` | Targets with a real install snippet today. |
| `getUnsupportedInstallTargets()` | `TargetName[]` | Targets with none (empty today, kept for forward-compatibility). |

Expand All @@ -515,7 +523,13 @@ Option objects:
- `diffTarget` — `{ cwd?, configPath?, target, against }`
- `prune` / `clean` — `{ cwd?, configPath?, target?, dryRun?, force? }`

The result and config types (`Artifact`, `DiffResult`/`DiffEntry`, `ValidationResult`/`ValidationIssue`, `CleanupResult`/`CleanupEntry`, `ResolvedProject`, `PluginpackConfig`, `TargetConfig`, `TargetName`, …) are all exported for use in TypeScript.
The result and config types (`Artifact`, `DiffResult`/`DiffEntry`, `ValidationResult`/`ValidationIssue`, `CleanupResult`/`CleanupEntry`, `ResolvedProject`, `PluginpackConfig`, `TargetConfig`, `TargetName`, `Citation`, `InstallSnippet`, …) are all exported for use in TypeScript.

A few things worth knowing about this surface before depending on it:

- **The target set is closed.** `TargetName` is `"claude" | "cursor" | "antigravity" | "copilot" | "codex"` today, with no public API for registering a sixth target — adding one means a PR to this repo (see `PluginTargetDefinition` in `src/targets/types.ts`, not exported). There is no supported third-party target-extension mechanism.
- **The package is ESM-only.** `package.json`'s `exports` map has no `require` condition; a CommonJS consumer needs dynamic `import()`. This is a deliberate choice, not a tsup default left unexamined.
- **`Artifact.files` and `ResolvedProject.plugins` are `Map`s, not plain objects.** `JSON.stringify()` on either silently produces `{}` — iterate with `for...of`/`Object.fromEntries()` instead of serializing directly if you need to log or transport a result.

<!-- pluginpack-cli:start -->

Expand Down
32 changes: 32 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,35 @@ release process. It will prompt you to to choose the version number after which
you will have the chance to hand tweak the changelog to be used (for the
`CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging,
pushing the tag and commits, etc.

## What counts as a breaking change

pluginpack's public contract isn't just its TypeScript API (`src/index.ts`)
and CLI flags — it's also the _shape of the files it writes into a consumer's
repo_ (managed-file layout, manifest field names, default directory
conventions). A change that alters generated output for an existing config
with no code-level signature change (e.g. a different default `outDir`
convention, a renamed manifest field, a different `.pluginpack/<target>.json`
shape) is breaking for the same reason a database migration is breaking: it
can affect a consumer's already-generated, already-committed repo on their
next build, even though nothing in `import { ... } from "@gleanwork/pluginpack"`
changed. Label PRs with `breaking` under that broader definition, not just for
API/CLI signature changes.

## PR label enforcement

CI (`.github/workflows/ci.yml`, `labels` job) requires every PR to carry one
of the five labels above before merge — this catches an unlabeled PR, but it
cannot judge whether the label chosen is the _correct_ one. Whether a given
diff is actually breaking under the definition above is still a human call at
review time.

## Accepted risk: dev-only audit findings

`npm audit` (unscoped) reports vulnerabilities in `@release-it-plugins/lerna-changelog`'s
dependency chain (`lerna-changelog` → `make-fetch-happen` → `cacache`/`tar`),
with no upstream fix available. `npm run audit` (what CI runs) is scoped to
`--omit=dev` and reports clean, per `CLAUDE.md`'s documented rationale:
devDependencies never ship in the published package. This is a tracked,
accepted gap, not an oversight — revisit if `lerna-changelog` is ever run
against untrusted input, or replace it if a maintained alternative appears.
11 changes: 11 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,15 @@ export default tseslint.config(
"@typescript-eslint/no-explicit-any": "off",
},
},
{
// Plain Node scripts run outside the TS/`@types/node` toolchain, so
// js.configs.recommended's no-undef doesn't otherwise know these globals.
files: ["scripts/**/*.mjs"],
languageOptions: {
globals: {
console: "readonly",
process: "readonly",
},
},
},
);
Loading