Skip to content

Commit 6a96abb

Browse files
refactor: delete legacy per-target emitters/validators now that all targets are migrated (#17)
* refactor: migrate cursor to the target registry (no behavior change) Re-checking the originally planned Cursor "fixes" against the vendored plugin.schema.json / marketplace.schema.json (this repo's own conformance oracle, already passing against the current shape) showed they were wrong: - displayName/category/tags ARE valid plugin.json fields per the schema (additionalProperties: false, and they're explicitly listed) — not marketplace-entry-only fields as previously assumed. - Marketplace `owner` is genuinely optional (required: ["name", "plugins"] does not include it) — not required as previously assumed. Moving category/tags to the marketplace entry would have been actively wrong: entries only allow name/source/description (additionalProperties: false). None of that is changed here. What this commit actually does: - Migrates cursor onto PluginTargetDefinition, preserving every existing field and behavior (verified by the vendored-schema conformance test staying green). - Fixes one genuine, low-risk issue: the manifest builder's own hardcoded default-components list could diverge from this target's actual defaultComponents (components.ts). Replaced both with one list of schema-valid pointer fields, checked directly against the plugin's real resolved componentDirs — eliminates the divergence risk with no observable behavior change (confirmed via a new test exercising the one case that could have differed: an explicit `components: [...]` override). - Ports update-check's hook-injection into the new shared engine (src/targets/engine.ts), which previously only existed in the legacy emitCursor/emitClaude path — migrating cursor without this would have silently dropped update-check support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: apply the per-plugin version override in cursor's manifest buildPluginManifest used the target-level `version` param directly instead of `pluginConfig.version ?? version`, silently dropping a per-plugin version override — a real regression from the pre-migration behavior, caught by porting the equivalent test from the claude migration (no test previously covered this for cursor specifically). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: migrate claude to the target registry (no behavior change) Ports emitClaude/validateClaude into src/targets/claude.ts as a PluginTargetDefinition, verified directly against `claude plugin validate --strict`: a minimal manifest with only `name` fails that check, confirming the existing version/description/author.name requirements already match the real CLI rather than over-constraining it. Applies the per-plugin version override in buildPluginManifest (pluginConfig.version ?? version) up front, matching the identical fix just made to cursor's copy of this pattern. * fix: correct Codex plugin output for the target registry Ports emitCodex/validateCodex into src/targets/codex.ts as a PluginTargetDefinition, correcting the plugin format's real shape — re-verified directly against developers.openai.com/codex/plugins/build (fetched twice, independently, for consistency) since Codex has no CLI validator or vendored schema to check against: - plugin.json requires only "name"; version/description/author etc. are optional. The previous validator wrongly required version and description. - Every marketplace entry needs policy.installation, policy.authentication, and category — previously unvalidated, so an incomplete entry shipped silently. pluginpack can't infer these, so the base entry stays guess-free and validateOutput now errors clearly when an author never supplies them via the per-plugin `entry` passthrough (already how the existing conformance fixture supplies them). - A marketplace entry's source is a bare string only for local plugins (the only shape pluginpack itself ever emits); url/git-subdir/npm sources are structured objects with an inner "source" discriminator. validateMarketplaceEntry now accepts either shape instead of the previously shared, string-only validator. - plugin.json now declares a `hooks` pointer when hooks/ is present, matching skills/mcpServers (previously only skills/mcpServers were declared, so hooks were emitted but never referenced). Updates CONFORMANCE.md's Codex section, which had pinned a stale, bare-string-only shape from an earlier doc retrieval. * fix: restore deeper hooks.json validation dropped during target migration Every migrated target's validateOutput calls validateHooksShape (added in the registry scaffold), which only checked that hooks.json has a "hooks" object — narrower than the legacy per-target validateHooks it replaced, which also required each event's entries to be an array, rejected empty "command" strings, and errored if a command referenced the generated update-check script without that script actually being present. None of that depth had a regression test, so the narrowing was silent. Ports the full check into validateHooksShape once, so every target that already calls it (all 5, post-migration) regains it for free instead of needing the fix repeated per target file. * refactor: delete legacy per-target emitters/validators now that all targets are migrated All 5 targets (copilot, antigravity, cursor, claude, codex) now have a PluginTargetDefinition in src/targets/registry.ts, so the legacy fallback path adapters.ts existed for is dead: - Deletes src/targets.ts and src/validate.ts entirely (their only consumer was adapters.ts's legacyAdapters map). - Moves withRootFiles into src/targets/engine.ts, next to the artifact helper it depends on. - Tightens the registry's type from Partial<Record<TargetName, ...>> to Record<TargetName, ...> now that every target has an entry — a new TargetName won't build until it has a registry entry, the same exhaustiveness guarantee the deleted legacyAdapters map used to provide. - Simplifies adapters.ts to a thin emitTarget/validateOutput wrapper around the registry + engine, dropping the now-pointless TargetAdapter/adapters indirection that existed only to switch between legacy and registry per target. - Removes targetDefaultComponents/resolveTargetComponents from components.ts (superseded by each target's own defaultComponents). - Updates CLAUDE.md's Architecture/Targets sections and a couple of stale doc-comment references to match. * docs: note the shared hooks validation depth in CONFORMANCE.md Cross-references the validateHooksShape fix from the prior commit — what it checks and where it's shared from, for the conformance doc's own "what's actually verified and how" mandate. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 8eccdb1 commit 6a96abb

12 files changed

Lines changed: 206 additions & 1499 deletions

CLAUDE.md

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

5262
## Targets
5363

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

6070
## Conformance
6171

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

8191
## Conventions
8292

83-
- Strict TypeScript, no `any` (the one exception is `readJson` in `validate.ts`).
93+
- Strict TypeScript, no `any` (the one exception is `readJson` in
94+
`src/targets/validation-shared.ts`).
8495
- Prettier + eslint enforced by the gate.
8596
- Conventional commits. Keep the README CLI reference regenerated.

CONFORMANCE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ cursor manifest's `hooks` key validates against the vendored plugin schema, and
9090
`claude plugin validate --strict` exercises the claude hooks file when the CLI
9191
is present).
9292

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

