Skip to content

feat: skills command installs each CLI's Claude Code skills; deprecate plugin (#201) - #203

Merged
garretpremo merged 6 commits into
devfrom
issues/201-cli-skills
Nov 5, 2026
Merged

garretpremo merged 6 commits into
devfrom
issues/201-cli-skills

Conversation

@garretpremo

@garretpremo garretpremo commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #201

Summary

A new skills command installs each CLI's Claude Code skills as personal skills, including skills the CLI defines itself. setup offers to install them, and a quiet notice says when they've changed. plugin is deprecated.

What changes

skills command (src/commands/skills/, src/skills/)

  • skills install [names...] [--force], update, status [--json] and uninstall install into <claude-dir>/skills/<cli>-<skill>/, where <claude-dir> is $CLAUDE_CONFIG_DIR or ~/.claude.
  • Sources:
    • Built-in write-routine for every CLI, and setup-api for the bare apijack CLI.
    • The CLI's own skills from .apijack/skills/ (shared binary; requires name in .apijack.json, otherwise ignored with a warning) or createCli({ skillsDir }).
    • A same-named CLI skill replaces a built-in. skills.builtin: false / builtinSkills: false drops the built-ins.
  • Rendering:
    • {{cli}} in .md files becomes the CLI's name, and the name: frontmatter is rewritten to the install name.
    • Frontmatter with a BOM, CRLF, an empty block or no trailing newline keeps its description.
    • Symlinks are followed; a link back to a parent directory is skipped, and a broken link fails loudly.
  • Ownership:
    • A .apijack-skill.json marker plus the expected directory name decide what this CLI owns. Install, update and uninstall never touch anything else (--force for install only), including copies of an installed skill.
    • Status values: not installed, up to date, outdated, conflict, orphaned.
  • State: ~/.apijack/skills-state.json per CLI records a declined offer, the last notice time, and which skills existed at the last install. update only adds skills that are new since then, never ones the user left out. Hand-edited, malformed state is ignored.

Wiring

  • Setup offer: setup and the first-run setup offer to install, but only in a TTY, when the Claude dir exists, when nothing is installed yet, and when the user hasn't declined before.
  • Out-of-date notice: shown at most once a day, on stderr. It's never shown when stdout or stderr isn't a TTY, with --json, -o or --quiet, for skills, mcp or routine run, or when nothing is installed.
  • Name collisions: the built-in skills is registered last, so an API group or project command named (or aliased) skills wins; that command then needs the first-run setup like any other.
  • Post-update refresh: updates refresh installed skills through APIJACK_REFRESH_SKILLS, which run() handles before reading argv. Passing skills update as arguments would have gone to an API's own skills group.
  • Names: a CLI name with no letter or digit (createCli name / programName, or .apijack.json name) now fails at startup. Non-ASCII names are fine.
  • routine dir: new command that prints the routines directory.

Built-in skills

  • write-routine and setup-api use only CLI commands: -o routine-step for discovery, routine dir, routine validate, {{cli}} everywhere. No MCP tool references; a test enforces this.

Upgrading from the old plugin

  • plugin install no longer ships skills, and every upgrade path runs it. Older versions' upgrade and update prompt spawn <cli> plugin install directly, and this version re-runs it after updating.
  • So the migration lives in plugin install. Before clearing the plugin's skills, it installs them as personal skills when the plugin still holds skills, this CLI has never installed any itself, and the user hasn't declined.
  • The plugin keeps its copies when that install fails, when any skill was skipped (a conflict), when the CLI ships no skills, or when an API group or project command owns the skills name.
  • The leftover-plugin warning shows only while the plugin still holds skills, and says that uninstalling it also removes its MCP server.
  • Skills and plugin paths use $HOME before os.homedir(). Bun fixes homedir() at startup, so tests that pointed HOME at a temp dir were reading and writing the real ~/.apijack.

plugin deprecated

Docs

  • "Claude Code Skills" section, routine dir, .apijack/skills/ and the plugin deprecation in CLAUDE.md, AGENTS.md, GEMINI.md and README.md.
  • Wiki pages to update at release: Building-a-CLI, Project-Mode, MCP-Server-Integration, CLI-Command-Reference.

Acceptance criteria

  • skills install / update / status / uninstall behave as described, including ownership and conflicts
  • Project skills and {{cli}} templating work on the shared binary and with createCli({ skillsDir })
  • The setup offer and the out-of-date notice follow the rules above
  • An API group or project command named skills wins over the built-in, and the CLI still starts
  • routine dir prints the routines directory in project and global mode
  • Built-in skills contain no MCP tool references
  • plugin is hidden from bare help, marked deprecated in --help, and warns when run
  • Upgrading with the old plugin installed moves the user onto personal skills instead of losing them

