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
33 changes: 22 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,35 @@ diff, prune, and validate all derive from it.
generated output is never misread as source), and the root-skills plugin.
- `src/render.ts` — `collectPluginFiles` (component dirs + static files, with
`targets/<name>/` override resolution) and `resolveMcpServers`.
- `src/targets.ts` — `emitTarget` + per-target emitters. `cursor`/`claude`/
`antigravity` share the `emitPlugins` engine via callbacks; `emitCopilot` is
bespoke (no per-plugin manifest, dual marketplace). Manifest builders live here
too.
- `src/targets/registry.ts` — `targets: Record<TargetName, PluginTargetDefinition>`,
one file per target (`src/targets/<name>.ts`). Everything that varies by
target — default components, manifest/marketplace builders, output paths,
validation, install snippet — lives on that target's own
`PluginTargetDefinition` (`src/targets/types.ts`).
- `src/targets/engine.ts` — `emitFromDefinition`/`validateFromDefinition`: the
one emit/validate engine every target runs through, driven by its
`PluginTargetDefinition`. Also `withRootFiles` (injects per-target
repo-root files into the artifact).
- `src/targets/validation-shared.ts` — validators shared across targets whose
shape actually matches (bare-string marketplace `source`, hooks.json shape,
frontmatter conventions); a target with a genuinely different shape (e.g.
Codex's structured `source`) writes its own instead of forcing a fit.
- `src/adapters.ts` — `emitTarget`/`validateOutput`/`targetNames`, thin
wrappers around the registry + engine.
- `src/build.ts` — `build()`: emit all targets → `assertNoCrossTargetCollisions`
→ write/prune/manifest. Holds the delete guard.
- `src/managed.ts` — the managed-file manifest (`.pluginpack/<target>.json`),
`prune`/`clean`, the delete guard, and path-safety checks.
- `src/diff.ts` — `diffTarget`: build to a temp dir and compare against an
existing target repo (the CI staleness gate).
- `src/validate.ts` — per-target output validation.

## Targets

`cursor`, `claude`, `antigravity`, `copilot`. Adding a target currently touches ~5
places: the `TargetName` union (`types.ts`), the `targets` array + `parseTarget`
(`cli.ts`), `allTargets` (`build.ts`), the `emitters` map + a new `emitFoo`
(`targets.ts`), and a branch + `validateFoo` (`validate.ts`). If you are adding a
target, consider introducing a single target registry first to localize this.
`copilot`, `antigravity`, `cursor`, `claude`, `codex`. Adding a target means one
new file implementing `PluginTargetDefinition` (`src/targets/<name>.ts`) plus one
new entry in `src/targets/registry.ts` — `TargetName` (`types.ts`) is still a
separate union to extend, but everything else (CLI `--target` choices, `build()`'s
target set, emit/validate dispatch) derives from the registry automatically.

## Conformance

Expand All @@ -80,6 +90,7 @@ schemas at runtime — vendor a pinned copy with recorded provenance.

## Conventions

- Strict TypeScript, no `any` (the one exception is `readJson` in `validate.ts`).
- Strict TypeScript, no `any` (the one exception is `readJson` in
`src/targets/validation-shared.ts`).
- Prettier + eslint enforced by the gate.
- Conventional commits. Keep the README CLI reference regenerated.
6 changes: 6 additions & 0 deletions CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ cursor manifest's `hooks` key validates against the vendored plugin schema, and
`claude plugin validate --strict` exercises the claude hooks file when the CLI
is present).

Every target's `validateOutput` shares one `validateHooksShape`
(`src/targets/validation-shared.ts`) for the parts of a hooks file that are
target-agnostic: each event's entries must be an array, no `command` string
may be empty, and any command referencing the generated update-check script
(`scripts/pluginpack-update-check.sh`) must ship that script.

## Refreshing vendored schemas

The Cursor schemas are pinned copies. To update them, re-fetch from the source
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ Exit codes:
Compile configured source plugins into target-native plugin payloads.

```bash
pluginpack build [--target cursor|claude|antigravity|copilot|codex] [--out-dir <path>] [--dry-run]
pluginpack build [--target copilot|antigravity|cursor|claude|codex] [--out-dir <path>] [--dry-run]
```

Options:
Expand All @@ -506,7 +506,7 @@ Exit codes:
Validate an existing target output directory for native manifest, path, and frontmatter requirements.

```bash
pluginpack validate --target cursor|claude|antigravity|copilot|codex [--dir <path>]
pluginpack validate --target copilot|antigravity|cursor|claude|codex [--dir <path>]
```

Options:
Expand All @@ -528,7 +528,7 @@ Exit codes:
Build into a temporary directory and compare generated managed files with an existing target repo.

```bash
pluginpack diff --target cursor|claude|antigravity|copilot|codex --against <path>
pluginpack diff --target copilot|antigravity|cursor|claude|codex --against <path>
```

Options:
Expand All @@ -550,7 +550,7 @@ Exit codes:
Remove stale managed files that are no longer emitted by the current config.

```bash
pluginpack prune [--target cursor|claude|antigravity|copilot|codex] [--dry-run]
pluginpack prune [--target copilot|antigravity|cursor|claude|codex] [--dry-run]
```

Options:
Expand All @@ -574,7 +574,7 @@ Exit codes:
Remove all managed files for configured target outputs.

