@linchpinagency/skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +337 -0
  2. package/bin/install.mjs +231 -0
  3. package/package.json +44 -0
  4. package/skills/browser-automation/SKILL.md +93 -0
  5. package/skills/commit-and-release/SKILL.md +135 -0
  6. package/skills/dependency-updates/SKILL.md +102 -0
  7. package/skills/design-previews/SKILL.md +118 -0
  8. package/skills/engagement-types/SKILL.md +108 -0
  9. package/skills/investigate/SKILL.md +95 -0
  10. package/skills/project-context/SKILL.md +89 -0
  11. package/skills/quality-gates/SKILL.md +94 -0
  12. package/skills/quality-gates/references/toolchain.md +104 -0
  13. package/skills/safety-hooks/SKILL.md +121 -0
  14. package/skills/safety-hooks/scripts/check-destructive.sh +80 -0
  15. package/skills/safety-hooks/scripts/check-edit-boundary.sh +65 -0
  16. package/skills/support-triage/SKILL.md +103 -0
  17. package/skills/task-tracking/SKILL.md +243 -0
  18. package/skills/web-qa/SKILL.md +108 -0
  19. package/skills/web-qa/references/qa-checklist.md +98 -0
  20. package/skills/wordpress-blocks/SKILL.md +110 -0
  21. package/skills/wordpress-blocks/references/block-grammar.md +94 -0
  22. package/skills/wordpress-blocks/references/core-blocks.md +123 -0
  23. package/skills/wordpress-blocks/references/patterns-and-parts.md +70 -0
  24. package/skills/wordpress-blocks/references/recipes/faq.md +56 -0
  25. package/skills/wordpress-blocks/references/recipes/hero.md +74 -0
  26. package/skills/wordpress-blocks/references/recipes/pricing-table.md +77 -0
  27. package/skills/wordpress-blocks/references/tool-contract.md +167 -0
  28. package/skills/wordpress-blocks/references/validation.md +38 -0
  29. package/skills/wp-audit/SKILL.md +115 -0
  30. package/skills/wp-block-conventions/SKILL.md +134 -0
  31. package/skills/wp-block-conventions/references/block-anatomy.md +175 -0
  32. package/skills/wp-implementation-choice/SKILL.md +88 -0
  33. package/skills/wp-local-setup/SKILL.md +262 -0
  34. package/skills/wp-pressable/SKILL.md +172 -0
  35. package/skills/wp-studio-cli/SKILL.md +165 -0
  36. package/skills/write-a-linchpin-skill/SKILL.md +195 -0
  37. package/skills/write-a-linchpin-skill/references/template.md +83 -0
  38. package/upstream.json +20 -0
