@ahood/cli 0.2.0 → 0.2.2
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 +2 -2
- package/dist/commands/view.js +16 -5
- package/dist/exit-code.js +26 -0
- package/dist/help.js +6 -3
- package/dist/http.js +18 -1
- package/dist/index.js +3 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ For CI or any non-interactive environment, set `AHOOD_TOKEN` instead of running
|
|
|
39
39
|
| `ahood logout` | Removes the stored token. |
|
|
40
40
|
| `ahood whoami [--json]` | Reports whether your stored token still authenticates. |
|
|
41
41
|
| `ahood search <query> [--json] [--limit <n>]` | Search published skills. |
|
|
42
|
-
| `ahood view <owner>/<skill> [--json] [--web]` | Show a single skill's details without installing it, or open its page in a browser. |
|
|
42
|
+
| `ahood view <owner>/<skill> [--json] [--web]` (alias: `show`) | Show a single skill's details -- tags, license, homepage, repository, dates, etc. -- without installing it, or open its page in a browser. |
|
|
43
43
|
| `ahood list-mine [--json]` | List your own skills, public and private, with their visibility and download/star counts. |
|
|
44
44
|
| `ahood add <owner>/<skill>[@version]` | Install into `.claude/skills/<owner>/<skill>/`, pinned in `.claude/skills.lock.json`. |
|
|
45
45
|
| `ahood update [<owner>/<skill> ...]` | Move the lockfile pin(s) forward to the latest version. One skill failing to update doesn't stop the rest. |
|
|
@@ -63,6 +63,6 @@ Run `ahood help`, `ahood help <command>`, or `ahood <command> --help` for the sa
|
|
|
63
63
|
| `2` | Usage or validation error (bad arguments, or the server rejected the request as invalid) |
|
|
64
64
|
| `4` | Authentication required or rejected (not logged in, or the token was refused) |
|
|
65
65
|
| `5` | Not found |
|
|
66
|
-
| `6` | Network/transport error |
|
|
66
|
+
| `6` | Network/transport error, or an upstream server (5xx) error |
|
|
67
67
|
|
|
68
68
|
Full reference, including personal API tokens, CI usage, the public REST API, and the MCP server: **https://ahood.vercel.app/docs**
|
package/dist/commands/view.js
CHANGED
|
@@ -39,11 +39,22 @@ export async function view(args) {
|
|
|
39
39
|
console.log(JSON.stringify(detail));
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
-
console.log(
|
|
43
|
-
console.log(detail.
|
|
42
|
+
const field = (label, value) => console.log(` ${label.padEnd(13)}${value}`);
|
|
43
|
+
console.log(`${detail.owner}/${detail.slug}`);
|
|
44
|
+
field("name:", detail.name);
|
|
44
45
|
if (detail.tagline)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
field("tagline:", detail.tagline);
|
|
47
|
+
field("tags:", detail.tags.length > 0 ? detail.tags.join(", ") : "-");
|
|
48
|
+
field("license:", detail.license ?? "-");
|
|
49
|
+
field("visibility:", detail.visibility);
|
|
50
|
+
field("homepage:", detail.homepage ?? "-");
|
|
51
|
+
field("repository:", detail.repository ?? "-");
|
|
52
|
+
field("version:", detail.skill_versions ? detail.skill_versions.version : "no published version");
|
|
53
|
+
field("downloads:", String(detail.downloads_count));
|
|
54
|
+
field("stars:", String(detail.stars_count));
|
|
55
|
+
field("created:", detail.created_at);
|
|
56
|
+
field("updated:", detail.updated_at);
|
|
57
|
+
if (detail.is_starred !== null)
|
|
58
|
+
field("starred:", detail.is_starred ? "yes" : "no");
|
|
48
59
|
console.log(url);
|
|
49
60
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ApiError, NetworkError } from "./http.js";
|
|
2
|
+
// A hung/black-holed connection throws undici's generic TypeError("fetch
|
|
3
|
+
// failed") -- http.ts wraps that into NetworkError so it gets its own exit
|
|
4
|
+
// code here, distinct from a server-returned ApiError. Kept in its own
|
|
5
|
+
// module (rather than inline in index.ts) so it's importable in tests
|
|
6
|
+
// without triggering index.ts's unconditional top-level main() call.
|
|
7
|
+
export function exitCodeFor(error) {
|
|
8
|
+
if (error instanceof ApiError) {
|
|
9
|
+
if (error.status === 401 || error.status === 403)
|
|
10
|
+
return 4;
|
|
11
|
+
if (error.status === 404)
|
|
12
|
+
return 5;
|
|
13
|
+
// A 5xx is an upstream/server-side failure, not something wrong with the
|
|
14
|
+
// request -- previously fell through to 2 ("usage/validation error"),
|
|
15
|
+
// which told scripts/agents to fix their input when the actual problem
|
|
16
|
+
// was on the server (ahood-cli#31). Treated like a network/transport
|
|
17
|
+
// failure (6) since neither is something retrying with different input
|
|
18
|
+
// would fix.
|
|
19
|
+
if (error.status >= 500)
|
|
20
|
+
return 6;
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
if (error instanceof NetworkError)
|
|
24
|
+
return 6;
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
package/dist/help.js
CHANGED
|
@@ -17,7 +17,7 @@ export const COMMANDS_HELP = [
|
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
usage: "ahood view <owner>/<skill> [--json] [--web]",
|
|
20
|
-
desc: "Show a single skill's details without installing it.",
|
|
20
|
+
desc: "Show a single skill's details (tags, license, homepage, repository, dates, etc.) without installing it. Alias: ahood show.",
|
|
21
21
|
flags: [
|
|
22
22
|
"--json Emit the raw skill object instead of formatted lines.",
|
|
23
23
|
"--web Open the skill's page in your browser instead of printing.",
|
|
@@ -89,8 +89,10 @@ export const COMMANDS_HELP = [
|
|
|
89
89
|
examples: ["ahood completion bash >> ~/.bashrc"],
|
|
90
90
|
},
|
|
91
91
|
];
|
|
92
|
+
const COMMAND_ALIASES = { show: "view" };
|
|
92
93
|
export function findCommandHelp(command) {
|
|
93
|
-
|
|
94
|
+
const resolved = COMMAND_ALIASES[command] ?? command;
|
|
95
|
+
return COMMANDS_HELP.find((c) => c.usage.startsWith(`ahood ${resolved} `) || c.usage === `ahood ${resolved}`);
|
|
94
96
|
}
|
|
95
97
|
export function formatCommandHelp(entry) {
|
|
96
98
|
const lines = [entry.usage, "", entry.desc];
|
|
@@ -118,7 +120,8 @@ export function formatHelp() {
|
|
|
118
120
|
"Run `ahood --version` to print the installed CLI version.",
|
|
119
121
|
"",
|
|
120
122
|
"Exit codes: 0 success, 1 general error, 2 usage/validation error,",
|
|
121
|
-
" 4 authentication required or rejected, 5 not found,
|
|
123
|
+
" 4 authentication required or rejected, 5 not found,",
|
|
124
|
+
" 6 network/transport error or upstream server (5xx) error.",
|
|
122
125
|
"",
|
|
123
126
|
"Full reference: https://ahood.vercel.app/docs",
|
|
124
127
|
].join("\n");
|
package/dist/http.js
CHANGED
|
@@ -36,6 +36,23 @@ export async function apiFetch(path, init = {}) {
|
|
|
36
36
|
throw new NetworkError(`Request to ${getApiUrl()}${path} failed (${message}${cause})`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
40
|
+
// Defense in depth (ahood-cli#31): the API is expected to never forward a raw
|
|
41
|
+
// upstream error body (an HTML block page from a proxy/WAF, a giant stack
|
|
42
|
+
// trace, etc.) into `error`, but this CLI shouldn't trust that unconditionally
|
|
43
|
+
// -- a regression on any endpoint would otherwise dump multi-KB infra details
|
|
44
|
+
// straight to a user's terminal. Anything HTML-shaped or implausibly long for
|
|
45
|
+
// a normal error string is replaced with a short, safe summary instead.
|
|
46
|
+
function sanitizeErrorMessage(message) {
|
|
47
|
+
const looksLikeHtml = /<!DOCTYPE|<html[\s>]/i.test(message);
|
|
48
|
+
if (!looksLikeHtml && message.length <= MAX_ERROR_MESSAGE_LENGTH)
|
|
49
|
+
return message;
|
|
50
|
+
// No preview of the raw content: an HTML-shaped body's first bytes are
|
|
51
|
+
// exactly where infra details (e.g. a Cloudflare block page's title, Ray
|
|
52
|
+
// ID) live, so a "helpful" excerpt would leak the same details this
|
|
53
|
+
// exists to suppress. Length alone is safe to report.
|
|
54
|
+
return `Request failed with an unexpected, oversized, or HTML-shaped error response (${message.length} bytes) -- this usually means an upstream proxy/WAF failure, not a problem with your request.`;
|
|
55
|
+
}
|
|
39
56
|
export async function apiJson(path, init = {}) {
|
|
40
57
|
const res = await apiFetch(path, init);
|
|
41
58
|
if (!res.ok) {
|
|
@@ -44,7 +61,7 @@ export async function apiJson(path, init = {}) {
|
|
|
44
61
|
// rather than crashing on `body.error` of something that isn't an object.
|
|
45
62
|
const body = await res.json().catch(() => undefined);
|
|
46
63
|
const message = body && typeof body === "object" && typeof body.error === "string"
|
|
47
|
-
? body.error
|
|
64
|
+
? sanitizeErrorMessage(body.error)
|
|
48
65
|
: `Request failed with status ${res.status}`;
|
|
49
66
|
throw new ApiError(res.status, message);
|
|
50
67
|
}
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,8 @@ import { star, unstar } from "./commands/star.js";
|
|
|
15
15
|
import { view } from "./commands/view.js";
|
|
16
16
|
import { completion } from "./commands/completion.js";
|
|
17
17
|
import { formatHelp, formatCommandHelp, findCommandHelp } from "./help.js";
|
|
18
|
-
import { ApiError
|
|
18
|
+
import { ApiError } from "./http.js";
|
|
19
|
+
import { exitCodeFor } from "./exit-code.js";
|
|
19
20
|
import { CLI_NAME, CLI_VERSION } from "./version.js";
|
|
20
21
|
const COMMANDS = {
|
|
21
22
|
login: () => login(),
|
|
@@ -33,6 +34,7 @@ const COMMANDS = {
|
|
|
33
34
|
star,
|
|
34
35
|
unstar,
|
|
35
36
|
view,
|
|
37
|
+
show: view, // alias -- "ahood show" is the name issue #30 proposed; same command as "view"
|
|
36
38
|
completion,
|
|
37
39
|
};
|
|
38
40
|
function levenshtein(a, b) {
|
|
@@ -60,21 +62,6 @@ function closestCommand(input) {
|
|
|
60
62
|
}
|
|
61
63
|
return bestDistance <= 2 ? best : undefined;
|
|
62
64
|
}
|
|
63
|
-
// A hung/black-holed connection throws undici's generic TypeError("fetch
|
|
64
|
-
// failed") -- http.ts wraps that into NetworkError so it gets its own exit
|
|
65
|
-
// code here, distinct from a server-returned ApiError.
|
|
66
|
-
function exitCodeFor(error) {
|
|
67
|
-
if (error instanceof ApiError) {
|
|
68
|
-
if (error.status === 401 || error.status === 403)
|
|
69
|
-
return 4;
|
|
70
|
-
if (error.status === 404)
|
|
71
|
-
return 5;
|
|
72
|
-
return 2;
|
|
73
|
-
}
|
|
74
|
-
if (error instanceof NetworkError)
|
|
75
|
-
return 6;
|
|
76
|
-
return 1;
|
|
77
|
-
}
|
|
78
65
|
async function main() {
|
|
79
66
|
const [command, ...args] = process.argv.slice(2);
|
|
80
67
|
if (command === "--version" || command === "-v") {
|
package/package.json
CHANGED