95101
The Cursor schemas are pinned copies. To update them, re-fetch from the source

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ Exit codes:
481481
Compile configured source plugins into target-native plugin payloads.
482482

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

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

508508
```bash
509-
pluginpack validate --target cursor|claude|antigravity|copilot|codex [--dir <path>]
509+
pluginpack validate --target copilot|antigravity|cursor|claude|codex [--dir <path>]
510510
```
511511

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

530530
```bash
531-
pluginpack diff --target cursor|claude|antigravity|copilot|codex --against <path>
531+
pluginpack diff --target copilot|antigravity|cursor|claude|codex --against <path>
532532
```
533533

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

552552
```bash
553-
pluginpack prune [--target cursor|claude|antigravity|copilot|codex] [--dry-run]
553+
pluginpack prune [--target copilot|antigravity|cursor|claude|codex] [--dry-run]
554554
```
555555

556556
Options:
@@ -574,7 +574,7 @@ Exit codes:
574574
Remove all managed files for configured target outputs.
575575

576576
```bash
577-
pluginpack clean [--target cursor|claude|antigravity|copilot|codex] [--dry-run]
577+
pluginpack clean [--target copilot|antigravity|cursor|claude|codex] [--dry-run]
578578
```
579579

580580
Options:

src/adapters.ts

Lines changed: 6 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,20 @@
11
import path from "node:path";
2-
import {
3-
emitAntigravity,
4-
emitClaude,
5-
emitCodex,
6-
emitCopilot,
7-
emitCursor,
8-
withRootFiles,
9-
} from "./targets.js";
10-
import {
11-
validateAntigravity,
12-
validateClaude,
13-
validateCodex,
14-
validateCopilot,
15-
validateCursor,
16-
} from "./validate.js";
172
import {
183
emitFromDefinition,
194
validateFromDefinition,
5+
withRootFiles,
206
} from "./targets/engine.js";
217
import { targets as registry } from "./targets/registry.js";
228
import type {
239
Artifact,
2410
ResolvedProject,
25-
TargetConfig,
2611
TargetName,
2712
ValidationIssue,
2813
ValidationResult,
2914
} from "./types.js";
3015

