@ahood/cli 0.2.1 → 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
@@ -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**
@@ -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
@@ -120,7 +120,8 @@ export function formatHelp() {
120
120
  "Run `ahood --version` to print the installed CLI version.",
121
121
  "",
122
122
  "Exit codes: 0 success, 1 general error, 2 usage/validation error,",
123
- " 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.",
124
125
  "",
125
126
  "Full reference: https://ahood.vercel.app/docs",
126
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(),
@@ -61,21 +62,6 @@ function closestCommand(input) {
61
62
  }
62
63
  return bestDistance <= 2 ? best : undefined;
63
64
  }
64
- // A hung/black-holed connection throws undici's generic TypeError("fetch
65
- // failed") -- http.ts wraps that into NetworkError so it gets its own exit
66
- // code here, distinct from a server-returned ApiError.
67
- function exitCodeFor(error) {
68
- if (error instanceof ApiError) {
69
- if (error.status === 401 || error.status === 403)
70
- return 4;
71
- if (error.status === 404)
72
- return 5;
73
- return 2;
74
- }
75
- if (error instanceof NetworkError)
76
- return 6;
77
- return 1;
78
- }
79
65
  async function main() {
80
66
  const [command, ...args] = process.argv.slice(2);
81
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.1",
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",