@xynogen/pix-9router 0.3.20 → 0.3.21

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/http.ts +80 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-9router",
3
- "version": "0.3.20",
3
+ "version": "0.3.21",
4
4
  "description": "Pi extension — 9Router provider + fetch/search tools via router API",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
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 { type ExecFileException, execFile } from "node:child_process";
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
- /** curl --max-time in seconds (default 25). */
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 stdin body piped into curl. */
50
+ /** Optional request body (was piped to curl stdin). */
52
51
  stdin?: string;
53
52
  }
54
53
 
55
- /** Run curl with sane defaults, rejecting on non-zero exit or timeout. */
56
- export function curl(args: string[], opts: CurlOptions = {}): Promise<string> {
57
- const timeoutMs = opts.timeoutMs ?? ioTimeoutMs();
58
- const maxTime = opts.maxTime ?? timeoutMs / 1000;
59
- const { stdin } = opts;
60
- return new Promise((resolve, reject) => {
61
- const child = execFile(
62
- "curl",
63
- ["-sS", "--connect-timeout", "10", "--max-time", String(maxTime), ...args],
64
- { maxBuffer: 10 * 1024 * 1024, timeout: timeoutMs },
65
- (err, stdout, stderr) => {
66
- if (err) {
67
- const e = err as ExecFileException;
68
- const msg = e.killed
69
- ? "curl timed out"
70
- : `curl exit ${e.code ?? "??"}: ${stderr.slice(0, 300)}`;
71
- reject(new Error(msg));
72
- return;
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
- resolve(stdout);
75
- },
76
- );
77
- if (stdin && child.stdin) {
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 {