@ahood/cli 0.3.6 → 0.4.1

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.
package/README.md CHANGED
@@ -1,31 +1,158 @@
1
1
  # ahood
2
2
 
3
- CLI for [ahood](https://ahood.vercel.app), a registry for installing and publishing Claude Code skills.
3
+ **The command-line client for [ahood](https://ahood.vercel.app) — a registry for installing and publishing [Claude Code](https://claude.com/claude-code) Skills.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@ahood/cli.svg?color=blue)](https://www.npmjs.com/package/@ahood/cli)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@ahood/cli.svg)](https://www.npmjs.com/package/@ahood/cli)
7
+ [![CI](https://github.com/AlexKay28/ahood-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/AlexKay28/ahood-cli/actions/workflows/ci.yml)
8
+ [![License: MIT](https://img.shields.io/npm/l/@ahood/cli.svg)](LICENSE)
9
+ [![Node.js](https://img.shields.io/node/v/@ahood/cli.svg)](package.json)
10
+
11
+ `ahood` is `npm` for Claude Code Skills: publish a skill folder to the registry with one command, install anyone else's with another, and pin exact versions in a lockfile so a team (or a fleet of agents) all run the same thing. It's built to be driven by humans and AI agents equally — every command has a `--json` mode, exit codes are stable and documented, and the whole thing works headlessly with a single environment variable.
12
+
13
+ ```
14
+ $ ahood skill search pdf
15
+ alice/pdf-tools Merge, split, and compress PDFs from the command line
16
+ bob/pdf-form-filler Fill PDF form fields from a JSON or CSV data source
17
+
18
+ $ ahood skill add alice/pdf-tools
19
+ Installed alice/pdf-tools@1.4.0 to .claude/skills/alice/pdf-tools
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Contents
25
+
26
+ - [Quick start](#quick-start)
27
+ - [Install](#install)
28
+ - [Why ahood](#why-ahood)
29
+ - [Configuration](#configuration)
30
+ - [Usage examples](#usage-examples)
31
+ - [Commands](#commands)
32
+ - [Exit codes](#exit-codes)
33
+ - [Shell completion](#shell-completion)
34
+ - [Using ahood from an AI agent](#using-ahood-from-an-ai-agent)
35
+ - [Development](#development)
36
+ - [License](#license)
4
37
 
5
38
  ## Quick start
6
39
 
7
40
  ```
8
41
  npx @ahood/cli@latest login
9
- npx @ahood/cli@latest search <something>
10
- npx @ahood/cli@latest add <owner>/<skill>
42
+ npx @ahood/cli@latest skill search <something>
43
+ npx @ahood/cli@latest skill add <owner>/<skill>
11
44
  ```
12
45
 
46
+ That's it — `login` opens a device-code flow in your browser and stores a token locally; every command after that just works.
47
+
13
48
  ## Install
14
49
 
50
+ Run it on demand with `npx`, no install step required:
51
+
15
52
  ```
16
53
  npx @ahood/cli@latest <command>
17
54
  ```
18
55
 
19
- or install it globally, so the plain `ahood` command works without `npx`:
56
+ Or install it globally so the plain `ahood` command works everywhere:
20
57
 
21
58
  ```
22
59
  npm i -g @ahood/cli
23
60
  ahood <command>
24
61
  ```
25
62
 
26
- For CI or any non-interactive environment, set `AHOOD_TOKEN` instead of running `login` -- every command checks it first.
63
+ Requires Node.js 18 or later.
64
+
65
+ ## Why ahood
66
+
67
+ - **One command to publish, one to install.** `ahood skill publish owner/skill@1.0.0 --path ./my-skill` uploads a folder containing a `SKILL.md`; `ahood skill add owner/skill` installs it into `.claude/skills/`. No registry account setup beyond `ahood login`.
68
+ - **Real versioning, not just a snapshot.** Every publish is a semver version with its own changelog (`ahood skill versions`), and installs are pinned by checksum in a lockfile (`.claude/skills.lock.json`) so `ahood skill update` only ever moves forward on purpose.
69
+ - **Mistakes are recoverable.** Published the wrong version? `ahood skill unpublish owner/skill@1.2.3` yanks just that one — existing installs keep working, new ones are warned off it — without deleting the skill's whole history.
70
+ - **Public or private, your call.** `ahood skill edit owner/skill --visibility private` scopes a skill to just you; `ahood skill list` shows both.
71
+ - **Scriptable by design.** Every read command and most write commands support `--json`; `ahood skill publish --json` emits a single structured result object instead of progress text, and exit codes (below) are stable enough to branch on in a script.
72
+ - **Headless-first.** Set `AHOOD_TOKEN` and skip `login` entirely — every command checks it first, which is what CI pipelines and AI agents both actually want.
73
+
74
+ ## Configuration
75
+
76
+ | Variable | Purpose |
77
+ | --- | --- |
78
+ | `AHOOD_TOKEN` | A personal API token (create one with `ahood token create <name>` after logging in once). When set, every command uses it instead of the stored browser-login credentials — the standard way to authenticate in CI or any non-interactive environment. |
79
+ | `AHOOD_API_URL` | Overrides the registry endpoint (default: `https://ahood.vercel.app`). Must be `https://`, except for `localhost`/`127.0.0.1` or a `.test`/`.invalid`/`.example`/`.localhost` host, which may use plain `http://` for local development. |
80
+ | `XDG_CONFIG_HOME` | If set, browser-login credentials are stored under `$XDG_CONFIG_HOME/ahood/credentials.json` instead of the default `~/.config/ahood/credentials.json`. |
81
+
82
+ Two files `ahood` reads and writes in your project, neither of which needs to be gitignored (the lockfile is meant to be committed, same as `package-lock.json`):
83
+
84
+ | Path | What's in it |
85
+ | --- | --- |
86
+ | `.claude/skills/<owner>/<skill>/` | Installed skill files. |
87
+ | `.claude/skills.lock.json` | Exact installed version + checksum per skill, written by `add`/`update`, read by every install to verify integrity. |
88
+
89
+ ## Usage examples
90
+
91
+ <details>
92
+ <summary><strong>Search, inspect, then install</strong></summary>
93
+
94
+ ```
95
+ $ ahood skill search pdf --json | jq '.[0]'
96
+ {"slug": "pdf-tools", "owner": "alice", "tagline": "Merge, split, and compress PDFs", ...}
97
+
98
+ $ ahood skill view alice/pdf-tools
99
+ alice/pdf-tools
100
+ name: PDF Tools
101
+ tagline: Merge, split, and compress PDFs from the command line
102
+ license: MIT
103
+ version: 1.4.0
104
+ ...
105
+
106
+ $ ahood skill add alice/pdf-tools@1.4.0
107
+ Installed alice/pdf-tools@1.4.0 to .claude/skills/alice/pdf-tools
108
+ ```
109
+ </details>
110
+
111
+ <details>
112
+ <summary><strong>Scaffold and publish a new skill</strong></summary>
113
+
114
+ ```
115
+ $ ahood skill init my-skill
116
+ Created ./my-skill/SKILL.md
117
+
118
+ $ ahood skill publish alice/my-skill@1.0.0 --path my-skill --name "My Skill" --tagline "Does a thing"
119
+ Created alice/my-skill -- publishing its first version now.
120
+ Uploaded alice/my-skill@1.0.0 -- processing...
121
+ Published alice/my-skill@1.0.0 (published)
122
+ ```
123
+ </details>
124
+
125
+ <details>
126
+ <summary><strong>Preview and apply updates safely</strong></summary>
27
127
 
28
- `AHOOD_API_URL` overrides the registry endpoint (defaults to `https://ahood.vercel.app`). It must be `https://`, except for `localhost`/`127.0.0.1` or an `.test`/`.invalid`/`.example`/`.localhost` host, which may use plain `http://` for local development.
128
+ ```
129
+ $ ahood skill update --dry-run
130
+ SKILL CURRENT LATEST STATUS
131
+ alice/pdf-tools 1.4.0 1.5.0 update available
132
+ bob/pdf-form-filler 2.0.0 2.0.0 up to date
133
+
134
+ $ ahood skill update
135
+ Updated alice/pdf-tools to 1.5.0.
136
+ ```
137
+ </details>
138
+
139
+ <details>
140
+ <summary><strong>Yank a bad publish without deleting everything</strong></summary>
141
+
142
+ ```
143
+ $ ahood skill unpublish alice/pdf-tools@1.5.0 --yes
144
+ Yanked alice/pdf-tools@1.5.0. Existing lockfile pins still resolve; new installs will be warned off it.
145
+ ```
146
+ </details>
147
+
148
+ <details>
149
+ <summary><strong>CI / non-interactive use</strong></summary>
150
+
151
+ ```
152
+ export AHOOD_TOKEN=ahd_your_token_here
153
+ ahood skill publish alice/pdf-tools@1.5.1 --path ./pdf-tools --json
154
+ ```
155
+ </details>
29
156
 
30
157
  ## Commands
31
158
 
@@ -34,31 +161,40 @@ For CI or any non-interactive environment, set `AHOOD_TOKEN` instead of running
34
161
  changing COMMANDS_HELP to regenerate it. -->
35
162
 
36
163
  <!-- COMMANDS_TABLE_START -->
164
+ ### Account
165
+
37
166
  | Command | What it does |
38
167
  | --- | --- |
39
168
  | `ahood login` | Device-code browser login, stores a token locally. |
40
169
  | `ahood logout` | Removes the stored token. |
41
170
  | `ahood whoami [--json]` | Reports whether your stored token still authenticates. |
42
- | `ahood search <query> [--json] [--limit <n>]` | Search published skills. |
43
- | `ahood view\|show <owner>/<skill> [--json] [--web]` | Show a single skill's details -- tags, license, homepage, repository, dates, and more -- without installing it (alias: ahood show). |
44
- | `ahood versions <owner>/<skill> [--json]` | List a skill's published-version history -- version, changelog, size, and publish date. |
45
- | `ahood list-mine [--json]` | List your own skills, public and private. |
46
- | `ahood add <owner>/<skill>[@version]` | Install a skill into .claude/skills/, pinned in the lockfile. |
47
- | `ahood update [<owner>/<skill> ...] [--dry-run] [--json]` | Move the lockfile pin(s) forward to the latest version, for one skill or all installed skills at once. |
48
- | `ahood remove <owner>/<skill>` | Uninstall and unpin a skill (local only). |
49
- | `ahood edit <owner>/<skill> [--tagline] [--tags] [--license] [--visibility] [--homepage] [--repository]` | Update a skill you own, changing only the flags you pass. |
50
- | `ahood unpublish <owner>/<skill>[@version] [--yes]` | Delete a skill from the registry for every consumer, or yank a single version, not just your local install (prompts for confirmation unless --yes is passed). |
51
- | `ahood star <owner>/<skill>` | Star a skill. |
52
- | `ahood unstar <owner>/<skill>` | Remove your star from a skill. |
53
- | `ahood init [name]` | Scaffold a new skill folder with a minimal, valid SKILL.md. |
54
- | `ahood publish <owner>/<skill>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--json]` | Publish a new version of a skill from a folder containing SKILL.md, creating the skill first if it doesn't already exist. |
55
171
  | `ahood token create <name>\|list [--json]\|revoke <id> [--yes]` | Manage personal API tokens. |
56
172
  | `ahood completion <bash\|zsh\|fish>` | Print a shell completion script for the command names. |
173
+
174
+ ### Skill
175
+
176
+ | Command | What it does |
177
+ | --- | --- |
178
+ | `ahood skill search <query> [--json] [--limit <n>]` | Search published skills. |
179
+ | `ahood skill view\|show <owner>/<skill> [--json] [--web]` | Show a single skill's details -- tags, license, homepage, repository, dates, and more -- without installing it (alias: ahood skill show). |
180
+ | `ahood skill versions <owner>/<skill> [--json]` | List a skill's published-version history -- version, changelog, size, and publish date. |
181
+ | `ahood skill list [--json]` | List your own skills, public and private. |
182
+ | `ahood skill add <owner>/<skill>[@version]` | Install a skill into .claude/skills/, pinned in the lockfile. |
183
+ | `ahood skill update [<owner>/<skill> ...] [--dry-run] [--json]` | Move the lockfile pin(s) forward to the latest version, for one skill or all installed skills at once. |
184
+ | `ahood skill remove <owner>/<skill>` | Uninstall and unpin a skill (local only). |
185
+ | `ahood skill edit <owner>/<skill> [--tagline] [--tags] [--license] [--visibility] [--homepage] [--repository]` | Update a skill you own, changing only the flags you pass. |
186
+ | `ahood skill unpublish <owner>/<skill>[@version] [--yes]` | Delete a skill from the registry for every consumer, or yank a single version, not just your local install (prompts for confirmation unless --yes is passed). |
187
+ | `ahood skill star <owner>/<skill>` | Star a skill. |
188
+ | `ahood skill unstar <owner>/<skill>` | Remove your star from a skill. |
189
+ | `ahood skill init [name]` | Scaffold a new skill folder with a minimal, valid SKILL.md. |
190
+ | `ahood skill publish <owner>/<skill>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--json]` | Publish a new version of a skill from a folder containing SKILL.md, creating the skill first if it doesn't already exist. |
57
191
  <!-- COMMANDS_TABLE_END -->
58
192
 
59
- Run `ahood help`, `ahood help <command>`, or `ahood <command> --help` for the same reference directly in your terminal. `ahood --version` prints the installed CLI version.
193
+ Run `ahood --help`, `ahood skill --help`, or `ahood <command> --help` (also `ahood help <command>` / `ahood help skill <verb>`) for the same reference — including per-command flags and examples — directly in your terminal. `ahood --version` prints the installed CLI version.
60
194
 
61
- ### Exit codes
195
+ ## Exit codes
196
+
197
+ Stable across releases, safe to branch on in a script:
62
198
 
63
199
  | Code | Meaning |
64
200
  | --- | --- |
@@ -69,4 +205,42 @@ Run `ahood help`, `ahood help <command>`, or `ahood <command> --help` for the sa
69
205
  | `5` | Not found |
70
206
  | `6` | Network/transport error, or an upstream server (5xx) error |
71
207
 
72
- Full reference, including personal API tokens, CI usage, the public REST API, and the MCP server: **https://ahood.vercel.app/docs**
208
+ ## Shell completion
209
+
210
+ ```
211
+ # bash
212
+ ahood completion bash >> ~/.bashrc
213
+
214
+ # zsh
215
+ ahood completion zsh >> ~/.zshrc
216
+
217
+ # fish
218
+ ahood completion fish > ~/.config/fish/completions/ahood.fish
219
+ ```
220
+
221
+ ## Using ahood from an AI agent
222
+
223
+ `ahood` is built to be driven by an AI agent (Claude Code or otherwise) as comfortably as by a human at a terminal:
224
+
225
+ - **No interactive login required.** Set `AHOOD_TOKEN` once (a personal API token from `ahood token create <name>`) and every command works non-interactively.
226
+ - **Structured output everywhere.** `--json` is available on every read command and on `publish`/`update`, so an agent never has to scrape human-formatted text.
227
+ - **Predictable failure.** A stable, documented [exit code](#exit-codes) per failure class, and error messages that never leak raw upstream infrastructure details — safe to surface directly to an agent's reasoning loop.
228
+ - **A real "start here."** `ahood skill init <name>` scaffolds a valid `SKILL.md` an agent can extend, rather than requiring it to know the frontmatter format up front.
229
+
230
+ ## Development
231
+
232
+ ```
233
+ git clone https://github.com/AlexKay28/ahood-cli.git
234
+ cd ahood-cli
235
+ npm install
236
+ npm run build # compile TypeScript -> dist/
237
+ npm test # run the vitest suite
238
+ ```
239
+
240
+ If you add or change a command, update `src/help.ts`'s `COMMANDS_HELP` and then run `npm run sync-readme` to regenerate the table above — CI fails (`node scripts/sync-readme.mjs --check`) if it's out of sync.
241
+
242
+ Full reference, including personal API tokens, the public REST API, and the MCP server: **https://ahood.vercel.app/docs**
243
+
244
+ ## License
245
+
246
+ [MIT](LICENSE)
@@ -1,37 +1,61 @@
1
- import { COMMANDS_HELP, COMMAND_ALIASES } from "../help.js";
1
+ import { TOP_LEVEL_COMMANDS_HELP, SKILL_COMMANDS_HELP, COMMAND_ALIASES } from "../help.js";
2
2
  const USAGE = "Usage: ahood completion <bash|zsh|fish>";
3
- // Includes alias names (e.g. "show" for "view") alongside the primary
4
- // command names, so shell completion offers every name a user might type,
5
- // not just the ones with their own COMMANDS_HELP entry.
6
- function commandNames() {
7
- const primary = COMMANDS_HELP.map((c) => c.usage.split(" ")[1]);
3
+ // Position 1 (the word right after "ahood"): every top-level command plus
4
+ // the "skill" entity group itself.
5
+ function topLevelNames() {
6
+ return [...TOP_LEVEL_COMMANDS_HELP.map((c) => c.usage.split(" ")[1]), "skill"];
7
+ }
8
+ // Position 2+, once "skill" has been typed: every skill verb, plus alias
9
+ // names (e.g. "show" for "view") so completion offers every name a user
10
+ // might type, not just the ones with their own SKILL_COMMANDS_HELP entry.
11
+ function skillNames() {
12
+ const primary = SKILL_COMMANDS_HELP.map((c) => c.usage.split(" ")[2]);
8
13
  return [...primary, ...Object.keys(COMMAND_ALIASES)];
9
14
  }
10
15
  function bashCompletion() {
11
- const names = commandNames().join(" ");
16
+ const top = topLevelNames().join(" ");
17
+ const skill = skillNames().join(" ");
12
18
  return [
13
19
  "_ahood_completions() {",
14
20
  ' local cur="${COMP_WORDS[COMP_CWORD]}"',
15
- ` COMPREPLY=($(compgen -W "${names}" -- "$cur"))`,
21
+ ` local top_words="${top}"`,
22
+ ` local skill_words="${skill}"`,
23
+ ' if [[ "${COMP_WORDS[1]}" == "skill" && $COMP_CWORD -ge 2 ]]; then',
24
+ ' COMPREPLY=($(compgen -W "$skill_words" -- "$cur"))',
25
+ " else",
26
+ ' COMPREPLY=($(compgen -W "$top_words" -- "$cur"))',
27
+ " fi",
16
28
  "}",
17
29
  "complete -F _ahood_completions ahood",
18
30
  ].join("\n");
19
31
  }
20
32
  function zshCompletion() {
21
- const names = commandNames().join(" ");
33
+ const top = topLevelNames().join(" ");
34
+ const skill = skillNames().join(" ");
22
35
  return [
23
36
  "#compdef ahood",
24
- `_arguments '1: :(${names})'`,
37
+ "local -a top_cmds skill_cmds",
38
+ `top_cmds=(${top})`,
39
+ `skill_cmds=(${skill})`,
40
+ "case $words[2] in",
41
+ " skill)",
42
+ ' _describe "skill command" skill_cmds',
43
+ " ;;",
44
+ " *)",
45
+ ' _describe "command" top_cmds',
46
+ " ;;",
47
+ "esac",
25
48
  ].join("\n");
26
49
  }
27
50
  function fishCompletion() {
28
- return commandNames()
29
- .map((name) => `complete -c ahood -n "__fish_use_subcommand" -a "${name}"`)
30
- .join("\n");
51
+ return [
52
+ ...topLevelNames().map((name) => `complete -c ahood -n "__fish_use_subcommand" -a "${name}"`),
53
+ ...skillNames().map((name) => `complete -c ahood -n "__fish_seen_subcommand_from skill" -a "${name}"`),
54
+ ].join("\n");
31
55
  }
32
- // Command-name completion only (not per-command flags) -- still the primary
33
- // way users discover the available subcommands without reading docs, which
34
- // this CLI otherwise has no mechanism for at all.
56
+ // Position-aware command-name completion (not per-command flags) -- still
57
+ // the primary way users discover the available subcommands without reading
58
+ // docs, which this CLI otherwise has no mechanism for at all.
35
59
  export async function completion(args) {
36
60
  const shell = args[0];
37
61
  switch (shell) {
@@ -1,5 +1,5 @@
1
1
  import { apiJson } from "../http.js";
2
- export async function listMine(args = []) {
2
+ export async function listSkills(args = []) {
3
3
  const jsonOutput = args.includes("--json");
4
4
  const { skills } = await apiJson("/api/v1/skills?mine=true");
5
5
  if (jsonOutput) {
package/dist/help.js CHANGED
@@ -1,4 +1,4 @@
1
- export const COMMANDS_HELP = [
1
+ export const TOP_LEVEL_COMMANDS_HELP = [
2
2
  {
3
3
  usage: "ahood login",
4
4
  summary: "Device-code browser login, stores a token locally.",
@@ -12,59 +12,74 @@ export const COMMANDS_HELP = [
12
12
  flags: ["--json Emit a machine-readable result instead of prose."],
13
13
  },
14
14
  {
15
- usage: "ahood search <query> [--json] [--limit <n>]",
15
+ usage: "ahood token create <name>|list [--json]|revoke <id> [--yes]",
16
+ summary: "Manage personal API tokens.",
17
+ desc: "Manage personal API tokens. `create` requires an existing browser-backed session.",
18
+ flags: ["--yes (revoke only) Skip the confirmation prompt and revoke immediately."],
19
+ examples: ["ahood token create ci-runner", "ahood token list --json", "ahood token revoke <id>", "ahood token revoke <id> --yes"],
20
+ },
21
+ {
22
+ usage: "ahood completion <bash|zsh|fish>",
23
+ summary: "Print a shell completion script for the command names.",
24
+ desc: "Print a shell completion script for the command names.",
25
+ examples: ["ahood completion bash >> ~/.bashrc"],
26
+ },
27
+ ];
28
+ export const SKILL_COMMANDS_HELP = [
29
+ {
30
+ usage: "ahood skill search <query> [--json] [--limit <n>]",
16
31
  summary: "Search published skills.",
17
32
  desc: "Search published skills.",
18
33
  flags: [
19
34
  "--json Emit the raw skill objects instead of formatted lines.",
20
35
  "--limit <n> Cap the number of results.",
21
36
  ],
22
- examples: ["ahood search pdf-tools", "ahood search pdf-tools --json"],
37
+ examples: ["ahood skill search pdf-tools", "ahood skill search pdf-tools --json"],
23
38
  },
24
39
  {
25
- usage: "ahood view <owner>/<skill> [--json] [--web]",
26
- summary: "Show a single skill's details -- tags, license, homepage, repository, dates, and more -- without installing it (alias: ahood show).",
27
- desc: "Show a single skill's details (tags, license, homepage, repository, dates, etc.) without installing it. Alias: ahood show.",
40
+ usage: "ahood skill view <owner>/<skill> [--json] [--web]",
41
+ summary: "Show a single skill's details -- tags, license, homepage, repository, dates, and more -- without installing it (alias: ahood skill show).",
42
+ desc: "Show a single skill's details (tags, license, homepage, repository, dates, etc.) without installing it. Alias: ahood skill show.",
28
43
  flags: [
29
44
  "--json Emit the raw skill object instead of formatted lines.",
30
45
  "--web Open the skill's page in your browser instead of printing.",
31
46
  ],
32
47
  },
33
48
  {
34
- usage: "ahood versions <owner>/<skill> [--json]",
49
+ usage: "ahood skill versions <owner>/<skill> [--json]",
35
50
  summary: "List a skill's published-version history -- version, changelog, size, and publish date.",
36
51
  desc: "List a skill's published-version history: version, changelog, size, and publish date. Most-recent first.",
37
52
  flags: ["--json Emit the raw version objects instead of formatted text."],
38
53
  },
39
54
  {
40
- usage: "ahood list-mine [--json]",
55
+ usage: "ahood skill list [--json]",
41
56
  summary: "List your own skills, public and private.",
42
57
  desc: "List your own skills, public and private.",
43
58
  flags: ["--json Emit the raw skill objects instead of formatted lines."],
44
59
  },
45
60
  {
46
- usage: "ahood add <owner>/<skill>[@version]",
61
+ usage: "ahood skill add <owner>/<skill>[@version]",
47
62
  summary: "Install a skill into .claude/skills/, pinned in the lockfile.",
48
63
  desc: "Install a skill into .claude/skills/, pinned in the lockfile.",
49
- examples: ["ahood add alice/pdf-tools", "ahood add alice/pdf-tools@1.2.0"],
64
+ examples: ["ahood skill add alice/pdf-tools", "ahood skill add alice/pdf-tools@1.2.0"],
50
65
  },
51
66
  {
52
- usage: "ahood update [<owner>/<skill> ...] [--dry-run] [--json]",
67
+ usage: "ahood skill update [<owner>/<skill> ...] [--dry-run] [--json]",
53
68
  summary: "Move the lockfile pin(s) forward to the latest version, for one skill or all installed skills at once.",
54
69
  desc: "Move the lockfile pin(s) forward to the latest version. With no argument, updates every installed skill; one failure doesn't stop the rest.",
55
70
  flags: [
56
71
  "--dry-run Preview current vs. latest version (and the changelog for anything behind) without installing anything.",
57
72
  "--json With --dry-run, emit structured preview objects instead of a formatted table.",
58
73
  ],
59
- examples: ["ahood update --dry-run", "ahood update alice/pdf-tools --dry-run --json"],
74
+ examples: ["ahood skill update --dry-run", "ahood skill update alice/pdf-tools --dry-run --json"],
60
75
  },
61
76
  {
62
- usage: "ahood remove <owner>/<skill>",
77
+ usage: "ahood skill remove <owner>/<skill>",
63
78
  summary: "Uninstall and unpin a skill (local only).",
64
79
  desc: "Uninstall and unpin (local only).",
65
80
  },
66
81
  {
67
- usage: "ahood edit <owner>/<skill> [--tagline] [--tags] [--license] [--visibility] [--homepage] [--repository]",
82
+ usage: "ahood skill edit <owner>/<skill> [--tagline] [--tags] [--license] [--visibility] [--homepage] [--repository]",
68
83
  summary: "Update a skill you own, changing only the flags you pass.",
69
84
  desc: "Update a skill you own. Only the flags you pass are changed. Every flag also accepts " +
70
85
  "the --flag=value form (e.g. --tagline=--fast and cheap), needed when a value itself starts with --.",
@@ -76,10 +91,10 @@ export const COMMANDS_HELP = [
76
91
  "--homepage <url> The skill's homepage URL.",
77
92
  "--repository <url> The skill's source repository URL.",
78
93
  ],
79
- examples: ['ahood edit alice/pdf-tools --tagline "Merge and split PDFs"'],
94
+ examples: ['ahood skill edit alice/pdf-tools --tagline "Merge and split PDFs"'],
80
95
  },
81
96
  {
82
- usage: "ahood unpublish <owner>/<skill>[@version] [--yes]",
97
+ usage: "ahood skill unpublish <owner>/<skill>[@version] [--yes]",
83
98
  summary: "Delete a skill from the registry for every consumer, or yank a single version, not just your local install (prompts for confirmation unless --yes is passed).",
84
99
  desc: "Without @version: delete a skill from the registry for every consumer (not just your local install). " +
85
100
  "With @version (e.g. alice/foo@1.2.3): yank that single version instead of deleting the whole skill -- " +
@@ -87,26 +102,26 @@ export const COMMANDS_HELP = [
87
102
  "installs are warned off it. Prompts for a typed \"yes\" unless --yes is passed.",
88
103
  flags: ["--yes Skip the interactive confirmation, for scripts/CI."],
89
104
  },
90
- { usage: "ahood star <owner>/<skill>", summary: "Star a skill.", desc: "Star a skill." },
105
+ { usage: "ahood skill star <owner>/<skill>", summary: "Star a skill.", desc: "Star a skill." },
91
106
  {
92
- usage: "ahood unstar <owner>/<skill>",
107
+ usage: "ahood skill unstar <owner>/<skill>",
93
108
  summary: "Remove your star from a skill.",
94
109
  desc: "Remove your star from a skill.",
95
110
  },
96
111
  {
97
- usage: "ahood init [name]",
112
+ usage: "ahood skill init [name]",
98
113
  summary: "Scaffold a new skill folder with a minimal, valid SKILL.md.",
99
114
  desc: "Scaffold a new skill folder with a minimal, valid SKILL.md. Creates ./<name>/SKILL.md if a name is given, " +
100
115
  "or ./SKILL.md in the current directory otherwise. Refuses to overwrite an existing SKILL.md at the target path.",
101
- examples: ["ahood init pdf-tools", "ahood init"],
116
+ examples: ["ahood skill init pdf-tools", "ahood skill init"],
102
117
  },
103
118
  {
104
- usage: "ahood publish <owner>/<skill>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--json]",
119
+ usage: "ahood skill publish <owner>/<skill>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--json]",
105
120
  summary: "Publish a new version of a skill from a folder containing SKILL.md, creating the skill first if it doesn't already exist.",
106
121
  desc: "Publish a new version of a skill from a folder containing SKILL.md. If the skill doesn't exist yet, this " +
107
122
  "creates it first -- pass --name (required in that case) and optionally --tagline/--tags/--license/--homepage/--repository. " +
108
123
  "Processing happens server-side after upload; this command polls and reports the final published/failed status. " +
109
- "The legacy form `ahood publish <path> --owner <owner> --slug <skill> --version <x.y.z>` is still accepted. " +
124
+ "The legacy form `ahood skill publish <path> --owner <owner> --slug <skill> --version <x.y.z>` is still accepted. " +
110
125
  "Every flag also accepts the --flag=value form, needed when a value itself starts with --.",
111
126
  flags: [
112
127
  "--name <text> Required only when creating the skill on this publish.",
@@ -118,44 +133,42 @@ export const COMMANDS_HELP = [
118
133
  "--json Suppress human progress lines; print one {version,status,...} JSON object on completion (or {error} on failure).",
119
134
  ],
120
135
  examples: [
121
- "ahood publish alice/pdf-tools@1.1.0",
122
- 'ahood publish alice/pdf-tools@1.0.0 --name "PDF Tools" --tagline "Merge and split PDFs"',
123
- "ahood publish alice/pdf-tools@1.1.0 --path ./pdf-tools",
136
+ "ahood skill publish alice/pdf-tools@1.1.0",
137
+ 'ahood skill publish alice/pdf-tools@1.0.0 --name "PDF Tools" --tagline "Merge and split PDFs"',
138
+ "ahood skill publish alice/pdf-tools@1.1.0 --path ./pdf-tools",
124
139
  ],
125
140
  },
126
- {
127
- usage: "ahood token create <name>|list [--json]|revoke <id> [--yes]",
128
- summary: "Manage personal API tokens.",
129
- desc: "Manage personal API tokens. `create` requires an existing browser-backed session.",
130
- flags: ["--yes (revoke only) Skip the confirmation prompt and revoke immediately."],
131
- examples: ["ahood token create ci-runner", "ahood token list --json", "ahood token revoke <id>", "ahood token revoke <id> --yes"],
132
- },
133
- {
134
- usage: "ahood completion <bash|zsh|fish>",
135
- summary: "Print a shell completion script for the command names.",
136
- desc: "Print a shell completion script for the command names.",
137
- examples: ["ahood completion bash >> ~/.bashrc"],
138
- },
139
141
  ];
140
142
  // Exported so other consumers of the command list (e.g. shell completion) can
141
- // surface aliases without needing their own copy of this map.
143
+ // surface aliases without needing their own copy of this map. Aliases apply
144
+ // at the skill-verb level today (e.g. "show" for "view"); nothing at the
145
+ // top level currently has one.
142
146
  export const COMMAND_ALIASES = { show: "view" };
143
- export function findCommandHelp(command) {
147
+ // Two-token lookup for a skill-entity verb (findCommandHelp("skill", "search"))
148
+ // or a single-token lookup for a top-level command (findCommandHelp("whoami")).
149
+ export function findCommandHelp(command, subcommand) {
150
+ if (command === "skill" && subcommand !== undefined) {
151
+ const resolved = COMMAND_ALIASES[subcommand] ?? subcommand;
152
+ return SKILL_COMMANDS_HELP.find((c) => c.usage.startsWith(`ahood skill ${resolved} `) || c.usage === `ahood skill ${resolved}`);
153
+ }
144
154
  const resolved = COMMAND_ALIASES[command] ?? command;
145
- return COMMANDS_HELP.find((c) => c.usage.startsWith(`ahood ${resolved} `) || c.usage === `ahood ${resolved}`);
155
+ return TOP_LEVEL_COMMANDS_HELP.find((c) => c.usage.startsWith(`ahood ${resolved} `) || c.usage === `ahood ${resolved}`);
146
156
  }
147
- // Renders a COMMANDS_HELP entry's usage line with any known aliases folded in
148
- // after the primary command name, e.g. "ahood view <owner>/<skill> ..." ->
149
- // "ahood view|show <owner>/<skill> ...". Used by the top-level --help listing
157
+ // Renders a CommandHelp entry's usage line with any known aliases folded in
158
+ // after the primary command name, e.g. "ahood skill view <owner>/<skill> ..."
159
+ // -> "ahood skill view|show <owner>/<skill> ...". Used by the --help listings
150
160
  // so aliases (like "show" for "view") aren't invisible to users who only
151
- // skim `ahood --help`.
161
+ // skim `ahood --help` / `ahood skill --help`. The alias-carrying word is
162
+ // parts[2] for a skill-entity usage string ("ahood skill <verb> ...") and
163
+ // parts[1] for a top-level one ("ahood <command> ...").
152
164
  export function usageWithAliases(entry) {
153
165
  const parts = entry.usage.split(" ");
154
- const name = parts[1];
166
+ const idx = parts[1] === "skill" ? 2 : 1;
167
+ const name = parts[idx];
155
168
  const aliases = Object.keys(COMMAND_ALIASES).filter((alias) => COMMAND_ALIASES[alias] === name);
156
169
  if (aliases.length === 0)
157
170
  return entry.usage;
158
- parts[1] = [name, ...aliases].join("|");
171
+ parts[idx] = [name, ...aliases].join("|");
159
172
  return parts.join(" ");
160
173
  }
161
174
  export function formatCommandHelp(entry) {
@@ -166,21 +179,43 @@ export function formatCommandHelp(entry) {
166
179
  lines.push("", "Examples:", ...entry.examples.map((e) => ` ${e}`));
167
180
  return lines.join("\n");
168
181
  }
182
+ // `ahood skill --help` -- the group-level listing for every skill verb.
183
+ export function formatSkillHelp() {
184
+ const width = Math.max(...SKILL_COMMANDS_HELP.map((c) => usageWithAliases(c).length));
185
+ const lines = SKILL_COMMANDS_HELP.map((c) => ` ${usageWithAliases(c).padEnd(width + 2)}${c.summary}`);
186
+ return [
187
+ "ahood skill -- manage skills in the ahood registry",
188
+ "",
189
+ "Commands:",
190
+ ...lines,
191
+ "",
192
+ "Run `ahood skill <command> --help` for a single command's flags and examples.",
193
+ ].join("\n");
194
+ }
195
+ // `ahood --help` -- the top-level listing: account commands rendered in
196
+ // full, plus a single summary line pointing at `ahood skill --help` for
197
+ // the (much longer) skill-entity command list.
169
198
  export function formatHelp() {
170
- const width = Math.max(...COMMANDS_HELP.map((c) => usageWithAliases(c).length));
171
- const lines = COMMANDS_HELP.map((c) => ` ${usageWithAliases(c).padEnd(width + 2)}${c.summary}`);
199
+ const skillGroupUsage = "ahood skill <command>";
200
+ const skillGroupSummary = "Search, install, and publish skills -- run `ahood skill --help` for the full list.";
201
+ const width = Math.max(...TOP_LEVEL_COMMANDS_HELP.map((c) => usageWithAliases(c).length), skillGroupUsage.length);
202
+ const lines = [
203
+ ...TOP_LEVEL_COMMANDS_HELP.map((c) => ` ${usageWithAliases(c).padEnd(width + 2)}${c.summary}`),
204
+ ` ${skillGroupUsage.padEnd(width + 2)}${skillGroupSummary}`,
205
+ ];
172
206
  return [
173
207
  "ahood -- CLI for the ahood skills registry (https://ahood.vercel.app)",
174
208
  "",
175
209
  "Quick start:",
176
210
  " ahood login",
177
- " ahood search <something>",
178
- " ahood add <owner>/<skill>",
211
+ " ahood skill search <something>",
212
+ " ahood skill add <owner>/<skill>",
179
213
  "",
180
214
  "Commands:",
181
215
  ...lines,
182
216
  "",
183
217
  "Run `ahood <command> --help` (or `ahood help <command>`) for a single command's flags and examples.",
218
+ "Run `ahood skill --help` for the full list of skill commands.",
184
219
  "Run `ahood --version` to print the installed CLI version.",
185
220
  "",
186
221
  "Exit codes: 0 success, 1 general error, 2 usage/validation error,",
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
2
3
  import { login } from "./commands/login.js";
3
4
  import { logout } from "./commands/logout.js";
4
5
  import { whoami } from "./commands/whoami.js";
@@ -10,37 +11,51 @@ import { publish } from "./commands/publish.js";
10
11
  import { token } from "./commands/token.js";
11
12
  import { edit } from "./commands/edit.js";
12
13
  import { unpublish } from "./commands/unpublish.js";
13
- import { listMine } from "./commands/list-mine.js";
14
+ import { listSkills } from "./commands/list.js";
14
15
  import { star, unstar } from "./commands/star.js";
15
16
  import { view } from "./commands/view.js";
16
17
  import { versions } from "./commands/versions.js";
17
18
  import { completion } from "./commands/completion.js";
18
19
  import { init } from "./commands/init.js";
19
- import { formatHelp, formatCommandHelp, findCommandHelp } from "./help.js";
20
+ import { formatHelp, formatSkillHelp, formatCommandHelp, findCommandHelp } from "./help.js";
20
21
  import { ApiError } from "./http.js";
21
22
  import { exitCodeFor } from "./exit-code.js";
22
23
  import { CLI_NAME, CLI_VERSION } from "./version.js";
23
- const COMMANDS = {
24
- login: () => login(),
25
- logout: () => logout(),
26
- whoami,
24
+ // Every skill-entity verb, reached only as `ahood skill <verb>` -- see
25
+ // dispatchSkill() below. "show" is an alias for "view" (issue #30).
26
+ const SKILL_COMMANDS = {
27
27
  search,
28
+ view,
29
+ show: view,
30
+ versions,
31
+ list: listSkills,
28
32
  add,
29
33
  update,
30
34
  remove,
31
- publish,
32
- init,
33
- token,
34
35
  edit,
35
36
  unpublish,
36
- "list-mine": listMine,
37
37
  star,
38
38
  unstar,
39
- view,
40
- show: view, // alias -- "ahood show" is the name issue #30 proposed; same command as "view"
41
- versions,
39
+ init,
40
+ publish,
41
+ };
42
+ // Top level is now just account/auth-scoped commands (not entity-specific --
43
+ // same reasoning `gh auth login` isn't `gh account login`) plus the `skill`
44
+ // entity group. Future entities (e.g. "personality", "protocols") get their
45
+ // own entry here alongside "skill".
46
+ const COMMANDS = {
47
+ login: () => login(),
48
+ logout: () => logout(),
49
+ whoami,
50
+ token,
42
51
  completion,
52
+ skill: dispatchSkill,
43
53
  };
54
+ // Commands whose handler owns its own --help handling (at both the group
55
+ // level, e.g. `ahood skill --help`, and the per-verb level, e.g.
56
+ // `ahood skill add --help`) instead of the generic top-level interception
57
+ // in main().
58
+ const GROUP_COMMANDS = new Set(["skill"]);
44
59
  function levenshtein(a, b) {
45
60
  const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
46
61
  for (let i = 0; i <= a.length; i++)
@@ -54,10 +69,10 @@ function levenshtein(a, b) {
54
69
  }
55
70
  return dp[a.length][b.length];
56
71
  }
57
- function closestCommand(input) {
72
+ function closestOf(input, candidates) {
58
73
  let best;
59
74
  let bestDistance = Infinity;
60
- for (const name of Object.keys(COMMANDS)) {
75
+ for (const name of candidates) {
61
76
  const distance = levenshtein(input, name);
62
77
  if (distance < bestDistance) {
63
78
  bestDistance = distance;
@@ -66,6 +81,38 @@ function closestCommand(input) {
66
81
  }
67
82
  return bestDistance <= 2 ? best : undefined;
68
83
  }
84
+ function closestCommand(input) {
85
+ return closestOf(input, Object.keys(COMMANDS));
86
+ }
87
+ function closestSkillCommand(input) {
88
+ return closestOf(input, Object.keys(SKILL_COMMANDS));
89
+ }
90
+ // `ahood skill <verb> [...]` -- owns its own help handling at both the
91
+ // group level (`ahood skill` / `ahood skill --help`) and the per-verb level
92
+ // (`ahood skill <verb> --help`), since neither should fall through to
93
+ // main()'s generic top-level --help interception (see GROUP_COMMANDS).
94
+ async function dispatchSkill(args) {
95
+ const [sub, ...rest] = args;
96
+ if (!sub || sub === "--help" || sub === "-h") {
97
+ console.log(formatSkillHelp());
98
+ return;
99
+ }
100
+ const handler = SKILL_COMMANDS[sub];
101
+ if (!handler) {
102
+ console.error(`Unknown skill command: ${sub}`);
103
+ const suggestion = closestSkillCommand(sub);
104
+ if (suggestion)
105
+ console.error(`Did you mean '${suggestion}'?`);
106
+ console.error("Run `ahood skill --help` for a list of commands.");
107
+ process.exit(2);
108
+ }
109
+ if (rest.includes("--help") || rest.includes("-h")) {
110
+ const entry = findCommandHelp("skill", sub);
111
+ console.log(entry ? formatCommandHelp(entry) : formatSkillHelp());
112
+ return;
113
+ }
114
+ await handler(rest);
115
+ }
69
116
  async function main() {
70
117
  const [command, ...args] = process.argv.slice(2);
71
118
  if (command === "--version" || command === "-v") {
@@ -74,6 +121,12 @@ async function main() {
74
121
  }
75
122
  if (command === "help") {
76
123
  const sub = args[0];
124
+ if (sub === "skill") {
125
+ const verb = args[1];
126
+ const entry = verb ? findCommandHelp("skill", verb) : undefined;
127
+ console.log(entry ? formatCommandHelp(entry) : formatSkillHelp());
128
+ return;
129
+ }
77
130
  const entry = sub ? findCommandHelp(sub) : undefined;
78
131
  console.log(entry ? formatCommandHelp(entry) : formatHelp());
79
132
  return;
@@ -87,13 +140,24 @@ async function main() {
87
140
  const handler = COMMANDS[command];
88
141
  if (!handler) {
89
142
  console.error(`Unknown command: ${command}`);
90
- const suggestion = closestCommand(command);
91
- if (suggestion)
92
- console.error(`Did you mean '${suggestion}'?`);
143
+ // Every previously-flat skill command moved under `ahood skill <verb>`
144
+ // in this release -- if the typo looks like one of those, point at the
145
+ // new form specifically rather than just the generic top-level list.
146
+ const skillSuggestion = closestSkillCommand(command);
147
+ if (skillSuggestion) {
148
+ console.error(`Did you mean 'ahood skill ${skillSuggestion}'?`);
149
+ }
150
+ else {
151
+ const suggestion = closestCommand(command);
152
+ if (suggestion)
153
+ console.error(`Did you mean '${suggestion}'?`);
154
+ }
93
155
  console.error("Run `ahood help` for a list of commands.");
94
156
  process.exit(2);
95
157
  }
96
- if (args.includes("--help") || args.includes("-h")) {
158
+ // Group commands (currently just "skill") own their own --help handling
159
+ // at both the group and per-verb level -- don't intercept here.
160
+ if (!GROUP_COMMANDS.has(command) && (args.includes("--help") || args.includes("-h"))) {
97
161
  const entry = findCommandHelp(command);
98
162
  console.log(entry ? formatCommandHelp(entry) : formatHelp());
99
163
  return;
@@ -110,4 +174,19 @@ async function main() {
110
174
  process.exit(exitCodeFor(error));
111
175
  }
112
176
  }
113
- main();
177
+ // Only auto-run when this module is the actual entrypoint (i.e. invoked as
178
+ // `ahood ...` / `node dist/index.js ...`), not when it's imported by a test
179
+ // -- process.argv[1] is dist/index.js in the former case, something else
180
+ // (the test runner) in the latter. Comparing realpaths (not raw argv[1])
181
+ // is required here: npm's installed `ahood` binary is a symlink to
182
+ // dist/index.js, so process.argv[1] is the symlink's path while
183
+ // import.meta.url resolves through the symlink to the real file -- a raw
184
+ // string comparison never matches for any real npm-installed invocation,
185
+ // which silently skipped main() entirely (every command exited 0 with no
186
+ // output, since nothing ever ran).
187
+ const isEntrypoint = process.argv[1] !== undefined && import.meta.url === `file://${realpathSync(process.argv[1])}`;
188
+ if (isEntrypoint) {
189
+ main();
190
+ }
191
+ // Exported for tests only -- the CLI itself only ever calls main() above.
192
+ export { main, dispatchSkill, COMMANDS, SKILL_COMMANDS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahood/cli",
3
- "version": "0.3.6",
3
+ "version": "0.4.1",
4
4
  "description": "CLI for ahood, a registry for installing and publishing Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/AlexKay28/ahood-cli",
@@ -26,7 +26,7 @@
26
26
  "dist"
27
27
  ],
28
28
  "scripts": {
29
- "build": "tsc -p tsconfig.json",
29
+ "build": "rm -rf dist && tsc -p tsconfig.json",
30
30
  "prepublishOnly": "npm run build",
31
31
  "test": "vitest run",
32
32
  "sync-readme": "node scripts/sync-readme.mjs"