Test plan

  • bun test: 1465 pass, 0 fail (nothing under the real ~/.apijack or ~/.claude touched)
  • bun run lint: 0 errors
  • bunx tsc --noEmit -p .: no new errors (same 9 pre-existing files as dev)
  • Unit tests under tests/skills/ and src/commands/skills/; wiring in tests/cli-builder.test.ts (skills wiring, plugin deprecation); shared binary in tests/bin-apijack.integration.test.ts
  • Manual, the reviewer's scenario: temp HOME, stub claude, an old plugin holding skills/write-routine/, and APIJACK_SKIP_UPDATE=1 bun bin/apijack.ts plugin install (what v1.18's updater runs). It installs apijack-setup-api and apijack-write-routine and then removes the plugin's copies. With a conflicting ~/.claude/skills/apijack-write-routine it skips that skill and the plugin keeps its copies. With an unwritable skills dir it keeps the copies and says why.
  • Manual, in a pty: the setup offer installs <cli>-write-routine and prints the reload hint; the notice fires once, is throttled on rerun and stays silent when piped; the first-run offer asks and remembers a decline

…e plugin

`<cli> skills install | update | status | uninstall` installs the CLI's
Claude Code skills as personal skills in `~/.claude/skills/<cli>-<skill>/`
(or under `$CLAUDE_CONFIG_DIR`). Skills come from apijack's built-ins
(`write-routine` for every CLI, `setup-api` for the bare apijack CLI) plus
the CLI's own: `.apijack/skills/` on the shared binary (requires `name` in
`.apijack.json`) or `createCli({ skillsDir })`. A same-named CLI skill
replaces a built-in, and `skills.builtin: false` drops them. `{{cli}}` in
`.md` files becomes the CLI's name.

Each install carries an ownership marker; install, update and uninstall
never touch a directory this CLI didn't install. `setup` offers to install
the skills, and a once-a-day notice (interactive terminals only) says when
`skills update` has something to refresh. Updates refresh installed skills.

Also:
- `routine dir` prints where routine files live
- `write-routine` and `setup-api` use CLI commands only (no MCP tools)
- `plugin` is deprecated: hidden from the bare listing, marked
  `(deprecated)` in --help, warns when run, and `plugin install` no
  longer copies skills
- the built-in `skills` command is registered last, so an API or project
  command named (or aliased) `skills` wins
- A CLI name (createCli `name` / `programName`, or `.apijack.json` `name`)
  with no letter or digit now fails at startup instead of producing an
  empty prefix like `-write-routine`. Non-ASCII names are fine.
- When an API group or project command owns the `skills` name, running it
  goes through the first-run setup like any other API command.
- SKILL.md frontmatter with a BOM, CRLF, an empty block or a closing `---`
  at EOF keeps its description when the name is rewritten.
- Symlinked skill directories and files are followed. A link back to a
  parent directory is skipped, and a broken link fails loudly.
- `skills update` and `skills uninstall` say which directory they used.
- The post-update refresh no longer tells users to uninstall the old
  plugin right before re-installing it.
