feat: add partials for shared, inlined text fragments across skills - #21
Merged
Conversation
Skills that need the same procedural prose repeated across many files
(a consistent auth flow is the motivating case: "try OAuth first, fall
back to a token, or ask for the user's email and look up credentials")
had no reuse mechanism — every SKILL.md that needed it had to
duplicate the prose by hand.
- New source.partials config option: a project-level directory of
reusable {{> name}} text fragments, shared across every source
plugin (not scoped to one), resolved once at build time via the
real `mustache` library.
- Substitution hooks into collectPluginFiles (render.ts) and
withRootFiles (targets/engine.ts) — every .md/.mdc/.markdown/.txt
file pluginpack emits participates, including additionalFiles
(merged into the same map before collectPluginFiles sees it) and a
target's rootFiles.
- View is always {} — no config/env data is ever exposed to
interpolation. An earlier, broader "dynamic value injection" idea
(secrets, env-specific config) was considered and dropped: build
output is committed to a repo the target host installs from, and
baking secret-like values into that committed output would be a
real security footgun. This feature is pure text reuse, full stop.
- Missing partial -> renders as "" (Mustache's own documented
behavior), not a build error — a standalone {{> name}} line
disappears with no orphaned blank line, matching how the library
already handles this. Partials may reference other partials
(native recursive composition); a circular reference is the one
thing pluginpack itself must catch, since Mustache.js has no
protection against it ("just avoid infinite loops") — checked via a
dependency-graph cycle detection at load time, before any render
call happens.
- Known, documented limitation: because substitution is real Mustache
rendering, any other {{...}}-looking text in the same file (e.g. a
skill documenting Handlebars/Angular/Go templates/Jinja/Mustache
itself) is also processed against the empty view and typically
disappears. A narrower, hand-rolled {{> }}-only parser was
considered specifically to avoid this, but reproducing Mustache's
standalone-line whitespace handling and nested-partial resolution by
hand means reinventing battle-tested, easy-to-get-subtly-wrong
infrastructure a mature library already solves correctly — the
tradeoff was made deliberately in favor of the real library.
12 new tests in tests/core.test.ts covering the happy path, missing
partial, nested composition, circular reference, multi-marker files,
cross-plugin sharing, backward compatibility (no-op when
unconfigured), byte-safety (non-UTF8 content untouched when no {{
marker present), duplicate partial names, a missing partials
directory, rootFiles substitution, and the documented collision
behavior (locked in as a regression guard, not something to "fix").
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Skills that need the same procedural prose repeated across many files (a consistent auth flow — "try OAuth first, fall back to a token, or ask for the user's email and look up credentials" — is the motivating case) had no reuse mechanism. This adds a
partials/directory of reusable{{> name}}text fragments, resolved once atpluginpack buildtime.source.partialsconfig option: a project-level directory (not scoped to one source plugin), mirroringsource.skills's shape.mustachelibrary (new production dependency,@types/mustachein dev), not a hand-rolled parser — gets missing-partial-renders-empty and clean standalone-line whitespace handling for free, both verified directly against the Mustache spec's own test suite before deciding.{}— no config/env data is ever exposed to interpolation. An earlier, broader "dynamic value injection" idea (secrets, env-specific config) was explicitly scoped out: build output is committed to a repo the target host installs from, and baking secret-like values into that committed output would be a real security footgun. This is pure text reuse, full stop.collectPluginFiles(render.ts) andwithRootFiles(targets/engine.ts) — covers skills/agents/commands/rules,additionalFiles, and a target'srootFiles.{{...}}-looking text in the same file (e.g. a skill documenting Handlebars/Jinja/Mustache itself) is also processed against the empty view and typically disappears. Considered and rejected a narrower hand-rolled{{> }}-only parser to avoid this — reproducing Mustache's standalone-line whitespace handling and nested-partial resolution by hand means reinventing battle-tested infra a mature library already solves correctly.Test plan
npm run typechecknpm run lintnpm test(92/92 — 12 new tests: happy path, missing partial, nested composition, circular reference, multi-marker, cross-plugin sharing, no-op backward compat, byte-safety, duplicate name, missing directory,rootFiles, documented collision behavior)npm run audit(production-scoped) — 0 vulnerabilities,mustacheitself is cleannpm run build && node dist/cli.js docs --checknpm run docs(includesmarkdown-code check)npm pack --dry-runsource.partialsconfigured against claude + cursor targets, confirmed the substituted auth-flow text reads naturally in context in both, andpluginpack validatepasses for both