@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 +196 -22
- package/dist/commands/completion.js +40 -16
- package/dist/commands/{list-mine.js → list.js} +1 -1
- package/dist/help.js +86 -51
- package/dist/index.js +99 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,31 +1,158 @@
|
|
|
1
1
|
# ahood
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@ahood/cli)
|
|
6
|
+
[](https://www.npmjs.com/package/@ahood/cli)
|
|
7
|
+
[](https://github.com/AlexKay28/ahood-cli/actions/workflows/ci.yml)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
`
|
|
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
|
|
33
|
+
const top = topLevelNames().join(" ");
|
|
34
|
+
const skill = skillNames().join(" ");
|
|
22
35
|
return [
|
|
23
36
|
"#compdef ahood",
|
|
24
|
-
|
|
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
|
|
29
|
-
.map((name) => `complete -c ahood -n "__fish_use_subcommand" -a "${name}"`)
|
|
30
|
-
.
|
|
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
|
-
//
|
|
33
|
-
// way users discover the available subcommands without reading
|
|
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) {
|
package/dist/help.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
155
|
+
return TOP_LEVEL_COMMANDS_HELP.find((c) => c.usage.startsWith(`ahood ${resolved} `) || c.usage === `ahood ${resolved}`);
|
|
146
156
|
}
|
|
147
|
-
// Renders a
|
|
148
|
-
// after the primary command name, e.g. "ahood view <owner>/<skill> ..."
|
|
149
|
-
// "ahood view|show <owner>/<skill> ...". Used by the
|
|
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
|
|
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[
|
|
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
|
|
171
|
-
const
|
|
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 {
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
72
|
+
function closestOf(input, candidates) {
|
|
58
73
|
let best;
|
|
59
74
|
let bestDistance = Infinity;
|
|
60
|
-
for (const name of
|
|
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
|
-
|
|
91
|
-
if
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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"
|