31-
type TargetEmitter = (
32-
project: ResolvedProject,
33-
target: TargetName,
34-
targetConfig: TargetConfig,
35-
outDir: string,
36-
) => Promise<Artifact>;
37-
38-
type TargetValidator = (
39-
root: string,
40-
issues: ValidationIssue[],
41-
) => Promise<void>;
42-
43-
/** The emit and validate functions for one target. */
44-
export type TargetAdapter = {
45-
emit: TargetEmitter;
46-
validate: TargetValidator;
47-
};
48-
49-
// Legacy per-target functions, used only for targets not yet migrated to
50-
// src/targets/registry.ts. Delete this map (and ../targets.ts/../validate.ts's
51-
// per-target functions) once every TargetName has a registry entry.
52-
const legacyAdapters: Record<TargetName, TargetAdapter> = {
53-
cursor: { emit: emitCursor, validate: validateCursor },
54-
claude: { emit: emitClaude, validate: validateClaude },
55-
antigravity: { emit: emitAntigravity, validate: validateAntigravity },
56-
copilot: { emit: emitCopilot, validate: validateCopilot },
57-
codex: { emit: emitCodex, validate: validateCodex },
58-
};
59-
60-
/**
61-
* The one place a target is wired. `Record<TargetName, …>` is exhaustive at
62-
* compile time — a new `TargetName` won't build until it has an entry here —
63-
* so emit dispatch, validate dispatch, the CLI `--target` choices, and the
64-
* set `build()` iterates all derive from this single source instead of
65-
* parallel maps.
66-
*
67-
* During migration, a target resolves to the new registry
68-
* (`src/targets/*.ts`) if it has an entry there, otherwise falls back to the
69-
* legacy function — this map's shape stays the same either way, so callers
70-
* never notice.
71-
*/
72-
export const adapters: Record<TargetName, TargetAdapter> = Object.fromEntries(
73-
(Object.keys(legacyAdapters) as TargetName[]).map((target) => {
74-
const definition = registry[target];
75-
const adapter: TargetAdapter = definition
76-
? {
77-
emit: (project, targetName, targetConfig, outDir) =>
78-
emitFromDefinition(
79-
project,
80-
targetName,
81-
targetConfig,
82-
outDir,
83-
definition,
84-
),
85-
validate: (root, issues) =>
86-
validateFromDefinition(root, issues, definition),
87-
}
88-
: legacyAdapters[target];
89-
return [target, adapter];
90-
}),
91-
) as Record<TargetName, TargetAdapter>;
92-
93-
/** Every target name with an adapter — the exhaustive list `build()` and the CLI derive from. */
94-
export const targetNames = Object.keys(adapters) as TargetName[];
16+
/** Every target name with a registry entry — the exhaustive list `build()` and the CLI derive from. */
17+
export const targetNames = Object.keys(registry) as TargetName[];
9518

9619
/** Emits one target's output and applies its `rootFiles`, resolving `outDir` from config if omitted. */
9720
export async function emitTarget(
@@ -107,11 +30,12 @@ export async function emitTarget(
10730
project.rootDir,
10831
outDir ?? targetConfig.outDir,
10932
);
110-
const result = await adapters[target].emit(
33+
const result = await emitFromDefinition(
11134
project,
11235
target,
11336
targetConfig,
11437
resolvedOutDir,
38+
registry[target],
11539
);
11640
return withRootFiles(project, targetConfig, result);
11741
}
@@ -123,7 +47,7 @@ export async function validateOutput(
12347
): Promise<ValidationResult> {
12448
const root = path.resolve(dir);
12549
const issues: ValidationIssue[] = [];
126-
await adapters[target].validate(root, issues);
50+
await validateFromDefinition(root, issues, registry[target]);
12751
return {
12852
ok: issues.every((issue) => issue.level !== "error"),
12953
issues,

src/components.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { TargetName } from "./types.js";
2-
31
/** Every recognized component directory, across all targets. */
42
export const componentDirs = [
53
"skills",
@@ -16,23 +14,6 @@ export const componentDirs = [
1614
/** Files copied verbatim to a target's output root rather than treated as components. */
1715
export const staticFiles = ["README.md", "CHANGELOG.md", "LICENSE"];
1816

19-
/** Component directories emitted for a target when a plugin has no `components` override. */
20-
export const targetDefaultComponents: Record<TargetName, readonly string[]> = {
21-
claude: ["skills", "agents", "hooks", "scripts", "assets"],
22-
copilot: ["skills", "agents", "hooks", "scripts", "assets"],
23-
cursor: ["skills", "agents", "rules", "hooks", "scripts", "assets"],
24-
antigravity: ["skills", "agents", "rules", "hooks", "scripts", "assets"],
25-
codex: ["skills", "hooks", "scripts", "assets"],
26-
};
27-
28-
/** Resolves a plugin's component set from its own override, or the target's default. */
29-
export function resolveTargetComponents(
30-
target: TargetName,
31-
pluginConfig: { components?: string[] },
32-
): Set<string> {
33-
return new Set(pluginConfig.components ?? targetDefaultComponents[target]);
34-
}
35-
3617
/** Whether a relative path falls under a recognized component directory. */
3718
export function isComponentPath(relativePath: string): boolean {
3819
return componentDirs.includes(relativePath.split("/")[0]);

0 commit comments

Comments
 (0)