```bash
pluginpack clean [--target cursor|claude|antigravity|copilot|codex] [--dry-run]
pluginpack clean [--target copilot|antigravity|cursor|claude|codex] [--dry-run]
```

Options:
Expand Down
88 changes: 6 additions & 82 deletions src/adapters.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,20 @@
import path from "node:path";
import {
emitAntigravity,
emitClaude,
emitCodex,
emitCopilot,
emitCursor,
withRootFiles,
} from "./targets.js";
import {
validateAntigravity,
validateClaude,
validateCodex,
validateCopilot,
validateCursor,
} from "./validate.js";
import {
emitFromDefinition,
validateFromDefinition,
withRootFiles,
} from "./targets/engine.js";
import { targets as registry } from "./targets/registry.js";
import type {
Artifact,
ResolvedProject,
TargetConfig,
TargetName,
ValidationIssue,
ValidationResult,
} from "./types.js";

type TargetEmitter = (
project: ResolvedProject,
target: TargetName,
targetConfig: TargetConfig,
outDir: string,
) => Promise<Artifact>;

type TargetValidator = (
root: string,
issues: ValidationIssue[],
) => Promise<void>;

/** The emit and validate functions for one target. */
export type TargetAdapter = {
emit: TargetEmitter;
validate: TargetValidator;
};

// Legacy per-target functions, used only for targets not yet migrated to
// src/targets/registry.ts. Delete this map (and ../targets.ts/../validate.ts's
// per-target functions) once every TargetName has a registry entry.
const legacyAdapters: Record<TargetName, TargetAdapter> = {
cursor: { emit: emitCursor, validate: validateCursor },
claude: { emit: emitClaude, validate: validateClaude },
antigravity: { emit: emitAntigravity, validate: validateAntigravity },
copilot: { emit: emitCopilot, validate: validateCopilot },
codex: { emit: emitCodex, validate: validateCodex },
};

/**
* The one place a target is wired. `Record<TargetName, …>` is exhaustive at
* compile time — a new `TargetName` won't build until it has an entry here —
* so emit dispatch, validate dispatch, the CLI `--target` choices, and the
* set `build()` iterates all derive from this single source instead of
* parallel maps.
*
* During migration, a target resolves to the new registry
* (`src/targets/*.ts`) if it has an entry there, otherwise falls back to the
* legacy function — this map's shape stays the same either way, so callers
* never notice.
*/
export const adapters: Record<TargetName, TargetAdapter> = Object.fromEntries(
(Object.keys(legacyAdapters) as TargetName[]).map((target) => {
const definition = registry[target];
const adapter: TargetAdapter = definition
? {
emit: (project, targetName, targetConfig, outDir) =>
emitFromDefinition(
project,
targetName,
targetConfig,
outDir,
definition,
),
validate: (root, issues) =>
validateFromDefinition(root, issues, definition),
}
: legacyAdapters[target];
return [target, adapter];
}),
) as Record<TargetName, TargetAdapter>;

/** Every target name with an adapter — the exhaustive list `build()` and the CLI derive from. */
export const targetNames = Object.keys(adapters) as TargetName[];
/** Every target name with a registry entry — the exhaustive list `build()` and the CLI derive from. */
export const targetNames = Object.keys(registry) as TargetName[];

/** Emits one target's output and applies its `rootFiles`, resolving `outDir` from config if omitted. */
export async function emitTarget(
Expand All @@ -107,11 +30,12 @@ export async function emitTarget(
project.rootDir,
outDir ?? targetConfig.outDir,
);
const result = await adapters[target].emit(
const result = await emitFromDefinition(
project,
target,
targetConfig,
resolvedOutDir,
registry[target],
);
return withRootFiles(project, targetConfig, result);
}
Expand All @@ -123,7 +47,7 @@ export async function validateOutput(
): Promise<ValidationResult> {
const root = path.resolve(dir);
const issues: ValidationIssue[] = [];
await adapters[target].validate(root, issues);
await validateFromDefinition(root, issues, registry[target]);
return {
ok: issues.every((issue) => issue.level !== "error"),
issues,
Expand Down
19 changes: 0 additions & 19 deletions src/components.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { TargetName } from "./types.js";

/** Every recognized component directory, across all targets. */
export const componentDirs = [
"skills",
Expand All @@ -16,23 +14,6 @@ export const componentDirs = [
/** Files copied verbatim to a target's output root rather than treated as components. */
export const staticFiles = ["README.md", "CHANGELOG.md", "LICENSE"];

/** Component directories emitted for a target when a plugin has no `components` override. */
export const targetDefaultComponents: Record<TargetName, readonly string[]> = {
claude: ["skills", "agents", "hooks", "scripts", "assets"],
copilot: ["skills", "agents", "hooks", "scripts", "assets"],
cursor: ["skills", "agents", "rules", "hooks", "scripts", "assets"],
antigravity: ["skills", "agents", "rules", "hooks", "scripts", "assets"],
codex: ["skills", "hooks", "scripts", "assets"],
};

/** Resolves a plugin's component set from its own override, or the target's default. */
export function resolveTargetComponents(
target: TargetName,
pluginConfig: { components?: string[] },
): Set<string> {
return new Set(pluginConfig.components ?? targetDefaultComponents[target]);
}

/** Whether a relative path falls under a recognized component directory. */
export function isComponentPath(relativePath: string): boolean {
return componentDirs.includes(relativePath.split("/")[0]);
Expand Down
Loading