@github-actions github-actions Bot added the needs review Open PR awaiting review label Nov 5, 2026
`plugin install` no longer ships skills, and an upgrade re-runs it, so a
user of the old plugin would lose their skills on upgrade. The
post-update refresh now installs them the new way when the old plugin is
present and this CLI has never installed skills itself (and the user
hasn't declined). A CLI whose `skills` name belongs to an API group or
project command isn't migrated, since it couldn't manage the result.

If that refresh fails, the update doesn't re-run `plugin install`, so
the plugin keeps its skills. The "old plugin is still installed" warning
is dropped only when a plugin install actually follows.

Also: skills and plugin paths use $HOME before os.homedir(). Bun fixes
homedir() at startup, so tests that pointed HOME at a temp dir were
reading and writing the real ~/.apijack.
@garretpremo
garretpremo changed the base branch from issues/200-disable-commands to dev November 5, 2026 19:51
@garretpremo garretpremo added review in progress Review is actively underway and removed needs review Open PR awaiting review labels Nov 5, 2026

@garretpremo garretpremo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review by claude — generated by the review-issue skill. Treat as advisory; a human still owns the merge decision.

The skills command, sources, templating, ownership, setup offer, notice, routine dir, the rewritten built-in skills and the plugin deprecation all meet #201's criteria, and CI is green (locally 1455/1456, where the one failure is tests/plugin/paths.test.ts checking that the checkout path contains apijack, which a /tmp worktree doesn't). One problem blocks the merge: the upgrade migration doesn't run on the one upgrade it's meant for, so users of the old plugin lose their skills.

Blocking:

  • Old-plugin users still lose their skills when they upgrade to this release. The steps after an upgrade run in the old process. In every published version (v1.18.0 on main has no post-update.ts), src/updater.ts and src/commands/upgrade/upgrade.ts just spawn <cli> plugin install, without APIJACK_REFRESH_SKILLS. The child process runs this PR's code, and its plugin install deletes <pluginDir>/skills, runs claude plugin install apijack@apijack, and installs nothing in their place. The new refreshAfterUpdate / skillsAfterUpdateAction path only runs one release later, after the skills are already gone. I reproduced it with a temp HOME, a stub claude on PATH, and a plugin marketplace that had skills/write-routine/. Running APIJACK_SKIP_UPDATE=1 bun bin/apijack.ts plugin install the way v1.18's updater does printed only the deprecation line. Afterwards the plugin's skills/ was gone, ~/.claude/skills didn't exist, and skills status showed both skills as not installed. The same setup with APIJACK_REFRESH_SKILLS=then-plugin-install did install them, so the migration logic works but nothing on the real upgrade path reaches it. This makes the PR's last acceptance criterion untrue, along with the docs line "An update installs the skills the new way for anyone who has the old plugin installed … so nobody loses them". The skills wiring test "an upgrade moves old-plugin users onto personal skills" only covers the env-var path. A likely fix is to move the migration into plugin install, since that's the entry point old versions call. Before clearing <pluginDir>/skills, it would install personal skills when: the plugin had skills, this CLI owns no personal installs, seen is unset, the user hasn't declined, and skills isn't taken by an API group or project command. If that install fails, it should leave the plugin's skills in place. Please add a test that runs plugin install with no APIJACK_REFRESH_SKILLS set.
Non-blocking observations
  • LEFTOVER_PLUGIN_WARNING still says the plugin "may duplicate these skills" after the new plugin install has emptied its skills/. It then suggests claude plugin uninstall apijack@apijack, which also removes the MCP server registration, and the PR says there's no clean replacement for that yet (#194). Consider mentioning that the plugin also provides the MCP server, or showing the warning only when <pluginDir>/skills still exists.
  • ownedInstalls() (src/skills/store.ts) filters on entry.isDirectory(), which is false for a symlink, but skillStatuses() follows symlinks through existsSync / readMarker. So an install directory that's a symlink (e.g. managed by dotfiles) shows up to date in skills status. Meanwhile uninstall skips it, and the notice and update treat the CLI as having nothing installed.
  • If every name passed to skills install is unknown (e.g. skills install write-routin), nothing is installed but seen is still set to every source. After that, update treats every skill as deliberately left out, and skillsAfterUpdateAction won't migrate because seen is no longer undefined. Consider recording seen only when the install wrote something, or when all the names were valid.
  • loadSources() runs on every eligible interactive command while installed skills are up to date, because the throttle only moves forward when the notice actually prints. So a project skill warning (e.g. a directory name with uppercase letters) is printed on stderr on every command. Fail-loudly may be intended, but it gets noisy.
Nitpicks
  • skills/write-routine/SKILL.md still has the heading # Writing apijack Routines; # Writing {{cli}} Routines would match the rest of the templating.

@garretpremo garretpremo added changes requested Review found blocking issues and removed review in progress Review is actively underway labels Nov 5, 2026
…ade path reaches

Review of #203 found the migration never ran on the real upgrade: the
steps after an upgrade run in the old process, and every published
version just spawns `<cli> plugin install`, which now clears the
plugin's skills. The migration therefore lives in `plugin install`:

- Before clearing the plugin's skills, it installs them as personal
  skills when the plugin still holds skills, this CLI has never
  installed any itself, and the user hasn't declined.
- The plugin keeps its copies when that install fails, when any skill
  was skipped (a conflict), when the CLI ships no skills, or when the
  `skills` name belongs to an API group or project command.
- The post-update refresh goes back to only updating installed skills.

Also from the review:
- the leftover-plugin warning shows only while the plugin still holds
  skills, and says uninstalling it also removes its MCP server
- symlinked install directories are never treated as owned
- `skills install` with only unknown names doesn't record `seen`
- the daily notice loads skill sources without repeating warnings
- write-routine's heading uses `{{cli}}`
@garretpremo garretpremo added review in progress Review is actively underway and removed changes requested Review found blocking issues labels Nov 5, 2026

@garretpremo garretpremo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review by claude — generated by the review-issue skill. Treat as advisory; a human still owns the merge decision.

2b6e3d5 fixes the blocker from the last review. The migration now runs inside plugin install, which is the command every published version's upgrade calls, so users of the old plugin no longer lose their skills. All of #201's criteria and this PR's extra upgrade criterion are met, and CI is green.

I re-ran last round's reproduction (temp HOME, a stub claude on PATH, a plugin with skills/write-routine and skills/setup-api) with APIJACK_SKIP_UPDATE=1 bun bin/apijack.ts plugin install and no APIJACK_REFRESH_SKILLS:

  • Clean migration: installs apijack-setup-api and apijack-write-routine, then clears the plugin's skills/. skills status shows both as up to date, and seen is recorded.
  • ~/.claude/skills is a file: prints Installing Claude Code skills failed (ENOTDIR …); keeping the plugin's copies.. The plugin's skills/ stays, and no seen is written.
  • Declined earlier: no personal skills are installed and the plugin's copies are cleared, as intended.

All of last round's non-blocking items are fixed too:

  • The leftover warning is reworded and only shows while the plugin still holds skills.
  • Symlinked installs count as conflict, not as owned.
  • seen isn't recorded when every name was a typo.
  • The notice no longer repeats project-skill warnings on every run.
  • The write-routine heading is templated.

Locally bun test gives 1464 pass, 1 fail. The failure is the same tests/plugin/paths.test.ts check that the checkout path contains apijack, which a /tmp worktree doesn't.

Non-blocking observations
  • The PR description is out of date after 2b6e3d5. Under "Upgrading from the old plugin" it still says the post-update refresh does the migration, that APIJACK_REFRESH_SKILLS=then-plugin-install tells the child a plugin install follows, and that a failed refresh skips plugin install. The commit removed all three. The test-plan count (1456) is also stale. Worth refreshing, since the release notes link back here.
  • The migrateSkills glue in createCli has no test. That's the closure in src/cli-builder.ts:671-682, including the !skillsRegistered → keep-copies branch. Each piece is unit-tested (pluginInstallAction in tests/plugin/register.test.ts, skillsMigrateFromPluginAction in skills.test.ts), and the manual run above shows the path works. But last round's bug was exactly this kind: the logic worked and the real entry point never reached it. A cli-builder wiring test that runs plugin install with installPlugin / claude stubbed would guard that. One case should have an API group named skills and assert the plugin keeps its copies.
  • A user who declined loses the plugin's copies without a message. If they declined the setup offer earlier, the next plugin install clears the plugin's skills and prints only the deprecation line and "installed". Declining is a clear enough signal, but a one-line note (e.g. "removed the old plugin's copies of the skills; run skills install to get them back") would make it visible. Related: the doc line in CLAUDE.md / AGENTS.md / GEMINI.md / README.md says the migration covers "anyone who hasn't installed them through skills install yet", without mentioning the decline exception.

@garretpremo garretpremo added first pass reviewed Review passed with no blocking issues and removed review in progress Review is actively underway labels Nov 5, 2026

@garretpremo garretpremo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Final review by claude — generated by the final-review skill. CI is green, no blockers, soak window elapsed. Marking approved and merging.

Approved.

Outstanding observations from first-pass review

  • The PR description is out of date after 2b6e3d5. Under "Upgrading from the old plugin" it still says the post-update refresh does the migration, that APIJACK_REFRESH_SKILLS=then-plugin-install tells the child a plugin install follows, and that a failed refresh skips plugin install. The commit removed all three. The test-plan count (1456) is also stale. Worth refreshing, since the release notes link back here.
  • The migrateSkills glue in createCli has no test. That's the closure in src/cli-builder.ts:671-682, including the !skillsRegistered → keep-copies branch. Each piece is unit-tested (pluginInstallAction in tests/plugin/register.test.ts, skillsMigrateFromPluginAction in skills.test.ts), and the manual run above shows the path works. But last round's bug was exactly this kind: the logic worked and the real entry point never reached it. A cli-builder wiring test that runs plugin install with installPlugin / claude stubbed would guard that. One case should have an API group named skills and assert the plugin keeps its copies.
  • A user who declined loses the plugin's copies without a message. If they declined the setup offer earlier, the next plugin install clears the plugin's skills and prints only the deprecation line and "installed". Declining is a clear enough signal, but a one-line note (e.g. "removed the old plugin's copies of the skills; run skills install to get them back") would make it visible. Related: the doc line in CLAUDE.md / AGENTS.md / GEMINI.md / README.md says the migration covers "anyone who hasn't installed them through skills install yet", without mentioning the decline exception.

@garretpremo
garretpremo merged commit 125ef54 into dev Nov 5, 2026
10 checks passed
@garretpremo garretpremo added approved PR has been fully approved and is ready to merge and removed first pass reviewed Review passed with no blocking issues labels Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved PR has been fully approved and is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant