postgresai 0.17.0-dev.4 → 0.17.0-dev.5
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 +3 -3
- package/dist/bin/postgres-ai.js +713 -214
- package/dist/sql/06.helpers.sql +10 -0
- package/dist/sql/sql/06.helpers.sql +10 -0
- package/package.json +1 -1
- package/schemas/A002.schema.json +2 -0
- package/schemas/A003.schema.json +2 -0
- package/schemas/A004.schema.json +2 -0
- package/schemas/A007.schema.json +2 -0
- package/schemas/A013.schema.json +2 -0
- package/schemas/D001.schema.json +2 -0
- package/schemas/D004.schema.json +2 -0
- package/schemas/F001.schema.json +2 -0
- package/schemas/F002.schema.json +1 -1
- package/schemas/F003.schema.json +2 -0
- package/schemas/F004.schema.json +2 -0
- package/schemas/F005.schema.json +2 -0
- package/schemas/F009.schema.json +2 -0
- package/schemas/G001.schema.json +2 -0
- package/schemas/G003.schema.json +2 -0
- package/schemas/H001.schema.json +2 -0
- package/schemas/H002.schema.json +2 -0
- package/schemas/H004.schema.json +2 -0
- package/schemas/I001.schema.json +2 -0
- package/schemas/K001.schema.json +2 -0
- package/schemas/K003.schema.json +2 -0
- package/schemas/K004.schema.json +2 -0
- package/schemas/K005.schema.json +2 -0
- package/schemas/K006.schema.json +2 -0
- package/schemas/K007.schema.json +2 -0
- package/schemas/K008.schema.json +2 -0
- package/schemas/M001.schema.json +2 -0
- package/schemas/M002.schema.json +2 -0
- package/schemas/M003.schema.json +2 -0
- package/schemas/N001.schema.json +2 -0
- package/sql/06.helpers.sql +10 -0
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13533,7 +13533,7 @@ var {
|
|
|
13533
13533
|
// package.json
|
|
13534
13534
|
var package_default = {
|
|
13535
13535
|
name: "postgresai",
|
|
13536
|
-
version: "0.17.0-dev.
|
|
13536
|
+
version: "0.17.0-dev.5",
|
|
13537
13537
|
description: "postgres_ai CLI",
|
|
13538
13538
|
license: "Apache-2.0",
|
|
13539
13539
|
private: false,
|
|
@@ -16893,7 +16893,7 @@ var Result = import_lib.default.Result;
|
|
|
16893
16893
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16894
16894
|
var defaults = import_lib.default.defaults;
|
|
16895
16895
|
// package.json
|
|
16896
|
-
var version = "0.17.0-dev.
|
|
16896
|
+
var version = "0.17.0-dev.5";
|
|
16897
16897
|
var package_default2 = {
|
|
16898
16898
|
name: "postgresai",
|
|
16899
16899
|
version,
|
|
@@ -17169,6 +17169,22 @@ class HttpStatusError extends Error {
|
|
|
17169
17169
|
function isRetryableHttpStatus(status) {
|
|
17170
17170
|
return status >= 500 || status === 429;
|
|
17171
17171
|
}
|
|
17172
|
+
function isUnknownRpcSignature(status, text, rpc) {
|
|
17173
|
+
if (status !== 404)
|
|
17174
|
+
return false;
|
|
17175
|
+
try {
|
|
17176
|
+
const parsed = JSON.parse(text);
|
|
17177
|
+
return typeof parsed.message === "string" && /Could not find the .* function/.test(parsed.message) && parsed.message.includes(`${rpc}(`);
|
|
17178
|
+
} catch {
|
|
17179
|
+
return false;
|
|
17180
|
+
}
|
|
17181
|
+
}
|
|
17182
|
+
function positiveSeconds(v) {
|
|
17183
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
17184
|
+
}
|
|
17185
|
+
function asyncPollDelayMs(attempt) {
|
|
17186
|
+
return Math.min(1000 * 2 ** attempt, 15000);
|
|
17187
|
+
}
|
|
17172
17188
|
var DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 25000;
|
|
17173
17189
|
|
|
17174
17190
|
class HttpRequestTimeoutError extends Error {
|
|
@@ -21466,7 +21482,8 @@ function summarizeF001(nodeData) {
|
|
|
21466
21482
|
return `Autovacuum config: ${parts.join(", ")} finding${fired.length > 1 ? "s" : ""}`;
|
|
21467
21483
|
};
|
|
21468
21484
|
if (severity === "CRITICAL" || severity === "WARNING") {
|
|
21469
|
-
|
|
21485
|
+
const top = fired.find((r) => r.severity === "CRITICAL") ?? fired.find((r) => r.severity === "WARNING");
|
|
21486
|
+
return { status: "warning", message: top?.conclusion ? `${summarize()}. ${top.conclusion}` : summarize() };
|
|
21470
21487
|
}
|
|
21471
21488
|
return { status: "info", message: summarize() };
|
|
21472
21489
|
}
|
|
@@ -21625,7 +21642,7 @@ function summarizeG003(nodeData) {
|
|
|
21625
21642
|
|
|
21626
21643
|
// lib/checkup.ts
|
|
21627
21644
|
var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
|
|
21628
|
-
var CONTRACT_VERSION = "1.
|
|
21645
|
+
var CONTRACT_VERSION = "1.1.0";
|
|
21629
21646
|
var SECONDS_PER_DAY = 86400;
|
|
21630
21647
|
var SECONDS_PER_HOUR = 3600;
|
|
21631
21648
|
var SECONDS_PER_MINUTE = 60;
|
|
@@ -27817,46 +27834,58 @@ async function verifyInitSetup(params) {
|
|
|
27817
27834
|
}
|
|
27818
27835
|
|
|
27819
27836
|
// lib/joe.ts
|
|
27837
|
+
var JOE_ASYNC_FALLBACK_WAIT_MS = 10 * 60 * 1000;
|
|
27838
|
+
var JOE_ASYNC_MAX_WAIT_MS = 60 * 60 * 1000;
|
|
27820
27839
|
async function callRpc(params) {
|
|
27840
|
+
return (await callRpcRaw(params)).data;
|
|
27841
|
+
}
|
|
27842
|
+
async function callRpcRaw(params) {
|
|
27821
27843
|
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
27822
27844
|
if (!apiKey) {
|
|
27823
27845
|
throw new Error("API key is required");
|
|
27824
27846
|
}
|
|
27825
27847
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
27826
27848
|
const url = new URL(`${base}/rpc/${fn}`);
|
|
27827
|
-
const payload = JSON.stringify(body);
|
|
27828
27849
|
const headers = buildAuthHeaders(apiKey, params.orgScope);
|
|
27829
|
-
|
|
27830
|
-
const
|
|
27831
|
-
|
|
27832
|
-
|
|
27833
|
-
|
|
27834
|
-
|
|
27835
|
-
|
|
27836
|
-
|
|
27837
|
-
|
|
27838
|
-
|
|
27839
|
-
|
|
27840
|
-
|
|
27841
|
-
|
|
27842
|
-
|
|
27843
|
-
|
|
27844
|
-
|
|
27845
|
-
|
|
27846
|
-
|
|
27850
|
+
const send = async (bodyObj) => {
|
|
27851
|
+
const payload = JSON.stringify(bodyObj);
|
|
27852
|
+
if (debug) {
|
|
27853
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
27854
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
27855
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
27856
|
+
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
27857
|
+
}
|
|
27858
|
+
let response;
|
|
27859
|
+
const requestTimeout = requestTimeoutSignal(params.timeoutMs);
|
|
27860
|
+
try {
|
|
27861
|
+
response = await fetch(url.toString(), {
|
|
27862
|
+
method: "POST",
|
|
27863
|
+
headers,
|
|
27864
|
+
body: payload,
|
|
27865
|
+
signal: requestTimeout.signal
|
|
27866
|
+
});
|
|
27867
|
+
} catch (err) {
|
|
27868
|
+
if (isFetchTimeout(err)) {
|
|
27869
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
27870
|
+
}
|
|
27871
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
27847
27872
|
}
|
|
27848
|
-
|
|
27849
|
-
|
|
27850
|
-
|
|
27851
|
-
|
|
27852
|
-
|
|
27853
|
-
|
|
27873
|
+
const text = await response.text();
|
|
27874
|
+
if (debug) {
|
|
27875
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
27876
|
+
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
27877
|
+
}
|
|
27878
|
+
return { response, text };
|
|
27879
|
+
};
|
|
27880
|
+
let { response, text } = await send(body);
|
|
27881
|
+
if (params.fallbackBody && isUnknownRpcSignature(response.status, text, fn)) {
|
|
27882
|
+
({ response, text } = await send(params.fallbackBody));
|
|
27854
27883
|
}
|
|
27855
27884
|
if (!response.ok) {
|
|
27856
27885
|
throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
|
|
27857
27886
|
}
|
|
27858
27887
|
try {
|
|
27859
|
-
return JSON.parse(text);
|
|
27888
|
+
return { data: JSON.parse(text), status: response.status };
|
|
27860
27889
|
} catch {
|
|
27861
27890
|
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
27862
27891
|
}
|
|
@@ -27905,6 +27934,23 @@ function isNumericProjectRef(ref) {
|
|
|
27905
27934
|
}
|
|
27906
27935
|
|
|
27907
27936
|
// lib/connect.ts
|
|
27937
|
+
var money = (cents, currency) => currency.toLowerCase() === "usd" ? `$${(cents / 100).toFixed(2)}` : `${(cents / 100).toFixed(2)} ${currency.toUpperCase()}`;
|
|
27938
|
+
function priceText(q) {
|
|
27939
|
+
if (!q.billed)
|
|
27940
|
+
return q.free_slots.total ? `free (${q.free_slots.remaining} of ${q.free_slots.total} free slots)` : "free";
|
|
27941
|
+
const { amount, currency, interval } = q.price;
|
|
27942
|
+
const base = `${money(amount, currency)}/${interval} per box (${q.plan} plan)${q.subscription ? `, box ${q.quantity + 1} on the subscription` : ""}`;
|
|
27943
|
+
if (q.promo?.valid && q.amount_after_promo !== undefined && q.amount_after_promo !== null) {
|
|
27944
|
+
const after = `${money(q.amount_after_promo, currency)}`;
|
|
27945
|
+
const how = `with ${q.promo.code} (${q.promo.discount_description})`;
|
|
27946
|
+
if (q.promo.duration === "forever")
|
|
27947
|
+
return `${after}/${interval} ${how}, instead of ${base}`;
|
|
27948
|
+
if (q.promo.duration === "repeating")
|
|
27949
|
+
return `${after}/${interval} for ${q.promo.duration_in_months} months ${how}, then ${base}`;
|
|
27950
|
+
return `${after} the first ${interval} ${how}, then ${base}`;
|
|
27951
|
+
}
|
|
27952
|
+
return base;
|
|
27953
|
+
}
|
|
27908
27954
|
var FIRST_CHECKUP_DELAY_MS = 30 * 60000;
|
|
27909
27955
|
var POLL_MS = 15000;
|
|
27910
27956
|
var PROVIDERS = ["clickhouse", "rds", "supabase", "self-managed"];
|
|
@@ -27960,7 +28006,7 @@ function stateOf(raw) {
|
|
|
27960
28006
|
|
|
27961
28007
|
class ClickhouseKeyError extends Error {
|
|
27962
28008
|
}
|
|
27963
|
-
var serverOf = (name) => name.split("/")[0];
|
|
28009
|
+
var serverOf = (name) => name.split("/")[0].toLowerCase().replace(/\.(?=$|:)/, "");
|
|
27964
28010
|
var errorText = (err) => err instanceof Error ? err.message : String(err);
|
|
27965
28011
|
var urlPassword = (u) => decodeURIComponent(u.password) || u.searchParams.get("password") || "";
|
|
27966
28012
|
var URL_PARAMS_KEPT = ["sslmode", "channel_binding", "application_name"];
|
|
@@ -28023,15 +28069,39 @@ function checkupLines(c) {
|
|
|
28023
28069
|
return [`Express checkup could not run: ${c.error}`];
|
|
28024
28070
|
const warnings = c.findings.filter((f) => f.status === "warning");
|
|
28025
28071
|
const ok = c.findings.filter((f) => f.status === "ok");
|
|
28072
|
+
const failed = c.failed ?? [];
|
|
28073
|
+
const counts = [`${warnings.length} warning${warnings.length === 1 ? "" : "s"}`, `${ok.length} ok`, `${c.info.length} info`, ...failed.length ? [`${failed.length} could not run`] : []];
|
|
28026
28074
|
return [
|
|
28027
|
-
`Express checkup while the box starts (${c.checks} checks: ${
|
|
28075
|
+
`Express checkup while the box starts (${c.checks} checks: ${counts.join(", ")}):`,
|
|
28028
28076
|
...warnings.map((f) => ` ${f.check_id} ${f.title}: ${f.message}`),
|
|
28029
28077
|
...ok.length ? [` ok: ${ok.map((f) => f.check_id).join(" ")}`] : [],
|
|
28030
|
-
...c.
|
|
28078
|
+
...c.info.length ? [` info: ${c.info.join(" ")}`] : [],
|
|
28079
|
+
...failed.length ? [` could not run: ${failed.join(" ")}`] : [],
|
|
28080
|
+
c.report_id ? `Saved as report ${c.report_id}: pgai reports files ${c.report_id}` : `Not saved to PostgresAI: ${c.upload_error}`,
|
|
28031
28081
|
"The full checkup (query analysis and trends) follows on the box."
|
|
28032
28082
|
];
|
|
28033
28083
|
}
|
|
28084
|
+
var boxState = (row) => row.status === "launch_requested" && row.registered_at ? "registered" : row.status ?? "starting";
|
|
28085
|
+
async function billingFor(q, name, opts, deps) {
|
|
28086
|
+
const billing = { price: priceText(q), requires_payment_method: q.requires_payment_method };
|
|
28087
|
+
if (opts.coupon) {
|
|
28088
|
+
billing.coupon = q.promo?.valid ? { code: q.promo.code, valid: true, description: q.promo.discount_description } : { code: opts.coupon, valid: false, error: q.promo?.error ?? "not valid" };
|
|
28089
|
+
}
|
|
28090
|
+
const billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${deps.billingUrl(q.org_alias)}, then re-run` };
|
|
28091
|
+
const stop = (next) => ({ billing, billingPage, accepted: false, stop: { ...billing, next } });
|
|
28092
|
+
if (billing.coupon && !billing.coupon.valid) {
|
|
28093
|
+
return stop(`Promo code ${opts.coupon}: ${billing.coupon.error}. Nothing was changed: re-run with a valid code, or without ${opts.agent ? "coupon" : "--coupon"}`);
|
|
28094
|
+
}
|
|
28095
|
+
if (q.requires_payment_method)
|
|
28096
|
+
return { billing, billingPage, accepted: false, stop: billingPage };
|
|
28097
|
+
if (q.billed && !opts.yes && !await deps.confirm(`${name} is billed: ${billing.price}. Provision it? (y/N): `)) {
|
|
28098
|
+
return stop(opts.agent ? `Call connect_database again with yes: true to accept ${billing.price}` : `Re-run with --yes to accept ${billing.price}`);
|
|
28099
|
+
}
|
|
28100
|
+
return { billing, billingPage, accepted: q.billed };
|
|
28101
|
+
}
|
|
28034
28102
|
async function connect(url, opts, deps) {
|
|
28103
|
+
const started = deps.now();
|
|
28104
|
+
const progress = (event, message, more = {}) => deps.progress({ event, elapsed_s: Math.round((deps.now() - started) / 1000), message, ...more });
|
|
28035
28105
|
const provider = opts.provider ?? detectCloudProvider(url);
|
|
28036
28106
|
if (!PROVIDERS.includes(provider))
|
|
28037
28107
|
throw new Error(`--provider must be one of: ${PROVIDERS.join(", ")}`);
|
|
@@ -28054,6 +28124,9 @@ async function connect(url, opts, deps) {
|
|
|
28054
28124
|
const fresh = !row;
|
|
28055
28125
|
let note = "";
|
|
28056
28126
|
let checkup;
|
|
28127
|
+
let billing = {};
|
|
28128
|
+
let billingPage;
|
|
28129
|
+
let accepted = false;
|
|
28057
28130
|
const findOrg = async () => {
|
|
28058
28131
|
try {
|
|
28059
28132
|
return await deps.clickhouseOrg(new URL(url).hostname, key.keyId, key.keySecret);
|
|
@@ -28082,55 +28155,112 @@ async function connect(url, opts, deps) {
|
|
|
28082
28155
|
}
|
|
28083
28156
|
ch = found;
|
|
28084
28157
|
}
|
|
28085
|
-
|
|
28086
|
-
|
|
28087
|
-
if (others.length) {
|
|
28088
|
-
return { status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} would cut off the monitoring of ${others.map((d) => d.name).join(", ")} on this server: set PGAI_MON_PASSWORD to its password instead` };
|
|
28089
|
-
}
|
|
28090
|
-
}
|
|
28091
|
-
deps.progress(`Preparing ${maskConnectionString(url)}`);
|
|
28092
|
-
const prepared = await deps.prepare(url, provider, { resetPassword: opts.resetPassword });
|
|
28093
|
-
if ("next" in prepared)
|
|
28094
|
-
return { status: "action_required", provider, name, ...prepared };
|
|
28095
|
-
if (prepared.note)
|
|
28096
|
-
note = `; ${prepared.note}`;
|
|
28097
|
-
if (opts.selfHosted) {
|
|
28098
|
-
await deps.selfHosted(prepared.monitoringUrl, key && ch ? { CLICKHOUSE_ORG_ID: ch.orgId, CLICKHOUSE_KEY_ID: key.keyId, CLICKHOUSE_KEY_SECRET: key.keySecret } : {});
|
|
28099
|
-
return { status: "connected", provider, name, dashboard_url: "http://localhost:3000", host_metrics: !!ch, next: `pgai mon health${note}` };
|
|
28100
|
-
}
|
|
28101
|
-
deps.progress(`Provisioning monitoring for ${name}`);
|
|
28102
|
-
const undo = async () => {
|
|
28103
|
-
if (prepared.generated)
|
|
28158
|
+
const undo = async (p) => {
|
|
28159
|
+
if ("generated" in p && p.generated)
|
|
28104
28160
|
await deps.unprepare(url).catch(() => false);
|
|
28105
28161
|
};
|
|
28106
|
-
|
|
28107
|
-
|
|
28108
|
-
|
|
28109
|
-
|
|
28110
|
-
|
|
28111
|
-
|
|
28112
|
-
|
|
28113
|
-
|
|
28114
|
-
|
|
28115
|
-
|
|
28116
|
-
|
|
28117
|
-
|
|
28162
|
+
let lockId;
|
|
28163
|
+
const othersOnServer = (list) => list.filter((d) => d.name !== name && !disconnecting(d.status) && serverOf(d.name) === serverOf(name));
|
|
28164
|
+
const cutOff = (others) => ({ status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} would cut off the monitoring of ${others.map((d) => d.name).join(", ")} on this server: set PGAI_MON_PASSWORD to its password instead` });
|
|
28165
|
+
if (opts.resetPassword) {
|
|
28166
|
+
if (othersOnServer(rows).length)
|
|
28167
|
+
return cutOff(othersOnServer(rows));
|
|
28168
|
+
}
|
|
28169
|
+
if (opts.coupon !== undefined && !opts.coupon.trim()) {
|
|
28170
|
+
return { status: "action_required", provider, name, next: `${opts.agent ? "coupon" : "--coupon"} is empty: pass a promotion code, or leave ${opts.agent ? "coupon" : "--coupon"} out` };
|
|
28171
|
+
}
|
|
28172
|
+
if (!opts.selfHosted) {
|
|
28173
|
+
const b = await billingFor(await deps.quote(opts.coupon), name, opts, deps);
|
|
28174
|
+
if (b.stop)
|
|
28175
|
+
return { status: "action_required", provider, name, ...b.stop };
|
|
28176
|
+
({ billing, billingPage, accepted } = b);
|
|
28177
|
+
progress("billing", `Billing: ${billing.price}`);
|
|
28178
|
+
}
|
|
28179
|
+
if (opts.resetPassword) {
|
|
28180
|
+
try {
|
|
28181
|
+
lockId = (await deps.resetLock(serverOf(name))).lock_id;
|
|
28182
|
+
} catch (err) {
|
|
28183
|
+
if (err instanceof HttpStatusError && err.status === 409) {
|
|
28184
|
+
return { status: "action_required", provider, name, next: `Another pgai connect --reset-password for ${serverOf(name)} is running: wait for it to finish, then re-run` };
|
|
28185
|
+
}
|
|
28186
|
+
if (err instanceof HttpStatusError && err.status === 404) {
|
|
28187
|
+
return { status: "action_required", provider, name, next: `This platform cannot lock the server for --reset-password yet: set PGAI_MON_PASSWORD to ${DEFAULT_MONITORING_USER}'s password instead` };
|
|
28188
|
+
}
|
|
28189
|
+
throw err;
|
|
28190
|
+
}
|
|
28191
|
+
}
|
|
28192
|
+
let prepared;
|
|
28193
|
+
let created;
|
|
28194
|
+
try {
|
|
28195
|
+
if (lockId) {
|
|
28196
|
+
const now = await deps.list();
|
|
28197
|
+
const same = now.find((d) => d.name === name && !disconnecting(d.status));
|
|
28198
|
+
if (same)
|
|
28199
|
+
return { status: "action_required", provider, name, id: same.id, next: `${name} is already connected, and its monitoring uses the current password: pgai disconnect ${name} --yes first` };
|
|
28200
|
+
const others = othersOnServer(now);
|
|
28201
|
+
if (others.length)
|
|
28202
|
+
return cutOff(others);
|
|
28203
|
+
}
|
|
28204
|
+
progress("preparing", `Preparing ${maskConnectionString(url)}`);
|
|
28205
|
+
prepared = await deps.prepare(url, provider, { resetPassword: opts.resetPassword });
|
|
28206
|
+
if ("next" in prepared)
|
|
28207
|
+
return { status: "action_required", provider, name, ...billing, ...prepared };
|
|
28208
|
+
if (prepared.note)
|
|
28209
|
+
note = `; ${prepared.note}`;
|
|
28210
|
+
if (opts.selfHosted) {
|
|
28211
|
+
await deps.selfHosted(prepared.monitoringUrl, key && ch ? { CLICKHOUSE_ORG_ID: ch.orgId, CLICKHOUSE_KEY_ID: key.keyId, CLICKHOUSE_KEY_SECRET: key.keySecret } : {});
|
|
28212
|
+
return { status: "connected", provider, name, dashboard_url: "http://localhost:3000", host_metrics: !!ch, next: `pgai mon health${note}` };
|
|
28213
|
+
}
|
|
28214
|
+
progress("provisioning", `Provisioning monitoring for ${name}`);
|
|
28215
|
+
created = await deps.create({
|
|
28216
|
+
db_url: prepared.monitoringUrl,
|
|
28217
|
+
...provider === "clickhouse" ? { provider } : {},
|
|
28218
|
+
...key && ch ? { clickhouse_org_id: ch.orgId, clickhouse_key_id: key.keyId, clickhouse_key_secret: key.keySecret } : {},
|
|
28219
|
+
...opts.coupon ? { promo_code: opts.coupon } : {},
|
|
28220
|
+
...accepted ? { accept_price: true } : {}
|
|
28221
|
+
}).catch(async (err) => {
|
|
28222
|
+
if (err instanceof HttpStatusError && err.status >= 400 && err.status < 500)
|
|
28223
|
+
await undo(prepared);
|
|
28224
|
+
if (err instanceof HttpStatusError && err.status === 402)
|
|
28225
|
+
return /declined/i.test(err.message) ? "declined" : "payment";
|
|
28226
|
+
if (err instanceof HttpStatusError && err.status === 412)
|
|
28227
|
+
return "price";
|
|
28228
|
+
throw err;
|
|
28229
|
+
});
|
|
28230
|
+
if (created === "payment")
|
|
28231
|
+
return { status: "action_required", provider, name, ...billingPage };
|
|
28232
|
+
if (created === "declined") {
|
|
28233
|
+
return { status: "action_required", provider, name, ...billingPage, next: billingPage.next.replace(/^Add a payment method at /, "The payment method on file was declined: update it at ") };
|
|
28234
|
+
}
|
|
28235
|
+
if (created === "price")
|
|
28236
|
+
return { status: "action_required", provider, name, ...billing, next: "The price changed since it was shown: re-run pgai connect to see it" };
|
|
28237
|
+
if (created.status === "failed") {
|
|
28238
|
+
await undo(prepared);
|
|
28239
|
+
return { status: "failed", provider, name, id: created.id, next: `${created.error} Re-run pgai connect later.` };
|
|
28240
|
+
}
|
|
28241
|
+
} finally {
|
|
28242
|
+
if (lockId)
|
|
28243
|
+
await deps.resetUnlock(lockId).catch(() => {});
|
|
28118
28244
|
}
|
|
28119
28245
|
row = { id: created.id, name: created.name, provider, status: created.status, dashboard_url: null, host_metrics: !!ch };
|
|
28120
|
-
checkup = await deps.checkup(withLocalTls(prepared.monitoringUrl, url)).catch((err) => ({ error: errorText(err) }));
|
|
28121
|
-
|
|
28122
|
-
|
|
28246
|
+
checkup = await deps.checkup(withLocalTls(prepared.monitoringUrl, url), created.name).catch((err) => ({ error: errorText(err) }));
|
|
28247
|
+
progress("checkup", checkupLines(checkup).join(`
|
|
28248
|
+
`), { checkup });
|
|
28123
28249
|
}
|
|
28124
|
-
|
|
28250
|
+
let shown;
|
|
28251
|
+
for (const deadline = deps.now() + opts.waitMs;; ) {
|
|
28125
28252
|
const result = connectStatus(row, provider, fresh);
|
|
28126
|
-
|
|
28253
|
+
const state = boxState(row);
|
|
28254
|
+
if (state !== shown && (shown !== undefined || result.status === "provisioning"))
|
|
28255
|
+
progress("box", `Monitoring box: ${state}`, { state });
|
|
28256
|
+
shown = state;
|
|
28257
|
+
if (result.status !== "provisioning" || deps.now() >= deadline) {
|
|
28127
28258
|
if (result.status === "connected" && provider === "clickhouse" && !row.host_metrics) {
|
|
28128
28259
|
result.next += "; for CPU, memory and disk, disconnect and reconnect with --clickhouse-key <key-id>:<key-secret>";
|
|
28129
28260
|
}
|
|
28130
28261
|
const { next, ...rest } = result;
|
|
28131
|
-
return { ...rest, ...checkup ? { checkup } : {}, next: next + note };
|
|
28262
|
+
return { ...rest, ...billing, ...checkup ? { checkup } : {}, next: next + note };
|
|
28132
28263
|
}
|
|
28133
|
-
deps.progress(`Waiting for the monitoring box (${row.status ?? "starting"})`);
|
|
28134
28264
|
await deps.sleep(POLL_MS);
|
|
28135
28265
|
row = (await deps.list().catch(() => [])).find((d) => d.id === row.id) ?? row;
|
|
28136
28266
|
}
|
|
@@ -28255,14 +28385,52 @@ ${redactPasswordsInSql(s.sql)}`).join(`
|
|
|
28255
28385
|
}
|
|
28256
28386
|
async function expressCheckup(url, opts = {}) {
|
|
28257
28387
|
const { client } = await openConnection(url, opts);
|
|
28388
|
+
let reports;
|
|
28389
|
+
const failed = [];
|
|
28258
28390
|
try {
|
|
28259
|
-
|
|
28260
|
-
const reports = await generateAllReports(client, "node-01", undefined, (f) => failed.push(f.checkId));
|
|
28261
|
-
const findings = Object.values(reports).map((r) => ({ check_id: r.checkId, title: r.checkTitle, status: r.summary.status, message: r.summary.message })).filter((f) => f.status !== "info").sort((a, b) => Number(b.status === "warning") - Number(a.status === "warning"));
|
|
28262
|
-
return { checks: Object.keys(reports).length + failed.length, findings, ...failed.length ? { failed } : {} };
|
|
28391
|
+
reports = await generateAllReports(client, "node-01", undefined, (f) => failed.push(f.checkId));
|
|
28263
28392
|
} finally {
|
|
28264
28393
|
await client.end();
|
|
28265
28394
|
}
|
|
28395
|
+
const all = Object.values(reports).map((r) => ({ check_id: r.checkId, title: r.checkTitle, status: r.summary.status, message: r.summary.message }));
|
|
28396
|
+
const findings = all.filter((f) => f.status !== "info").sort((a, b) => Number(b.status === "warning") - Number(a.status === "warning"));
|
|
28397
|
+
const result = { checks: all.length + failed.length, findings, info: all.filter((f) => f.status === "info").map((f) => f.check_id), ...failed.length ? { failed } : {} };
|
|
28398
|
+
if (!opts.save)
|
|
28399
|
+
return result;
|
|
28400
|
+
try {
|
|
28401
|
+
return { ...result, report_id: await opts.save(reports) };
|
|
28402
|
+
} catch (err) {
|
|
28403
|
+
return { ...result, upload_error: errorText(err) };
|
|
28404
|
+
}
|
|
28405
|
+
}
|
|
28406
|
+
async function saveCheckupReport(rpc, accessToken, project, reports) {
|
|
28407
|
+
const idOf = (fn, answer, ...keys) => {
|
|
28408
|
+
const a = answer ?? {};
|
|
28409
|
+
const id = Number(keys.map((k) => a[k]).find((v) => v !== undefined));
|
|
28410
|
+
if (!(id > 0))
|
|
28411
|
+
throw new Error(typeof a.message === "string" ? a.message : `Unexpected ${fn} answer: ${JSON.stringify(answer)}`);
|
|
28412
|
+
return id;
|
|
28413
|
+
};
|
|
28414
|
+
const reportId = idOf("checkup_report_create", await rpc("checkup_report_create", { access_token: accessToken, project }), "report_id");
|
|
28415
|
+
const setStatus = (status) => rpc("checkup_report_status_update", { access_token: accessToken, report_id: reportId, status });
|
|
28416
|
+
try {
|
|
28417
|
+
for (const [checkId, report] of Object.entries(reports)) {
|
|
28418
|
+
idOf("checkup_report_file_post", await rpc("checkup_report_file_post", {
|
|
28419
|
+
access_token: accessToken,
|
|
28420
|
+
checkup_report_id: reportId,
|
|
28421
|
+
filename: `${checkId}.json`,
|
|
28422
|
+
check_id: checkId,
|
|
28423
|
+
data: JSON.stringify(report, null, 2),
|
|
28424
|
+
type: "json",
|
|
28425
|
+
generate_issue: true
|
|
28426
|
+
}), "report_chunck_id", "report_chunk_id");
|
|
28427
|
+
}
|
|
28428
|
+
} catch (err) {
|
|
28429
|
+
await setStatus("failed").catch(() => {});
|
|
28430
|
+
throw err;
|
|
28431
|
+
}
|
|
28432
|
+
await setStatus("completed");
|
|
28433
|
+
return reportId;
|
|
28266
28434
|
}
|
|
28267
28435
|
async function clickhouseOrgFor(host, keyId, keySecret) {
|
|
28268
28436
|
const apiUrl = process.env.CLICKHOUSE_API_URL || "https://api.clickhouse.cloud";
|
|
@@ -28299,16 +28467,21 @@ function platformDeps(p) {
|
|
|
28299
28467
|
list: () => rpc("cloud_monitoring_list"),
|
|
28300
28468
|
create: (body) => rpc("cloud_monitoring_connect", body),
|
|
28301
28469
|
disconnect: (id) => rpc("cloud_monitoring_disconnect", { instance_id: id }),
|
|
28470
|
+
resetLock: (server) => rpc("cloud_monitoring_reset_lock", { server }),
|
|
28471
|
+
resetUnlock: (lockId) => rpc("cloud_monitoring_reset_unlock", { lock_id: lockId }),
|
|
28302
28472
|
prepare: (url, provider, o = {}) => prepareDatabase(url, provider, { ...o, agent: p.agent }),
|
|
28303
28473
|
unprepare: (url) => unprepareDatabase(url, { agent: p.agent }),
|
|
28304
28474
|
clickhouseOrg: clickhouseOrgFor,
|
|
28305
|
-
checkup: (url) => expressCheckup(url, { agent: p.agent }),
|
|
28475
|
+
checkup: (url, project) => expressCheckup(url, { agent: p.agent, save: (reports) => saveCheckupReport(rpc, p.apiKey, project, reports) }),
|
|
28476
|
+
quote: (coupon) => rpc("cloud_monitoring_quote", coupon ? { promo_code: coupon } : {}),
|
|
28477
|
+
billingUrl: (orgAlias) => `${p.uiBaseUrl}/${orgAlias}/billing`,
|
|
28306
28478
|
handoffUrl: async (provider) => {
|
|
28307
28479
|
const orgs = await listOrgs({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl });
|
|
28308
28480
|
const org = orgs.length === 1 ? orgs[0] : orgs.find((o) => o.alias === p.orgScope?.alias || o.org_id === p.orgScope?.id);
|
|
28309
28481
|
return `${p.uiBaseUrl}/${org?.alias ?? "<org>"}/monitoring/scale/create/${provider}`;
|
|
28310
28482
|
},
|
|
28311
|
-
sleep: (ms) => new Promise((r) => setTimeout(r, ms))
|
|
28483
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
28484
|
+
now: () => Date.now()
|
|
28312
28485
|
};
|
|
28313
28486
|
}
|
|
28314
28487
|
|
|
@@ -34995,6 +35168,8 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
34995
35168
|
const result = await connect(url, {
|
|
34996
35169
|
provider: args.provider ? String(args.provider) : undefined,
|
|
34997
35170
|
clickhouseKey: args.clickhouse_key ? String(args.clickhouse_key) : undefined,
|
|
35171
|
+
coupon: args.coupon === undefined || args.coupon === null ? undefined : String(args.coupon),
|
|
35172
|
+
yes: args.yes === true,
|
|
34998
35173
|
waitMs: 0,
|
|
34999
35174
|
agent: true
|
|
35000
35175
|
}, {
|
|
@@ -35003,6 +35178,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
35003
35178
|
throw new Error("A self-hosted stack is set up from the CLI: pgai connect <url> --self-hosted");
|
|
35004
35179
|
},
|
|
35005
35180
|
localStackRunning: () => false,
|
|
35181
|
+
confirm: async () => false,
|
|
35006
35182
|
progress: () => {}
|
|
35007
35183
|
});
|
|
35008
35184
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: result.status === "failed" };
|
|
@@ -35568,7 +35744,7 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
35568
35744
|
},
|
|
35569
35745
|
{
|
|
35570
35746
|
name: "connect_database",
|
|
35571
|
-
description: "Put a Postgres database under PostgresAI Cloud monitoring (same as `pgai connect`): prepares the monitoring role (an admin URL creates it; otherwise returns the SQL), provisions the monitoring box, and returns JSON with status (connected | provisioning | disconnecting | action_required | failed), dashboard_url and next (the exact next action). It does not wait for the box: call it again to see the status. Safe to call again. ClickHouse Managed Postgres, RDS and Supabase are detected from the host. The URL must carry its password, and only these query parameters: sslmode, channel_binding, application_name. PGAI_MON_PASSWORD, PGPASSWORD and CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET of the server process are not used (pass clickhouse_key), and a TLS failure is not retried in plaintext (say sslmode=disable for a server without TLS).",
|
|
35747
|
+
description: "Put a Postgres database under PostgresAI Cloud monitoring (same as `pgai connect`): prepares the monitoring role (an admin URL creates it; otherwise returns the SQL), provisions the monitoring box, and returns JSON with status (connected | provisioning | disconnecting | action_required | failed), dashboard_url and next (the exact next action). A billed box is provisioned only with yes: true: without it the result is action_required with the price (and requires_payment_method), so show the price to the user first. It does not wait for the box: call it again to see the status. Safe to call again. ClickHouse Managed Postgres, RDS and Supabase are detected from the host. The URL must carry its password, and only these query parameters: sslmode, channel_binding, application_name. PGAI_MON_PASSWORD, PGPASSWORD and CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET of the server process are not used (pass clickhouse_key), and a TLS failure is not retried in plaintext (say sslmode=disable for a server without TLS).",
|
|
35572
35748
|
inputSchema: {
|
|
35573
35749
|
type: "object",
|
|
35574
35750
|
properties: {
|
|
@@ -35576,6 +35752,8 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
35576
35752
|
database_url: { type: "string", description: "postgresql:// URL with the password in it (admin, or the postgres_ai_mon role)" },
|
|
35577
35753
|
provider: { type: "string", description: "clickhouse | rds | supabase | self-managed (default: detected from the host)" },
|
|
35578
35754
|
clickhouse_key: { type: "string", description: "ClickHouse Cloud API key as <key-id>:<key-secret> (Basic Service API Reader), for CPU, memory and disk" },
|
|
35755
|
+
yes: { type: "boolean", description: "Accept the price of a billed box. Without it, a billed box is not provisioned: the result has status action_required and the price; ask the user, then call again with yes: true" },
|
|
35756
|
+
coupon: { type: "string", description: "Stripe promotion code for the organization's monitoring subscription (applies when it is first created)" },
|
|
35579
35757
|
debug: { type: "boolean", description: "Enable verbose debug logs" }
|
|
35580
35758
|
},
|
|
35581
35759
|
required: ["database_url"],
|
|
@@ -36478,6 +36656,23 @@ function renderMarkdownForTerminal(md) {
|
|
|
36478
36656
|
}
|
|
36479
36657
|
|
|
36480
36658
|
// lib/connect.ts
|
|
36659
|
+
var money2 = (cents, currency) => currency.toLowerCase() === "usd" ? `$${(cents / 100).toFixed(2)}` : `${(cents / 100).toFixed(2)} ${currency.toUpperCase()}`;
|
|
36660
|
+
function priceText2(q) {
|
|
36661
|
+
if (!q.billed)
|
|
36662
|
+
return q.free_slots.total ? `free (${q.free_slots.remaining} of ${q.free_slots.total} free slots)` : "free";
|
|
36663
|
+
const { amount, currency, interval } = q.price;
|
|
36664
|
+
const base = `${money2(amount, currency)}/${interval} per box (${q.plan} plan)${q.subscription ? `, box ${q.quantity + 1} on the subscription` : ""}`;
|
|
36665
|
+
if (q.promo?.valid && q.amount_after_promo !== undefined && q.amount_after_promo !== null) {
|
|
36666
|
+
const after = `${money2(q.amount_after_promo, currency)}`;
|
|
36667
|
+
const how = `with ${q.promo.code} (${q.promo.discount_description})`;
|
|
36668
|
+
if (q.promo.duration === "forever")
|
|
36669
|
+
return `${after}/${interval} ${how}, instead of ${base}`;
|
|
36670
|
+
if (q.promo.duration === "repeating")
|
|
36671
|
+
return `${after}/${interval} for ${q.promo.duration_in_months} months ${how}, then ${base}`;
|
|
36672
|
+
return `${after} the first ${interval} ${how}, then ${base}`;
|
|
36673
|
+
}
|
|
36674
|
+
return base;
|
|
36675
|
+
}
|
|
36481
36676
|
var FIRST_CHECKUP_DELAY_MS2 = 30 * 60000;
|
|
36482
36677
|
var POLL_MS2 = 15000;
|
|
36483
36678
|
var PROVIDERS2 = ["clickhouse", "rds", "supabase", "self-managed"];
|
|
@@ -36533,7 +36728,7 @@ function stateOf2(raw) {
|
|
|
36533
36728
|
|
|
36534
36729
|
class ClickhouseKeyError2 extends Error {
|
|
36535
36730
|
}
|
|
36536
|
-
var serverOf2 = (name) => name.split("/")[0];
|
|
36731
|
+
var serverOf2 = (name) => name.split("/")[0].toLowerCase().replace(/\.(?=$|:)/, "");
|
|
36537
36732
|
var errorText2 = (err) => err instanceof Error ? err.message : String(err);
|
|
36538
36733
|
var urlPassword2 = (u) => decodeURIComponent(u.password) || u.searchParams.get("password") || "";
|
|
36539
36734
|
var URL_PARAMS_KEPT2 = ["sslmode", "channel_binding", "application_name"];
|
|
@@ -36596,15 +36791,48 @@ function checkupLines2(c) {
|
|
|
36596
36791
|
return [`Express checkup could not run: ${c.error}`];
|
|
36597
36792
|
const warnings = c.findings.filter((f) => f.status === "warning");
|
|
36598
36793
|
const ok = c.findings.filter((f) => f.status === "ok");
|
|
36794
|
+
const failed = c.failed ?? [];
|
|
36795
|
+
const counts = [`${warnings.length} warning${warnings.length === 1 ? "" : "s"}`, `${ok.length} ok`, `${c.info.length} info`, ...failed.length ? [`${failed.length} could not run`] : []];
|
|
36599
36796
|
return [
|
|
36600
|
-
`Express checkup while the box starts (${c.checks} checks: ${
|
|
36797
|
+
`Express checkup while the box starts (${c.checks} checks: ${counts.join(", ")}):`,
|
|
36601
36798
|
...warnings.map((f) => ` ${f.check_id} ${f.title}: ${f.message}`),
|
|
36602
36799
|
...ok.length ? [` ok: ${ok.map((f) => f.check_id).join(" ")}`] : [],
|
|
36603
|
-
...c.
|
|
36800
|
+
...c.info.length ? [` info: ${c.info.join(" ")}`] : [],
|
|
36801
|
+
...failed.length ? [` could not run: ${failed.join(" ")}`] : [],
|
|
36802
|
+
c.report_id ? `Saved as report ${c.report_id}: pgai reports files ${c.report_id}` : `Not saved to PostgresAI: ${c.upload_error}`,
|
|
36604
36803
|
"The full checkup (query analysis and trends) follows on the box."
|
|
36605
36804
|
];
|
|
36606
36805
|
}
|
|
36806
|
+
function progressText(e) {
|
|
36807
|
+
const s = Math.round(e.elapsed_s);
|
|
36808
|
+
const elapsed = s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
36809
|
+
const [first, ...rest] = e.message.split(`
|
|
36810
|
+
`);
|
|
36811
|
+
const colon = first.endsWith(":") ? ":" : "";
|
|
36812
|
+
return [`${first.slice(0, first.length - colon.length)} (+${elapsed})${colon}`, ...rest].join(`
|
|
36813
|
+
`);
|
|
36814
|
+
}
|
|
36815
|
+
var boxState2 = (row) => row.status === "launch_requested" && row.registered_at ? "registered" : row.status ?? "starting";
|
|
36816
|
+
async function billingFor2(q, name, opts, deps) {
|
|
36817
|
+
const billing = { price: priceText2(q), requires_payment_method: q.requires_payment_method };
|
|
36818
|
+
if (opts.coupon) {
|
|
36819
|
+
billing.coupon = q.promo?.valid ? { code: q.promo.code, valid: true, description: q.promo.discount_description } : { code: opts.coupon, valid: false, error: q.promo?.error ?? "not valid" };
|
|
36820
|
+
}
|
|
36821
|
+
const billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${deps.billingUrl(q.org_alias)}, then re-run` };
|
|
36822
|
+
const stop = (next) => ({ billing, billingPage, accepted: false, stop: { ...billing, next } });
|
|
36823
|
+
if (billing.coupon && !billing.coupon.valid) {
|
|
36824
|
+
return stop(`Promo code ${opts.coupon}: ${billing.coupon.error}. Nothing was changed: re-run with a valid code, or without ${opts.agent ? "coupon" : "--coupon"}`);
|
|
36825
|
+
}
|
|
36826
|
+
if (q.requires_payment_method)
|
|
36827
|
+
return { billing, billingPage, accepted: false, stop: billingPage };
|
|
36828
|
+
if (q.billed && !opts.yes && !await deps.confirm(`${name} is billed: ${billing.price}. Provision it? (y/N): `)) {
|
|
36829
|
+
return stop(opts.agent ? `Call connect_database again with yes: true to accept ${billing.price}` : `Re-run with --yes to accept ${billing.price}`);
|
|
36830
|
+
}
|
|
36831
|
+
return { billing, billingPage, accepted: q.billed };
|
|
36832
|
+
}
|
|
36607
36833
|
async function connect2(url, opts, deps) {
|
|
36834
|
+
const started = deps.now();
|
|
36835
|
+
const progress = (event, message, more = {}) => deps.progress({ event, elapsed_s: Math.round((deps.now() - started) / 1000), message, ...more });
|
|
36608
36836
|
const provider = opts.provider ?? detectCloudProvider2(url);
|
|
36609
36837
|
if (!PROVIDERS2.includes(provider))
|
|
36610
36838
|
throw new Error(`--provider must be one of: ${PROVIDERS2.join(", ")}`);
|
|
@@ -36627,6 +36855,9 @@ async function connect2(url, opts, deps) {
|
|
|
36627
36855
|
const fresh = !row;
|
|
36628
36856
|
let note = "";
|
|
36629
36857
|
let checkup;
|
|
36858
|
+
let billing = {};
|
|
36859
|
+
let billingPage;
|
|
36860
|
+
let accepted = false;
|
|
36630
36861
|
const findOrg = async () => {
|
|
36631
36862
|
try {
|
|
36632
36863
|
return await deps.clickhouseOrg(new URL(url).hostname, key.keyId, key.keySecret);
|
|
@@ -36655,55 +36886,112 @@ async function connect2(url, opts, deps) {
|
|
|
36655
36886
|
}
|
|
36656
36887
|
ch = found;
|
|
36657
36888
|
}
|
|
36658
|
-
|
|
36659
|
-
|
|
36660
|
-
if (others.length) {
|
|
36661
|
-
return { status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} would cut off the monitoring of ${others.map((d) => d.name).join(", ")} on this server: set PGAI_MON_PASSWORD to its password instead` };
|
|
36662
|
-
}
|
|
36663
|
-
}
|
|
36664
|
-
deps.progress(`Preparing ${maskConnectionString(url)}`);
|
|
36665
|
-
const prepared = await deps.prepare(url, provider, { resetPassword: opts.resetPassword });
|
|
36666
|
-
if ("next" in prepared)
|
|
36667
|
-
return { status: "action_required", provider, name, ...prepared };
|
|
36668
|
-
if (prepared.note)
|
|
36669
|
-
note = `; ${prepared.note}`;
|
|
36670
|
-
if (opts.selfHosted) {
|
|
36671
|
-
await deps.selfHosted(prepared.monitoringUrl, key && ch ? { CLICKHOUSE_ORG_ID: ch.orgId, CLICKHOUSE_KEY_ID: key.keyId, CLICKHOUSE_KEY_SECRET: key.keySecret } : {});
|
|
36672
|
-
return { status: "connected", provider, name, dashboard_url: "http://localhost:3000", host_metrics: !!ch, next: `pgai mon health${note}` };
|
|
36673
|
-
}
|
|
36674
|
-
deps.progress(`Provisioning monitoring for ${name}`);
|
|
36675
|
-
const undo = async () => {
|
|
36676
|
-
if (prepared.generated)
|
|
36889
|
+
const undo = async (p) => {
|
|
36890
|
+
if ("generated" in p && p.generated)
|
|
36677
36891
|
await deps.unprepare(url).catch(() => false);
|
|
36678
36892
|
};
|
|
36679
|
-
|
|
36680
|
-
|
|
36681
|
-
|
|
36682
|
-
|
|
36683
|
-
|
|
36684
|
-
|
|
36685
|
-
|
|
36686
|
-
|
|
36687
|
-
|
|
36688
|
-
|
|
36689
|
-
|
|
36690
|
-
|
|
36893
|
+
let lockId;
|
|
36894
|
+
const othersOnServer = (list) => list.filter((d) => d.name !== name && !disconnecting2(d.status) && serverOf2(d.name) === serverOf2(name));
|
|
36895
|
+
const cutOff = (others) => ({ status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} would cut off the monitoring of ${others.map((d) => d.name).join(", ")} on this server: set PGAI_MON_PASSWORD to its password instead` });
|
|
36896
|
+
if (opts.resetPassword) {
|
|
36897
|
+
if (othersOnServer(rows).length)
|
|
36898
|
+
return cutOff(othersOnServer(rows));
|
|
36899
|
+
}
|
|
36900
|
+
if (opts.coupon !== undefined && !opts.coupon.trim()) {
|
|
36901
|
+
return { status: "action_required", provider, name, next: `${opts.agent ? "coupon" : "--coupon"} is empty: pass a promotion code, or leave ${opts.agent ? "coupon" : "--coupon"} out` };
|
|
36902
|
+
}
|
|
36903
|
+
if (!opts.selfHosted) {
|
|
36904
|
+
const b = await billingFor2(await deps.quote(opts.coupon), name, opts, deps);
|
|
36905
|
+
if (b.stop)
|
|
36906
|
+
return { status: "action_required", provider, name, ...b.stop };
|
|
36907
|
+
({ billing, billingPage, accepted } = b);
|
|
36908
|
+
progress("billing", `Billing: ${billing.price}`);
|
|
36909
|
+
}
|
|
36910
|
+
if (opts.resetPassword) {
|
|
36911
|
+
try {
|
|
36912
|
+
lockId = (await deps.resetLock(serverOf2(name))).lock_id;
|
|
36913
|
+
} catch (err) {
|
|
36914
|
+
if (err instanceof HttpStatusError && err.status === 409) {
|
|
36915
|
+
return { status: "action_required", provider, name, next: `Another pgai connect --reset-password for ${serverOf2(name)} is running: wait for it to finish, then re-run` };
|
|
36916
|
+
}
|
|
36917
|
+
if (err instanceof HttpStatusError && err.status === 404) {
|
|
36918
|
+
return { status: "action_required", provider, name, next: `This platform cannot lock the server for --reset-password yet: set PGAI_MON_PASSWORD to ${DEFAULT_MONITORING_USER}'s password instead` };
|
|
36919
|
+
}
|
|
36920
|
+
throw err;
|
|
36921
|
+
}
|
|
36922
|
+
}
|
|
36923
|
+
let prepared;
|
|
36924
|
+
let created;
|
|
36925
|
+
try {
|
|
36926
|
+
if (lockId) {
|
|
36927
|
+
const now = await deps.list();
|
|
36928
|
+
const same = now.find((d) => d.name === name && !disconnecting2(d.status));
|
|
36929
|
+
if (same)
|
|
36930
|
+
return { status: "action_required", provider, name, id: same.id, next: `${name} is already connected, and its monitoring uses the current password: pgai disconnect ${name} --yes first` };
|
|
36931
|
+
const others = othersOnServer(now);
|
|
36932
|
+
if (others.length)
|
|
36933
|
+
return cutOff(others);
|
|
36934
|
+
}
|
|
36935
|
+
progress("preparing", `Preparing ${maskConnectionString(url)}`);
|
|
36936
|
+
prepared = await deps.prepare(url, provider, { resetPassword: opts.resetPassword });
|
|
36937
|
+
if ("next" in prepared)
|
|
36938
|
+
return { status: "action_required", provider, name, ...billing, ...prepared };
|
|
36939
|
+
if (prepared.note)
|
|
36940
|
+
note = `; ${prepared.note}`;
|
|
36941
|
+
if (opts.selfHosted) {
|
|
36942
|
+
await deps.selfHosted(prepared.monitoringUrl, key && ch ? { CLICKHOUSE_ORG_ID: ch.orgId, CLICKHOUSE_KEY_ID: key.keyId, CLICKHOUSE_KEY_SECRET: key.keySecret } : {});
|
|
36943
|
+
return { status: "connected", provider, name, dashboard_url: "http://localhost:3000", host_metrics: !!ch, next: `pgai mon health${note}` };
|
|
36944
|
+
}
|
|
36945
|
+
progress("provisioning", `Provisioning monitoring for ${name}`);
|
|
36946
|
+
created = await deps.create({
|
|
36947
|
+
db_url: prepared.monitoringUrl,
|
|
36948
|
+
...provider === "clickhouse" ? { provider } : {},
|
|
36949
|
+
...key && ch ? { clickhouse_org_id: ch.orgId, clickhouse_key_id: key.keyId, clickhouse_key_secret: key.keySecret } : {},
|
|
36950
|
+
...opts.coupon ? { promo_code: opts.coupon } : {},
|
|
36951
|
+
...accepted ? { accept_price: true } : {}
|
|
36952
|
+
}).catch(async (err) => {
|
|
36953
|
+
if (err instanceof HttpStatusError && err.status >= 400 && err.status < 500)
|
|
36954
|
+
await undo(prepared);
|
|
36955
|
+
if (err instanceof HttpStatusError && err.status === 402)
|
|
36956
|
+
return /declined/i.test(err.message) ? "declined" : "payment";
|
|
36957
|
+
if (err instanceof HttpStatusError && err.status === 412)
|
|
36958
|
+
return "price";
|
|
36959
|
+
throw err;
|
|
36960
|
+
});
|
|
36961
|
+
if (created === "payment")
|
|
36962
|
+
return { status: "action_required", provider, name, ...billingPage };
|
|
36963
|
+
if (created === "declined") {
|
|
36964
|
+
return { status: "action_required", provider, name, ...billingPage, next: billingPage.next.replace(/^Add a payment method at /, "The payment method on file was declined: update it at ") };
|
|
36965
|
+
}
|
|
36966
|
+
if (created === "price")
|
|
36967
|
+
return { status: "action_required", provider, name, ...billing, next: "The price changed since it was shown: re-run pgai connect to see it" };
|
|
36968
|
+
if (created.status === "failed") {
|
|
36969
|
+
await undo(prepared);
|
|
36970
|
+
return { status: "failed", provider, name, id: created.id, next: `${created.error} Re-run pgai connect later.` };
|
|
36971
|
+
}
|
|
36972
|
+
} finally {
|
|
36973
|
+
if (lockId)
|
|
36974
|
+
await deps.resetUnlock(lockId).catch(() => {});
|
|
36691
36975
|
}
|
|
36692
36976
|
row = { id: created.id, name: created.name, provider, status: created.status, dashboard_url: null, host_metrics: !!ch };
|
|
36693
|
-
checkup = await deps.checkup(withLocalTls2(prepared.monitoringUrl, url)).catch((err) => ({ error: errorText2(err) }));
|
|
36694
|
-
|
|
36695
|
-
|
|
36977
|
+
checkup = await deps.checkup(withLocalTls2(prepared.monitoringUrl, url), created.name).catch((err) => ({ error: errorText2(err) }));
|
|
36978
|
+
progress("checkup", checkupLines2(checkup).join(`
|
|
36979
|
+
`), { checkup });
|
|
36696
36980
|
}
|
|
36697
|
-
|
|
36981
|
+
let shown;
|
|
36982
|
+
for (const deadline = deps.now() + opts.waitMs;; ) {
|
|
36698
36983
|
const result = connectStatus2(row, provider, fresh);
|
|
36699
|
-
|
|
36984
|
+
const state = boxState2(row);
|
|
36985
|
+
if (state !== shown && (shown !== undefined || result.status === "provisioning"))
|
|
36986
|
+
progress("box", `Monitoring box: ${state}`, { state });
|
|
36987
|
+
shown = state;
|
|
36988
|
+
if (result.status !== "provisioning" || deps.now() >= deadline) {
|
|
36700
36989
|
if (result.status === "connected" && provider === "clickhouse" && !row.host_metrics) {
|
|
36701
36990
|
result.next += "; for CPU, memory and disk, disconnect and reconnect with --clickhouse-key <key-id>:<key-secret>";
|
|
36702
36991
|
}
|
|
36703
36992
|
const { next, ...rest } = result;
|
|
36704
|
-
return { ...rest, ...checkup ? { checkup } : {}, next: next + note };
|
|
36993
|
+
return { ...rest, ...billing, ...checkup ? { checkup } : {}, next: next + note };
|
|
36705
36994
|
}
|
|
36706
|
-
deps.progress(`Waiting for the monitoring box (${row.status ?? "starting"})`);
|
|
36707
36995
|
await deps.sleep(POLL_MS2);
|
|
36708
36996
|
row = (await deps.list().catch(() => [])).find((d) => d.id === row.id) ?? row;
|
|
36709
36997
|
}
|
|
@@ -36828,14 +37116,52 @@ ${redactPasswordsInSql(s.sql)}`).join(`
|
|
|
36828
37116
|
}
|
|
36829
37117
|
async function expressCheckup2(url, opts = {}) {
|
|
36830
37118
|
const { client } = await openConnection2(url, opts);
|
|
37119
|
+
let reports;
|
|
37120
|
+
const failed = [];
|
|
36831
37121
|
try {
|
|
36832
|
-
|
|
36833
|
-
const reports = await generateAllReports(client, "node-01", undefined, (f) => failed.push(f.checkId));
|
|
36834
|
-
const findings = Object.values(reports).map((r) => ({ check_id: r.checkId, title: r.checkTitle, status: r.summary.status, message: r.summary.message })).filter((f) => f.status !== "info").sort((a, b) => Number(b.status === "warning") - Number(a.status === "warning"));
|
|
36835
|
-
return { checks: Object.keys(reports).length + failed.length, findings, ...failed.length ? { failed } : {} };
|
|
37122
|
+
reports = await generateAllReports(client, "node-01", undefined, (f) => failed.push(f.checkId));
|
|
36836
37123
|
} finally {
|
|
36837
37124
|
await client.end();
|
|
36838
37125
|
}
|
|
37126
|
+
const all = Object.values(reports).map((r) => ({ check_id: r.checkId, title: r.checkTitle, status: r.summary.status, message: r.summary.message }));
|
|
37127
|
+
const findings = all.filter((f) => f.status !== "info").sort((a, b) => Number(b.status === "warning") - Number(a.status === "warning"));
|
|
37128
|
+
const result = { checks: all.length + failed.length, findings, info: all.filter((f) => f.status === "info").map((f) => f.check_id), ...failed.length ? { failed } : {} };
|
|
37129
|
+
if (!opts.save)
|
|
37130
|
+
return result;
|
|
37131
|
+
try {
|
|
37132
|
+
return { ...result, report_id: await opts.save(reports) };
|
|
37133
|
+
} catch (err) {
|
|
37134
|
+
return { ...result, upload_error: errorText2(err) };
|
|
37135
|
+
}
|
|
37136
|
+
}
|
|
37137
|
+
async function saveCheckupReport2(rpc, accessToken, project, reports) {
|
|
37138
|
+
const idOf = (fn, answer, ...keys) => {
|
|
37139
|
+
const a = answer ?? {};
|
|
37140
|
+
const id = Number(keys.map((k) => a[k]).find((v) => v !== undefined));
|
|
37141
|
+
if (!(id > 0))
|
|
37142
|
+
throw new Error(typeof a.message === "string" ? a.message : `Unexpected ${fn} answer: ${JSON.stringify(answer)}`);
|
|
37143
|
+
return id;
|
|
37144
|
+
};
|
|
37145
|
+
const reportId = idOf("checkup_report_create", await rpc("checkup_report_create", { access_token: accessToken, project }), "report_id");
|
|
37146
|
+
const setStatus = (status) => rpc("checkup_report_status_update", { access_token: accessToken, report_id: reportId, status });
|
|
37147
|
+
try {
|
|
37148
|
+
for (const [checkId, report] of Object.entries(reports)) {
|
|
37149
|
+
idOf("checkup_report_file_post", await rpc("checkup_report_file_post", {
|
|
37150
|
+
access_token: accessToken,
|
|
37151
|
+
checkup_report_id: reportId,
|
|
37152
|
+
filename: `${checkId}.json`,
|
|
37153
|
+
check_id: checkId,
|
|
37154
|
+
data: JSON.stringify(report, null, 2),
|
|
37155
|
+
type: "json",
|
|
37156
|
+
generate_issue: true
|
|
37157
|
+
}), "report_chunck_id", "report_chunk_id");
|
|
37158
|
+
}
|
|
37159
|
+
} catch (err) {
|
|
37160
|
+
await setStatus("failed").catch(() => {});
|
|
37161
|
+
throw err;
|
|
37162
|
+
}
|
|
37163
|
+
await setStatus("completed");
|
|
37164
|
+
return reportId;
|
|
36839
37165
|
}
|
|
36840
37166
|
async function clickhouseOrgFor2(host, keyId, keySecret) {
|
|
36841
37167
|
const apiUrl = process.env.CLICKHOUSE_API_URL || "https://api.clickhouse.cloud";
|
|
@@ -36872,84 +37198,124 @@ function platformDeps2(p) {
|
|
|
36872
37198
|
list: () => rpc("cloud_monitoring_list"),
|
|
36873
37199
|
create: (body) => rpc("cloud_monitoring_connect", body),
|
|
36874
37200
|
disconnect: (id) => rpc("cloud_monitoring_disconnect", { instance_id: id }),
|
|
37201
|
+
resetLock: (server) => rpc("cloud_monitoring_reset_lock", { server }),
|
|
37202
|
+
resetUnlock: (lockId) => rpc("cloud_monitoring_reset_unlock", { lock_id: lockId }),
|
|
36875
37203
|
prepare: (url, provider, o = {}) => prepareDatabase2(url, provider, { ...o, agent: p.agent }),
|
|
36876
37204
|
unprepare: (url) => unprepareDatabase2(url, { agent: p.agent }),
|
|
36877
37205
|
clickhouseOrg: clickhouseOrgFor2,
|
|
36878
|
-
checkup: (url) => expressCheckup2(url, { agent: p.agent }),
|
|
37206
|
+
checkup: (url, project) => expressCheckup2(url, { agent: p.agent, save: (reports) => saveCheckupReport2(rpc, p.apiKey, project, reports) }),
|
|
37207
|
+
quote: (coupon) => rpc("cloud_monitoring_quote", coupon ? { promo_code: coupon } : {}),
|
|
37208
|
+
billingUrl: (orgAlias) => `${p.uiBaseUrl}/${orgAlias}/billing`,
|
|
36879
37209
|
handoffUrl: async (provider) => {
|
|
36880
37210
|
const orgs = await listOrgs({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl });
|
|
36881
37211
|
const org = orgs.length === 1 ? orgs[0] : orgs.find((o) => o.alias === p.orgScope?.alias || o.org_id === p.orgScope?.id);
|
|
36882
37212
|
return `${p.uiBaseUrl}/${org?.alias ?? "<org>"}/monitoring/scale/create/${provider}`;
|
|
36883
37213
|
},
|
|
36884
|
-
sleep: (ms) => new Promise((r) => setTimeout(r, ms))
|
|
37214
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
37215
|
+
now: () => Date.now()
|
|
36885
37216
|
};
|
|
36886
37217
|
}
|
|
36887
37218
|
|
|
36888
37219
|
// lib/joe.ts
|
|
36889
37220
|
var DEFAULT_BUDGET_MS = 25000;
|
|
36890
37221
|
var DEFAULT_POLL_INTERVAL_MS = 800;
|
|
37222
|
+
var JOE_ASYNC_FALLBACK_WAIT_MS2 = 10 * 60 * 1000;
|
|
37223
|
+
var JOE_ASYNC_MAX_WAIT_MS2 = 60 * 60 * 1000;
|
|
37224
|
+
var JOE_ASYNC_MAX_POLL_FAILURES = 5;
|
|
36891
37225
|
async function callRpc2(params) {
|
|
37226
|
+
return (await callRpcRaw2(params)).data;
|
|
37227
|
+
}
|
|
37228
|
+
async function callRpcRaw2(params) {
|
|
36892
37229
|
const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
|
|
36893
37230
|
if (!apiKey) {
|
|
36894
37231
|
throw new Error("API key is required");
|
|
36895
37232
|
}
|
|
36896
37233
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
36897
37234
|
const url = new URL(`${base}/rpc/${fn}`);
|
|
36898
|
-
const payload = JSON.stringify(body);
|
|
36899
37235
|
const headers = buildAuthHeaders(apiKey, params.orgScope);
|
|
36900
|
-
|
|
36901
|
-
const
|
|
36902
|
-
|
|
36903
|
-
|
|
36904
|
-
|
|
36905
|
-
|
|
36906
|
-
|
|
36907
|
-
|
|
36908
|
-
|
|
36909
|
-
|
|
36910
|
-
|
|
36911
|
-
|
|
36912
|
-
|
|
36913
|
-
|
|
36914
|
-
|
|
36915
|
-
|
|
36916
|
-
|
|
36917
|
-
|
|
37236
|
+
const send = async (bodyObj) => {
|
|
37237
|
+
const payload = JSON.stringify(bodyObj);
|
|
37238
|
+
if (debug) {
|
|
37239
|
+
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
37240
|
+
console.error(`Debug: POST URL: ${url.toString()}`);
|
|
37241
|
+
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
37242
|
+
console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
|
|
37243
|
+
}
|
|
37244
|
+
let response;
|
|
37245
|
+
const requestTimeout = requestTimeoutSignal(params.timeoutMs);
|
|
37246
|
+
try {
|
|
37247
|
+
response = await fetch(url.toString(), {
|
|
37248
|
+
method: "POST",
|
|
37249
|
+
headers,
|
|
37250
|
+
body: payload,
|
|
37251
|
+
signal: requestTimeout.signal
|
|
37252
|
+
});
|
|
37253
|
+
} catch (err) {
|
|
37254
|
+
if (isFetchTimeout(err)) {
|
|
37255
|
+
throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
|
|
37256
|
+
}
|
|
37257
|
+
throw new Error(describeFetchError(operation, base, err));
|
|
36918
37258
|
}
|
|
36919
|
-
|
|
36920
|
-
|
|
36921
|
-
|
|
36922
|
-
|
|
36923
|
-
|
|
36924
|
-
|
|
37259
|
+
const text = await response.text();
|
|
37260
|
+
if (debug) {
|
|
37261
|
+
console.error(`Debug: Response status: ${response.status}`);
|
|
37262
|
+
console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
|
|
37263
|
+
}
|
|
37264
|
+
return { response, text };
|
|
37265
|
+
};
|
|
37266
|
+
let { response, text } = await send(body);
|
|
37267
|
+
if (params.fallbackBody && isUnknownRpcSignature(response.status, text, fn)) {
|
|
37268
|
+
({ response, text } = await send(params.fallbackBody));
|
|
36925
37269
|
}
|
|
36926
37270
|
if (!response.ok) {
|
|
36927
37271
|
throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
|
|
36928
37272
|
}
|
|
36929
37273
|
try {
|
|
36930
|
-
return JSON.parse(text);
|
|
37274
|
+
return { data: JSON.parse(text), status: response.status };
|
|
36931
37275
|
} catch {
|
|
36932
37276
|
throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
|
|
36933
37277
|
}
|
|
36934
37278
|
}
|
|
36935
|
-
|
|
37279
|
+
function joeAsyncWaitMs(handle) {
|
|
37280
|
+
const expires = positiveSeconds(handle.expires_in_s);
|
|
37281
|
+
return Math.min(expires ? expires * 1000 : JOE_ASYNC_FALLBACK_WAIT_MS2, JOE_ASYNC_MAX_WAIT_MS2);
|
|
37282
|
+
}
|
|
37283
|
+
function asyncHandleCommandId(reply, operation) {
|
|
37284
|
+
const commandId = reply?.command_id;
|
|
37285
|
+
if (typeof commandId !== "string" || !/^[0-9]+$/.test(commandId)) {
|
|
37286
|
+
throw new Error(`${operation}: the platform accepted the command over the job channel but ` + `returned no command id: ${redactSecretsForLog(JSON.stringify(reply))}`);
|
|
37287
|
+
}
|
|
37288
|
+
return commandId;
|
|
37289
|
+
}
|
|
37290
|
+
async function startCommandDetailed(params) {
|
|
36936
37291
|
const { apiKey, apiBaseUrl, instanceId, command, debug, timeoutMs } = params;
|
|
36937
37292
|
if (!String(command ?? "").trim()) {
|
|
36938
37293
|
throw new Error("command text is required");
|
|
36939
37294
|
}
|
|
36940
|
-
const
|
|
37295
|
+
const operation = "Failed to run Joe command";
|
|
37296
|
+
const body = { instance_id: instanceId, command };
|
|
37297
|
+
const { data, status } = await callRpcRaw2({
|
|
36941
37298
|
apiKey,
|
|
36942
37299
|
apiBaseUrl,
|
|
36943
37300
|
fn: "joe_command_run",
|
|
36944
|
-
body: {
|
|
36945
|
-
|
|
37301
|
+
body: { ...body, accept_async: true },
|
|
37302
|
+
fallbackBody: body,
|
|
37303
|
+
operation,
|
|
36946
37304
|
debug,
|
|
36947
37305
|
timeoutMs
|
|
36948
37306
|
});
|
|
36949
|
-
if (
|
|
36950
|
-
|
|
37307
|
+
if (status === 202) {
|
|
37308
|
+
const handle = data ?? {};
|
|
37309
|
+
return {
|
|
37310
|
+
commandId: asyncHandleCommandId(data, operation),
|
|
37311
|
+
asyncWaitMs: joeAsyncWaitMs(handle),
|
|
37312
|
+
firstAnswerEstimateS: positiveSeconds(handle.first_answer_estimate_s)
|
|
37313
|
+
};
|
|
36951
37314
|
}
|
|
36952
|
-
|
|
37315
|
+
if (typeof data !== "string" || !/^[0-9]+$/.test(data)) {
|
|
37316
|
+
throw new Error(`${operation}: expected a command id string, got: ${redactSecretsForLog(JSON.stringify(data))}`);
|
|
37317
|
+
}
|
|
37318
|
+
return { commandId: data, asyncWaitMs: null, firstAnswerEstimateS: null };
|
|
36953
37319
|
}
|
|
36954
37320
|
async function getCommandOutput(params) {
|
|
36955
37321
|
const { apiKey, apiBaseUrl, commandId, debug, timeoutMs } = params;
|
|
@@ -37079,18 +37445,29 @@ function buildJoeCommandText(command, input = {}) {
|
|
|
37079
37445
|
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
37080
37446
|
async function runCommand(params) {
|
|
37081
37447
|
const { apiKey, apiBaseUrl, instanceId, command, debug } = params;
|
|
37082
|
-
const
|
|
37083
|
-
const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
37448
|
+
const explicitBudgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) && params.budgetMs >= 0 ? params.budgetMs : null;
|
|
37084
37449
|
const now = params.now ?? Date.now;
|
|
37085
37450
|
const sleep = params.sleep ?? defaultSleep;
|
|
37086
|
-
const
|
|
37451
|
+
const started = await startCommandDetailed({ apiKey, apiBaseUrl, instanceId, command, debug });
|
|
37452
|
+
const commandId = started.commandId;
|
|
37453
|
+
const budgetMs = explicitBudgetMs ?? started.asyncWaitMs ?? DEFAULT_BUDGET_MS;
|
|
37454
|
+
const pollDelayMs = (attempt) => params.pollIntervalMs ?? (started.asyncWaitMs === null ? DEFAULT_POLL_INTERVAL_MS : asyncPollDelayMs(attempt));
|
|
37087
37455
|
const deadline = now() + budgetMs;
|
|
37088
37456
|
let status = "pending";
|
|
37089
37457
|
const remainingRequestMs = () => Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, deadline - now()));
|
|
37090
37458
|
if (now() >= deadline) {
|
|
37091
|
-
return { commandId, status, output: null, budgetExpired: true };
|
|
37459
|
+
return { commandId, status, output: null, budgetExpired: true, budgetMs };
|
|
37092
37460
|
}
|
|
37093
|
-
|
|
37461
|
+
let announced = false;
|
|
37462
|
+
const announce = () => {
|
|
37463
|
+
if (announced || started.asyncWaitMs === null)
|
|
37464
|
+
return;
|
|
37465
|
+
announced = true;
|
|
37466
|
+
const estimate = started.firstAnswerEstimateS;
|
|
37467
|
+
console.error(`Queued on the Joe job channel (command ${commandId}` + (estimate ? `, reaches Joe in about ${estimate}s` : "") + `); waiting up to ${Math.round(budgetMs / 1000)}s...`);
|
|
37468
|
+
};
|
|
37469
|
+
let transientFailures = 0;
|
|
37470
|
+
for (let attempt = 0;; attempt++) {
|
|
37094
37471
|
let output;
|
|
37095
37472
|
try {
|
|
37096
37473
|
output = await getCommandOutput({
|
|
@@ -37102,25 +37479,28 @@ async function runCommand(params) {
|
|
|
37102
37479
|
});
|
|
37103
37480
|
} catch (err) {
|
|
37104
37481
|
if (err instanceof HttpRequestTimeoutError) {
|
|
37105
|
-
return { commandId, status, output: null, budgetExpired: true };
|
|
37482
|
+
return { commandId, status, output: null, budgetExpired: true, budgetMs };
|
|
37106
37483
|
}
|
|
37107
37484
|
if (err instanceof HttpStatusError && isRetryableHttpStatus(err.status)) {
|
|
37108
|
-
|
|
37109
|
-
|
|
37485
|
+
transientFailures += 1;
|
|
37486
|
+
if (now() >= deadline || started.asyncWaitMs !== null && transientFailures >= JOE_ASYNC_MAX_POLL_FAILURES) {
|
|
37487
|
+
return { commandId, status, output: null, budgetExpired: true, budgetMs };
|
|
37110
37488
|
}
|
|
37111
|
-
await sleep(
|
|
37489
|
+
await sleep(pollDelayMs(attempt));
|
|
37112
37490
|
continue;
|
|
37113
37491
|
}
|
|
37114
37492
|
throw err;
|
|
37115
37493
|
}
|
|
37494
|
+
transientFailures = 0;
|
|
37116
37495
|
status = output.status;
|
|
37117
37496
|
if (status === "ok" || status === "error") {
|
|
37118
|
-
return { commandId, status, output, budgetExpired: false };
|
|
37497
|
+
return { commandId, status, output, budgetExpired: false, budgetMs };
|
|
37119
37498
|
}
|
|
37120
37499
|
if (now() >= deadline) {
|
|
37121
|
-
return { commandId, status, output: null, budgetExpired: true };
|
|
37500
|
+
return { commandId, status, output: null, budgetExpired: true, budgetMs };
|
|
37122
37501
|
}
|
|
37123
|
-
|
|
37502
|
+
announce();
|
|
37503
|
+
await sleep(pollDelayMs(attempt));
|
|
37124
37504
|
}
|
|
37125
37505
|
}
|
|
37126
37506
|
async function executeJoeCommand(params) {
|
|
@@ -37305,22 +37685,6 @@ async function postDblabRpc(rpc, bodyObj, params) {
|
|
|
37305
37685
|
}
|
|
37306
37686
|
return { response, text };
|
|
37307
37687
|
}
|
|
37308
|
-
function isUnknownRpcSignature(status, text, rpc) {
|
|
37309
|
-
if (status !== 404)
|
|
37310
|
-
return false;
|
|
37311
|
-
try {
|
|
37312
|
-
const parsed = JSON.parse(text);
|
|
37313
|
-
return typeof parsed.message === "string" && /Could not find the .* function/.test(parsed.message) && parsed.message.includes(`${rpc}(`);
|
|
37314
|
-
} catch {
|
|
37315
|
-
return false;
|
|
37316
|
-
}
|
|
37317
|
-
}
|
|
37318
|
-
function positiveSeconds(v) {
|
|
37319
|
-
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
37320
|
-
}
|
|
37321
|
-
function dblabPollDelay(attempt) {
|
|
37322
|
-
return Math.min(1000 * 2 ** attempt, 15000);
|
|
37323
|
-
}
|
|
37324
37688
|
var DBLAB_ASYNC_FALLBACK_WAIT_MS = 15 * 60 * 1000;
|
|
37325
37689
|
var DBLAB_ASYNC_MAX_WAIT_MS = 60 * 60 * 1000;
|
|
37326
37690
|
var DBLAB_POLL_MAX_CONSECUTIVE_FAILURES = 5;
|
|
@@ -37407,7 +37771,7 @@ async function awaitDblabCall(handle, params) {
|
|
|
37407
37771
|
announced = true;
|
|
37408
37772
|
console.error(`Waiting for the DBLab instance to pick up the call (job ${jobId}` + (estimate ? `, typically ~${estimate}s` : "") + ")...");
|
|
37409
37773
|
}
|
|
37410
|
-
const delay =
|
|
37774
|
+
const delay = asyncPollDelayMs(attempt);
|
|
37411
37775
|
if (now() + delay >= deadline) {
|
|
37412
37776
|
throw new Error(`${operation}: timed out waiting for the DBLab instance (job ${jobId}, last status ${lastStatus}). ` + dblabNotResentAdvice(jobId));
|
|
37413
37777
|
}
|
|
@@ -38965,6 +39329,10 @@ async function applyUninitPlan(params) {
|
|
|
38965
39329
|
}
|
|
38966
39330
|
return { applied, errors };
|
|
38967
39331
|
}
|
|
39332
|
+
async function legacyHelperWarnings(client) {
|
|
39333
|
+
const r = await client.query("select to_regprocedure('postgres_ai.explain_generic(text,text,text)') is not null as present");
|
|
39334
|
+
return r.rows?.[0]?.present ? ["legacy SECURITY DEFINER helper postgres_ai.explain_generic(text, text, text) is still installed; its owner or a superuser must drop it"] : [];
|
|
39335
|
+
}
|
|
38968
39336
|
async function verifyInitSetup2(params) {
|
|
38969
39337
|
await params.client.query("begin isolation level repeatable read;");
|
|
38970
39338
|
try {
|
|
@@ -40137,7 +40505,7 @@ import * as childProcess from "child_process";
|
|
|
40137
40505
|
import * as fs9 from "fs";
|
|
40138
40506
|
import * as path8 from "path";
|
|
40139
40507
|
var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
|
|
40140
|
-
var CONTRACT_VERSION2 = "1.
|
|
40508
|
+
var CONTRACT_VERSION2 = "1.1.0";
|
|
40141
40509
|
var SECONDS_PER_DAY2 = 86400;
|
|
40142
40510
|
var SECONDS_PER_HOUR2 = 3600;
|
|
40143
40511
|
var SECONDS_PER_MINUTE2 = 60;
|
|
@@ -42998,7 +43366,8 @@ function summarizeF0012(nodeData) {
|
|
|
42998
43366
|
return `Autovacuum config: ${parts.join(", ")} finding${fired.length > 1 ? "s" : ""}`;
|
|
42999
43367
|
};
|
|
43000
43368
|
if (severity === "CRITICAL" || severity === "WARNING") {
|
|
43001
|
-
|
|
43369
|
+
const top = fired.find((r) => r.severity === "CRITICAL") ?? fired.find((r) => r.severity === "WARNING");
|
|
43370
|
+
return { status: "warning", message: top?.conclusion ? `${summarize()}. ${top.conclusion}` : summarize() };
|
|
43002
43371
|
}
|
|
43003
43372
|
return { status: "info", message: summarize() };
|
|
43004
43373
|
}
|
|
@@ -43155,6 +43524,24 @@ function summarizeG0032(nodeData) {
|
|
|
43155
43524
|
};
|
|
43156
43525
|
}
|
|
43157
43526
|
|
|
43527
|
+
// lib/json-stderr.ts
|
|
43528
|
+
import { format } from "node:util";
|
|
43529
|
+
function writeEvent(event) {
|
|
43530
|
+
process.stderr.write(`${JSON.stringify(event)}
|
|
43531
|
+
`);
|
|
43532
|
+
}
|
|
43533
|
+
async function withJsonStderr(fn) {
|
|
43534
|
+
const { error, warn } = console;
|
|
43535
|
+
console.error = (...args) => writeEvent({ event: "log", level: "error", message: format(...args) });
|
|
43536
|
+
console.warn = (...args) => writeEvent({ event: "log", level: "warn", message: format(...args) });
|
|
43537
|
+
try {
|
|
43538
|
+
return await fn();
|
|
43539
|
+
} finally {
|
|
43540
|
+
console.error = error;
|
|
43541
|
+
console.warn = warn;
|
|
43542
|
+
}
|
|
43543
|
+
}
|
|
43544
|
+
|
|
43158
43545
|
// lib/instances.ts
|
|
43159
43546
|
import * as fs10 from "fs";
|
|
43160
43547
|
class InstancesParseError2 extends Error {
|
|
@@ -43903,12 +44290,47 @@ function sanitizeTagForBackup(tag) {
|
|
|
43903
44290
|
const stripped = stripMatchingQuotes(tag);
|
|
43904
44291
|
return /^[A-Za-z0-9._-]{1,64}$/.test(stripped) ? stripped : null;
|
|
43905
44292
|
}
|
|
43906
|
-
function
|
|
44293
|
+
function planUpdateTag(current, cliVersion) {
|
|
44294
|
+
const semver = (v) => {
|
|
44295
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(v);
|
|
44296
|
+
return m ? { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] } : null;
|
|
44297
|
+
};
|
|
44298
|
+
const cli = semver(cliVersion);
|
|
44299
|
+
if (!cli || cli.pre) {
|
|
44300
|
+
return { tag: null, note: `PGAI_TAG stays ${current ?? "unset"}: this CLI (${cliVersion}) is not a release. To pick a stack version, set PGAI_TAG=<version> in .env and re-run 'postgresai mon update'` };
|
|
44301
|
+
}
|
|
44302
|
+
if (current === cliVersion)
|
|
44303
|
+
return { tag: null, note: `PGAI_TAG is ${cliVersion}, matching this CLI` };
|
|
44304
|
+
const cur = current ? semver(current) : null;
|
|
44305
|
+
if (cur) {
|
|
44306
|
+
const i = cur.core.findIndex((part, j) => part !== cli.core[j]);
|
|
44307
|
+
if (i >= 0 && cur.core[i] > cli.core[i]) {
|
|
44308
|
+
return { tag: null, note: `PGAI_TAG stays ${current}: it is newer than this CLI (${cliVersion}). Upgrade the CLI to move the stack: npm install -g postgresai@latest` };
|
|
44309
|
+
}
|
|
44310
|
+
}
|
|
44311
|
+
return { tag: cliVersion, note: `PGAI_TAG: ${current ?? "unset"} -> ${cliVersion}` };
|
|
44312
|
+
}
|
|
44313
|
+
var ENV_TAG_LINE = /^([ \t]*(?:export[ \t]+)?PGAI_TAG[ \t]*=[ \t]*)(["']?)([A-Za-z0-9._-]*)\2((?:[ \t]+#[^\r\n]*)?[ \t]*\r?)$/gm;
|
|
44314
|
+
var ENV_TAG_ANY = /^[ \t]*(?:export[ \t]+)?PGAI_TAG[ \t]*[=:]/gm;
|
|
44315
|
+
function readEnvTag(projectDir) {
|
|
43907
44316
|
const envFile = path9.resolve(projectDir, ".env");
|
|
43908
44317
|
if (!fs11.existsSync(envFile))
|
|
43909
|
-
return null;
|
|
43910
|
-
const
|
|
43911
|
-
|
|
44318
|
+
return { tag: null, plain: true };
|
|
44319
|
+
const content = fs11.readFileSync(envFile, "utf8");
|
|
44320
|
+
const plainLines = [...content.matchAll(ENV_TAG_LINE)];
|
|
44321
|
+
if (plainLines.length !== [...content.matchAll(ENV_TAG_ANY)].length)
|
|
44322
|
+
return { tag: null, plain: false };
|
|
44323
|
+
const last = plainLines.at(-1);
|
|
44324
|
+
return { tag: last && last[3] ? last[3] : null, plain: true };
|
|
44325
|
+
}
|
|
44326
|
+
function writeEnvTag(projectDir, tag) {
|
|
44327
|
+
const envFile = path9.resolve(projectDir, ".env");
|
|
44328
|
+
const existing = fs11.existsSync(envFile) ? fs11.readFileSync(envFile, "utf8") : "";
|
|
44329
|
+
const content = new RegExp(ENV_TAG_LINE.source, "m").test(existing) ? existing.replace(ENV_TAG_LINE, (_match, prefix, quote, _value, rest) => `${prefix}${quote}${tag}${quote}${rest}`) : `${existing}${existing && !existing.endsWith(`
|
|
44330
|
+
`) ? `
|
|
44331
|
+
` : ""}PGAI_TAG=${tag}
|
|
44332
|
+
`;
|
|
44333
|
+
writeEnvFile(envFile, content);
|
|
43912
44334
|
}
|
|
43913
44335
|
function isValidComposeYaml(text) {
|
|
43914
44336
|
const trimmed = text.trim();
|
|
@@ -43959,7 +44381,7 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
|
43959
44381
|
const existing = fs11.readFileSync(composeFile, "utf8");
|
|
43960
44382
|
if (composeContentEqual(existing, fetched))
|
|
43961
44383
|
return false;
|
|
43962
|
-
const deployedTag = sanitizeTagForBackup(oldTag ??
|
|
44384
|
+
const deployedTag = sanitizeTagForBackup(oldTag ?? readEnvTag(projectDir).tag);
|
|
43963
44385
|
const tagPart = deployedTag ?? new Date().toISOString().replace(/[:.]/g, "-");
|
|
43964
44386
|
const oldHash = crypto2.createHash("sha256").update(existing).digest("hex").slice(0, 8);
|
|
43965
44387
|
const backup = path9.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
|
|
@@ -44030,6 +44452,9 @@ function withOrgOptions(command) {
|
|
|
44030
44452
|
ORG_SCOPED_COMMANDS.add(command);
|
|
44031
44453
|
return command.option("--org <alias>", `organization alias (or ${ORG_ENV}); required with a global token`).option("--org-id <id>", `organization id (or ${ORG_ID_ENV}); alternative to --org`);
|
|
44032
44454
|
}
|
|
44455
|
+
function jsonConnect(command) {
|
|
44456
|
+
return command.parent === program2 && command.name() === "connect" && (!!command.opts().json || !process.stdout.isTTY);
|
|
44457
|
+
}
|
|
44033
44458
|
program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
44034
44459
|
const initPointsToConnect = actionCommand.parent === program2 && actionCommand.name() === "init" && !interactive(actionCommand.opts().json);
|
|
44035
44460
|
if (!ORG_SCOPED_COMMANDS.has(actionCommand) || initPointsToConnect) {
|
|
@@ -44042,7 +44467,10 @@ program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
44042
44467
|
setActiveOrgScope(requireOrgScope(opts, apiKey));
|
|
44043
44468
|
} catch (err) {
|
|
44044
44469
|
if (err instanceof OrgScopeError2) {
|
|
44045
|
-
|
|
44470
|
+
if (jsonConnect(actionCommand))
|
|
44471
|
+
writeEvent({ event: "log", level: "error", message: err.message });
|
|
44472
|
+
else
|
|
44473
|
+
console.error(err.message);
|
|
44046
44474
|
process.exit(1);
|
|
44047
44475
|
}
|
|
44048
44476
|
throw err;
|
|
@@ -44264,6 +44692,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44264
44692
|
monitoringUser: opts.monitoringUser,
|
|
44265
44693
|
includeOptionalPermissions
|
|
44266
44694
|
});
|
|
44695
|
+
v.missingOptional.push(...await legacyHelperWarnings(supabaseClient));
|
|
44267
44696
|
if (v.ok) {
|
|
44268
44697
|
if (jsonOutput) {
|
|
44269
44698
|
const result = {
|
|
@@ -44386,6 +44815,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44386
44815
|
client: supabaseClient,
|
|
44387
44816
|
plan: effectivePlan
|
|
44388
44817
|
});
|
|
44818
|
+
const legacyWarnings = await legacyHelperWarnings(supabaseClient);
|
|
44389
44819
|
if (jsonOutput) {
|
|
44390
44820
|
const result = {
|
|
44391
44821
|
success: true,
|
|
@@ -44395,7 +44825,10 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44395
44825
|
monitoringUser: opts.monitoringUser,
|
|
44396
44826
|
applied,
|
|
44397
44827
|
skippedOptional,
|
|
44398
|
-
warnings:
|
|
44828
|
+
warnings: [
|
|
44829
|
+
...skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : [],
|
|
44830
|
+
...legacyWarnings
|
|
44831
|
+
]
|
|
44399
44832
|
};
|
|
44400
44833
|
if (passwordGenerated) {
|
|
44401
44834
|
result.generatedPassword = monPassword;
|
|
@@ -44411,6 +44844,8 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44411
44844
|
for (const s of skippedOptional)
|
|
44412
44845
|
console.error(`- ${s}`);
|
|
44413
44846
|
}
|
|
44847
|
+
for (const w of legacyWarnings)
|
|
44848
|
+
console.error(`\u26A0 ${w}`);
|
|
44414
44849
|
if (process.stdout.isTTY) {
|
|
44415
44850
|
console.log(`Applied ${applied.length} steps`);
|
|
44416
44851
|
}
|
|
@@ -44552,6 +44987,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44552
44987
|
includeOptionalPermissions,
|
|
44553
44988
|
provider
|
|
44554
44989
|
});
|
|
44990
|
+
v.missingOptional.push(...await legacyHelperWarnings(client));
|
|
44555
44991
|
if (v.ok) {
|
|
44556
44992
|
if (jsonOutput) {
|
|
44557
44993
|
outputJson({
|
|
@@ -44670,6 +45106,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44670
45106
|
return;
|
|
44671
45107
|
}
|
|
44672
45108
|
const { applied, skippedOptional } = await applyInitPlan2({ client, plan: effectivePlan });
|
|
45109
|
+
const legacyWarnings = await legacyHelperWarnings(client);
|
|
44673
45110
|
if (jsonOutput) {
|
|
44674
45111
|
const result = {
|
|
44675
45112
|
success: true,
|
|
@@ -44679,7 +45116,10 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44679
45116
|
monitoringUser: opts.monitoringUser,
|
|
44680
45117
|
applied,
|
|
44681
45118
|
skippedOptional,
|
|
44682
|
-
warnings:
|
|
45119
|
+
warnings: [
|
|
45120
|
+
...skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : [],
|
|
45121
|
+
...legacyWarnings
|
|
45122
|
+
]
|
|
44683
45123
|
};
|
|
44684
45124
|
if (passwordGenerated) {
|
|
44685
45125
|
result.generatedPassword = monPassword;
|
|
@@ -44692,6 +45132,8 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44692
45132
|
for (const s of skippedOptional)
|
|
44693
45133
|
console.error(`- ${s}`);
|
|
44694
45134
|
}
|
|
45135
|
+
for (const w of legacyWarnings)
|
|
45136
|
+
console.error(`\u26A0 ${w}`);
|
|
44695
45137
|
if (process.stdout.isTTY) {
|
|
44696
45138
|
console.log(`Applied ${applied.length} steps`);
|
|
44697
45139
|
}
|
|
@@ -45910,7 +46352,22 @@ function signedIn(opts) {
|
|
|
45910
46352
|
}
|
|
45911
46353
|
return !!cloudApi().apiKey;
|
|
45912
46354
|
}
|
|
45913
|
-
|
|
46355
|
+
function runConnect(url, opts) {
|
|
46356
|
+
return opts.json || !process.stdout.isTTY ? withJsonStderr(() => connectCommand(url, opts, true)) : connectCommand(url, opts, false);
|
|
46357
|
+
}
|
|
46358
|
+
function runLocalInstall(args, env, json) {
|
|
46359
|
+
if (!json)
|
|
46360
|
+
return Promise.resolve(childProcess.spawnSync(process.execPath, args, { stdio: ["ignore", 2, 2], env }).status);
|
|
46361
|
+
const child = childProcess.spawn(process.execPath, args, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
46362
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
46363
|
+
createInterface({ input: stream }).on("line", (line) => writeEvent({ event: "log", level: "info", source: "mon local-install", message: line }));
|
|
46364
|
+
}
|
|
46365
|
+
return new Promise((resolve) => {
|
|
46366
|
+
child.on("error", () => resolve(null));
|
|
46367
|
+
child.on("close", (code) => resolve(code));
|
|
46368
|
+
});
|
|
46369
|
+
}
|
|
46370
|
+
async function connectCommand(url, opts, json) {
|
|
45914
46371
|
const name = (() => {
|
|
45915
46372
|
try {
|
|
45916
46373
|
return databaseName2(url);
|
|
@@ -45926,38 +46383,56 @@ async function runConnect(url, opts) {
|
|
|
45926
46383
|
if (!Number.isFinite(waitMinutes) || waitMinutes < 0) {
|
|
45927
46384
|
return emitConnect({ status: "failed", provider, name, next: "--wait must be a number of minutes (0 = do not wait)" }, opts.json);
|
|
45928
46385
|
}
|
|
45929
|
-
if (!opts.selfHosted && !signedIn(opts)) {
|
|
45930
|
-
return emitConnect({ status: "action_required", provider, name, next: "Sign in: pgai auth login (agents: set PGAI_API_KEY), then re-run" }, opts.json);
|
|
45931
|
-
}
|
|
45932
|
-
const api = cloudApi(opts.debug);
|
|
45933
46386
|
try {
|
|
46387
|
+
if (!opts.selfHosted && !signedIn(opts)) {
|
|
46388
|
+
return emitConnect({ status: "action_required", provider, name, next: "Sign in: pgai auth login (agents: set PGAI_API_KEY), then re-run" }, opts.json);
|
|
46389
|
+
}
|
|
46390
|
+
const api = cloudApi(opts.debug);
|
|
45934
46391
|
const result = await connect2(url, { ...opts, waitMs: waitMinutes * 60000 }, {
|
|
45935
46392
|
...api,
|
|
45936
46393
|
selfHosted: async (monitoringUrl, env) => {
|
|
45937
46394
|
const scope = getActiveOrgScope2();
|
|
45938
46395
|
const org = scope?.alias ? ["--org", scope.alias] : scope?.id ? ["--org-id", String(scope.id)] : [];
|
|
45939
46396
|
const project = name.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
45940
|
-
const
|
|
45941
|
-
|
|
45942
|
-
env: { ...process.env, ...env, PGAI_DB_URL: monitoringUrl, PGAI_API_BASE_URL: api.apiBaseUrl, ...api.apiKey ? { PGAI_API_KEY: api.apiKey } : {} }
|
|
45943
|
-
});
|
|
45944
|
-
if (r.status !== 0)
|
|
46397
|
+
const status = await runLocalInstall([process.argv[1], "mon", "local-install", "-y", ...org, "--project", project], { ...process.env, ...env, PGAI_DB_URL: monitoringUrl, PGAI_API_BASE_URL: api.apiBaseUrl, ...api.apiKey ? { PGAI_API_KEY: api.apiKey } : {} }, json);
|
|
46398
|
+
if (status !== 0)
|
|
45945
46399
|
throw new Error("mon local-install failed (see above)");
|
|
45946
46400
|
},
|
|
45947
46401
|
localStackRunning: () => checkRunningContainers().running,
|
|
45948
|
-
|
|
46402
|
+
confirm: async (q) => {
|
|
46403
|
+
if (!interactive(opts.json))
|
|
46404
|
+
return false;
|
|
46405
|
+
process.exitCode = 130;
|
|
46406
|
+
const answer = await question(q);
|
|
46407
|
+
process.exitCode = undefined;
|
|
46408
|
+
closeReadline();
|
|
46409
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
46410
|
+
},
|
|
46411
|
+
progress: (e) => json ? writeEvent(e) : console.error(progressText(e))
|
|
45949
46412
|
});
|
|
45950
46413
|
emitConnect(result, opts.json);
|
|
45951
46414
|
} catch (err) {
|
|
45952
46415
|
emitConnect({ status: "failed", provider, name, next: err instanceof Error ? err.message : String(err) }, opts.json);
|
|
45953
46416
|
}
|
|
45954
46417
|
}
|
|
45955
|
-
withOrgOptions(program2.command("connect <database-url>")).
|
|
46418
|
+
withOrgOptions(program2.command("connect <database-url>")).configureOutput({
|
|
46419
|
+
writeErr: (text) => {
|
|
46420
|
+
if (!(process.argv.includes("--json") || !process.stdout.isTTY))
|
|
46421
|
+
return void process.stderr.write(text);
|
|
46422
|
+
for (const line of text.split(`
|
|
46423
|
+
`).filter((l) => l.trim() !== ""))
|
|
46424
|
+
writeEvent({ event: "log", level: "error", message: line });
|
|
46425
|
+
}
|
|
46426
|
+
}).description("put a database under PostgresAI's care: prepare it, provision monitoring, print the dashboard").option("--provider <provider>", "clickhouse | rds | supabase | self-managed (default: detected from the host)").option("--clickhouse-key <id:secret>", "ClickHouse Cloud API key (Basic Service API Reader) for CPU, memory and disk; or CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET").option("--self-hosted", "run the monitoring stack on this machine (mon local-install) instead of PostgresAI Cloud").option("--reset-password", "postgres_ai_mon exists and its password is lost: set a new one (admin URL; refused while another database on the server is monitored)").option("--wait <minutes>", "how long to wait for the monitoring box (0 = do not wait)", "20").option("--coupon <code>", "promotion code for the organization's monitoring subscription (applies when it is first created)").option("-y, --yes", "never prompt: accept the price of a billed box").option("--json", "JSON output (default when stdout is not a TTY)").option("--debug", "print HTTP requests (secrets masked)").addHelpText("after", [
|
|
45956
46427
|
"",
|
|
45957
46428
|
"Steps (each skipped when already done; safe to re-run): sign in, prepare the database",
|
|
45958
46429
|
"(an admin URL creates the postgres_ai_mon role; otherwise the SQL is printed), provision",
|
|
45959
46430
|
"the monitoring box, run the express checkup while it starts, wait, print the dashboard URL.",
|
|
45960
46431
|
"",
|
|
46432
|
+
'Billing: before a new box, connect shows its price (or "free (N of M free slots)"). A billed',
|
|
46433
|
+
"box is provisioned only when accepted: at the prompt, or with --yes. With no payment method",
|
|
46434
|
+
"it stops (exit 3) and names the console page to add one. --coupon applies a promotion code.",
|
|
46435
|
+
"",
|
|
45961
46436
|
'Exit codes: 0 connected or provisioning, 1 failed, 3 action required (see "next").',
|
|
45962
46437
|
"",
|
|
45963
46438
|
"Environment: PGAI_API_KEY (instead of signing in), PGAI_MON_PASSWORD (the password of",
|
|
@@ -46697,6 +47172,18 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
46697
47172
|
} else {
|
|
46698
47173
|
console.log("\u2713 .env is up to date");
|
|
46699
47174
|
}
|
|
47175
|
+
const envTag = readEnvTag(projectDir);
|
|
47176
|
+
const deployedTag = envTag.tag;
|
|
47177
|
+
const tagPlan = envTag.plain ? planUpdateTag(deployedTag, package_default.version) : { tag: null, note: `PGAI_TAG in .env is not a plain value, so it is left as is. To move the stack to ${package_default.version}, set PGAI_TAG=${package_default.version} in .env and re-run 'postgresai mon update'` };
|
|
47178
|
+
if (process.env.PGAI_TAG && process.env.PGAI_TAG !== (tagPlan.tag ?? deployedTag)) {
|
|
47179
|
+
console.error(`\u26A0 PGAI_TAG=${process.env.PGAI_TAG} is set in the environment, and docker compose prefers it over .env: unset it before 'postgresai mon stop && postgresai mon start'`);
|
|
47180
|
+
}
|
|
47181
|
+
if (tagPlan.tag) {
|
|
47182
|
+
writeEnvTag(projectDir, tagPlan.tag);
|
|
47183
|
+
if (process.env.PGAI_TAG === deployedTag)
|
|
47184
|
+
process.env.PGAI_TAG = tagPlan.tag;
|
|
47185
|
+
}
|
|
47186
|
+
console.log(`${tagPlan.tag ? "\u2713 " : ""}${tagPlan.note}`);
|
|
46700
47187
|
console.log();
|
|
46701
47188
|
const gitDir = path9.resolve(projectDir, ".git");
|
|
46702
47189
|
if (fs11.existsSync(gitDir)) {
|
|
@@ -46710,7 +47197,7 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
46710
47197
|
console.log(pullOut);
|
|
46711
47198
|
} else {
|
|
46712
47199
|
console.log("(not a git checkout \u2014 checking bundled docker-compose.yml)");
|
|
46713
|
-
await refreshBundledComposeIfStale(projectDir);
|
|
47200
|
+
await refreshBundledComposeIfStale(projectDir, deployedTag);
|
|
46714
47201
|
}
|
|
46715
47202
|
console.log(`
|
|
46716
47203
|
Updating Docker images...`);
|
|
@@ -46730,7 +47217,7 @@ Updating Docker images...`);
|
|
|
46730
47217
|
\u2713 Update completed successfully`);
|
|
46731
47218
|
console.log(`
|
|
46732
47219
|
To apply updates, restart monitoring services:`);
|
|
46733
|
-
console.log("
|
|
47220
|
+
console.log(" postgresai mon stop && postgresai mon start");
|
|
46734
47221
|
} else {
|
|
46735
47222
|
console.error(`
|
|
46736
47223
|
\u2717 Docker image update failed`);
|
|
@@ -47042,6 +47529,7 @@ async function syncVmalert(projectDir, apply, addOnly = false) {
|
|
|
47042
47529
|
setEnvValues(projectDir, { COMPOSE_PROFILES: composeProfilesValue(current, want, "host-metrics") });
|
|
47043
47530
|
return true;
|
|
47044
47531
|
}
|
|
47532
|
+
var looksLikeConnStr = (value) => /^postgres(ql)?:\/\//i.test(value);
|
|
47045
47533
|
async function addTarget(file, projectDir, connStr, name, env, { apply = true } = {}) {
|
|
47046
47534
|
if (!connStr) {
|
|
47047
47535
|
console.error("Connection string required: postgresql://user:pass@host:port/db");
|
|
@@ -47140,8 +47628,17 @@ Re-running with the same name and connection string is safe; retry after fixing
|
|
|
47140
47628
|
|
|
47141
47629
|
Supabase: with PGAI_SUPABASE_HOST_METRICS=true (environment or .env), writes host-metrics/supabase-<name>.yml.
|
|
47142
47630
|
RDS instance endpoint: writes RDS_DB_INSTANCE_IDENTIFIER, AWS_REGION, PGAI_CLUSTER and PGAI_NODE_NAME to .env for rds-host-stats.
|
|
47631
|
+
|
|
47632
|
+
Environment:
|
|
47633
|
+
PGAI_DB_URL the connection string, when none is given (keeps the password out of argv);
|
|
47634
|
+
the only argument is then the name: PGAI_DB_URL=... postgres-ai mon targets add my-db
|
|
47143
47635
|
`).action(async (connStr, name) => {
|
|
47144
47636
|
const { instancesFile: file, projectDir } = await resolveOrInitPaths();
|
|
47637
|
+
const envUrl = process.env.PGAI_DB_URL || undefined;
|
|
47638
|
+
if (envUrl && name === undefined && (connStr === undefined || !looksLikeConnStr(connStr))) {
|
|
47639
|
+
name = connStr;
|
|
47640
|
+
connStr = envUrl;
|
|
47641
|
+
}
|
|
47145
47642
|
await addTarget(file, projectDir, connStr, name, process.env);
|
|
47146
47643
|
});
|
|
47147
47644
|
targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
|
|
@@ -48541,7 +49038,7 @@ function tryParseJson(s) {
|
|
|
48541
49038
|
}
|
|
48542
49039
|
}
|
|
48543
49040
|
function printJoeOutcome(outcome, json, budgetMs) {
|
|
48544
|
-
const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
|
|
49041
|
+
const effectiveBudgetMs = Number.isFinite(outcome.budgetMs) ? outcome.budgetMs : typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
|
|
48545
49042
|
const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
|
|
48546
49043
|
if (outcome.budgetExpired) {
|
|
48547
49044
|
if (json) {
|
|
@@ -49050,9 +49547,10 @@ export {
|
|
|
49050
49547
|
monitoringHealthServices,
|
|
49051
49548
|
persistInstanceId,
|
|
49052
49549
|
planMonitoringRegistration,
|
|
49550
|
+
planUpdateTag,
|
|
49053
49551
|
program2 as program,
|
|
49054
49552
|
readComposeProfiles,
|
|
49055
|
-
|
|
49553
|
+
readEnvTag,
|
|
49056
49554
|
refreshBundledComposeIfStale,
|
|
49057
49555
|
registerMonitoringInstance,
|
|
49058
49556
|
resolveAdoptedProject,
|
|
@@ -49060,5 +49558,6 @@ export {
|
|
|
49060
49558
|
selfRegisterAndPersist,
|
|
49061
49559
|
shouldScaleOutNodeExporter,
|
|
49062
49560
|
statOwner,
|
|
49063
|
-
updatePgwatchConfig
|
|
49561
|
+
updatePgwatchConfig,
|
|
49562
|
+
writeEnvTag
|
|
49064
49563
|
};
|