@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 +1 -1
- package/dist/exit-code.js +26 -0
- package/dist/help.js +2 -1
- package/dist/http.js +18 -1
- package/dist/index.js +2 -16
- package/package.json +1 -1
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,
|
|
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
|
|
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