postgresai 0.16.0-dev.8 → 0.16.0-dev.9
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/bin/postgres-ai.ts +8 -3
- package/dist/bin/postgres-ai.js +49 -18
- package/lib/dblab.ts +9 -2
- package/lib/joe.ts +14 -6
- package/lib/util.ts +17 -0
- package/package.json +1 -1
- package/test/joe.cli.test.ts +20 -0
- package/test/util.test.ts +26 -1
package/bin/postgres-ai.ts
CHANGED
|
@@ -5619,13 +5619,18 @@ withJoeOptions(
|
|
|
5619
5619
|
.command("terminate <pid>")
|
|
5620
5620
|
.description("pg_terminate_backend(pid) on the clone")
|
|
5621
5621
|
).action(async (pid: string, opts: JoeCliOpts) => {
|
|
5622
|
-
|
|
5623
|
-
|
|
5622
|
+
// A pid must be a bare non-negative integer. parseInt() silently accepts
|
|
5623
|
+
// trailing garbage ("12x"→12), a sign ("-5"→-5), decimals ("1.5"→1) and hex
|
|
5624
|
+
// ("0x10"→0) — any of which would submit a terminate against the WRONG
|
|
5625
|
+
// backend pid. Require a fully-numeric token so a mistyped pid is a clean,
|
|
5626
|
+
// typed rejection that never reaches the submit rpc.
|
|
5627
|
+
const pidStr = (pid ?? "").trim();
|
|
5628
|
+
if (!/^[0-9]+$/.test(pidStr)) {
|
|
5624
5629
|
console.error("pid must be a number");
|
|
5625
5630
|
process.exitCode = 1;
|
|
5626
5631
|
return;
|
|
5627
5632
|
}
|
|
5628
|
-
await runJoeCli("terminate", null, { pid:
|
|
5633
|
+
await runJoeCli("terminate", null, { pid: parseInt(pidStr, 10) }, opts);
|
|
5629
5634
|
});
|
|
5630
5635
|
|
|
5631
5636
|
withJoeOptions(
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13325,7 +13325,8 @@ __export(exports_util2, {
|
|
|
13325
13325
|
resolveBaseUrls: () => resolveBaseUrls2,
|
|
13326
13326
|
normalizeBaseUrl: () => normalizeBaseUrl2,
|
|
13327
13327
|
maskSecret: () => maskSecret2,
|
|
13328
|
-
formatHttpError: () => formatHttpError2
|
|
13328
|
+
formatHttpError: () => formatHttpError2,
|
|
13329
|
+
describeFetchError: () => describeFetchError2
|
|
13329
13330
|
});
|
|
13330
13331
|
function isHtmlContent2(text) {
|
|
13331
13332
|
const trimmed = text.trim();
|
|
@@ -13365,6 +13366,11 @@ ${bodyDetails}`;
|
|
|
13365
13366
|
}
|
|
13366
13367
|
return errMsg + remediation;
|
|
13367
13368
|
}
|
|
13369
|
+
function describeFetchError2(operation, url, err) {
|
|
13370
|
+
const cause = err?.cause;
|
|
13371
|
+
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
13372
|
+
return `${operation}: could not reach ${url} (${detail})`;
|
|
13373
|
+
}
|
|
13368
13374
|
function maskSecret2(secret) {
|
|
13369
13375
|
if (!secret)
|
|
13370
13376
|
return "";
|
|
@@ -13444,7 +13450,7 @@ var {
|
|
|
13444
13450
|
// package.json
|
|
13445
13451
|
var package_default = {
|
|
13446
13452
|
name: "postgresai",
|
|
13447
|
-
version: "0.16.0-dev.
|
|
13453
|
+
version: "0.16.0-dev.9",
|
|
13448
13454
|
description: "postgres_ai CLI",
|
|
13449
13455
|
license: "Apache-2.0",
|
|
13450
13456
|
private: false,
|
|
@@ -16275,7 +16281,7 @@ var Result = import_lib.default.Result;
|
|
|
16275
16281
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16276
16282
|
var defaults = import_lib.default.defaults;
|
|
16277
16283
|
// package.json
|
|
16278
|
-
var version = "0.16.0-dev.
|
|
16284
|
+
var version = "0.16.0-dev.9";
|
|
16279
16285
|
var package_default2 = {
|
|
16280
16286
|
name: "postgresai",
|
|
16281
16287
|
version,
|
|
@@ -16465,6 +16471,11 @@ ${bodyDetails}`;
|
|
|
16465
16471
|
}
|
|
16466
16472
|
return errMsg + remediation;
|
|
16467
16473
|
}
|
|
16474
|
+
function describeFetchError(operation, url, err) {
|
|
16475
|
+
const cause = err?.cause;
|
|
16476
|
+
const detail = cause?.code || cause?.message || (err instanceof Error && err.message ? err.message : String(err));
|
|
16477
|
+
return `${operation}: could not reach ${url} (${detail})`;
|
|
16478
|
+
}
|
|
16468
16479
|
function maskSecret(secret) {
|
|
16469
16480
|
if (!secret)
|
|
16470
16481
|
return "";
|
|
@@ -17357,11 +17368,16 @@ async function callRpc(params) {
|
|
|
17357
17368
|
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
17358
17369
|
console.error(`Debug: Request body: ${payload}`);
|
|
17359
17370
|
}
|
|
17360
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17371
|
+
let response;
|
|
17372
|
+
try {
|
|
17373
|
+
response = await fetch(url.toString(), {
|
|
17374
|
+
method: "POST",
|
|
17375
|
+
headers,
|
|
17376
|
+
body: payload
|
|
17377
|
+
});
|
|
17378
|
+
} catch (err) {
|
|
17379
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
17380
|
+
}
|
|
17365
17381
|
const text = await response.text();
|
|
17366
17382
|
if (debug) {
|
|
17367
17383
|
console.error(`Debug: Response status: ${response.status}`);
|
|
@@ -17686,7 +17702,12 @@ async function callDblabApi(params) {
|
|
|
17686
17702
|
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
17687
17703
|
console.error(`Debug: Request body: ${body}`);
|
|
17688
17704
|
}
|
|
17689
|
-
|
|
17705
|
+
let response;
|
|
17706
|
+
try {
|
|
17707
|
+
response = await fetch(url.toString(), { method: "POST", headers, body });
|
|
17708
|
+
} catch (err) {
|
|
17709
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
17710
|
+
}
|
|
17690
17711
|
const text = await response.text();
|
|
17691
17712
|
if (debug) {
|
|
17692
17713
|
console.error(`Debug: Response status: ${response.status}`);
|
|
@@ -27009,11 +27030,16 @@ async function callRpc2(params) {
|
|
|
27009
27030
|
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
27010
27031
|
console.error(`Debug: Request body: ${payload}`);
|
|
27011
27032
|
}
|
|
27012
|
-
|
|
27013
|
-
|
|
27014
|
-
|
|
27015
|
-
|
|
27016
|
-
|
|
27033
|
+
let response;
|
|
27034
|
+
try {
|
|
27035
|
+
response = await fetch(url.toString(), {
|
|
27036
|
+
method: "POST",
|
|
27037
|
+
headers,
|
|
27038
|
+
body: payload
|
|
27039
|
+
});
|
|
27040
|
+
} catch (err) {
|
|
27041
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
27042
|
+
}
|
|
27017
27043
|
const text = await response.text();
|
|
27018
27044
|
if (debug) {
|
|
27019
27045
|
console.error(`Debug: Response status: ${response.status}`);
|
|
@@ -27424,7 +27450,12 @@ async function callDblabApi2(params) {
|
|
|
27424
27450
|
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
27425
27451
|
console.error(`Debug: Request body: ${body}`);
|
|
27426
27452
|
}
|
|
27427
|
-
|
|
27453
|
+
let response;
|
|
27454
|
+
try {
|
|
27455
|
+
response = await fetch(url.toString(), { method: "POST", headers, body });
|
|
27456
|
+
} catch (err) {
|
|
27457
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
27458
|
+
}
|
|
27428
27459
|
const text = await response.text();
|
|
27429
27460
|
if (debug) {
|
|
27430
27461
|
console.error(`Debug: Response status: ${response.status}`);
|
|
@@ -39839,13 +39870,13 @@ withJoeOptions(joe.command("activity").description("running-activity snapshot (p
|
|
|
39839
39870
|
await runJoeCli("activity", null, null, opts);
|
|
39840
39871
|
});
|
|
39841
39872
|
withJoeOptions(joe.command("terminate <pid>").description("pg_terminate_backend(pid) on the clone")).action(async (pid, opts) => {
|
|
39842
|
-
const
|
|
39843
|
-
if (
|
|
39873
|
+
const pidStr = (pid ?? "").trim();
|
|
39874
|
+
if (!/^[0-9]+$/.test(pidStr)) {
|
|
39844
39875
|
console.error("pid must be a number");
|
|
39845
39876
|
process.exitCode = 1;
|
|
39846
39877
|
return;
|
|
39847
39878
|
}
|
|
39848
|
-
await runJoeCli("terminate", null, { pid:
|
|
39879
|
+
await runJoeCli("terminate", null, { pid: parseInt(pidStr, 10) }, opts);
|
|
39849
39880
|
});
|
|
39850
39881
|
withJoeOptions(joe.command("reset").description("reset/recreate the session's thin clone")).action(async (opts) => {
|
|
39851
39882
|
await runJoeCli("reset", null, null, opts);
|
package/lib/dblab.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* read/create verbs by `joe:plan` — a `PT403` surfaces as an HTTP 403 here.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
|
|
26
|
+
import { formatHttpError, maskSecret, normalizeBaseUrl, describeFetchError } from "./util";
|
|
27
27
|
import { listProjects, type ProjectListItem } from "./joe";
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
@@ -186,7 +186,14 @@ async function callDblabApi<T>(params: DblabApiCallParams): Promise<T> {
|
|
|
186
186
|
console.error(`Debug: Request body: ${body}`);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
let response: Response;
|
|
190
|
+
try {
|
|
191
|
+
response = await fetch(url.toString(), { method: "POST", headers, body });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
// Transport failure (connection refused, DNS, TLS, bad host/port) — surface
|
|
194
|
+
// the real cause + URL rather than undici's opaque "fetch failed".
|
|
195
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
196
|
+
}
|
|
190
197
|
const text = await response.text();
|
|
191
198
|
|
|
192
199
|
if (debug) {
|
package/lib/joe.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as crypto from "node:crypto";
|
|
4
|
-
import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
|
|
4
|
+
import { formatHttpError, maskSecret, normalizeBaseUrl, describeFetchError } from "./util";
|
|
5
5
|
import * as config from "./config";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -162,11 +162,19 @@ async function callRpc<T>(params: RpcCallParams): Promise<T> {
|
|
|
162
162
|
console.error(`Debug: Request body: ${payload}`);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
let response: Response;
|
|
166
|
+
try {
|
|
167
|
+
response = await fetch(url.toString(), {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers,
|
|
170
|
+
body: payload,
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
// A transport failure (connection refused, DNS, TLS, bad host/port) never
|
|
174
|
+
// reaches `response.ok`; undici throws with the real reason in `err.cause`.
|
|
175
|
+
// Surface it — a bare "fetch failed" hides which URL/why. See util.describeFetchError.
|
|
176
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
177
|
+
}
|
|
170
178
|
|
|
171
179
|
const text = await response.text();
|
|
172
180
|
|
package/lib/util.ts
CHANGED
|
@@ -116,6 +116,23 @@ export function formatHttpError(
|
|
|
116
116
|
return errMsg + remediation;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Turn a low-level `fetch` failure into an actionable message. Node's fetch
|
|
121
|
+
* (undici) throws a `TypeError('fetch failed')` and stashes the real cause
|
|
122
|
+
* (`ECONNREFUSED`, DNS failure, `bad port`, TLS error, …) in `err.cause` — the
|
|
123
|
+
* opaque top-level message on its own tells the user nothing. This surfaces the
|
|
124
|
+
* cause and the URL that could not be reached, e.g.
|
|
125
|
+
* "Failed to list projects: could not reach http://127.0.0.1:1 (ECONNREFUSED)"
|
|
126
|
+
*/
|
|
127
|
+
export function describeFetchError(operation: string, url: string, err: unknown): string {
|
|
128
|
+
const cause = (err as { cause?: { code?: string; message?: string } } | null | undefined)?.cause;
|
|
129
|
+
const detail =
|
|
130
|
+
cause?.code ||
|
|
131
|
+
cause?.message ||
|
|
132
|
+
(err instanceof Error && err.message ? err.message : String(err));
|
|
133
|
+
return `${operation}: could not reach ${url} (${detail})`;
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
export function maskSecret(secret: string): string {
|
|
120
137
|
if (!secret) return "";
|
|
121
138
|
if (secret.length <= 8) return "****";
|
package/package.json
CHANGED
package/test/joe.cli.test.ts
CHANGED
|
@@ -291,6 +291,26 @@ describe("CLI Joe command surface (grouped under `pgai joe …`)", () => {
|
|
|
291
291
|
}
|
|
292
292
|
});
|
|
293
293
|
|
|
294
|
+
test("joe terminate rejects a non-numeric / trailing-garbage pid (no submit)", async () => {
|
|
295
|
+
// A pid must be a bare non-negative integer. parseInt() would silently accept
|
|
296
|
+
// "12x"→12, "-5"→-5, "1.5"→1 and submit a WRONG pg_terminate_backend target.
|
|
297
|
+
// Those must be a clean, typed rejection that never reaches the submit rpc.
|
|
298
|
+
const api = startFakeApi();
|
|
299
|
+
try {
|
|
300
|
+
for (const bad of ["12x", "abc", "1.5", "0x10", " "]) {
|
|
301
|
+
const r = await runCliAsync(
|
|
302
|
+
["joe", "terminate", bad, "--project", "12"],
|
|
303
|
+
isolatedEnv({ PGAI_API_KEY: "k", PGAI_API_BASE_URL: api.baseUrl })
|
|
304
|
+
);
|
|
305
|
+
expect(r.status).toBe(1);
|
|
306
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("pid must be a number");
|
|
307
|
+
}
|
|
308
|
+
expect(api.requests.some((x) => x.pathname.endsWith("/rpc/joe_command_submit"))).toBe(false);
|
|
309
|
+
} finally {
|
|
310
|
+
api.stop();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
294
314
|
test("joe describe maps args.object (and --variant)", async () => {
|
|
295
315
|
const api = startFakeApi();
|
|
296
316
|
try {
|
package/test/util.test.ts
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { formatHttpError } from "../lib/util";
|
|
3
|
+
import { formatHttpError, describeFetchError } from "../lib/util";
|
|
4
|
+
|
|
5
|
+
describe("describeFetchError", () => {
|
|
6
|
+
test("surfaces the connection cause + url instead of a bare 'fetch failed'", () => {
|
|
7
|
+
// Node's fetch (undici) throws TypeError('fetch failed') and stashes the real
|
|
8
|
+
// reason (ECONNREFUSED etc.) in err.cause — the CLI must surface it, not the
|
|
9
|
+
// opaque top-level message.
|
|
10
|
+
const err = Object.assign(new TypeError("fetch failed"), {
|
|
11
|
+
cause: { code: "ECONNREFUSED", message: "connect ECONNREFUSED 127.0.0.1:1" },
|
|
12
|
+
});
|
|
13
|
+
const msg = describeFetchError("Failed to list projects", "http://127.0.0.1:1", err);
|
|
14
|
+
expect(msg).toContain("Failed to list projects");
|
|
15
|
+
expect(msg).toContain("could not reach http://127.0.0.1:1");
|
|
16
|
+
expect(msg).toContain("ECONNREFUSED");
|
|
17
|
+
expect(msg).not.toBe("fetch failed");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("falls back to cause.message, then to the error message", () => {
|
|
21
|
+
const withMsg = Object.assign(new TypeError("fetch failed"), {
|
|
22
|
+
cause: { message: "bad port" },
|
|
23
|
+
});
|
|
24
|
+
expect(describeFetchError("op", "http://h:1", withMsg)).toContain("bad port");
|
|
25
|
+
const bare = new Error("boom");
|
|
26
|
+
expect(describeFetchError("op", "http://h", bare)).toContain("boom");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
4
29
|
|
|
5
30
|
describe("formatHttpError", () => {
|
|
6
31
|
test("appends auth remediation hint on 401 with JSON body", () => {
|