package/README.md ADDED
@@ -0,0 +1,337 @@
1
+ # Linchpin Skills
2
+
3
+ A shared library of **AI agent skills** for the kinds of projects Linchpin builds —
4
+ **WordPress**, **React**, and **Cloudflare Workers**. Skills are agent-agnostic
5
+ instruction sets ([Agent Skills](https://agentskills.io) format) that work in Claude Code,
6
+ GitHub Copilot, and other compatible coding agents.
7
+
8
+ ---
9
+
10
+ ## Getting started
11
+
12
+ ### What a skill actually is
13
+
14
+ A skill is a markdown file of instructions that your coding agent loads **when your request
15
+ matches its description**. It's how we make an agent behave like a Linchpin developer
16
+ instead of a generic one: it knows we run WordPress Studio, that PHPCS only applies where
17
+ `phpcs.xml.dist` exists, that release-please owns `CHANGELOG.md`, and that every commit
18
+ carries a ClickUp key.
19
+
20
+ Three things worth knowing up front:
21
+
22
+ - **You mostly don't invoke skills.** Ask for what you want — "this page is broken on
23
+ mobile", "get this ready to commit" — and the agent loads the matching skill on its own.
24
+ - **Skills are loaded by the tool, not the model.** Claude, GPT, or anything else running
25
+ inside Claude Code all read the same `.claude/skills` directory. Switching models changes
26
+ nothing about which skills exist; switching *tools* does.
27
+ - **They are just files.** Nothing is hosted, nothing phones home. You can read every one of
28
+ them in `skills/`.
29
+
30
+ ### Install (2 minutes)
31
+
32
+ Install once, globally, and every project you open gets them:
33
+
34
+ ```bash
35
+ npx -y github:linchpin/skills --global
36
+ ```
37
+
38
+ Verify:
39
+
40
+ ```bash
41
+ npx -y github:linchpin/skills --list
42
+ ```
43
+
44
+ You need GitHub access to this (private) repo, which everyone on the team has. There is no
45
+ npm package yet — see [publishing](#for-maintainers--publishing) for why that's a deliberate
46
+ open question rather than an oversight.
47
+
48
+ Then start a new session in your project and ask for something real — "what kind of project
49
+ is this?" should pull in `project-context` and get you a summary of the repo shape, local
50
+ environment, and host.
51
+
52
+ **Re-run the same command to update.** There's no upgrade command; the installer overwrites
53
+ in place and always pulls the current `main`. Do it every few weeks, or when someone
54
+ announces a new skill.
55
+
56
+ Full flag reference: [Install options](#install-options).
57
+
58
+ ### Your first day
59
+
60
+ The fastest way to understand the library is to run one loop end to end:
61
+
62
+ | You want to… | Ask for it naturally | Skill that fires |
63
+ | --- | --- | --- |
64
+ | Understand a repo you just cloned | "what am I working with here?" | `project-context` |
65
+ | Find out why something's broken | "the hero image 404s on mobile" | `investigate` |
66
+ | Test a site like a user, and fix what's found | "QA the checkout flow" | `web-qa` |
67
+ | Check it's ready to commit | "is this ready to commit?" | `quality-gates` |
68
+ | Commit and open the PR properly | "commit this and open a PR" | `commit-and-release` + `task-tracking` |
69
+ | Handle a client support ticket | "Vinfen says their form isn't sending" | `support-triage` |
70
+ | Add guardrails before touching prod | "careful mode — I'm on production" | `safety-hooks` |
71
+
72
+ The full list is in [Available skills](#available-skills) — 20 of them, each with a
73
+ `When to use` section that says exactly when it applies and which skill to use instead.
74
+
75
+ **When you want to be explicit**, name the skill: *"use the wp-audit skill on the homepage."*
76
+ Worth doing when a task straddles two skills, or when you want a specific procedure followed.
77
+
78
+ ### Working in Conductor
79
+
80
+ Conductor runs each workspace in its own **git worktree** — a separate checkout of the repo.
81
+ A project-level install (`./.claude/skills/`) therefore exists only in the workspace where
82
+ you ran it, and disappears the moment you create a new one.
83
+
84
+ **So for Conductor, install globally** (`--global`). `~/.claude/skills/` is outside the
85
+ worktree, so every workspace picks it up automatically with no per-workspace setup.
86
+
87
+ Two other things to expect: interactive prompts can behave differently than in a terminal
88
+ session — if a skill seems to be waiting on you, just answer in the chat — and because
89
+ workspaces are independent checkouts, a skill that reports on git state is describing *that*
90
+ workspace only.
91
+
92
+ ### Other tools and models
93
+
94
+ Claude Code is our primary. If you use something else, install into its directory too:
95
+
96
+ ```bash
97
+ npx -y github:linchpin/skills --global --agent all # Claude Code, Copilot, Codex, Cursor
98
+ ```
99
+
100
+ Skills are read by the **harness**, so Copilot running a Claude model still needs them in
101
+ Copilot's own folder. Everything here is plain markdown with no Claude-specific syntax, with
102
+ one deliberate exception: `safety-hooks` uses Claude Code hooks to *enforce* confirmation on
103
+ destructive commands, and degrades to documentation elsewhere.
104
+
105
+ ### When a skill is wrong
106
+
107
+ These encode how we work, so they go stale when how we work changes. If a skill tells the
108
+ agent something outdated, that's a bug worth fixing — open a PR, or an issue if you'd rather
109
+ someone else write it. [`write-a-linchpin-skill`](skills/write-a-linchpin-skill/SKILL.md) is
110
+ the standard, and `npm run validate` checks your work.
111
+
112
+ ---
113
+
114
+ ## Three tiers — and what belongs here
115
+
116
+ Knowledge about building WordPress lives at three altitudes. This repo owns **only the
117
+ middle one**:
118
+
119
+ | Tier | Where it lives | What it owns |
120
+ | --- | --- | --- |
121
+ | **Base layer** (upstream) | [`WordPress/agent-skills`](https://github.com/WordPress/agent-skills), vendored + pinned via [`upstream.json`](upstream.json) | Generic "how WordPress works" — block.json, theme.json mechanics, the Interactivity API, performance, WP-CLI ops. |
122
+ | **Linchpin tooling** (**this repo**) | `skills/` | **Portable, cross-project** ways the agency works — operating Studio/Pressable, tying work to ClickUp. Things true on *every* Linchpin project. |
123
+ | **Project layer** (per-repo) | that project's own `AGENTS.md` / `CLAUDE.md` | One project's specific blocks, theme conventions, file paths, and quirks. |
124
+
125
+ > **Project-specific conventions do NOT go here.** A given site's block/theme conventions
126
+ > (e.g. linchpin.com's color slugs, spacing scale, custom blocks, pattern structure) belong
127
+ > in **that project's repo** — next to the code, in its `AGENTS.md`/`CLAUDE.md` — not in the
128
+ > shared library. The test for "does it belong in this repo?" is: *would it be true on a
129
+ > different client's WordPress project?* If not, it's project layer.
130
+
131
+ The base layer is pinned to a commit SHA in [`upstream.json`](upstream.json) (upstream has
132
+ no releases yet) and fetched at install time. Bump the `ref` there deliberately and re-test;
133
+ don't float it, or agent behavior changes silently. New **generic** WordPress knowledge
134
+ should be contributed **upstream**, not added here.
135
+
136
+ ## Install options
137
+
138
+ Skills install via our own zero-dependency CLI, run straight from this repo with `npx`.
139
+ Run it from a project root:
140
+
141
+ ```bash
142
+ # Install every skill into this project's Claude Code skills dir (./.claude/skills)
143
+ npx -y github:linchpin/skills
144
+
145
+ # Install a specific skill
146
+ npx -y github:linchpin/skills wp-studio-cli
147
+
148
+ # List what's available
149
+ npx -y github:linchpin/skills --list
150
+
151
+ # Install for GitHub Copilot instead (-> ./.agents/skills + ./.github/skills)
152
+ npx -y github:linchpin/skills --agent github-copilot
153
+
154
+ # Install into every agent's directory at once (Claude Code, Copilot, Codex, Cursor)
155
+ npx -y github:linchpin/skills --agent all
156
+
157
+ # Install to your user-global dir (-> ~/.claude/skills) instead of the project
158
+ npx -y github:linchpin/skills --global
159
+
160
+ # Install the Linchpin skills only, without the upstream base layer
161
+ npx -y github:linchpin/skills --skip-upstream
162
+ ```
163
+
164
+ > `npx` installs from the repo's default branch, so you always get current `main`. There is
165
+ > no version pinning on this route — pin by publishing to a registry (below) if you ever
166
+ > need it.
167
+
168
+ > By default the installer also vendors the pinned upstream base layer (see
169
+ > [Three tiers](#three-tiers--and-what-belongs-here)) into the same directory. It needs
170
+ > network access and a system `tar`; if either is missing it warns and still installs the
171
+ > Linchpin skills. Pass `--skip-upstream` to install the Linchpin skills alone.
172
+
173
+ **Updating:** re-run the same command. The installer overwrites each skill in place, so a
174
+ fresh run always pulls the current `main`.
175
+
176
+ ### Where skills land
177
+
178
+ | Agent (`--agent`) | Project scope | Global scope (`--global`) |
179
+ | --- | --- | --- |
180
+ | `claude-code` (default) | `./.claude/skills/` | `~/.claude/skills/` |
181
+ | `github-copilot` | `./.agents/skills/` + `./.github/skills/` | `~/.copilot/skills/` |
182
+ | `codex` | `./.codex/skills/` | `~/.codex/skills/` |
183
+ | `cursor` | `./.cursor/skills/` | `~/.cursor/skills/` |
184
+ | `all` | every directory above | every directory above |
185
+
186
+ A project that wants skills in more than one agent's directory should run
187
+ `--agent all` rather than copying directories around by hand.
188
+
189
+ > Skills are loaded by the **agent/harness**, not the model — so "Copilot running Claude"
190
+ > still needs the skill installed in Copilot's own directory. The installer handles that.
191
+
192
+ ## Available skills
193
+
194
+ ### Linchpin tooling (this repo)
195
+
196
+ | Skill | Domain | What it does |
197
+ | --- | --- | --- |
198
+ | `wp-local-setup` | WordPress | Stand up the Linchpin baseline local environment — scaffold a new wp-content-shaped project repo (Composer plugins from wpackagist + packagist.linchpin.com, theme from `base-wp-theme-2026`) and/or wire a repo into a WordPress Studio site by symlinking it in as `wp-content`. |
199
+ | `wp-studio-cli` | WordPress | Operate a local WordPress Studio site — the wordpress-studio MCP first (`wp_cli`, `validate_blocks`, `take_screenshot`), the `studio` CLI as fallback — including the PHP-WASM `ABSPATH` rule. |
200
+ | `wp-audit` | WordPress | Audit a site for performance, accessibility, and visible frontend quality against Core Web Vitals thresholds; prioritized findings with labeled evidence, and before/after re-measurement. |
201
+ | `wp-pressable` | WordPress | Operate a Pressable-hosted site (prod/staging) via the Pressable MCP or SSH+WP-CLI; safely diagnose and fix the "renders locally but not on prod" FSE bug where DB template overrides shadow deployed theme files. |
202
+ | `wordpress-blocks` | WordPress | Author and edit page/post content as valid Gutenberg block markup from a chat interface — pattern-first (reuse the site's synced/registered patterns before composing core blocks), with the grammar rules and the validate-before-insert contract. |
203
+ | `wp-block-conventions` | WordPress | Build custom blocks the Linchpin way — apiVersion 3 under `linchpin/`, dynamic `render.php` + Interactivity API `view.js`, parent/child block context, and the `wp-scripts` build/registration chain shared by `linchpin-blocks` and project functionality plugins. |
204
+ | `wp-implementation-choice` | WordPress | Decide what a request should become — theme work, content, a custom block, a functionality plugin, or an existing plugin — before any code is written. |
205
+ | `design-previews` | Design | Generate three genuinely different visual directions as self-contained HTML previews, screenshot them at desktop and mobile via the Chrome DevTools MCP (or Playwright), and get a pick before theme or block work starts. |
206
+ | `project-context` | Workflow | Orient before acting — repo shape, local environment, host, ClickUp space, and release model, read from the project's own config rather than assumed. Referenced by other skills' Preflight. |
207
+ | `quality-gates` | Workflow | Run a project's own lint, PHPCS, PHPStan, and test gates before committing — detected from `composer.json`, `package.json`, `phpcs.xml.dist`, and `lint-staged`, never assumed. |
208
+ | `web-qa` | Workflow | QA like a real user and fix what you find — front end, wp-admin, and block editor, with severity, evidence, one atomic commit per fix, and a report-only mode. |
209
+ | `investigate` | Workflow | Root-cause a bug before changing anything — reproduce, read the real error, isolate the layer, explain the mechanism, with WordPress first checks. |
210
+ | `browser-automation` | Workflow | The browser ladder, owned once: Chrome DevTools MCP against real Chrome first, Playwright headless as fallback, plus auth handling and WordPress specifics. |
211
+ | `safety-hooks` | Workflow | Enforced guardrails — a `PreToolUse` hook that makes destructive commands (`wp db drop`, `search-replace` without `--dry-run`, force-push, `rm -rf`) require confirmation, plus an optional edit boundary. Claude Code only. |
212
+ | `engagement-types` | Project mgmt | Tell support, site maintenance, projects, product/plugin work, and pre-sales apart — each lives somewhere different in ClickUp and is planned and closed differently. |
213
+ | `support-triage` | Project mgmt | Run a client support request end to end — clarify the real need, reproduce, judge urgency and scope, fix in the right layer, verify, and close the loop with the requester. |
214
+ | `dependency-updates` | Workflow | Handle the dependency work Renovate can't automerge — majors, breaking changes, failing or conflicted bot PRs, security advisories, `@wordpress/*` package sets. |
215
+ | `commit-and-release` | Workflow | Write commits, branches, and PR titles that satisfy the repo's own commitlint rules, and stay out of release-please's way (it owns versions and `CHANGELOG.md`). |
216
+ | `task-tracking` | Workflow | Tie every unit of work to a ClickUp task (or explicit `NO-TASK`) with minimal friction via the ClickUp MCP — resolve/search a task, offer to create one before committing, update it when the work lands, and carry the task key in the conventional-commit scope. |
217
+ | `write-a-linchpin-skill` | Meta | The house standard for authoring skills in this library — placement test, tier model, required frontmatter, the section skeleton, and the four house rules. Enforced by `scripts/validate-skills.mjs`. |
218
+
219
+ _(More WordPress, React, Cloudflare Workers, marketing, and design skills to come.)_
220
+
221
+ ### Base layer (vendored from upstream, pinned)
222
+
223
+ Fetched at install time from [`WordPress/agent-skills`](https://github.com/WordPress/agent-skills)
224
+ at the SHA pinned in [`upstream.json`](upstream.json). Curate the set there. Currently:
225
+ `wp-block-development`, `wp-block-themes`, `wp-interactivity-api`, `wp-performance`,
226
+ `wp-wpcli-and-ops`, `wp-plugin-development`, `wp-rest-api`.
227
+
228
+ ## Adding a skill
229
+
230
+ **The standard lives in [`skills/write-a-linchpin-skill/`](skills/write-a-linchpin-skill/SKILL.md)** —
231
+ load that skill and follow it. It owns the placement test, the tier model (A: `SKILL.md`
232
+ only → B: `+ references/` → C: `+ scripts/`), required frontmatter, the section skeleton,
233
+ and the four house rules. It isn't restated here on purpose: one owner per concern.
234
+
235
+ The short version:
236
+
237
+ ```
238
+ skills/
239
+ <name>/
240
+ SKILL.md # required — frontmatter: name, description, version
241
+ references/*.md # Tier B — detail promoted out of SKILL.md
242
+ scripts/*.mjs # Tier C — only when determinism is genuinely needed
243
+ ```
244
+
245
+ - Directory name = frontmatter `name`, and names are **globally unique once installed**.
246
+ Prefix by domain — `wp-`, `react-`, `cf-`, `seo-`, `design-` — and leave cross-cutting
247
+ workflow skills (`task-tracking`, `quality-gates`) un-prefixed.
248
+ - Every `SKILL.md` needs `## When to use`, `## Guardrails`, and `## Done`.
249
+ - Add a row to the **Available skills** table above.
250
+
251
+ Then validate — CI runs the same command on every PR:
252
+
253
+ ```bash
254
+ npm run validate # every skill
255
+ node scripts/validate-skills.mjs <name> # just the one you touched
256
+ ```
257
+
258
+ > **Keep it portable.** Every skill here must be true on *any* Linchpin project of its kind
259
+ > — don't bake in one site's blocks, palette, or file paths. Project-specific conventions
260
+ > belong in that project's own `AGENTS.md`/`CLAUDE.md` (see
261
+ > [Three tiers](#three-tiers--and-what-belongs-here)).
262
+
263
+ ## For maintainers — publishing
264
+
265
+ **Nothing is published today.** `@linchpinagency/skills` does not exist on npm; the install
266
+ route is `npx -y github:linchpin/skills`, which works for anyone with access to this repo.
267
+ `package.json` is *configured* to publish (`publishConfig.access: "public"`), but running
268
+ `npm publish` is a deliberate decision that hasn't been made — for a good reason:
269
+
270
+ > **A public package makes this content public.** These skills describe how we run client
271
+ > work: engagement and support workflows, hosting and deploy specifics, and our internal
272
+ > conventions. Before anything is published publicly, someone needs to decide that is
273
+ > acceptable, and the content needs a pass for client-identifying detail (names, ClickUp
274
+ > ids, hostnames) — which belongs in project repos, not here, regardless of publishing.
275
+
276
+ ### How releases work
277
+
278
+ Releases follow the house convention — **release-please**, same as every other Linchpin repo:
279
+
280
+ 1. Merge conventional commits to `main`. `release-please.yml` keeps a rolling
281
+ **:gem: Automated Release** PR up to date, with the changelog it would write.
282
+ 2. Merge that PR when you want to cut a release. It bumps `package.json`, writes
283
+ `CHANGELOG.md`, tags `vX.Y.Z`, and publishes a GitHub Release.
284
+ 3. That release flips `release_created`, which triggers the `publish` job:
285
+ `npm run validate`, then `npm publish --provenance --access public`.
286
+
287
+ **Never hand-edit `package.json`'s version or `CHANGELOG.md`** — release-please owns both
288
+ (see [`commit-and-release`](skills/commit-and-release/SKILL.md)).
289
+
290
+ ### One-time setup before the first release
291
+
292
+ The automation is wired but has never run. Three things are needed:
293
+
294
+ 1. **Decide the content question above.** Everything else is plumbing; this isn't.
295
+ 2. **Create the package with one manual publish.** npm can't attach automation to a package
296
+ that doesn't exist yet:
297
+ ```bash
298
+ npm login # must be a member of the @linchpinagency org
299
+ npm publish --access public # prepublishOnly runs the validator first
300
+ ```
301
+ 3. **Give CI credentials**, either:
302
+ - **Trusted Publishing (preferred)** — on npmjs.com, set this repo and
303
+ `.github/workflows/release-please.yml` as a trusted publisher for the package. No
304
+ stored secret, and provenance is automatic.
305
+ - **`NPM_TOKEN` secret** — an npm *automation* token added to this repo's Actions
306
+ secrets. Simpler to set up, but it's a long-lived credential to rotate.
307
+
308
+ Until step 3 is done the `publish` job fails rather than skipping, so a release never looks
309
+ published when it isn't.
310
+
311
+ Options worth weighing first:
312
+
313
+ - **Stay on the GitHub route** (status quo). Private, zero setup, no registry to maintain.
314
+ Costs: needs repo access, so external collaborators can't install, and there's no version
315
+ pinning.
316
+ - **Publish privately** (paid npm org, or GitHub Packages). Keeps content private and adds
317
+ versioning — but consumers must `npm login` anyway, which removes most of the convenience
318
+ that made publishing attractive.
319
+ - **Publish publicly** after a content review. Best ergonomics; only appropriate if we're
320
+ comfortable with the playbook being readable by anyone.
321
+ - **Scope migration.** If the `@linchpin` npm scope is ever acquired, prefer it and deprecate
322
+ the old name with `npm deprecate @linchpinagency/skills "moved to @linchpin/skills"`.
323
+
324
+ The package would ship only `bin/`, `skills/`, `upstream.json`, and `README.md` (see `files`
325
+ in `package.json`).
326
+
327
+ ## License & attribution
328
+
329
+ - **This repo (overlay + installer):** UNLICENSED — internal Linchpin tooling. Update
330
+ `license` in `package.json` if this is ever released externally.
331
+ - **Base layer:** the upstream skills are **not stored in this repo** — the installer
332
+ fetches them from [`WordPress/agent-skills`](https://github.com/WordPress/agent-skills) at
333
+ the pinned SHA, onto the user's machine, at install time. They are
334
+ **GPL-2.0-or-later**, © WordPress Contributors. Credit to that project for the generic
335
+ WordPress expertise our overlay builds on. (Upstream is v1 and AI-authored then
336
+ human-reviewed — treat it as a strong baseline, which is exactly why house rules win on
337
+ conflict.)
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ // @linchpinagency/skills installer
3
+ // Copies bundled Linchpin skills into a coding agent's skills directory, and (by
4
+ // default) vendors a pinned base layer of upstream WordPress/agent-skills alongside them.
5
+ // Zero runtime dependencies — pure Node (>=18, uses global fetch) + the system `tar`.
6
+ // Re-run to update (it overwrites in place).
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import { execFileSync } from 'node:child_process';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const PKG_ROOT = path.resolve(__dirname, '..');
16
+ const SKILLS_ROOT = path.join(PKG_ROOT, 'skills');
17
+ const UPSTREAM_MANIFEST = path.join(PKG_ROOT, 'upstream.json');
18
+
19
+ // Per-agent install locations. `project` paths are relative to cwd, `global` to home.
20
+ // These follow the Agent Skills conventions each tool reads from. An agent may read more
21
+ // than one directory (Copilot honors both `.agents/skills` and `.github/skills`), so every
22
+ // entry is a list.
23
+ const AGENTS = {
24
+ 'claude-code': { label: 'Claude Code', project: ['.claude/skills'], global: ['.claude/skills'] },
25
+ 'github-copilot': { label: 'GitHub Copilot', project: ['.agents/skills', '.github/skills'], global: ['.copilot/skills'] },
26
+ codex: { label: 'Codex', project: ['.codex/skills'], global: ['.codex/skills'] },
27
+ cursor: { label: 'Cursor', project: ['.cursor/skills'], global: ['.cursor/skills'] },
28
+ };
29
+
30
+ function resolveAgents(id) {
31
+ if (id === 'all') return Object.keys(AGENTS);
32
+ return AGENTS[id] ? [id] : null;
33
+ }
34
+
35
+ function parseArgs(argv) {
36
+ const opts = { agent: 'claude-code', global: false, list: false, help: false, skipUpstream: false, skills: [] };
37
+ for (let i = 0; i < argv.length; i++) {
38
+ const a = argv[i];
39
+ if (a === '--global' || a === '-g') opts.global = true;
40
+ else if (a === '--list' || a === '-l') opts.list = true;
41
+ else if (a === '--help' || a === '-h') opts.help = true;
42
+ else if (a === '--skip-upstream') opts.skipUpstream = true;
43
+ else if (a === '--agent') opts.agent = argv[++i];
44
+ else if (a.startsWith('--agent=')) opts.agent = a.slice('--agent='.length);
45
+ else if (a.startsWith('-')) {
46
+ console.error(`Unknown option: ${a} (try --help)`);
47
+ process.exit(1);
48
+ } else opts.skills.push(a);
49
+ }
50
+ return opts;
51
+ }
52
+
53
+ function readDescription(skillDir) {
54
+ try {
55
+ const md = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
56
+ const fm = md.match(/^---\n([\s\S]*?)\n---/);
57
+ if (!fm) return '';
58
+ const d = fm[1].match(/^description:\s*(.*)$/m);
59
+ return d ? d[1].replace(/^["']|["']$/g, '').trim() : '';
60
+ } catch {
61
+ return '';
62
+ }
63
+ }
64
+
65
+ function availableSkills() {
66
+ if (!fs.existsSync(SKILLS_ROOT)) return [];
67
+ return fs
68
+ .readdirSync(SKILLS_ROOT, { withFileTypes: true })
69
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(SKILLS_ROOT, e.name, 'SKILL.md')))
70
+ .map((e) => e.name)
71
+ .sort();
72
+ }
73
+
74
+ function readUpstreamManifest() {
75
+ try {
76
+ const m = JSON.parse(fs.readFileSync(UPSTREAM_MANIFEST, 'utf8'));
77
+ return Array.isArray(m.sources) ? m.sources : [];
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+
83
+ // Fetch a repo tarball at a pinned ref, extract it once, and copy the requested skill
84
+ // dirs into every directory in `bases`. Best-effort: any failure (offline, no `tar`,
85
+ // missing skill) warns and returns false rather than aborting the Linchpin install.
86
+ async function installUpstreamSource(source, bases) {
87
+ const { repo, ref, skills = [] } = source;
88
+ if (!repo || !ref || !skills.length) return false;
89
+
90
+ const url = `https://codeload.github.com/${repo}/tar.gz/${ref}`;
91
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lp-skills-'));
92
+ const tarPath = path.join(tmp, 'src.tar.gz');
93
+ const extractDir = path.join(tmp, 'x');
94
+ fs.mkdirSync(extractDir);
95
+
96
+ try {
97
+ const res = await fetch(url);
98
+ if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
99
+ fs.writeFileSync(tarPath, Buffer.from(await res.arrayBuffer()));
100
+
101
+ // System tar handles gunzip + long paths + pax headers robustly (bsdtar/gnutar).
102
+ execFileSync('tar', ['-xzf', tarPath, '-C', extractDir], { stdio: ['ignore', 'ignore', 'pipe'] });
103
+
104
+ // GitHub tarballs wrap everything in a single top-level dir (`<repo>-<ref>`); find it
105
+ // rather than reconstruct its name.
106
+ const topdir = fs.readdirSync(extractDir).find((n) => fs.statSync(path.join(extractDir, n)).isDirectory());
107
+ if (!topdir) throw new Error('unexpected tarball layout (no top-level dir)');
108
+
109
+ let count = 0;
110
+ for (const name of skills) {
111
+ const from = path.join(extractDir, topdir, 'skills', name);
112
+ if (!fs.existsSync(path.join(from, 'SKILL.md'))) {
113
+ console.warn(` ! ${repo}:${name} not found at ${ref} — skipped`);
114
+ continue;
115
+ }
116
+ for (const base of bases) {
117
+ const dest = path.join(base, name);
118
+ fs.rmSync(dest, { recursive: true, force: true });
119
+ fs.cpSync(from, dest, { recursive: true });
120
+ }
121
+ console.log(` ✓ ${name} (${repo})`);
122
+ count++;
123
+ }
124
+ return count > 0;
125
+ } catch (err) {
126
+ console.warn(` ! Skipped base layer from ${repo}: ${err.message}`);
127
+ console.warn(` (Linchpin skills installed fine. Re-run online, or use --skip-upstream to silence.)`);
128
+ return false;
129
+ } finally {
130
+ fs.rmSync(tmp, { recursive: true, force: true });
131
+ }
132
+ }
133
+
134
+ function help() {
135
+ console.log(
136
+ `
137
+ @linchpinagency/skills — install Linchpin AI agent skills
138
+
139
+ Linchpin skills are portable, cross-project tooling. By default the installer also vendors
140
+ a pinned base layer of upstream WordPress/agent-skills (generic WordPress expertise)
141
+ alongside them.
142
+
143
+ Usage:
144
+ npx @linchpinagency/skills [skills...] [options]
145
+
146
+ Arguments:
147
+ skills One or more Linchpin skill names to install (default: all)
148
+
149
+ Options:
150
+ -l, --list List available skills (Linchpin + pinned base layer) and exit
151
+ -g, --global Install to the user-global skills dir instead of the project
152
+ --agent <id> Target agent: claude-code (default) | github-copilot | codex | cursor
153
+ | all (installs into every agent's directory)
154
+ --skip-upstream Install only Linchpin skills; don't vendor the upstream base layer
155
+ -h, --help Show this help
156
+
157
+ Examples:
158
+ npx @linchpinagency/skills # Linchpin skills + base layer -> ./.claude/skills
159
+ npx @linchpinagency/skills wp-studio-cli # one Linchpin skill (+ base layer)
160
+ npx @linchpinagency/skills --skip-upstream # Linchpin skills only
161
+ npx @linchpinagency/skills --agent github-copilot
162
+ npx @linchpinagency/skills --agent all # every agent dir in this project
163
+ npx @linchpinagency/skills --global # -> ~/.claude/skills
164
+ `.trimStart()
165
+ );
166
+ }
167
+
168
+ async function main() {
169
+ const opts = parseArgs(process.argv.slice(2));
170
+ if (opts.help) return help();
171
+
172
+ const all = availableSkills();
173
+ const sources = readUpstreamManifest();
174
+
175
+ if (opts.list) {
176
+ console.log('Linchpin skills (portable, cross-project):\n');
177
+ if (!all.length) console.log(' (none found in this package)');
178
+ for (const name of all) {
179
+ const desc = readDescription(path.join(SKILLS_ROOT, name));
180
+ console.log(` ${name}${desc ? ` — ${desc}` : ''}`);
181
+ }
182
+ for (const s of sources) {
183
+ console.log(`\nBase layer — ${s.repo} @ ${String(s.ref).slice(0, 12)} (${s.license || 'see repo'}):\n`);
184
+ for (const name of s.skills || []) console.log(` ${name}`);
185
+ }
186
+ console.log('\nThe base layer is fetched at install time unless --skip-upstream is passed.');
187
+ return;
188
+ }
189
+
190
+ const agentIds = resolveAgents(opts.agent);
191
+ if (!agentIds) {
192
+ console.error(`Unknown agent "${opts.agent}". Known: ${Object.keys(AGENTS).join(', ')}, all`);
193
+ process.exit(1);
194
+ }
195
+
196
+ // One flat list of destination dirs across every selected agent.
197
+ const bases = agentIds.flatMap((id) =>
198
+ (opts.global ? AGENTS[id].global : AGENTS[id].project).map((rel) =>
199
+ opts.global ? path.join(os.homedir(), rel) : path.join(process.cwd(), rel)
200
+ )
201
+ );
202
+
203
+ const wanted = opts.skills.length ? opts.skills : all;
204
+ const unknown = wanted.filter((s) => !all.includes(s));
205
+ if (unknown.length) {
206
+ console.error(`Unknown skill(s): ${unknown.join(', ')}`);
207
+ console.error('Run with --list to see available skills.');
208
+ process.exit(1);
209
+ }
210
+ if (!wanted.length && !sources.length) return console.log('No skills to install.');
211
+
212
+ for (const base of bases) {
213
+ fs.mkdirSync(base, { recursive: true });
214
+ for (const name of wanted) {
215
+ const dest = path.join(base, name);
216
+ fs.rmSync(dest, { recursive: true, force: true });
217
+ fs.cpSync(path.join(SKILLS_ROOT, name), dest, { recursive: true });
218
+ console.log(`✓ ${name} -> ${dest}`);
219
+ }
220
+ }
221
+ const labels = agentIds.map((id) => AGENTS[id].label).join(', ');
222
+ console.log(`\nInstalled ${wanted.length} Linchpin skill(s) for ${labels}.`);
223
+
224
+ if (!opts.skipUpstream && sources.length) {
225
+ console.log('\nVendoring pinned base layer (upstream WordPress/agent-skills):');
226
+ for (const s of sources) await installUpstreamSource(s, bases);
227
+ console.log('\nTip: --skip-upstream installs Linchpin skills only.');
228
+ }
229
+ }
230
+
231
+ main();
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@linchpinagency/skills",
3
+ "version": "0.1.0",
4
+ "description": "Linchpin's library of reusable AI agent skills for WordPress projects.",
5
+ "type": "module",
6
+ "bin": {
7
+ "skills": "bin/install.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "skills",
12
+ "upstream.json",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "scripts": {
19
+ "list": "node bin/install.mjs --list",
20
+ "validate": "node scripts/validate-skills.mjs",
21
+ "test": "npm run validate",
22
+ "prepublishOnly": "npm run validate"
23
+ },
24
+ "keywords": [
25
+ "claude",
26
+ "claude-code",
27
+ "github-copilot",
28
+ "agent-skills",
29
+ "wordpress",
30
+ "wordpress-studio"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/linchpin/skills.git"
35
+ },
36
+ "homepage": "https://github.com/linchpin/skills#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/linchpin/skills/issues"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "license": "UNLICENSED"
44
+ }