@xynogen/pix-9router 0.3.20 → 0.3.22
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/package.json +1 -1
- package/src/http.ts +80 -30
- package/src/render.test.ts +5 -6
- package/src/render.ts +4 -4
package/package.json
CHANGED
package/src/http.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* so both are parameters with the old per-file defaults.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { basename } from "node:path";
|
|
11
12
|
import { ioTimeoutMs, ioTimeoutSignal } from "@xynogen/pix-runtime/io";
|
|
12
13
|
import { routerBaseUrl } from "./data.js";
|
|
13
14
|
|
|
@@ -44,41 +45,90 @@ export async function apiPost(
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export interface CurlOptions {
|
|
47
|
-
/**
|
|
48
|
-
maxTime?: number;
|
|
49
|
-
/** child-process kill timeout in ms (default 30_000). */
|
|
48
|
+
/** Request timeout in ms (default ioTimeoutMs()). */
|
|
50
49
|
timeoutMs?: number;
|
|
51
|
-
/** Optional
|
|
50
|
+
/** Optional request body (was piped to curl stdin). */
|
|
52
51
|
stdin?: string;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
54
|
+
// ponytail: this is NOT a general curl shim — it parses only the flag forms the
|
|
55
|
+
// three fallback call sites actually pass (-L, -X, -H, -d, -F key=value /
|
|
56
|
+
// file=@path, plus the -sS/--connect-timeout/--max-time defaults added below).
|
|
57
|
+
// New curl flags won't work here; extend the parser or call fetch directly.
|
|
58
|
+
function curlArgsToRequest(args: string[]): { url: string; init: RequestInit } {
|
|
59
|
+
const headers = new Headers();
|
|
60
|
+
let method = "GET";
|
|
61
|
+
let body: string | undefined;
|
|
62
|
+
let form: FormData | undefined;
|
|
63
|
+
let url = "";
|
|
64
|
+
for (let i = 0; i < args.length; i++) {
|
|
65
|
+
const a = args[i] ?? "";
|
|
66
|
+
switch (a) {
|
|
67
|
+
case "-sS":
|
|
68
|
+
case "-L":
|
|
69
|
+
break; // silent / follow-redirects are fetch defaults
|
|
70
|
+
case "--connect-timeout":
|
|
71
|
+
case "--max-time":
|
|
72
|
+
i++; // value handled by AbortSignal below
|
|
73
|
+
break;
|
|
74
|
+
case "-X":
|
|
75
|
+
method = args[++i] ?? method;
|
|
76
|
+
break;
|
|
77
|
+
case "-H": {
|
|
78
|
+
const h = args[++i] ?? "";
|
|
79
|
+
const idx = h.indexOf(":");
|
|
80
|
+
if (idx >= 0) headers.set(h.slice(0, idx).trim(), h.slice(idx + 1).trim());
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "-d":
|
|
84
|
+
body = args[++i] ?? "";
|
|
85
|
+
break;
|
|
86
|
+
case "-F": {
|
|
87
|
+
form ??= new FormData();
|
|
88
|
+
const field = args[++i] ?? "";
|
|
89
|
+
const eq = field.indexOf("=");
|
|
90
|
+
const key = field.slice(0, eq);
|
|
91
|
+
const val = field.slice(eq + 1);
|
|
92
|
+
if (val.startsWith("@")) {
|
|
93
|
+
const path = val.slice(1);
|
|
94
|
+
form.set(key, new Blob([readFileSync(path)]), basename(path));
|
|
95
|
+
} else {
|
|
96
|
+
form.set(key, val);
|
|
73
97
|
}
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
child.stdin.write(stdin);
|
|
79
|
-
child.stdin.end();
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
if (!a.startsWith("-")) url = a; // positional = URL
|
|
80
102
|
}
|
|
81
|
-
}
|
|
103
|
+
}
|
|
104
|
+
const init: RequestInit = { method };
|
|
105
|
+
if (form)
|
|
106
|
+
init.body = form; // let fetch set the multipart boundary
|
|
107
|
+
else if (body !== undefined) init.body = body;
|
|
108
|
+
if ([...headers.keys()].length > 0) init.headers = headers;
|
|
109
|
+
return { url, init };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Raw fetch fallback (curl-signature compatible), rejecting on non-2xx or timeout. */
|
|
113
|
+
export async function curl(args: string[], opts: CurlOptions = {}): Promise<string> {
|
|
114
|
+
const timeoutMs = opts.timeoutMs ?? ioTimeoutMs();
|
|
115
|
+
const { url, init } = curlArgsToRequest(args);
|
|
116
|
+
if (opts.stdin !== undefined) init.body = opts.stdin;
|
|
117
|
+
init.signal = AbortSignal.timeout(timeoutMs);
|
|
118
|
+
let res: Response;
|
|
119
|
+
try {
|
|
120
|
+
res = await fetch(url, init);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
123
|
+
throw new Error("curl timed out");
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`curl failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
126
|
+
}
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
const errText = await res.text().catch(() => "");
|
|
129
|
+
throw new Error(`curl exit ${res.status}: ${errText.slice(0, 300)}`);
|
|
130
|
+
}
|
|
131
|
+
return res.text();
|
|
82
132
|
}
|
|
83
133
|
|
|
84
134
|
export function isCancelled(error: unknown, signal: AbortSignal | undefined): boolean {
|
package/src/render.test.ts
CHANGED
|
@@ -36,13 +36,12 @@ const config = {
|
|
|
36
36
|
meta: (details: { _type: "fetchResult"; chars: number }) => `${details.chars} chars`,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
test("renderCall shows title +
|
|
39
|
+
test("renderCall shows title + dim arg", () => {
|
|
40
40
|
const stub = stubComponent();
|
|
41
41
|
const rc = makeRenderCall<{ url: string }>("fetch", (a) => a.url);
|
|
42
42
|
rc({ url: "https://x.com" }, theme, context(stub));
|
|
43
|
-
// Only the tool name is accent/toolTitle-colored; the arg is
|
|
44
|
-
|
|
45
|
-
expect(stub.get()).toBe("[toolTitle]*fetch* [muted]https://x.com");
|
|
43
|
+
// Only the tool name is accent/toolTitle-colored; the arg is secondary.
|
|
44
|
+
expect(stub.get()).toBe("[toolTitle]*fetch* [dim]https://x.com");
|
|
46
45
|
});
|
|
47
46
|
|
|
48
47
|
test("renderCall hides only an effectively collapsed call row", () => {
|
|
@@ -56,7 +55,7 @@ test("renderCall hides only an effectively collapsed call row", () => {
|
|
|
56
55
|
theme,
|
|
57
56
|
context(stub, { state: { collapsed: true }, expanded: true }),
|
|
58
57
|
);
|
|
59
|
-
expect(stub.get()).toBe("[toolTitle]*fetch* [
|
|
58
|
+
expect(stub.get()).toBe("[toolTitle]*fetch* [dim]https://x.com");
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
test("renderResult renders structured compact details after collapse", () => {
|
|
@@ -157,7 +156,7 @@ test("renderResult renders empty and errors without compacting", () => {
|
|
|
157
156
|
theme,
|
|
158
157
|
context(empty),
|
|
159
158
|
);
|
|
160
|
-
expect(empty.get()).toBe(" [
|
|
159
|
+
expect(empty.get()).toBe(" [muted](empty)");
|
|
161
160
|
|
|
162
161
|
const failed = stubComponent();
|
|
163
162
|
rr(
|
package/src/render.ts
CHANGED
|
@@ -81,12 +81,12 @@ function dimBody(body: string, theme: Theme, expanded: boolean): string {
|
|
|
81
81
|
const out = shown.map((line) => ` ${theme.fg("dim", line)}`);
|
|
82
82
|
const remaining = lines.length - maxShow;
|
|
83
83
|
if (remaining > 0) {
|
|
84
|
-
out.push(` ${theme.fg("
|
|
84
|
+
out.push(` ${theme.fg("muted", `… ${remaining} more lines`)}`);
|
|
85
85
|
}
|
|
86
86
|
return out.join("\n");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/** Build a `renderCall` that shows `<title> <
|
|
89
|
+
/** Build a `renderCall` that shows `<title> <secondary arg>`. */
|
|
90
90
|
export function makeRenderCall<TArgs>(title: string, pickArg: (args: TArgs) => string) {
|
|
91
91
|
return (args: TArgs, theme: Theme, ctx: RenderCtx): Component => {
|
|
92
92
|
const text = getText(ctx.lastComponent);
|
|
@@ -97,7 +97,7 @@ export function makeRenderCall<TArgs>(title: string, pickArg: (args: TArgs) => s
|
|
|
97
97
|
)
|
|
98
98
|
return text;
|
|
99
99
|
const arg = pickArg(args);
|
|
100
|
-
text.setText(`${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("
|
|
100
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("dim", arg)}`);
|
|
101
101
|
return text;
|
|
102
102
|
};
|
|
103
103
|
}
|
|
@@ -118,7 +118,7 @@ export function makeRenderResult<TDetails>(config?: CompactRendererConfig<TDetai
|
|
|
118
118
|
return text;
|
|
119
119
|
}
|
|
120
120
|
if (!body.trim()) {
|
|
121
|
-
text.setText(` ${theme.fg("
|
|
121
|
+
text.setText(` ${theme.fg("muted", "(empty)")}`);
|
|
122
122
|
return text;
|
|
123
123
|
}
|
|
124
124
|
|