@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 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**
@@ -39,11 +39,22 @@ export async function view(args) {
39
39
  console.log(JSON.stringify(detail));
40
40
  return;
41
41
  }
42
- console.log(`${owner}/${detail.slug} (${detail.visibility})`);
43
- console.log(detail.name);
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
- console.log(detail.tagline);
46
- console.log(`${detail.downloads_count} downloads, ${detail.stars_count} stars`);
47
- console.log(detail.skill_versions ? `latest version: ${detail.skill_versions.version}` : "no published version");
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
- return COMMANDS_HELP.find((c) => c.usage.startsWith(`ahood ${command} `) || c.usage === `ahood ${command}`);
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, 6 network/transport error.",
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, NetworkError } from "./http.js";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahood/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",