postgresai 0.17.0-dev.5 → 0.17.0-dev.7
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 +51 -10
- package/dist/bin/postgres-ai.js +577 -307
- package/dist/sql/06.helpers.sql +0 -10
- package/dist/sql/sql/06.helpers.sql +0 -10
- package/package.json +1 -1
- package/schemas/A002.schema.json +0 -2
- package/schemas/A003.schema.json +0 -2
- package/schemas/A004.schema.json +0 -2
- package/schemas/A007.schema.json +0 -2
- package/schemas/A013.schema.json +0 -2
- package/schemas/D001.schema.json +0 -2
- package/schemas/D004.schema.json +0 -2
- package/schemas/F001.schema.json +0 -2
- package/schemas/F002.schema.json +1 -1
- package/schemas/F003.schema.json +0 -2
- package/schemas/F004.schema.json +0 -2
- package/schemas/F005.schema.json +0 -2
- package/schemas/F009.schema.json +0 -2
- package/schemas/G001.schema.json +0 -2
- package/schemas/G003.schema.json +0 -2
- package/schemas/H001.schema.json +0 -2
- package/schemas/H002.schema.json +0 -2
- package/schemas/H004.schema.json +0 -2
- package/schemas/I001.schema.json +0 -2
- package/schemas/K001.schema.json +0 -2
- package/schemas/K003.schema.json +0 -2
- package/schemas/K004.schema.json +0 -2
- package/schemas/K005.schema.json +0 -2
- package/schemas/K006.schema.json +0 -2
- package/schemas/K007.schema.json +0 -2
- package/schemas/K008.schema.json +0 -2
- package/schemas/M001.schema.json +0 -2
- package/schemas/M002.schema.json +0 -2
- package/schemas/M003.schema.json +0 -2
- package/schemas/N001.schema.json +0 -2
- package/sql/06.helpers.sql +0 -10
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.7",
|
|
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.7";
|
|
16897
16897
|
var package_default2 = {
|
|
16898
16898
|
name: "postgresai",
|
|
16899
16899
|
version,
|
|
@@ -21642,7 +21642,7 @@ function summarizeG003(nodeData) {
|
|
|
21642
21642
|
|
|
21643
21643
|
// lib/checkup.ts
|
|
21644
21644
|
var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
|
|
21645
|
-
var CONTRACT_VERSION = "1.
|
|
21645
|
+
var CONTRACT_VERSION = "1.0.0";
|
|
21646
21646
|
var SECONDS_PER_DAY = 86400;
|
|
21647
21647
|
var SECONDS_PER_HOUR = 3600;
|
|
21648
21648
|
var SECONDS_PER_MINUTE = 60;
|
|
@@ -23975,6 +23975,38 @@ function withCheckSummary(report) {
|
|
|
23975
23975
|
return report;
|
|
23976
23976
|
}
|
|
23977
23977
|
|
|
23978
|
+
// lib/checkup-upload.ts
|
|
23979
|
+
function responseId(fn, response, ...keys) {
|
|
23980
|
+
const answer = response ?? {};
|
|
23981
|
+
const id = Number(keys.map((key) => answer[key]).find((value) => value != null));
|
|
23982
|
+
if (!Number.isFinite(id) || id <= 0) {
|
|
23983
|
+
throw new Error(typeof answer.message === "string" ? answer.message : `Unexpected ${fn} response: ${JSON.stringify(response)}`);
|
|
23984
|
+
}
|
|
23985
|
+
return id;
|
|
23986
|
+
}
|
|
23987
|
+
async function createCheckupReport(rpc, params) {
|
|
23988
|
+
const fn = "checkup_report_create";
|
|
23989
|
+
const response = await rpc(fn, {
|
|
23990
|
+
access_token: params.apiKey,
|
|
23991
|
+
project: params.project,
|
|
23992
|
+
...params.status ? { status: params.status } : {}
|
|
23993
|
+
});
|
|
23994
|
+
return { reportId: responseId(fn, response, "report_id") };
|
|
23995
|
+
}
|
|
23996
|
+
async function uploadCheckupReportJson(rpc, params) {
|
|
23997
|
+
const fn = "checkup_report_file_post";
|
|
23998
|
+
const response = await rpc(fn, {
|
|
23999
|
+
access_token: params.apiKey,
|
|
24000
|
+
checkup_report_id: params.reportId,
|
|
24001
|
+
filename: params.filename,
|
|
24002
|
+
check_id: params.checkId,
|
|
24003
|
+
data: params.jsonText,
|
|
24004
|
+
type: "json",
|
|
24005
|
+
generate_issue: true
|
|
24006
|
+
});
|
|
24007
|
+
return { reportChunkId: responseId(fn, response, "report_chunck_id", "report_chunk_id") };
|
|
24008
|
+
}
|
|
24009
|
+
|
|
23978
24010
|
// lib/host-metrics.ts
|
|
23979
24011
|
import { createHash } from "node:crypto";
|
|
23980
24012
|
import { chmodSync as chmodSync3, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -27936,10 +27968,12 @@ function isNumericProjectRef(ref) {
|
|
|
27936
27968
|
// lib/connect.ts
|
|
27937
27969
|
var money = (cents, currency) => currency.toLowerCase() === "usd" ? `$${(cents / 100).toFixed(2)}` : `${(cents / 100).toFixed(2)} ${currency.toUpperCase()}`;
|
|
27938
27970
|
function priceText(q) {
|
|
27971
|
+
if (!q.billed && q.same_cluster)
|
|
27972
|
+
return `included: same database cluster as ${q.same_cluster}, no extra charge`;
|
|
27939
27973
|
if (!q.billed)
|
|
27940
27974
|
return q.free_slots.total ? `free (${q.free_slots.remaining} of ${q.free_slots.total} free slots)` : "free";
|
|
27941
27975
|
const { amount, currency, interval } = q.price;
|
|
27942
|
-
const base = `${money(amount, currency)}/${interval} per
|
|
27976
|
+
const base = `${money(amount, currency)}/${interval} per database cluster (${q.plan} plan)${q.subscription ? `, cluster ${q.quantity + 1} on the subscription` : ""}`;
|
|
27943
27977
|
if (q.promo?.valid && q.amount_after_promo !== undefined && q.amount_after_promo !== null) {
|
|
27944
27978
|
const after = `${money(q.amount_after_promo, currency)}`;
|
|
27945
27979
|
const how = `with ${q.promo.code} (${q.promo.discount_description})`;
|
|
@@ -28007,7 +28041,11 @@ function stateOf(raw) {
|
|
|
28007
28041
|
class ClickhouseKeyError extends Error {
|
|
28008
28042
|
}
|
|
28009
28043
|
var serverOf = (name) => name.split("/")[0].toLowerCase().replace(/\.(?=$|:)/, "");
|
|
28010
|
-
|
|
28044
|
+
function clusterOf(url) {
|
|
28045
|
+
const u = parseUrl(url);
|
|
28046
|
+
return u?.hostname ? `${u.hostname.toLowerCase()}:${u.port || "5432"}` : undefined;
|
|
28047
|
+
}
|
|
28048
|
+
var errorText = (err) => err instanceof AggregateError && !err.message ? err.errors.map(errorText).join("; ") : err instanceof Error ? err.message : String(err);
|
|
28011
28049
|
var urlPassword = (u) => decodeURIComponent(u.password) || u.searchParams.get("password") || "";
|
|
28012
28050
|
var URL_PARAMS_KEPT = ["sslmode", "channel_binding", "application_name"];
|
|
28013
28051
|
var URL_PARAMS_TLS = ["sslrootcert", "sslcert", "sslkey", "uselibpqcompat"];
|
|
@@ -28022,6 +28060,22 @@ function checkUrlParams(url, agent) {
|
|
|
28022
28060
|
if (moved.length)
|
|
28023
28061
|
throw new Error(`The URL's query string sets ${moved.join(" and ")}: put the host and the port in the URL itself (postgresql://user:password@host:5432/dbname), so that the server prepared is the server monitored`);
|
|
28024
28062
|
}
|
|
28063
|
+
function storedPasswordNeeds(monitoringUrl) {
|
|
28064
|
+
const query = new URL(monitoringUrl).search.replace(/^\?/, "");
|
|
28065
|
+
const parts = query ? query.split("&") : [];
|
|
28066
|
+
const keys = parts.map((p) => p.split("=")[0]);
|
|
28067
|
+
const twice = URL_PARAMS_KEPT.filter((k) => keys.indexOf(k) !== keys.lastIndexOf(k));
|
|
28068
|
+
const tls = !twice.includes("sslmode") && parts.some((p) => /^sslmode=(require|verify-ca|verify-full)$/.test(p));
|
|
28069
|
+
const needs = [
|
|
28070
|
+
...tls ? [] : [`sslmode=require (or verify-full)${twice.includes("sslmode") ? ", once" : ""}`],
|
|
28071
|
+
...twice.filter((k) => k !== "sslmode").map((k) => `${k} once`),
|
|
28072
|
+
...parts.some((p) => !p) ? ["no stray &"] : [],
|
|
28073
|
+
...keys.some((k) => k && !URL_PARAMS_KEPT.includes(k)) ? ["the parameter names without %-escapes"] : []
|
|
28074
|
+
];
|
|
28075
|
+
return needs.length ? needs.join(" and ") : undefined;
|
|
28076
|
+
}
|
|
28077
|
+
var shellWord = (s) => /^[\w./:@%+=,-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
28078
|
+
var disconnectEach = (names) => names.map((n) => `pgai disconnect ${shellWord(n)} --yes`).join(" and ");
|
|
28025
28079
|
function roleUrlFor(url, db, password, kept) {
|
|
28026
28080
|
const u = new URL(url);
|
|
28027
28081
|
u.username = DEFAULT_MONITORING_USER;
|
|
@@ -28030,6 +28084,7 @@ function roleUrlFor(url, db, password, kept) {
|
|
|
28030
28084
|
for (const name of [...u.searchParams.keys()])
|
|
28031
28085
|
if (!kept.includes(name))
|
|
28032
28086
|
u.searchParams.delete(name);
|
|
28087
|
+
u.search = u.search.replace(/@/g, "%40");
|
|
28033
28088
|
return u.toString();
|
|
28034
28089
|
}
|
|
28035
28090
|
var monitoringUrlFor = (url, db, password) => roleUrlFor(url, db, password, URL_PARAMS_KEPT);
|
|
@@ -28082,22 +28137,24 @@ function checkupLines(c) {
|
|
|
28082
28137
|
];
|
|
28083
28138
|
}
|
|
28084
28139
|
var boxState = (row) => row.status === "launch_requested" && row.registered_at ? "registered" : row.status ?? "starting";
|
|
28085
|
-
async function billingFor(q, name, opts, deps) {
|
|
28140
|
+
async function billingFor(q, name, opts, deps, show) {
|
|
28086
28141
|
const billing = { price: priceText(q), requires_payment_method: q.requires_payment_method };
|
|
28087
28142
|
if (opts.coupon) {
|
|
28088
28143
|
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
28144
|
}
|
|
28090
|
-
const
|
|
28091
|
-
const
|
|
28145
|
+
const billingUrl = q.billing_url ?? deps.billingUrl(q.org_alias);
|
|
28146
|
+
const billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${billingUrl}, then re-run` };
|
|
28147
|
+
const stop = (next) => ({ billing, billingPage, billingUrl, accepted: false, stop: { ...billing, next } });
|
|
28092
28148
|
if (billing.coupon && !billing.coupon.valid) {
|
|
28093
28149
|
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
28150
|
}
|
|
28095
28151
|
if (q.requires_payment_method)
|
|
28096
|
-
return { billing, billingPage, accepted: false, stop: billingPage };
|
|
28097
|
-
|
|
28152
|
+
return { billing, billingPage, billingUrl, accepted: false, stop: billingPage };
|
|
28153
|
+
show(billing.price);
|
|
28154
|
+
if (q.billed && !opts.yes && !await deps.confirm(`Provision ${name}? (y/N): `)) {
|
|
28098
28155
|
return stop(opts.agent ? `Call connect_database again with yes: true to accept ${billing.price}` : `Re-run with --yes to accept ${billing.price}`);
|
|
28099
28156
|
}
|
|
28100
|
-
return { billing, billingPage, accepted: q.billed };
|
|
28157
|
+
return { billing, billingPage, billingUrl, accepted: q.billed };
|
|
28101
28158
|
}
|
|
28102
28159
|
async function connect(url, opts, deps) {
|
|
28103
28160
|
const started = deps.now();
|
|
@@ -28117,7 +28174,7 @@ async function connect(url, opts, deps) {
|
|
|
28117
28174
|
return { status: "action_required", provider, name, next: "--reset-password works with PostgresAI Cloud only (it checks what else monitors this server): connect without --self-hosted, or set PGAI_MON_PASSWORD" };
|
|
28118
28175
|
}
|
|
28119
28176
|
if (opts.selfHosted && deps.localStackRunning()) {
|
|
28120
|
-
return { status: "action_required", provider, name, next: "A monitoring stack already runs on this machine: add the database with pgai mon targets add
|
|
28177
|
+
return { status: "action_required", provider, name, next: "A monitoring stack already runs on this machine: add the database with PGAI_DB_URL='<postgres_ai_mon URL>' pgai mon targets add" };
|
|
28121
28178
|
}
|
|
28122
28179
|
const rows = opts.selfHosted ? [] : await deps.list();
|
|
28123
28180
|
let row = rows.find((d) => d.name === name && !disconnecting(d.status));
|
|
@@ -28127,6 +28184,8 @@ async function connect(url, opts, deps) {
|
|
|
28127
28184
|
let billing = {};
|
|
28128
28185
|
let billingPage;
|
|
28129
28186
|
let accepted = false;
|
|
28187
|
+
let billingUrl;
|
|
28188
|
+
let undoRole;
|
|
28130
28189
|
const findOrg = async () => {
|
|
28131
28190
|
try {
|
|
28132
28191
|
return await deps.clickhouseOrg(new URL(url).hostname, key.keyId, key.keySecret);
|
|
@@ -28160,51 +28219,72 @@ async function connect(url, opts, deps) {
|
|
|
28160
28219
|
await deps.unprepare(url).catch(() => false);
|
|
28161
28220
|
};
|
|
28162
28221
|
let lockId;
|
|
28163
|
-
const
|
|
28164
|
-
const
|
|
28165
|
-
|
|
28166
|
-
|
|
28167
|
-
|
|
28168
|
-
|
|
28222
|
+
const unread = (d) => /^Monitoring [0-9a-f]{6}$/.test(d.name);
|
|
28223
|
+
const othersOnServer = (list) => list.filter((d) => d.name !== name && !disconnecting(d.status) && (unread(d) || serverOf(d.name) === serverOf(name)));
|
|
28224
|
+
const storedPassword = rows.some((d) => d.monitoring_password_stored && !/delet/.test(d.status ?? "") && serverOf(d.name) === serverOf(name)) || undefined;
|
|
28225
|
+
const cutOff = (others) => {
|
|
28226
|
+
const on = others.filter((d) => !unread(d)).map((d) => d.name).join(", ");
|
|
28227
|
+
const maybe = others.filter(unread).map((d) => d.name).join(", ");
|
|
28228
|
+
const what = [on && `would cut off the monitoring of ${on} on this server`, maybe && `may cut off the monitoring of ${maybe} (the platform could not read its URL, so its server is not known)`].filter(Boolean).join(", and ");
|
|
28229
|
+
const instead = [
|
|
28230
|
+
...storedPassword ? ["connect without --reset-password (PostgresAI keeps its password for this server, and sends it only over TLS)"] : [],
|
|
28231
|
+
"set PGAI_MON_PASSWORD to its password instead",
|
|
28232
|
+
`${disconnectEach(others.map((d) => d.name))} first`
|
|
28233
|
+
];
|
|
28234
|
+
return { status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} ${what}: ${instead.join(", or ")}` };
|
|
28235
|
+
};
|
|
28236
|
+
const others = othersOnServer(rows).filter((d) => !unread(d)).map((d) => d.name);
|
|
28237
|
+
if (opts.resetPassword && othersOnServer(rows).length)
|
|
28238
|
+
return cutOff(othersOnServer(rows));
|
|
28169
28239
|
if (opts.coupon !== undefined && !opts.coupon.trim()) {
|
|
28170
28240
|
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
28241
|
}
|
|
28242
|
+
const prepOpts = { resetPassword: opts.resetPassword, ...storedPassword ? { storedPassword, others } : {} };
|
|
28172
28243
|
if (!opts.selfHosted) {
|
|
28173
|
-
const
|
|
28244
|
+
const probe = await deps.prepare(url, provider, { ...prepOpts, check: true });
|
|
28245
|
+
if ("next" in probe)
|
|
28246
|
+
return { status: "action_required", provider, name, ...probe };
|
|
28247
|
+
const b = await billingFor(await deps.quote(opts.coupon, clusterOf(url)), name, opts, deps, (price) => progress("billing", `Billing: ${price}`));
|
|
28174
28248
|
if (b.stop)
|
|
28175
28249
|
return { status: "action_required", provider, name, ...b.stop };
|
|
28176
|
-
({ billing, billingPage, accepted } = b);
|
|
28177
|
-
progress("billing", `Billing: ${billing.price}`);
|
|
28250
|
+
({ billing, billingPage, billingUrl, accepted } = b);
|
|
28178
28251
|
}
|
|
28179
28252
|
if (opts.resetPassword) {
|
|
28253
|
+
let lock;
|
|
28180
28254
|
try {
|
|
28181
|
-
|
|
28255
|
+
lock = await deps.resetLock(serverOf(name));
|
|
28182
28256
|
} catch (err) {
|
|
28183
28257
|
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` };
|
|
28258
|
+
return { status: "action_required", provider, name, ...billing, next: `Another pgai connect --reset-password for ${serverOf(name)} is running: wait for it to finish, then re-run (a run that stopped frees the server 15 minutes after it started)` };
|
|
28185
28259
|
}
|
|
28186
28260
|
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` };
|
|
28261
|
+
return { status: "action_required", provider, name, ...billing, next: `This platform cannot lock the server for --reset-password yet: set PGAI_MON_PASSWORD to ${DEFAULT_MONITORING_USER}'s password instead` };
|
|
28188
28262
|
}
|
|
28189
28263
|
throw err;
|
|
28190
28264
|
}
|
|
28265
|
+
if (typeof lock.lock_id !== "string" || !lock.lock_id)
|
|
28266
|
+
throw new Error("cloud_monitoring_reset_lock returned no lock_id: nothing was changed");
|
|
28267
|
+
lockId = lock.lock_id;
|
|
28191
28268
|
}
|
|
28192
28269
|
let prepared;
|
|
28193
28270
|
let created;
|
|
28271
|
+
let unanswered = false;
|
|
28194
28272
|
try {
|
|
28195
28273
|
if (lockId) {
|
|
28196
28274
|
const now = await deps.list();
|
|
28197
28275
|
const same = now.find((d) => d.name === name && !disconnecting(d.status));
|
|
28198
28276
|
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` };
|
|
28277
|
+
return { status: "action_required", provider, name, id: same.id, ...billing, next: `${name} is already connected, and its monitoring uses the current password: pgai disconnect ${name} --yes first` };
|
|
28200
28278
|
const others = othersOnServer(now);
|
|
28201
28279
|
if (others.length)
|
|
28202
|
-
return cutOff(others);
|
|
28280
|
+
return { ...cutOff(others), ...billing };
|
|
28203
28281
|
}
|
|
28204
28282
|
progress("preparing", `Preparing ${maskConnectionString(url)}`);
|
|
28205
|
-
prepared = await deps.prepare(url, provider,
|
|
28283
|
+
prepared = await deps.prepare(url, provider, prepOpts);
|
|
28206
28284
|
if ("next" in prepared)
|
|
28207
28285
|
return { status: "action_required", provider, name, ...billing, ...prepared };
|
|
28286
|
+
if (!("monitoringUrl" in prepared))
|
|
28287
|
+
throw new Error("prepare returned no monitoring URL");
|
|
28208
28288
|
if (prepared.note)
|
|
28209
28289
|
note = `; ${prepared.note}`;
|
|
28210
28290
|
if (opts.selfHosted) {
|
|
@@ -28221,10 +28301,14 @@ async function connect(url, opts, deps) {
|
|
|
28221
28301
|
}).catch(async (err) => {
|
|
28222
28302
|
if (err instanceof HttpStatusError && err.status >= 400 && err.status < 500)
|
|
28223
28303
|
await undo(prepared);
|
|
28304
|
+
else
|
|
28305
|
+
unanswered = true;
|
|
28224
28306
|
if (err instanceof HttpStatusError && err.status === 402)
|
|
28225
28307
|
return /declined/i.test(err.message) ? "declined" : "payment";
|
|
28226
28308
|
if (err instanceof HttpStatusError && err.status === 412)
|
|
28227
28309
|
return "price";
|
|
28310
|
+
if (err instanceof HttpStatusError && err.status === 409 && "storedPassword" in prepared && prepared.storedPassword && /none is stored/.test(err.message))
|
|
28311
|
+
return "stored";
|
|
28228
28312
|
throw err;
|
|
28229
28313
|
});
|
|
28230
28314
|
if (created === "payment")
|
|
@@ -28234,21 +28318,43 @@ async function connect(url, opts, deps) {
|
|
|
28234
28318
|
}
|
|
28235
28319
|
if (created === "price")
|
|
28236
28320
|
return { status: "action_required", provider, name, ...billing, next: "The price changed since it was shown: re-run pgai connect to see it" };
|
|
28321
|
+
if (created === "stored") {
|
|
28322
|
+
const gone = `PostgresAI no longer keeps the password of ${DEFAULT_MONITORING_USER} for this server`;
|
|
28323
|
+
return { status: "action_required", provider, name, ...billing, next: opts.agent ? `${gone}: pass its URL as database_url, or run pgai connect <admin-url> --reset-password in a terminal` : `${gone}: re-run with --reset-password (anything else that logs in as ${DEFAULT_MONITORING_USER} then needs the new one), or set PGAI_MON_PASSWORD to its password` };
|
|
28324
|
+
}
|
|
28237
28325
|
if (created.status === "failed") {
|
|
28238
28326
|
await undo(prepared);
|
|
28239
28327
|
return { status: "failed", provider, name, id: created.id, next: `${created.error} Re-run pgai connect later.` };
|
|
28240
28328
|
}
|
|
28241
28329
|
} finally {
|
|
28242
|
-
if (lockId)
|
|
28330
|
+
if (lockId && !unanswered)
|
|
28243
28331
|
await deps.resetUnlock(lockId).catch(() => {});
|
|
28244
28332
|
}
|
|
28245
28333
|
row = { id: created.id, name: created.name, provider, status: created.status, dashboard_url: null, host_metrics: !!ch };
|
|
28246
|
-
|
|
28247
|
-
|
|
28334
|
+
const made = prepared;
|
|
28335
|
+
undoRole = () => undo(made);
|
|
28336
|
+
if (prepared.storedPassword) {
|
|
28337
|
+
progress("checkup", `Express checkup skipped: it runs only as ${DEFAULT_MONITORING_USER}, whose password PostgresAI keeps for this server. The full checkup follows on the box.`);
|
|
28338
|
+
} else {
|
|
28339
|
+
checkup = await deps.checkup(withLocalTls(prepared.monitoringUrl, url), created.name).catch((err) => ({ error: errorText(err) }));
|
|
28340
|
+
progress("checkup", checkupLines(checkup).join(`
|
|
28248
28341
|
`), { checkup });
|
|
28342
|
+
}
|
|
28249
28343
|
}
|
|
28250
28344
|
let shown;
|
|
28251
28345
|
for (const deadline = deps.now() + opts.waitMs;; ) {
|
|
28346
|
+
if (undoRole && row.billing_error && disconnecting(row.status)) {
|
|
28347
|
+
await undoRole();
|
|
28348
|
+
return {
|
|
28349
|
+
status: "action_required",
|
|
28350
|
+
provider,
|
|
28351
|
+
name,
|
|
28352
|
+
id: row.id,
|
|
28353
|
+
...billing,
|
|
28354
|
+
...checkup ? { checkup } : {},
|
|
28355
|
+
next: `The first charge failed (${row.billing_error}): the box was removed and nothing is billed. Update the payment method at ${billingUrl}, then re-run`
|
|
28356
|
+
};
|
|
28357
|
+
}
|
|
28252
28358
|
const result = connectStatus(row, provider, fresh);
|
|
28253
28359
|
const state = boxState(row);
|
|
28254
28360
|
if (state !== shown && (shown !== undefined || result.status === "provisioning"))
|
|
@@ -28262,12 +28368,22 @@ async function connect(url, opts, deps) {
|
|
|
28262
28368
|
return { ...rest, ...billing, ...checkup ? { checkup } : {}, next: next + note };
|
|
28263
28369
|
}
|
|
28264
28370
|
await deps.sleep(POLL_MS);
|
|
28265
|
-
|
|
28371
|
+
const listed = await deps.list().catch(() => {
|
|
28372
|
+
return;
|
|
28373
|
+
});
|
|
28374
|
+
const now = listed?.find((d) => d.id === row.id);
|
|
28375
|
+
if (listed && !now && undoRole) {
|
|
28376
|
+
await undoRole();
|
|
28377
|
+
return { status: "failed", provider, name, id: row.id, ...billing, next: "The monitoring box was removed before it became active: see pgai databases, then re-run pgai connect" };
|
|
28378
|
+
}
|
|
28379
|
+
row = now ?? row;
|
|
28266
28380
|
}
|
|
28267
28381
|
}
|
|
28268
|
-
function openConnection(url, opts) {
|
|
28382
|
+
async function openConnection(url, opts) {
|
|
28269
28383
|
const conn = resolveAdminConnection({ conn: url });
|
|
28270
|
-
|
|
28384
|
+
const fallback = !opts.agent && !!conn.sslFallbackEnabled;
|
|
28385
|
+
const { client, usedSsl } = await connectWithSslFallback(opts.Client ?? Client2, { ...conn, sslFallbackEnabled: fallback });
|
|
28386
|
+
return { client, usedSsl, refusedTls: fallback && !usedSsl };
|
|
28271
28387
|
}
|
|
28272
28388
|
async function dropMonitoringRole(client) {
|
|
28273
28389
|
for (const sql of [`drop role ${DEFAULT_MONITORING_USER}`, `drop owned by ${DEFAULT_MONITORING_USER}; drop role ${DEFAULT_MONITORING_USER}`]) {
|
|
@@ -28300,7 +28416,7 @@ async function prepareDatabase(url, provider, opts = {}) {
|
|
|
28300
28416
|
throw err;
|
|
28301
28417
|
}
|
|
28302
28418
|
};
|
|
28303
|
-
const { client } = await open(url).catch((err) => {
|
|
28419
|
+
const { client, usedSsl, refusedTls } = await open(url).catch((err) => {
|
|
28304
28420
|
if (/client password must be a string/.test(String(err?.message)))
|
|
28305
28421
|
throw new Error("The URL has no password (and PGPASSWORD is not set): pgai connect postgresql://user:password@host:5432/dbname");
|
|
28306
28422
|
throw err;
|
|
@@ -28314,7 +28430,7 @@ async function prepareDatabase(url, provider, opts = {}) {
|
|
|
28314
28430
|
and has_database_privilege(current_user, current_database(), 'CREATE')
|
|
28315
28431
|
and exists (select 1 from pg_extension where extname = 'pg_stat_statements')) as admin,
|
|
28316
28432
|
exists (select 1 from pg_roles where rolname = '${DEFAULT_MONITORING_USER}') as mon_exists,
|
|
28317
|
-
current_setting('scram_iterations', true) as iterations
|
|
28433
|
+
current_setting('scram_iterations', true) as iterations, current_setting('ssl') as ssl
|
|
28318
28434
|
from pg_roles where rolname = current_user`)).rows[0];
|
|
28319
28435
|
const notes = [caNote(url)].filter((n) => !!n);
|
|
28320
28436
|
const noted = () => notes.length ? { note: notes.join("; ") } : {};
|
|
@@ -28338,9 +28454,31 @@ async function prepareDatabase(url, provider, opts = {}) {
|
|
|
28338
28454
|
}
|
|
28339
28455
|
} else if (me.admin && pgProvider !== "supabase") {
|
|
28340
28456
|
const exists = `${DEFAULT_MONITORING_USER} already exists on this server`;
|
|
28457
|
+
const setPassword = `Set PGAI_MON_PASSWORD to the password of ${DEFAULT_MONITORING_USER}, or give it a new one: pgai connect <admin-url> --reset-password (anything else that logs in as ${DEFAULT_MONITORING_USER} then needs the new password)`;
|
|
28458
|
+
const needs = storedPasswordNeeds(monitoringUrlFor(url, me.db, ""));
|
|
28459
|
+
const noTls = refusedTls || !usedSsl && me.ssl === "off";
|
|
28460
|
+
const tlsOnly = `PostgresAI sends the password it keeps for ${DEFAULT_MONITORING_USER} only over TLS`;
|
|
28461
|
+
const others = opts.others?.length ? opts.others : undefined;
|
|
28462
|
+
const disconnectOthers = others ? disconnectEach(others) : "pgai disconnect the server's other databases (pgai databases)";
|
|
28463
|
+
const reconnectOthers = `(and connect ${others ? others.join(", ") : "them"} again with it)`;
|
|
28464
|
+
const noTlsWayOut = opts.agent ? `Pass its URL as database_url, or run pgai connect in a terminal with PGAI_MON_PASSWORD set to its password. If nobody has it, in a terminal: ${disconnectOthers}, then pgai connect <admin-url> --reset-password with PGAI_MON_PASSWORD set to a new one ${reconnectOthers}` : `Set PGAI_MON_PASSWORD to its password, or turn on TLS on the server (ssl = on) and put sslmode=require in the URL. If nobody has the password: ${disconnectOthers}, then re-run with --reset-password and PGAI_MON_PASSWORD set to a new one ${reconnectOthers}`;
|
|
28465
|
+
const reuse = me.mon_exists && !opts.resetPassword && !!opts.storedPassword && !(opts.agent ? "" : process.env.PGAI_MON_PASSWORD?.trim());
|
|
28466
|
+
if (reuse && needs) {
|
|
28467
|
+
if (noTls)
|
|
28468
|
+
return { next: `${exists}, but this server takes no TLS, and ${tlsOnly}. ${noTlsWayOut}` };
|
|
28469
|
+
return { next: `${exists}. ${opts.agent ? "database_url" : "The URL"} needs ${needs}: the password PostgresAI keeps for this server is sent only over TLS, to a URL with each parameter once` };
|
|
28470
|
+
}
|
|
28471
|
+
if (reuse) {
|
|
28472
|
+
if (opts.check)
|
|
28473
|
+
return { checked: true };
|
|
28474
|
+
const { password: unused } = await resolveMonitoringPassword({ monitoringUser: DEFAULT_MONITORING_USER });
|
|
28475
|
+
const plan = await buildInitPlan({ database: me.db, monitoringPassword: unused, iterations: Number(me.iterations), includeOptionalPermissions: true, provider: pgProvider, keepExistingPassword: true });
|
|
28476
|
+
await applyInitPlan({ client, plan });
|
|
28477
|
+
return { monitoringUrl: monitoringUrlFor(url, me.db, ""), storedPassword: true, ...noted() };
|
|
28478
|
+
}
|
|
28341
28479
|
if (me.mon_exists && opts.agent)
|
|
28342
28480
|
return { next: `${exists}. Pass its URL as database_url, or run pgai connect in a terminal with PGAI_MON_PASSWORD set to its password` };
|
|
28343
|
-
const
|
|
28481
|
+
const unset = !opts.storedPassword ? setPassword : noTls ? `This server takes no TLS, and ${tlsOnly}. ${noTlsWayOut}` : !needs ? `Unset PGAI_MON_PASSWORD: PostgresAI keeps the password of ${DEFAULT_MONITORING_USER} for this server` : `Unset PGAI_MON_PASSWORD, and the URL needs ${needs}: PostgresAI keeps the password of ${DEFAULT_MONITORING_USER} for this server, and sends it only over TLS, to a URL with each parameter once`;
|
|
28344
28482
|
const { password, generated } = await resolveMonitoringPassword({ passwordEnv: opts.agent ? undefined : process.env.PGAI_MON_PASSWORD, monitoringUser: DEFAULT_MONITORING_USER });
|
|
28345
28483
|
const reset = !!opts.resetPassword && me.mon_exists;
|
|
28346
28484
|
if (me.mon_exists && !reset) {
|
|
@@ -28350,13 +28488,16 @@ async function prepareDatabase(url, provider, opts = {}) {
|
|
|
28350
28488
|
try {
|
|
28351
28489
|
accepted = await logsIn(loginUrlFor(url, me.db, password));
|
|
28352
28490
|
} catch (err) {
|
|
28353
|
-
|
|
28491
|
+
const orUnset = opts.storedPassword && !noTls ? ", or unset PGAI_MON_PASSWORD: PostgresAI keeps its password for this server" : "";
|
|
28492
|
+
return { next: `${exists}, and PGAI_MON_PASSWORD could not be checked from this host (${err instanceof Error ? err.message : String(err)}). Run pgai connect from a host that ${DEFAULT_MONITORING_USER} may connect from${orUnset}` };
|
|
28354
28493
|
}
|
|
28355
28494
|
if (!accepted)
|
|
28356
|
-
return { next: `${exists} and PGAI_MON_PASSWORD is not its password. ${
|
|
28495
|
+
return { next: `${exists} and PGAI_MON_PASSWORD is not its password. ${unset}` };
|
|
28357
28496
|
if (await acceptsAnyPassword())
|
|
28358
28497
|
notes.push(unchecked("PGAI_MON_PASSWORD"));
|
|
28359
28498
|
}
|
|
28499
|
+
if (opts.check)
|
|
28500
|
+
return { checked: true };
|
|
28360
28501
|
const monitoringUrl = monitoringUrlFor(url, me.db, password);
|
|
28361
28502
|
const loginUrl = loginUrlFor(url, me.db, password);
|
|
28362
28503
|
const plan = await buildInitPlan({ database: me.db, monitoringPassword: password, iterations: Number(me.iterations), includeOptionalPermissions: true, provider: pgProvider, keepExistingPassword: !reset });
|
|
@@ -28404,26 +28545,17 @@ async function expressCheckup(url, opts = {}) {
|
|
|
28404
28545
|
}
|
|
28405
28546
|
}
|
|
28406
28547
|
async function saveCheckupReport(rpc, accessToken, project, reports) {
|
|
28407
|
-
const
|
|
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");
|
|
28548
|
+
const { reportId } = await createCheckupReport(rpc, { apiKey: accessToken, project });
|
|
28415
28549
|
const setStatus = (status) => rpc("checkup_report_status_update", { access_token: accessToken, report_id: reportId, status });
|
|
28416
28550
|
try {
|
|
28417
28551
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
28418
|
-
|
|
28419
|
-
|
|
28420
|
-
|
|
28552
|
+
await uploadCheckupReportJson(rpc, {
|
|
28553
|
+
apiKey: accessToken,
|
|
28554
|
+
reportId,
|
|
28421
28555
|
filename: `${checkId}.json`,
|
|
28422
|
-
|
|
28423
|
-
|
|
28424
|
-
|
|
28425
|
-
generate_issue: true
|
|
28426
|
-
}), "report_chunck_id", "report_chunk_id");
|
|
28556
|
+
checkId,
|
|
28557
|
+
jsonText: JSON.stringify(report, null, 2)
|
|
28558
|
+
});
|
|
28427
28559
|
}
|
|
28428
28560
|
} catch (err) {
|
|
28429
28561
|
await setStatus("failed").catch(() => {});
|
|
@@ -28473,7 +28605,7 @@ function platformDeps(p) {
|
|
|
28473
28605
|
unprepare: (url) => unprepareDatabase(url, { agent: p.agent }),
|
|
28474
28606
|
clickhouseOrg: clickhouseOrgFor,
|
|
28475
28607
|
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 } : {}),
|
|
28608
|
+
quote: (coupon, cluster) => rpc("cloud_monitoring_quote", { ...coupon ? { promo_code: coupon } : {}, ...cluster ? { db_server: cluster } : {} }),
|
|
28477
28609
|
billingUrl: (orgAlias) => `${p.uiBaseUrl}/${orgAlias}/billing`,
|
|
28478
28610
|
handoffUrl: async (provider) => {
|
|
28479
28611
|
const orgs = await listOrgs({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl });
|
|
@@ -35744,7 +35876,7 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
35744
35876
|
},
|
|
35745
35877
|
{
|
|
35746
35878
|
name: "connect_database",
|
|
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).",
|
|
35879
|
+
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). For another database on a server PostgresAI already monitors for the org, an admin URL with sslmode=require (or verify-*), each query parameter once, is enough: PostgresAI fills in the password of postgres_ai_mon it keeps, over TLS only (on a server without TLS, pass the postgres_ai_mon URL; if nobody has its password, the user runs in a terminal: pgai disconnect the server's other databases, then pgai connect <admin-url> --reset-password with PGAI_MON_PASSWORD set to a new one, then connects them again with it).",
|
|
35748
35880
|
inputSchema: {
|
|
35749
35881
|
type: "object",
|
|
35750
35882
|
properties: {
|
|
@@ -36658,10 +36790,12 @@ function renderMarkdownForTerminal(md) {
|
|
|
36658
36790
|
// lib/connect.ts
|
|
36659
36791
|
var money2 = (cents, currency) => currency.toLowerCase() === "usd" ? `$${(cents / 100).toFixed(2)}` : `${(cents / 100).toFixed(2)} ${currency.toUpperCase()}`;
|
|
36660
36792
|
function priceText2(q) {
|
|
36793
|
+
if (!q.billed && q.same_cluster)
|
|
36794
|
+
return `included: same database cluster as ${q.same_cluster}, no extra charge`;
|
|
36661
36795
|
if (!q.billed)
|
|
36662
36796
|
return q.free_slots.total ? `free (${q.free_slots.remaining} of ${q.free_slots.total} free slots)` : "free";
|
|
36663
36797
|
const { amount, currency, interval } = q.price;
|
|
36664
|
-
const base = `${money2(amount, currency)}/${interval} per
|
|
36798
|
+
const base = `${money2(amount, currency)}/${interval} per database cluster (${q.plan} plan)${q.subscription ? `, cluster ${q.quantity + 1} on the subscription` : ""}`;
|
|
36665
36799
|
if (q.promo?.valid && q.amount_after_promo !== undefined && q.amount_after_promo !== null) {
|
|
36666
36800
|
const after = `${money2(q.amount_after_promo, currency)}`;
|
|
36667
36801
|
const how = `with ${q.promo.code} (${q.promo.discount_description})`;
|
|
@@ -36729,7 +36863,11 @@ function stateOf2(raw) {
|
|
|
36729
36863
|
class ClickhouseKeyError2 extends Error {
|
|
36730
36864
|
}
|
|
36731
36865
|
var serverOf2 = (name) => name.split("/")[0].toLowerCase().replace(/\.(?=$|:)/, "");
|
|
36732
|
-
|
|
36866
|
+
function clusterOf2(url) {
|
|
36867
|
+
const u = parseUrl2(url);
|
|
36868
|
+
return u?.hostname ? `${u.hostname.toLowerCase()}:${u.port || "5432"}` : undefined;
|
|
36869
|
+
}
|
|
36870
|
+
var errorText2 = (err) => err instanceof AggregateError && !err.message ? err.errors.map(errorText2).join("; ") : err instanceof Error ? err.message : String(err);
|
|
36733
36871
|
var urlPassword2 = (u) => decodeURIComponent(u.password) || u.searchParams.get("password") || "";
|
|
36734
36872
|
var URL_PARAMS_KEPT2 = ["sslmode", "channel_binding", "application_name"];
|
|
36735
36873
|
var URL_PARAMS_TLS2 = ["sslrootcert", "sslcert", "sslkey", "uselibpqcompat"];
|
|
@@ -36744,6 +36882,22 @@ function checkUrlParams2(url, agent) {
|
|
|
36744
36882
|
if (moved.length)
|
|
36745
36883
|
throw new Error(`The URL's query string sets ${moved.join(" and ")}: put the host and the port in the URL itself (postgresql://user:password@host:5432/dbname), so that the server prepared is the server monitored`);
|
|
36746
36884
|
}
|
|
36885
|
+
function storedPasswordNeeds2(monitoringUrl) {
|
|
36886
|
+
const query = new URL(monitoringUrl).search.replace(/^\?/, "");
|
|
36887
|
+
const parts = query ? query.split("&") : [];
|
|
36888
|
+
const keys = parts.map((p) => p.split("=")[0]);
|
|
36889
|
+
const twice = URL_PARAMS_KEPT2.filter((k) => keys.indexOf(k) !== keys.lastIndexOf(k));
|
|
36890
|
+
const tls = !twice.includes("sslmode") && parts.some((p) => /^sslmode=(require|verify-ca|verify-full)$/.test(p));
|
|
36891
|
+
const needs = [
|
|
36892
|
+
...tls ? [] : [`sslmode=require (or verify-full)${twice.includes("sslmode") ? ", once" : ""}`],
|
|
36893
|
+
...twice.filter((k) => k !== "sslmode").map((k) => `${k} once`),
|
|
36894
|
+
...parts.some((p) => !p) ? ["no stray &"] : [],
|
|
36895
|
+
...keys.some((k) => k && !URL_PARAMS_KEPT2.includes(k)) ? ["the parameter names without %-escapes"] : []
|
|
36896
|
+
];
|
|
36897
|
+
return needs.length ? needs.join(" and ") : undefined;
|
|
36898
|
+
}
|
|
36899
|
+
var shellWord2 = (s) => /^[\w./:@%+=,-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
36900
|
+
var disconnectEach2 = (names) => names.map((n) => `pgai disconnect ${shellWord2(n)} --yes`).join(" and ");
|
|
36747
36901
|
function roleUrlFor2(url, db, password, kept) {
|
|
36748
36902
|
const u = new URL(url);
|
|
36749
36903
|
u.username = DEFAULT_MONITORING_USER;
|
|
@@ -36752,6 +36906,7 @@ function roleUrlFor2(url, db, password, kept) {
|
|
|
36752
36906
|
for (const name of [...u.searchParams.keys()])
|
|
36753
36907
|
if (!kept.includes(name))
|
|
36754
36908
|
u.searchParams.delete(name);
|
|
36909
|
+
u.search = u.search.replace(/@/g, "%40");
|
|
36755
36910
|
return u.toString();
|
|
36756
36911
|
}
|
|
36757
36912
|
var monitoringUrlFor2 = (url, db, password) => roleUrlFor2(url, db, password, URL_PARAMS_KEPT2);
|
|
@@ -36813,22 +36968,24 @@ function progressText(e) {
|
|
|
36813
36968
|
`);
|
|
36814
36969
|
}
|
|
36815
36970
|
var boxState2 = (row) => row.status === "launch_requested" && row.registered_at ? "registered" : row.status ?? "starting";
|
|
36816
|
-
async function billingFor2(q, name, opts, deps) {
|
|
36971
|
+
async function billingFor2(q, name, opts, deps, show) {
|
|
36817
36972
|
const billing = { price: priceText2(q), requires_payment_method: q.requires_payment_method };
|
|
36818
36973
|
if (opts.coupon) {
|
|
36819
36974
|
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
36975
|
}
|
|
36821
|
-
const
|
|
36822
|
-
const
|
|
36976
|
+
const billingUrl = q.billing_url ?? deps.billingUrl(q.org_alias);
|
|
36977
|
+
const billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${billingUrl}, then re-run` };
|
|
36978
|
+
const stop = (next) => ({ billing, billingPage, billingUrl, accepted: false, stop: { ...billing, next } });
|
|
36823
36979
|
if (billing.coupon && !billing.coupon.valid) {
|
|
36824
36980
|
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
36981
|
}
|
|
36826
36982
|
if (q.requires_payment_method)
|
|
36827
|
-
return { billing, billingPage, accepted: false, stop: billingPage };
|
|
36828
|
-
|
|
36983
|
+
return { billing, billingPage, billingUrl, accepted: false, stop: billingPage };
|
|
36984
|
+
show(billing.price);
|
|
36985
|
+
if (q.billed && !opts.yes && !await deps.confirm(`Provision ${name}? (y/N): `)) {
|
|
36829
36986
|
return stop(opts.agent ? `Call connect_database again with yes: true to accept ${billing.price}` : `Re-run with --yes to accept ${billing.price}`);
|
|
36830
36987
|
}
|
|
36831
|
-
return { billing, billingPage, accepted: q.billed };
|
|
36988
|
+
return { billing, billingPage, billingUrl, accepted: q.billed };
|
|
36832
36989
|
}
|
|
36833
36990
|
async function connect2(url, opts, deps) {
|
|
36834
36991
|
const started = deps.now();
|
|
@@ -36848,7 +37005,7 @@ async function connect2(url, opts, deps) {
|
|
|
36848
37005
|
return { status: "action_required", provider, name, next: "--reset-password works with PostgresAI Cloud only (it checks what else monitors this server): connect without --self-hosted, or set PGAI_MON_PASSWORD" };
|
|
36849
37006
|
}
|
|
36850
37007
|
if (opts.selfHosted && deps.localStackRunning()) {
|
|
36851
|
-
return { status: "action_required", provider, name, next: "A monitoring stack already runs on this machine: add the database with pgai mon targets add
|
|
37008
|
+
return { status: "action_required", provider, name, next: "A monitoring stack already runs on this machine: add the database with PGAI_DB_URL='<postgres_ai_mon URL>' pgai mon targets add" };
|
|
36852
37009
|
}
|
|
36853
37010
|
const rows = opts.selfHosted ? [] : await deps.list();
|
|
36854
37011
|
let row = rows.find((d) => d.name === name && !disconnecting2(d.status));
|
|
@@ -36858,6 +37015,8 @@ async function connect2(url, opts, deps) {
|
|
|
36858
37015
|
let billing = {};
|
|
36859
37016
|
let billingPage;
|
|
36860
37017
|
let accepted = false;
|
|
37018
|
+
let billingUrl;
|
|
37019
|
+
let undoRole;
|
|
36861
37020
|
const findOrg = async () => {
|
|
36862
37021
|
try {
|
|
36863
37022
|
return await deps.clickhouseOrg(new URL(url).hostname, key.keyId, key.keySecret);
|
|
@@ -36891,51 +37050,72 @@ async function connect2(url, opts, deps) {
|
|
|
36891
37050
|
await deps.unprepare(url).catch(() => false);
|
|
36892
37051
|
};
|
|
36893
37052
|
let lockId;
|
|
36894
|
-
const
|
|
36895
|
-
const
|
|
36896
|
-
|
|
36897
|
-
|
|
36898
|
-
|
|
36899
|
-
|
|
37053
|
+
const unread = (d) => /^Monitoring [0-9a-f]{6}$/.test(d.name);
|
|
37054
|
+
const othersOnServer = (list) => list.filter((d) => d.name !== name && !disconnecting2(d.status) && (unread(d) || serverOf2(d.name) === serverOf2(name)));
|
|
37055
|
+
const storedPassword = rows.some((d) => d.monitoring_password_stored && !/delet/.test(d.status ?? "") && serverOf2(d.name) === serverOf2(name)) || undefined;
|
|
37056
|
+
const cutOff = (others) => {
|
|
37057
|
+
const on = others.filter((d) => !unread(d)).map((d) => d.name).join(", ");
|
|
37058
|
+
const maybe = others.filter(unread).map((d) => d.name).join(", ");
|
|
37059
|
+
const what = [on && `would cut off the monitoring of ${on} on this server`, maybe && `may cut off the monitoring of ${maybe} (the platform could not read its URL, so its server is not known)`].filter(Boolean).join(", and ");
|
|
37060
|
+
const instead = [
|
|
37061
|
+
...storedPassword ? ["connect without --reset-password (PostgresAI keeps its password for this server, and sends it only over TLS)"] : [],
|
|
37062
|
+
"set PGAI_MON_PASSWORD to its password instead",
|
|
37063
|
+
`${disconnectEach2(others.map((d) => d.name))} first`
|
|
37064
|
+
];
|
|
37065
|
+
return { status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} ${what}: ${instead.join(", or ")}` };
|
|
37066
|
+
};
|
|
37067
|
+
const others = othersOnServer(rows).filter((d) => !unread(d)).map((d) => d.name);
|
|
37068
|
+
if (opts.resetPassword && othersOnServer(rows).length)
|
|
37069
|
+
return cutOff(othersOnServer(rows));
|
|
36900
37070
|
if (opts.coupon !== undefined && !opts.coupon.trim()) {
|
|
36901
37071
|
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
37072
|
}
|
|
37073
|
+
const prepOpts = { resetPassword: opts.resetPassword, ...storedPassword ? { storedPassword, others } : {} };
|
|
36903
37074
|
if (!opts.selfHosted) {
|
|
36904
|
-
const
|
|
37075
|
+
const probe = await deps.prepare(url, provider, { ...prepOpts, check: true });
|
|
37076
|
+
if ("next" in probe)
|
|
37077
|
+
return { status: "action_required", provider, name, ...probe };
|
|
37078
|
+
const b = await billingFor2(await deps.quote(opts.coupon, clusterOf2(url)), name, opts, deps, (price) => progress("billing", `Billing: ${price}`));
|
|
36905
37079
|
if (b.stop)
|
|
36906
37080
|
return { status: "action_required", provider, name, ...b.stop };
|
|
36907
|
-
({ billing, billingPage, accepted } = b);
|
|
36908
|
-
progress("billing", `Billing: ${billing.price}`);
|
|
37081
|
+
({ billing, billingPage, billingUrl, accepted } = b);
|
|
36909
37082
|
}
|
|
36910
37083
|
if (opts.resetPassword) {
|
|
37084
|
+
let lock;
|
|
36911
37085
|
try {
|
|
36912
|
-
|
|
37086
|
+
lock = await deps.resetLock(serverOf2(name));
|
|
36913
37087
|
} catch (err) {
|
|
36914
37088
|
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` };
|
|
37089
|
+
return { status: "action_required", provider, name, ...billing, next: `Another pgai connect --reset-password for ${serverOf2(name)} is running: wait for it to finish, then re-run (a run that stopped frees the server 15 minutes after it started)` };
|
|
36916
37090
|
}
|
|
36917
37091
|
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` };
|
|
37092
|
+
return { status: "action_required", provider, name, ...billing, next: `This platform cannot lock the server for --reset-password yet: set PGAI_MON_PASSWORD to ${DEFAULT_MONITORING_USER}'s password instead` };
|
|
36919
37093
|
}
|
|
36920
37094
|
throw err;
|
|
36921
37095
|
}
|
|
37096
|
+
if (typeof lock.lock_id !== "string" || !lock.lock_id)
|
|
37097
|
+
throw new Error("cloud_monitoring_reset_lock returned no lock_id: nothing was changed");
|
|
37098
|
+
lockId = lock.lock_id;
|
|
36922
37099
|
}
|
|
36923
37100
|
let prepared;
|
|
36924
37101
|
let created;
|
|
37102
|
+
let unanswered = false;
|
|
36925
37103
|
try {
|
|
36926
37104
|
if (lockId) {
|
|
36927
37105
|
const now = await deps.list();
|
|
36928
37106
|
const same = now.find((d) => d.name === name && !disconnecting2(d.status));
|
|
36929
37107
|
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` };
|
|
37108
|
+
return { status: "action_required", provider, name, id: same.id, ...billing, next: `${name} is already connected, and its monitoring uses the current password: pgai disconnect ${name} --yes first` };
|
|
36931
37109
|
const others = othersOnServer(now);
|
|
36932
37110
|
if (others.length)
|
|
36933
|
-
return cutOff(others);
|
|
37111
|
+
return { ...cutOff(others), ...billing };
|
|
36934
37112
|
}
|
|
36935
37113
|
progress("preparing", `Preparing ${maskConnectionString(url)}`);
|
|
36936
|
-
prepared = await deps.prepare(url, provider,
|
|
37114
|
+
prepared = await deps.prepare(url, provider, prepOpts);
|
|
36937
37115
|
if ("next" in prepared)
|
|
36938
37116
|
return { status: "action_required", provider, name, ...billing, ...prepared };
|
|
37117
|
+
if (!("monitoringUrl" in prepared))
|
|
37118
|
+
throw new Error("prepare returned no monitoring URL");
|
|
36939
37119
|
if (prepared.note)
|
|
36940
37120
|
note = `; ${prepared.note}`;
|
|
36941
37121
|
if (opts.selfHosted) {
|
|
@@ -36952,10 +37132,14 @@ async function connect2(url, opts, deps) {
|
|
|
36952
37132
|
}).catch(async (err) => {
|
|
36953
37133
|
if (err instanceof HttpStatusError && err.status >= 400 && err.status < 500)
|
|
36954
37134
|
await undo(prepared);
|
|
37135
|
+
else
|
|
37136
|
+
unanswered = true;
|
|
36955
37137
|
if (err instanceof HttpStatusError && err.status === 402)
|
|
36956
37138
|
return /declined/i.test(err.message) ? "declined" : "payment";
|
|
36957
37139
|
if (err instanceof HttpStatusError && err.status === 412)
|
|
36958
37140
|
return "price";
|
|
37141
|
+
if (err instanceof HttpStatusError && err.status === 409 && "storedPassword" in prepared && prepared.storedPassword && /none is stored/.test(err.message))
|
|
37142
|
+
return "stored";
|
|
36959
37143
|
throw err;
|
|
36960
37144
|
});
|
|
36961
37145
|
if (created === "payment")
|
|
@@ -36965,21 +37149,43 @@ async function connect2(url, opts, deps) {
|
|
|
36965
37149
|
}
|
|
36966
37150
|
if (created === "price")
|
|
36967
37151
|
return { status: "action_required", provider, name, ...billing, next: "The price changed since it was shown: re-run pgai connect to see it" };
|
|
37152
|
+
if (created === "stored") {
|
|
37153
|
+
const gone = `PostgresAI no longer keeps the password of ${DEFAULT_MONITORING_USER} for this server`;
|
|
37154
|
+
return { status: "action_required", provider, name, ...billing, next: opts.agent ? `${gone}: pass its URL as database_url, or run pgai connect <admin-url> --reset-password in a terminal` : `${gone}: re-run with --reset-password (anything else that logs in as ${DEFAULT_MONITORING_USER} then needs the new one), or set PGAI_MON_PASSWORD to its password` };
|
|
37155
|
+
}
|
|
36968
37156
|
if (created.status === "failed") {
|
|
36969
37157
|
await undo(prepared);
|
|
36970
37158
|
return { status: "failed", provider, name, id: created.id, next: `${created.error} Re-run pgai connect later.` };
|
|
36971
37159
|
}
|
|
36972
37160
|
} finally {
|
|
36973
|
-
if (lockId)
|
|
37161
|
+
if (lockId && !unanswered)
|
|
36974
37162
|
await deps.resetUnlock(lockId).catch(() => {});
|
|
36975
37163
|
}
|
|
36976
37164
|
row = { id: created.id, name: created.name, provider, status: created.status, dashboard_url: null, host_metrics: !!ch };
|
|
36977
|
-
|
|
36978
|
-
|
|
37165
|
+
const made = prepared;
|
|
37166
|
+
undoRole = () => undo(made);
|
|
37167
|
+
if (prepared.storedPassword) {
|
|
37168
|
+
progress("checkup", `Express checkup skipped: it runs only as ${DEFAULT_MONITORING_USER}, whose password PostgresAI keeps for this server. The full checkup follows on the box.`);
|
|
37169
|
+
} else {
|
|
37170
|
+
checkup = await deps.checkup(withLocalTls2(prepared.monitoringUrl, url), created.name).catch((err) => ({ error: errorText2(err) }));
|
|
37171
|
+
progress("checkup", checkupLines2(checkup).join(`
|
|
36979
37172
|
`), { checkup });
|
|
37173
|
+
}
|
|
36980
37174
|
}
|
|
36981
37175
|
let shown;
|
|
36982
37176
|
for (const deadline = deps.now() + opts.waitMs;; ) {
|
|
37177
|
+
if (undoRole && row.billing_error && disconnecting2(row.status)) {
|
|
37178
|
+
await undoRole();
|
|
37179
|
+
return {
|
|
37180
|
+
status: "action_required",
|
|
37181
|
+
provider,
|
|
37182
|
+
name,
|
|
37183
|
+
id: row.id,
|
|
37184
|
+
...billing,
|
|
37185
|
+
...checkup ? { checkup } : {},
|
|
37186
|
+
next: `The first charge failed (${row.billing_error}): the box was removed and nothing is billed. Update the payment method at ${billingUrl}, then re-run`
|
|
37187
|
+
};
|
|
37188
|
+
}
|
|
36983
37189
|
const result = connectStatus2(row, provider, fresh);
|
|
36984
37190
|
const state = boxState2(row);
|
|
36985
37191
|
if (state !== shown && (shown !== undefined || result.status === "provisioning"))
|
|
@@ -36993,12 +37199,22 @@ async function connect2(url, opts, deps) {
|
|
|
36993
37199
|
return { ...rest, ...billing, ...checkup ? { checkup } : {}, next: next + note };
|
|
36994
37200
|
}
|
|
36995
37201
|
await deps.sleep(POLL_MS2);
|
|
36996
|
-
|
|
37202
|
+
const listed = await deps.list().catch(() => {
|
|
37203
|
+
return;
|
|
37204
|
+
});
|
|
37205
|
+
const now = listed?.find((d) => d.id === row.id);
|
|
37206
|
+
if (listed && !now && undoRole) {
|
|
37207
|
+
await undoRole();
|
|
37208
|
+
return { status: "failed", provider, name, id: row.id, ...billing, next: "The monitoring box was removed before it became active: see pgai databases, then re-run pgai connect" };
|
|
37209
|
+
}
|
|
37210
|
+
row = now ?? row;
|
|
36997
37211
|
}
|
|
36998
37212
|
}
|
|
36999
|
-
function openConnection2(url, opts) {
|
|
37213
|
+
async function openConnection2(url, opts) {
|
|
37000
37214
|
const conn = resolveAdminConnection({ conn: url });
|
|
37001
|
-
|
|
37215
|
+
const fallback = !opts.agent && !!conn.sslFallbackEnabled;
|
|
37216
|
+
const { client, usedSsl } = await connectWithSslFallback(opts.Client ?? Client2, { ...conn, sslFallbackEnabled: fallback });
|
|
37217
|
+
return { client, usedSsl, refusedTls: fallback && !usedSsl };
|
|
37002
37218
|
}
|
|
37003
37219
|
async function dropMonitoringRole2(client) {
|
|
37004
37220
|
for (const sql of [`drop role ${DEFAULT_MONITORING_USER}`, `drop owned by ${DEFAULT_MONITORING_USER}; drop role ${DEFAULT_MONITORING_USER}`]) {
|
|
@@ -37031,7 +37247,7 @@ async function prepareDatabase2(url, provider, opts = {}) {
|
|
|
37031
37247
|
throw err;
|
|
37032
37248
|
}
|
|
37033
37249
|
};
|
|
37034
|
-
const { client } = await open(url).catch((err) => {
|
|
37250
|
+
const { client, usedSsl, refusedTls } = await open(url).catch((err) => {
|
|
37035
37251
|
if (/client password must be a string/.test(String(err?.message)))
|
|
37036
37252
|
throw new Error("The URL has no password (and PGPASSWORD is not set): pgai connect postgresql://user:password@host:5432/dbname");
|
|
37037
37253
|
throw err;
|
|
@@ -37045,7 +37261,7 @@ async function prepareDatabase2(url, provider, opts = {}) {
|
|
|
37045
37261
|
and has_database_privilege(current_user, current_database(), 'CREATE')
|
|
37046
37262
|
and exists (select 1 from pg_extension where extname = 'pg_stat_statements')) as admin,
|
|
37047
37263
|
exists (select 1 from pg_roles where rolname = '${DEFAULT_MONITORING_USER}') as mon_exists,
|
|
37048
|
-
current_setting('scram_iterations', true) as iterations
|
|
37264
|
+
current_setting('scram_iterations', true) as iterations, current_setting('ssl') as ssl
|
|
37049
37265
|
from pg_roles where rolname = current_user`)).rows[0];
|
|
37050
37266
|
const notes = [caNote2(url)].filter((n) => !!n);
|
|
37051
37267
|
const noted = () => notes.length ? { note: notes.join("; ") } : {};
|
|
@@ -37069,9 +37285,31 @@ async function prepareDatabase2(url, provider, opts = {}) {
|
|
|
37069
37285
|
}
|
|
37070
37286
|
} else if (me.admin && pgProvider !== "supabase") {
|
|
37071
37287
|
const exists = `${DEFAULT_MONITORING_USER} already exists on this server`;
|
|
37288
|
+
const setPassword = `Set PGAI_MON_PASSWORD to the password of ${DEFAULT_MONITORING_USER}, or give it a new one: pgai connect <admin-url> --reset-password (anything else that logs in as ${DEFAULT_MONITORING_USER} then needs the new password)`;
|
|
37289
|
+
const needs = storedPasswordNeeds2(monitoringUrlFor2(url, me.db, ""));
|
|
37290
|
+
const noTls = refusedTls || !usedSsl && me.ssl === "off";
|
|
37291
|
+
const tlsOnly = `PostgresAI sends the password it keeps for ${DEFAULT_MONITORING_USER} only over TLS`;
|
|
37292
|
+
const others = opts.others?.length ? opts.others : undefined;
|
|
37293
|
+
const disconnectOthers = others ? disconnectEach2(others) : "pgai disconnect the server's other databases (pgai databases)";
|
|
37294
|
+
const reconnectOthers = `(and connect ${others ? others.join(", ") : "them"} again with it)`;
|
|
37295
|
+
const noTlsWayOut = opts.agent ? `Pass its URL as database_url, or run pgai connect in a terminal with PGAI_MON_PASSWORD set to its password. If nobody has it, in a terminal: ${disconnectOthers}, then pgai connect <admin-url> --reset-password with PGAI_MON_PASSWORD set to a new one ${reconnectOthers}` : `Set PGAI_MON_PASSWORD to its password, or turn on TLS on the server (ssl = on) and put sslmode=require in the URL. If nobody has the password: ${disconnectOthers}, then re-run with --reset-password and PGAI_MON_PASSWORD set to a new one ${reconnectOthers}`;
|
|
37296
|
+
const reuse = me.mon_exists && !opts.resetPassword && !!opts.storedPassword && !(opts.agent ? "" : process.env.PGAI_MON_PASSWORD?.trim());
|
|
37297
|
+
if (reuse && needs) {
|
|
37298
|
+
if (noTls)
|
|
37299
|
+
return { next: `${exists}, but this server takes no TLS, and ${tlsOnly}. ${noTlsWayOut}` };
|
|
37300
|
+
return { next: `${exists}. ${opts.agent ? "database_url" : "The URL"} needs ${needs}: the password PostgresAI keeps for this server is sent only over TLS, to a URL with each parameter once` };
|
|
37301
|
+
}
|
|
37302
|
+
if (reuse) {
|
|
37303
|
+
if (opts.check)
|
|
37304
|
+
return { checked: true };
|
|
37305
|
+
const { password: unused } = await resolveMonitoringPassword({ monitoringUser: DEFAULT_MONITORING_USER });
|
|
37306
|
+
const plan = await buildInitPlan({ database: me.db, monitoringPassword: unused, iterations: Number(me.iterations), includeOptionalPermissions: true, provider: pgProvider, keepExistingPassword: true });
|
|
37307
|
+
await applyInitPlan({ client, plan });
|
|
37308
|
+
return { monitoringUrl: monitoringUrlFor2(url, me.db, ""), storedPassword: true, ...noted() };
|
|
37309
|
+
}
|
|
37072
37310
|
if (me.mon_exists && opts.agent)
|
|
37073
37311
|
return { next: `${exists}. Pass its URL as database_url, or run pgai connect in a terminal with PGAI_MON_PASSWORD set to its password` };
|
|
37074
|
-
const
|
|
37312
|
+
const unset = !opts.storedPassword ? setPassword : noTls ? `This server takes no TLS, and ${tlsOnly}. ${noTlsWayOut}` : !needs ? `Unset PGAI_MON_PASSWORD: PostgresAI keeps the password of ${DEFAULT_MONITORING_USER} for this server` : `Unset PGAI_MON_PASSWORD, and the URL needs ${needs}: PostgresAI keeps the password of ${DEFAULT_MONITORING_USER} for this server, and sends it only over TLS, to a URL with each parameter once`;
|
|
37075
37313
|
const { password, generated } = await resolveMonitoringPassword({ passwordEnv: opts.agent ? undefined : process.env.PGAI_MON_PASSWORD, monitoringUser: DEFAULT_MONITORING_USER });
|
|
37076
37314
|
const reset = !!opts.resetPassword && me.mon_exists;
|
|
37077
37315
|
if (me.mon_exists && !reset) {
|
|
@@ -37081,13 +37319,16 @@ async function prepareDatabase2(url, provider, opts = {}) {
|
|
|
37081
37319
|
try {
|
|
37082
37320
|
accepted = await logsIn(loginUrlFor2(url, me.db, password));
|
|
37083
37321
|
} catch (err) {
|
|
37084
|
-
|
|
37322
|
+
const orUnset = opts.storedPassword && !noTls ? ", or unset PGAI_MON_PASSWORD: PostgresAI keeps its password for this server" : "";
|
|
37323
|
+
return { next: `${exists}, and PGAI_MON_PASSWORD could not be checked from this host (${err instanceof Error ? err.message : String(err)}). Run pgai connect from a host that ${DEFAULT_MONITORING_USER} may connect from${orUnset}` };
|
|
37085
37324
|
}
|
|
37086
37325
|
if (!accepted)
|
|
37087
|
-
return { next: `${exists} and PGAI_MON_PASSWORD is not its password. ${
|
|
37326
|
+
return { next: `${exists} and PGAI_MON_PASSWORD is not its password. ${unset}` };
|
|
37088
37327
|
if (await acceptsAnyPassword())
|
|
37089
37328
|
notes.push(unchecked("PGAI_MON_PASSWORD"));
|
|
37090
37329
|
}
|
|
37330
|
+
if (opts.check)
|
|
37331
|
+
return { checked: true };
|
|
37091
37332
|
const monitoringUrl = monitoringUrlFor2(url, me.db, password);
|
|
37092
37333
|
const loginUrl = loginUrlFor2(url, me.db, password);
|
|
37093
37334
|
const plan = await buildInitPlan({ database: me.db, monitoringPassword: password, iterations: Number(me.iterations), includeOptionalPermissions: true, provider: pgProvider, keepExistingPassword: !reset });
|
|
@@ -37135,26 +37376,17 @@ async function expressCheckup2(url, opts = {}) {
|
|
|
37135
37376
|
}
|
|
37136
37377
|
}
|
|
37137
37378
|
async function saveCheckupReport2(rpc, accessToken, project, reports) {
|
|
37138
|
-
const
|
|
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");
|
|
37379
|
+
const { reportId } = await createCheckupReport(rpc, { apiKey: accessToken, project });
|
|
37146
37380
|
const setStatus = (status) => rpc("checkup_report_status_update", { access_token: accessToken, report_id: reportId, status });
|
|
37147
37381
|
try {
|
|
37148
37382
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
37149
|
-
|
|
37150
|
-
|
|
37151
|
-
|
|
37383
|
+
await uploadCheckupReportJson(rpc, {
|
|
37384
|
+
apiKey: accessToken,
|
|
37385
|
+
reportId,
|
|
37152
37386
|
filename: `${checkId}.json`,
|
|
37153
|
-
|
|
37154
|
-
|
|
37155
|
-
|
|
37156
|
-
generate_issue: true
|
|
37157
|
-
}), "report_chunck_id", "report_chunk_id");
|
|
37387
|
+
checkId,
|
|
37388
|
+
jsonText: JSON.stringify(report, null, 2)
|
|
37389
|
+
});
|
|
37158
37390
|
}
|
|
37159
37391
|
} catch (err) {
|
|
37160
37392
|
await setStatus("failed").catch(() => {});
|
|
@@ -37192,6 +37424,22 @@ async function clickhouseOrgFor2(host, keyId, keySecret) {
|
|
|
37192
37424
|
}
|
|
37193
37425
|
throw failed ?? notFound;
|
|
37194
37426
|
}
|
|
37427
|
+
function disconnectBilling(reply) {
|
|
37428
|
+
const r = reply ?? {};
|
|
37429
|
+
if (r.billing?.subscription === "canceled" && r.billing.credited && r.billing_warning)
|
|
37430
|
+
return `subscription canceled: no further charges; the refund failed (${r.billing_warning})`;
|
|
37431
|
+
if (r.billing_warning)
|
|
37432
|
+
return `not released (${r.billing_warning}): contact support`;
|
|
37433
|
+
if (r.billing?.subscription === "canceled" && typeof r.billing.refunded === "number" && r.billing.refunded > 0) {
|
|
37434
|
+
return `subscription canceled: no further charges; refunded ${money2(r.billing.refunded, r.billing.currency ?? "usd")} to your card`;
|
|
37435
|
+
}
|
|
37436
|
+
if (r.billing?.subscription === "canceled")
|
|
37437
|
+
return "subscription canceled: no further charges; the unused part of this period is credited (prorated)";
|
|
37438
|
+
if (r.billing?.subscription === "active" && typeof r.billing.quantity === "number") {
|
|
37439
|
+
return `${r.billing.quantity} ${r.billing.quantity === 1 ? "database cluster" : "database clusters"} left on the subscription`;
|
|
37440
|
+
}
|
|
37441
|
+
return;
|
|
37442
|
+
}
|
|
37195
37443
|
function platformDeps2(p) {
|
|
37196
37444
|
const rpc = (fn, body = {}) => callRpc({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl, fn, body, operation: fn.replace(/_/g, " "), debug: p.debug, orgScope: p.orgScope });
|
|
37197
37445
|
return {
|
|
@@ -37204,7 +37452,7 @@ function platformDeps2(p) {
|
|
|
37204
37452
|
unprepare: (url) => unprepareDatabase2(url, { agent: p.agent }),
|
|
37205
37453
|
clickhouseOrg: clickhouseOrgFor2,
|
|
37206
37454
|
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 } : {}),
|
|
37455
|
+
quote: (coupon, cluster) => rpc("cloud_monitoring_quote", { ...coupon ? { promo_code: coupon } : {}, ...cluster ? { db_server: cluster } : {} }),
|
|
37208
37456
|
billingUrl: (orgAlias) => `${p.uiBaseUrl}/${orgAlias}/billing`,
|
|
37209
37457
|
handoffUrl: async (provider) => {
|
|
37210
37458
|
const orgs = await listOrgs({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl });
|
|
@@ -39329,10 +39577,6 @@ async function applyUninitPlan(params) {
|
|
|
39329
39577
|
}
|
|
39330
39578
|
return { applied, errors };
|
|
39331
39579
|
}
|
|
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
|
-
}
|
|
39336
39580
|
async function verifyInitSetup2(params) {
|
|
39337
39581
|
await params.client.query("begin isolation level repeatable read;");
|
|
39338
39582
|
try {
|
|
@@ -40497,7 +40741,7 @@ function feedbackMessage() {
|
|
|
40497
40741
|
}
|
|
40498
40742
|
|
|
40499
40743
|
// bin/postgres-ai.ts
|
|
40500
|
-
import { createInterface } from "readline";
|
|
40744
|
+
import { createInterface as createInterface2 } from "readline";
|
|
40501
40745
|
import { Writable } from "stream";
|
|
40502
40746
|
import * as childProcess from "child_process";
|
|
40503
40747
|
|
|
@@ -40505,7 +40749,7 @@ import * as childProcess from "child_process";
|
|
|
40505
40749
|
import * as fs9 from "fs";
|
|
40506
40750
|
import * as path8 from "path";
|
|
40507
40751
|
var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
|
|
40508
|
-
var CONTRACT_VERSION2 = "1.
|
|
40752
|
+
var CONTRACT_VERSION2 = "1.0.0";
|
|
40509
40753
|
var SECONDS_PER_DAY2 = 86400;
|
|
40510
40754
|
var SECONDS_PER_HOUR2 = 3600;
|
|
40511
40755
|
var SECONDS_PER_MINUTE2 = 60;
|
|
@@ -43090,48 +43334,11 @@ async function verifyApiKey(params) {
|
|
|
43090
43334
|
clearTimeout(timer);
|
|
43091
43335
|
}
|
|
43092
43336
|
}
|
|
43093
|
-
async function
|
|
43094
|
-
|
|
43095
|
-
const bodyObj = {
|
|
43096
|
-
access_token: apiKey,
|
|
43097
|
-
project
|
|
43098
|
-
};
|
|
43099
|
-
if (status)
|
|
43100
|
-
bodyObj.status = status;
|
|
43101
|
-
const resp = await postRpc({
|
|
43102
|
-
apiKey,
|
|
43103
|
-
apiBaseUrl,
|
|
43104
|
-
rpcName: "checkup_report_create",
|
|
43105
|
-
bodyObj
|
|
43106
|
-
});
|
|
43107
|
-
const reportId = Number(resp?.report_id);
|
|
43108
|
-
if (!Number.isFinite(reportId) || reportId <= 0) {
|
|
43109
|
-
throw new Error(`Unexpected checkup_report_create response: ${JSON.stringify(resp)}`);
|
|
43110
|
-
}
|
|
43111
|
-
return { reportId };
|
|
43337
|
+
async function createCheckupReport2(params) {
|
|
43338
|
+
return createCheckupReport((rpcName, bodyObj) => postRpc({ ...params, rpcName, bodyObj }), params);
|
|
43112
43339
|
}
|
|
43113
|
-
async function
|
|
43114
|
-
|
|
43115
|
-
const bodyObj = {
|
|
43116
|
-
access_token: apiKey,
|
|
43117
|
-
checkup_report_id: reportId,
|
|
43118
|
-
filename,
|
|
43119
|
-
check_id: checkId,
|
|
43120
|
-
data: jsonText,
|
|
43121
|
-
type: "json",
|
|
43122
|
-
generate_issue: true
|
|
43123
|
-
};
|
|
43124
|
-
const resp = await postRpc({
|
|
43125
|
-
apiKey,
|
|
43126
|
-
apiBaseUrl,
|
|
43127
|
-
rpcName: "checkup_report_file_post",
|
|
43128
|
-
bodyObj
|
|
43129
|
-
});
|
|
43130
|
-
const chunkId = Number(resp?.report_chunck_id ?? resp?.report_chunk_id);
|
|
43131
|
-
if (!Number.isFinite(chunkId) || chunkId <= 0) {
|
|
43132
|
-
throw new Error(`Unexpected checkup_report_file_post response: ${JSON.stringify(resp)}`);
|
|
43133
|
-
}
|
|
43134
|
-
return { reportChunkId: chunkId };
|
|
43340
|
+
async function uploadCheckupReportJson2(params) {
|
|
43341
|
+
return uploadCheckupReportJson((rpcName, bodyObj) => postRpc({ ...params, rpcName, bodyObj }), params);
|
|
43135
43342
|
}
|
|
43136
43343
|
async function convertCheckupReportJsonToMarkdown(params) {
|
|
43137
43344
|
const { apiKey, apiBaseUrl, checkId, jsonPayload, reportType } = params;
|
|
@@ -43525,21 +43732,45 @@ function summarizeG0032(nodeData) {
|
|
|
43525
43732
|
}
|
|
43526
43733
|
|
|
43527
43734
|
// lib/json-stderr.ts
|
|
43735
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
43736
|
+
import { createInterface } from "node:readline";
|
|
43528
43737
|
import { format } from "node:util";
|
|
43529
43738
|
function writeEvent(event) {
|
|
43530
43739
|
process.stderr.write(`${JSON.stringify(event)}
|
|
43531
43740
|
`);
|
|
43532
43741
|
}
|
|
43533
|
-
|
|
43742
|
+
function jsonConsole() {
|
|
43534
43743
|
const { error, warn } = console;
|
|
43535
|
-
|
|
43536
|
-
|
|
43537
|
-
|
|
43538
|
-
|
|
43539
|
-
|
|
43744
|
+
const log = (level) => (...args) => {
|
|
43745
|
+
const message = format(...args);
|
|
43746
|
+
writeEvent({ event: "log", level: /^\s*Debug:/.test(message) ? "debug" : /^\s*(?:\[[^\]]+\]\s*)?Warning:/.test(message) ? "warn" : level, message });
|
|
43747
|
+
};
|
|
43748
|
+
console.error = log("error");
|
|
43749
|
+
console.warn = log("warn");
|
|
43750
|
+
return () => {
|
|
43540
43751
|
console.error = error;
|
|
43541
43752
|
console.warn = warn;
|
|
43542
|
-
}
|
|
43753
|
+
};
|
|
43754
|
+
}
|
|
43755
|
+
var LOGIN = /^(\s*(?:Login|VictoriaMetrics Auth): ).+$/;
|
|
43756
|
+
function runChild(command, args, env, json, source) {
|
|
43757
|
+
if (!json)
|
|
43758
|
+
return Promise.resolve(spawnSync(command, args, { stdio: ["ignore", 2, 2], env }).status);
|
|
43759
|
+
return new Promise((resolve) => {
|
|
43760
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
43761
|
+
for (const [stream, level] of [[child.stdout, "info"], [child.stderr, "error"]]) {
|
|
43762
|
+
createInterface({ input: stream }).on("line", (line) => {
|
|
43763
|
+
const message = line.replace(LOGIN, "$1***** (pgai mon show-grafana-credentials)");
|
|
43764
|
+
if (line.trim() !== "")
|
|
43765
|
+
writeEvent({ event: "log", level, source, message });
|
|
43766
|
+
});
|
|
43767
|
+
}
|
|
43768
|
+
child.on("error", (err) => {
|
|
43769
|
+
writeEvent({ event: "log", level: "error", source, message: err.message });
|
|
43770
|
+
resolve(null);
|
|
43771
|
+
});
|
|
43772
|
+
child.on("close", (code) => resolve(code));
|
|
43773
|
+
});
|
|
43543
43774
|
}
|
|
43544
43775
|
|
|
43545
43776
|
// lib/instances.ts
|
|
@@ -43687,12 +43918,22 @@ function buildClientConfig(connStr, extra = {}) {
|
|
|
43687
43918
|
port: parsed.port ? Number(parsed.port) : undefined,
|
|
43688
43919
|
user: parsed.user,
|
|
43689
43920
|
password: parsed.password,
|
|
43690
|
-
database: parsed.database
|
|
43921
|
+
database: databaseName3(withoutSslmode(channelBinding.uri), parsed.database),
|
|
43691
43922
|
ssl: sslOptionFromSslmode(sslmode),
|
|
43692
43923
|
...enableChannelBinding ? { enableChannelBinding: true } : {},
|
|
43693
43924
|
...extra
|
|
43694
43925
|
};
|
|
43695
43926
|
}
|
|
43927
|
+
function databaseName3(connStr, parsed) {
|
|
43928
|
+
if (!parsed)
|
|
43929
|
+
return;
|
|
43930
|
+
try {
|
|
43931
|
+
const u = new URL(connStr);
|
|
43932
|
+
if (/^postgres(ql)?:$/.test(u.protocol))
|
|
43933
|
+
return decodeURIComponent(u.pathname.slice(1));
|
|
43934
|
+
} catch {}
|
|
43935
|
+
return parsed;
|
|
43936
|
+
}
|
|
43696
43937
|
function withoutSslmode(connStr) {
|
|
43697
43938
|
try {
|
|
43698
43939
|
const u = new URL(connStr);
|
|
@@ -43736,7 +43977,7 @@ Download: https://nodejs.org/`);
|
|
|
43736
43977
|
var rl = null;
|
|
43737
43978
|
function getReadline() {
|
|
43738
43979
|
if (!rl) {
|
|
43739
|
-
rl =
|
|
43980
|
+
rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
43740
43981
|
}
|
|
43741
43982
|
return rl;
|
|
43742
43983
|
}
|
|
@@ -43961,7 +44202,7 @@ async function execFilePromise(file, args, options = {}) {
|
|
|
43961
44202
|
});
|
|
43962
44203
|
});
|
|
43963
44204
|
}
|
|
43964
|
-
function
|
|
44205
|
+
function spawnSync3(cmd, args, options) {
|
|
43965
44206
|
const result = childProcess.spawnSync(cmd, args, {
|
|
43966
44207
|
stdio: options?.stdio === "inherit" ? "inherit" : "pipe",
|
|
43967
44208
|
env: options?.env,
|
|
@@ -43974,7 +44215,7 @@ function spawnSync2(cmd, args, options) {
|
|
|
43974
44215
|
stderr: typeof result.stderr === "string" ? result.stderr : ""
|
|
43975
44216
|
};
|
|
43976
44217
|
}
|
|
43977
|
-
function
|
|
44218
|
+
function spawn3(cmd, args, options) {
|
|
43978
44219
|
const proc = childProcess.spawn(cmd, args, {
|
|
43979
44220
|
stdio: options?.stdio ?? "pipe",
|
|
43980
44221
|
env: options?.env,
|
|
@@ -44005,7 +44246,7 @@ async function question(prompt) {
|
|
|
44005
44246
|
}
|
|
44006
44247
|
async function questionHidden(prompt) {
|
|
44007
44248
|
closeReadline();
|
|
44008
|
-
const hidden =
|
|
44249
|
+
const hidden = createInterface2({ input: process.stdin, output: new Writable({ write: (_chunk, _encoding, done) => done() }), terminal: true });
|
|
44009
44250
|
process.stdout.write(prompt);
|
|
44010
44251
|
return new Promise((resolve) => {
|
|
44011
44252
|
hidden.question("", (answer) => {
|
|
@@ -44136,7 +44377,7 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
|
|
|
44136
44377
|
}
|
|
44137
44378
|
async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
44138
44379
|
spinner.update("Creating remote checkup report");
|
|
44139
|
-
const created = await withRetry(() =>
|
|
44380
|
+
const created = await withRetry(() => createCheckupReport2({
|
|
44140
44381
|
apiKey: uploadCfg.apiKey,
|
|
44141
44382
|
apiBaseUrl: uploadCfg.apiBaseUrl,
|
|
44142
44383
|
project: uploadCfg.project
|
|
@@ -44149,7 +44390,7 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
|
44149
44390
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
44150
44391
|
spinner.update(`Uploading ${checkId}.json`);
|
|
44151
44392
|
const jsonText = JSON.stringify(report, null, 2);
|
|
44152
|
-
const r = await withRetry(() =>
|
|
44393
|
+
const r = await withRetry(() => uploadCheckupReportJson2({
|
|
44153
44394
|
apiKey: uploadCfg.apiKey,
|
|
44154
44395
|
apiBaseUrl: uploadCfg.apiBaseUrl,
|
|
44155
44396
|
reportId,
|
|
@@ -44290,47 +44531,12 @@ function sanitizeTagForBackup(tag) {
|
|
|
44290
44531
|
const stripped = stripMatchingQuotes(tag);
|
|
44291
44532
|
return /^[A-Za-z0-9._-]{1,64}$/.test(stripped) ? stripped : null;
|
|
44292
44533
|
}
|
|
44293
|
-
function
|
|
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) {
|
|
44534
|
+
function readDeployedTag(projectDir) {
|
|
44316
44535
|
const envFile = path9.resolve(projectDir, ".env");
|
|
44317
44536
|
if (!fs11.existsSync(envFile))
|
|
44318
|
-
return
|
|
44319
|
-
const
|
|
44320
|
-
|
|
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);
|
|
44537
|
+
return null;
|
|
44538
|
+
const m = fs11.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
|
|
44539
|
+
return m ? sanitizeTagForBackup(m[1]) : null;
|
|
44334
44540
|
}
|
|
44335
44541
|
function isValidComposeYaml(text) {
|
|
44336
44542
|
const trimmed = text.trim();
|
|
@@ -44381,7 +44587,7 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
|
44381
44587
|
const existing = fs11.readFileSync(composeFile, "utf8");
|
|
44382
44588
|
if (composeContentEqual(existing, fetched))
|
|
44383
44589
|
return false;
|
|
44384
|
-
const deployedTag = sanitizeTagForBackup(oldTag ??
|
|
44590
|
+
const deployedTag = sanitizeTagForBackup(oldTag ?? readDeployedTag(projectDir));
|
|
44385
44591
|
const tagPart = deployedTag ?? new Date().toISOString().replace(/[:.]/g, "-");
|
|
44386
44592
|
const oldHash = crypto2.createHash("sha256").update(existing).digest("hex").slice(0, 8);
|
|
44387
44593
|
const backup = path9.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
|
|
@@ -44431,6 +44637,56 @@ function printResult(result, json) {
|
|
|
44431
44637
|
var program2 = new Command2;
|
|
44432
44638
|
program2.name("postgres-ai").description("PostgresAI CLI").version(package_default.version).option("--api-key <key>", "API key (overrides PGAI_API_KEY)").option("--api-base-url <url>", "API base URL for backend RPC (overrides PGAI_API_BASE_URL)").option("--ui-base-url <url>", "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)").option("--storage-base-url <url>", "Storage base URL for file uploads (overrides PGAI_STORAGE_BASE_URL)");
|
|
44433
44639
|
program2.showHelpAfterError();
|
|
44640
|
+
program2.configureOutput({
|
|
44641
|
+
outputError: (text, write) => jsonConnectArgv() ? writeEvent({ event: "log", level: "error", message: text.trim().replace(/^(error: unknown option ')(--[^=]*|-[^-])[\s\S]*'(?=\n\(Did you mean [^\n]*\)$|$)/, "$1$2'") }) : write(text),
|
|
44642
|
+
writeErr: (text) => void (jsonConnectArgv() || process.stderr.write(text))
|
|
44643
|
+
});
|
|
44644
|
+
function splitArgv(options, argv) {
|
|
44645
|
+
const operands = [];
|
|
44646
|
+
const unknown = [];
|
|
44647
|
+
let dest = operands;
|
|
44648
|
+
let json = false;
|
|
44649
|
+
for (let i = 0;i < argv.length; i++) {
|
|
44650
|
+
const arg = argv[i];
|
|
44651
|
+
if (arg === "--") {
|
|
44652
|
+
dest.push(...dest === unknown ? argv.slice(i) : argv.slice(i + 1));
|
|
44653
|
+
break;
|
|
44654
|
+
}
|
|
44655
|
+
const option = options.find((o) => o.long === arg || o.short === arg);
|
|
44656
|
+
if (option) {
|
|
44657
|
+
if (option.required)
|
|
44658
|
+
i++;
|
|
44659
|
+
else if (option.long === "--json")
|
|
44660
|
+
json = true;
|
|
44661
|
+
continue;
|
|
44662
|
+
}
|
|
44663
|
+
if (options.some((o) => o.required && arg.startsWith(`${o.long}=`)))
|
|
44664
|
+
continue;
|
|
44665
|
+
if (dest === operands && arg.length > 1 && arg.startsWith("-"))
|
|
44666
|
+
dest = unknown;
|
|
44667
|
+
dest.push(arg);
|
|
44668
|
+
}
|
|
44669
|
+
return { operands, unknown, json };
|
|
44670
|
+
}
|
|
44671
|
+
function argvRun(argv = process.argv.slice(2)) {
|
|
44672
|
+
const root = splitArgv(program2.options, argv);
|
|
44673
|
+
let command = program2.commands.find((c) => c.name() === root.operands[0]);
|
|
44674
|
+
let rest = root.unknown;
|
|
44675
|
+
if (root.operands.length === 0) {
|
|
44676
|
+
const end = rest.indexOf("--");
|
|
44677
|
+
const at = rest.slice(0, end < 0 ? undefined : end).indexOf("connect");
|
|
44678
|
+
if (at >= 0)
|
|
44679
|
+
[command, rest] = [program2.commands.find((c) => c.name() === "connect"), rest.slice(at + 1)];
|
|
44680
|
+
}
|
|
44681
|
+
return { command, json: !!command && splitArgv(command.options, rest).json };
|
|
44682
|
+
}
|
|
44683
|
+
function jsonConnect(command, json) {
|
|
44684
|
+
return command?.parent === program2 && command.name() === "connect" && jsonOutput(json);
|
|
44685
|
+
}
|
|
44686
|
+
function jsonConnectArgv() {
|
|
44687
|
+
const run = argvRun();
|
|
44688
|
+
return jsonConnect(run.command, run.json);
|
|
44689
|
+
}
|
|
44434
44690
|
program2.addHelpText("after", () => `
|
|
44435
44691
|
\uD83D\uDCA1 Ideas / feedback: ${FEEDBACK_URL}
|
|
44436
44692
|
`);
|
|
@@ -44452,9 +44708,15 @@ function withOrgOptions(command) {
|
|
|
44452
44708
|
ORG_SCOPED_COMMANDS.add(command);
|
|
44453
44709
|
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`);
|
|
44454
44710
|
}
|
|
44455
|
-
|
|
44456
|
-
|
|
44457
|
-
|
|
44711
|
+
var restoreConsole;
|
|
44712
|
+
program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
44713
|
+
if (jsonConnect(actionCommand, actionCommand.opts().json))
|
|
44714
|
+
restoreConsole = jsonConsole();
|
|
44715
|
+
});
|
|
44716
|
+
program2.hook("postAction", () => {
|
|
44717
|
+
restoreConsole?.();
|
|
44718
|
+
restoreConsole = undefined;
|
|
44719
|
+
});
|
|
44458
44720
|
program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
44459
44721
|
const initPointsToConnect = actionCommand.parent === program2 && actionCommand.name() === "init" && !interactive(actionCommand.opts().json);
|
|
44460
44722
|
if (!ORG_SCOPED_COMMANDS.has(actionCommand) || initPointsToConnect) {
|
|
@@ -44467,10 +44729,7 @@ program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
44467
44729
|
setActiveOrgScope(requireOrgScope(opts, apiKey));
|
|
44468
44730
|
} catch (err) {
|
|
44469
44731
|
if (err instanceof OrgScopeError2) {
|
|
44470
|
-
|
|
44471
|
-
writeEvent({ event: "log", level: "error", message: err.message });
|
|
44472
|
-
else
|
|
44473
|
-
console.error(err.message);
|
|
44732
|
+
console.error(err.message);
|
|
44474
44733
|
process.exit(1);
|
|
44475
44734
|
}
|
|
44476
44735
|
throw err;
|
|
@@ -44485,7 +44744,7 @@ program2.command("feedback").description("share ideas or feedback about the CLI"
|
|
|
44485
44744
|
if (opts.open) {
|
|
44486
44745
|
const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
44487
44746
|
try {
|
|
44488
|
-
|
|
44747
|
+
spawn3(openCommand, [FEEDBACK_URL], { detached: true, stdio: "ignore" }).unref();
|
|
44489
44748
|
} catch {}
|
|
44490
44749
|
}
|
|
44491
44750
|
});
|
|
@@ -44692,7 +44951,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44692
44951
|
monitoringUser: opts.monitoringUser,
|
|
44693
44952
|
includeOptionalPermissions
|
|
44694
44953
|
});
|
|
44695
|
-
v.missingOptional.push(...await legacyHelperWarnings(supabaseClient));
|
|
44696
44954
|
if (v.ok) {
|
|
44697
44955
|
if (jsonOutput) {
|
|
44698
44956
|
const result = {
|
|
@@ -44815,7 +45073,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44815
45073
|
client: supabaseClient,
|
|
44816
45074
|
plan: effectivePlan
|
|
44817
45075
|
});
|
|
44818
|
-
const legacyWarnings = await legacyHelperWarnings(supabaseClient);
|
|
44819
45076
|
if (jsonOutput) {
|
|
44820
45077
|
const result = {
|
|
44821
45078
|
success: true,
|
|
@@ -44825,10 +45082,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44825
45082
|
monitoringUser: opts.monitoringUser,
|
|
44826
45083
|
applied,
|
|
44827
45084
|
skippedOptional,
|
|
44828
|
-
warnings: [
|
|
44829
|
-
...skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : [],
|
|
44830
|
-
...legacyWarnings
|
|
44831
|
-
]
|
|
45085
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
44832
45086
|
};
|
|
44833
45087
|
if (passwordGenerated) {
|
|
44834
45088
|
result.generatedPassword = monPassword;
|
|
@@ -44844,8 +45098,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44844
45098
|
for (const s of skippedOptional)
|
|
44845
45099
|
console.error(`- ${s}`);
|
|
44846
45100
|
}
|
|
44847
|
-
for (const w of legacyWarnings)
|
|
44848
|
-
console.error(`\u26A0 ${w}`);
|
|
44849
45101
|
if (process.stdout.isTTY) {
|
|
44850
45102
|
console.log(`Applied ${applied.length} steps`);
|
|
44851
45103
|
}
|
|
@@ -44987,7 +45239,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44987
45239
|
includeOptionalPermissions,
|
|
44988
45240
|
provider
|
|
44989
45241
|
});
|
|
44990
|
-
v.missingOptional.push(...await legacyHelperWarnings(client));
|
|
44991
45242
|
if (v.ok) {
|
|
44992
45243
|
if (jsonOutput) {
|
|
44993
45244
|
outputJson({
|
|
@@ -45106,7 +45357,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
45106
45357
|
return;
|
|
45107
45358
|
}
|
|
45108
45359
|
const { applied, skippedOptional } = await applyInitPlan2({ client, plan: effectivePlan });
|
|
45109
|
-
const legacyWarnings = await legacyHelperWarnings(client);
|
|
45110
45360
|
if (jsonOutput) {
|
|
45111
45361
|
const result = {
|
|
45112
45362
|
success: true,
|
|
@@ -45116,10 +45366,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
45116
45366
|
monitoringUser: opts.monitoringUser,
|
|
45117
45367
|
applied,
|
|
45118
45368
|
skippedOptional,
|
|
45119
|
-
warnings: [
|
|
45120
|
-
...skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : [],
|
|
45121
|
-
...legacyWarnings
|
|
45122
|
-
]
|
|
45369
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
45123
45370
|
};
|
|
45124
45371
|
if (passwordGenerated) {
|
|
45125
45372
|
result.generatedPassword = monPassword;
|
|
@@ -45132,8 +45379,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
45132
45379
|
for (const s of skippedOptional)
|
|
45133
45380
|
console.error(`- ${s}`);
|
|
45134
45381
|
}
|
|
45135
|
-
for (const w of legacyWarnings)
|
|
45136
|
-
console.error(`\u26A0 ${w}`);
|
|
45137
45382
|
if (process.stdout.isTTY) {
|
|
45138
45383
|
console.log(`Applied ${applied.length} steps`);
|
|
45139
45384
|
}
|
|
@@ -45784,7 +46029,7 @@ function ensureHostMetricsDir(projectDir) {
|
|
|
45784
46029
|
}
|
|
45785
46030
|
function isDockerRunning() {
|
|
45786
46031
|
try {
|
|
45787
|
-
const result =
|
|
46032
|
+
const result = spawnSync3("docker", ["info"], { stdio: "pipe", timeout: 5000 });
|
|
45788
46033
|
return result.status === 0;
|
|
45789
46034
|
} catch {
|
|
45790
46035
|
return false;
|
|
@@ -45797,7 +46042,7 @@ function shouldScaleOutNodeExporter(args) {
|
|
|
45797
46042
|
return !args.slice(upIndex + 1).some((a) => !a.startsWith("-"));
|
|
45798
46043
|
}
|
|
45799
46044
|
function getComposeCmd() {
|
|
45800
|
-
const tryCmd = (cmd, args) =>
|
|
46045
|
+
const tryCmd = (cmd, args) => spawnSync3(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
|
|
45801
46046
|
if (tryCmd("docker", ["compose", "version"]))
|
|
45802
46047
|
return ["docker", "compose"];
|
|
45803
46048
|
if (tryCmd("docker-compose", ["version"]))
|
|
@@ -45806,7 +46051,7 @@ function getComposeCmd() {
|
|
|
45806
46051
|
}
|
|
45807
46052
|
function checkRunningContainers() {
|
|
45808
46053
|
try {
|
|
45809
|
-
const result =
|
|
46054
|
+
const result = spawnSync3("docker", ["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf8", timeout: 5000 });
|
|
45810
46055
|
if (result.status === 0 && result.stdout) {
|
|
45811
46056
|
const containers = result.stdout.trim().split(`
|
|
45812
46057
|
`).filter(Boolean);
|
|
@@ -45949,10 +46194,10 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
45949
46194
|
console.error(`Warning: could not restrict permissions on ${configPath}: ${message}`);
|
|
45950
46195
|
}
|
|
45951
46196
|
}
|
|
45952
|
-
function applyApiKey(projectDir, apiKey) {
|
|
46197
|
+
function applyApiKey(projectDir, apiKey, opts) {
|
|
45953
46198
|
const configPath = path9.resolve(projectDir, ".pgwatch-config");
|
|
45954
46199
|
try {
|
|
45955
|
-
updatePgwatchConfig(configPath, { api_key: apiKey });
|
|
46200
|
+
updatePgwatchConfig(configPath, { api_key: apiKey, api_url: resolveBaseUrls2(opts).apiBaseUrl });
|
|
45956
46201
|
} catch (err) {
|
|
45957
46202
|
console.error(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
45958
46203
|
return false;
|
|
@@ -46151,7 +46396,7 @@ async function runCompose(args, grafanaPassword, extraEnv) {
|
|
|
46151
46396
|
finalArgs.push("--scale", "self-node-exporter=0");
|
|
46152
46397
|
}
|
|
46153
46398
|
return new Promise((resolve) => {
|
|
46154
|
-
const child =
|
|
46399
|
+
const child = spawn3(cmd[0], [...cmd.slice(1), "-f", composeFile, ...finalArgs], {
|
|
46155
46400
|
stdio: "inherit",
|
|
46156
46401
|
env,
|
|
46157
46402
|
cwd: projectDir
|
|
@@ -46341,7 +46586,7 @@ function emitConnect(result, json) {
|
|
|
46341
46586
|
process.exitCode = CONNECT_EXIT[result.status];
|
|
46342
46587
|
}
|
|
46343
46588
|
function failCloud(err, json) {
|
|
46344
|
-
printResult({ status: "failed", next:
|
|
46589
|
+
printResult({ status: "failed", next: errorText2(err) }, json);
|
|
46345
46590
|
process.exitCode = 1;
|
|
46346
46591
|
}
|
|
46347
46592
|
function signedIn(opts) {
|
|
@@ -46352,22 +46597,8 @@ function signedIn(opts) {
|
|
|
46352
46597
|
}
|
|
46353
46598
|
return !!cloudApi().apiKey;
|
|
46354
46599
|
}
|
|
46355
|
-
function runConnect(url, opts) {
|
|
46356
|
-
|
|
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) {
|
|
46600
|
+
async function runConnect(url, opts) {
|
|
46601
|
+
const json = jsonOutput(opts.json);
|
|
46371
46602
|
const name = (() => {
|
|
46372
46603
|
try {
|
|
46373
46604
|
return databaseName2(url);
|
|
@@ -46394,7 +46625,7 @@ async function connectCommand(url, opts, json) {
|
|
|
46394
46625
|
const scope = getActiveOrgScope2();
|
|
46395
46626
|
const org = scope?.alias ? ["--org", scope.alias] : scope?.id ? ["--org-id", String(scope.id)] : [];
|
|
46396
46627
|
const project = name.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
46397
|
-
const status = await
|
|
46628
|
+
const status = await runChild(process.execPath, [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, "mon local-install");
|
|
46398
46629
|
if (status !== 0)
|
|
46399
46630
|
throw new Error("mon local-install failed (see above)");
|
|
46400
46631
|
},
|
|
@@ -46412,31 +46643,32 @@ async function connectCommand(url, opts, json) {
|
|
|
46412
46643
|
});
|
|
46413
46644
|
emitConnect(result, opts.json);
|
|
46414
46645
|
} catch (err) {
|
|
46415
|
-
emitConnect({ status: "failed", provider, name, next:
|
|
46646
|
+
emitConnect({ status: "failed", provider, name, next: errorText2(err) }, opts.json);
|
|
46416
46647
|
}
|
|
46417
46648
|
}
|
|
46418
|
-
withOrgOptions(program2.command("connect <database-url>")).
|
|
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", [
|
|
46649
|
+
withOrgOptions(program2.command("connect <database-url>")).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", [
|
|
46427
46650
|
"",
|
|
46428
46651
|
"Steps (each skipped when already done; safe to re-run): sign in, prepare the database",
|
|
46429
46652
|
"(an admin URL creates the postgres_ai_mon role; otherwise the SQL is printed), provision",
|
|
46430
46653
|
"the monitoring box, run the express checkup while it starts, wait, print the dashboard URL.",
|
|
46431
46654
|
"",
|
|
46432
|
-
|
|
46655
|
+
"Billing is per Postgres cluster observed: another database in a cluster the organization already",
|
|
46656
|
+
'monitors is included. Once the URL is checked, connect shows the price (or "free (N of M free slots)"). A billed',
|
|
46433
46657
|
"box is provisioned only when accepted: at the prompt, or with --yes. With no payment method",
|
|
46434
46658
|
"it stops (exit 3) and names the console page to add one. --coupon applies a promotion code.",
|
|
46435
46659
|
"",
|
|
46436
46660
|
'Exit codes: 0 connected or provisioning, 1 failed, 3 action required (see "next").',
|
|
46437
46661
|
"",
|
|
46662
|
+
"JSON output (--json, or stdout not a TTY): stdout is the result; stderr is one JSON event a",
|
|
46663
|
+
'line: the steps, and {"event":"log","level":"error"|"warn"|"info"|"debug","message":...}',
|
|
46664
|
+
'for any other text (an error, a warning, --debug); "source":"mon local-install" for that',
|
|
46665
|
+
"child's lines (the logins it prints are masked: pgai mon show-grafana-credentials shows them).",
|
|
46666
|
+
"",
|
|
46438
46667
|
"Environment: PGAI_API_KEY (instead of signing in), PGAI_MON_PASSWORD (the password of",
|
|
46439
|
-
"postgres_ai_mon when the role already exists; it is checked, never changed
|
|
46668
|
+
"postgres_ai_mon when the role already exists; it is checked, never changed; not needed",
|
|
46669
|
+
"for another database on a server PostgresAI already monitors for the org, with",
|
|
46670
|
+
"sslmode=require or verify-* in the URL: PostgresAI fills in the password it keeps (over TLS only), and",
|
|
46671
|
+
"the express checkup is skipped, as it runs only as postgres_ai_mon),",
|
|
46440
46672
|
"PGPASSWORD (the password for a URL without one),",
|
|
46441
46673
|
"CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET (instead of --clickhouse-key).",
|
|
46442
46674
|
"",
|
|
@@ -46447,6 +46679,9 @@ withOrgOptions(program2.command("connect <database-url>")).configureOutput({
|
|
|
46447
46679
|
].join(`
|
|
46448
46680
|
`)).action(runConnect);
|
|
46449
46681
|
var interactive = (json) => !!process.stdin.isTTY && !!process.stdout.isTTY && !json;
|
|
46682
|
+
function jsonOutput(json) {
|
|
46683
|
+
return !!json || !process.stdout.isTTY;
|
|
46684
|
+
}
|
|
46450
46685
|
withOrgOptions(program2.command("init")).description("first run for a person at a terminal: sign in, ask for the database URL, then pgai connect").option("--json", "JSON output (init is interactive: prints the pgai connect command to use instead)").action(async (opts) => {
|
|
46451
46686
|
if (!interactive(opts.json)) {
|
|
46452
46687
|
return emitConnect({ status: "action_required", provider: "self-managed", name: "", next: "pgai init is for a person at a terminal; agents and scripts: pgai connect <database-url>" }, opts.json);
|
|
@@ -46489,8 +46724,8 @@ withOrgOptions(program2.command("disconnect <name>")).description("stop monitori
|
|
|
46489
46724
|
if (!opts.yes && !(process.stdin.isTTY && /^y/i.test(await question(`Disconnect ${row.name} and delete its monitoring box? (y/N): `)))) {
|
|
46490
46725
|
return emitConnect({ status: "action_required", provider: row.provider, name: row.name, id: row.id, next: `pgai disconnect ${row.name} --yes` }, opts.json);
|
|
46491
46726
|
}
|
|
46492
|
-
await cloudApi(opts.debug).disconnect(row.id);
|
|
46493
|
-
printResult({ status: "disconnected", name: row.name, id: row.id, next: row.host_metrics ? "Delete the ClickHouse Cloud API key you gave us" : "none" }, opts.json);
|
|
46727
|
+
const billing = disconnectBilling(await cloudApi(opts.debug).disconnect(row.id));
|
|
46728
|
+
printResult({ status: "disconnected", name: row.name, id: row.id, ...billing ? { billing } : {}, next: row.host_metrics ? "Delete the ClickHouse Cloud API key you gave us" : "none" }, opts.json);
|
|
46494
46729
|
} catch (err) {
|
|
46495
46730
|
failCloud(err, opts.json);
|
|
46496
46731
|
}
|
|
@@ -46580,7 +46815,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
46580
46815
|
if (apiKey) {
|
|
46581
46816
|
console.log("Using API key provided via --api-key parameter");
|
|
46582
46817
|
writeConfig({ apiKey });
|
|
46583
|
-
if (!applyApiKey(projectDir, apiKey)) {
|
|
46818
|
+
if (!applyApiKey(projectDir, apiKey, globalOpts)) {
|
|
46584
46819
|
process.exitCode = 1;
|
|
46585
46820
|
return;
|
|
46586
46821
|
}
|
|
@@ -46598,7 +46833,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
46598
46833
|
const trimmedKey = inputApiKey.trim();
|
|
46599
46834
|
if (trimmedKey) {
|
|
46600
46835
|
writeConfig({ apiKey: trimmedKey });
|
|
46601
|
-
if (!applyApiKey(projectDir, trimmedKey)) {
|
|
46836
|
+
if (!applyApiKey(projectDir, trimmedKey, globalOpts)) {
|
|
46602
46837
|
process.exitCode = 1;
|
|
46603
46838
|
return;
|
|
46604
46839
|
}
|
|
@@ -47076,9 +47311,9 @@ mon.command("health").description("health check for monitoring services").option
|
|
|
47076
47311
|
allHealthy = true;
|
|
47077
47312
|
for (const service of services) {
|
|
47078
47313
|
try {
|
|
47079
|
-
const result =
|
|
47314
|
+
const result = spawnSync3("docker", ["inspect", "-f", "{{.State.Status}}", service.container], { stdio: "pipe" });
|
|
47080
47315
|
const present = result.status === 0;
|
|
47081
|
-
const health = present && service.readHealth ? (
|
|
47316
|
+
const health = present && service.readHealth ? (spawnSync3("docker", ["inspect", "-f", "{{if .State.Health}}{{.State.Health.Status}}{{end}}", service.container], { stdio: "pipe" }).stdout || "").trim() : "";
|
|
47082
47317
|
const verdict = judgeServiceHealth(service, present, (result.stdout || "").trim(), health);
|
|
47083
47318
|
console.log(`${verdict.mark} ${service.name}: ${verdict.label}`);
|
|
47084
47319
|
if (!verdict.ok)
|
|
@@ -47172,18 +47407,6 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
47172
47407
|
} else {
|
|
47173
47408
|
console.log("\u2713 .env is up to date");
|
|
47174
47409
|
}
|
|
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}`);
|
|
47187
47410
|
console.log();
|
|
47188
47411
|
const gitDir = path9.resolve(projectDir, ".git");
|
|
47189
47412
|
if (fs11.existsSync(gitDir)) {
|
|
@@ -47197,7 +47420,7 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
47197
47420
|
console.log(pullOut);
|
|
47198
47421
|
} else {
|
|
47199
47422
|
console.log("(not a git checkout \u2014 checking bundled docker-compose.yml)");
|
|
47200
|
-
await refreshBundledComposeIfStale(projectDir
|
|
47423
|
+
await refreshBundledComposeIfStale(projectDir);
|
|
47201
47424
|
}
|
|
47202
47425
|
console.log(`
|
|
47203
47426
|
Updating Docker images...`);
|
|
@@ -47217,7 +47440,7 @@ Updating Docker images...`);
|
|
|
47217
47440
|
\u2713 Update completed successfully`);
|
|
47218
47441
|
console.log(`
|
|
47219
47442
|
To apply updates, restart monitoring services:`);
|
|
47220
|
-
console.log("
|
|
47443
|
+
console.log(" postgres-ai mon restart");
|
|
47221
47444
|
} else {
|
|
47222
47445
|
console.error(`
|
|
47223
47446
|
\u2717 Docker image update failed`);
|
|
@@ -47358,7 +47581,7 @@ async function sinkPrometheusMaybeUp() {
|
|
|
47358
47581
|
} catch {
|
|
47359
47582
|
return true;
|
|
47360
47583
|
}
|
|
47361
|
-
const ps =
|
|
47584
|
+
const ps = spawnSync3(cmd[0], [...cmd.slice(1), "-f", composeFile, "ps", "-a", "-q", "--status", "running", "--status", "paused", "sink-prometheus"]);
|
|
47362
47585
|
return ps.status !== 0 || ps.stdout.trim() !== "";
|
|
47363
47586
|
}
|
|
47364
47587
|
async function reloadHostMetrics(projectDir, job, file) {
|
|
@@ -47393,14 +47616,36 @@ async function reloadHostMetrics(projectDir, job, file) {
|
|
|
47393
47616
|
}
|
|
47394
47617
|
return true;
|
|
47395
47618
|
}
|
|
47619
|
+
var CONN_STR_FORMAT = "Invalid connection string format: use postgresql://user:password@host[:port]/database";
|
|
47396
47620
|
function defaultTargetName(connStr) {
|
|
47621
|
+
if (!/^postgres(ql)?:\/\//.test(connStr))
|
|
47622
|
+
return { error: CONN_STR_FORMAT };
|
|
47623
|
+
if (/[\x00-\x1f\x7f]/.test(connStr))
|
|
47624
|
+
return { error: "Invalid connection string format: remove the control character (such as a CR from a CRLF file), or percent-encode it" };
|
|
47625
|
+
const at = connStr.lastIndexOf("@");
|
|
47626
|
+
if (at < 0)
|
|
47627
|
+
return { error: CONN_STR_FORMAT };
|
|
47628
|
+
if (!/^(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*$/.test(connStr.slice(connStr.indexOf("//") + 2, at))) {
|
|
47629
|
+
return { error: "Invalid connection string format: percent-encode the user name and the password (all but ASCII letters, digits, '-', '.', '_' and '~'), and an '@' in the database name or the query" };
|
|
47630
|
+
}
|
|
47631
|
+
let url;
|
|
47397
47632
|
try {
|
|
47398
|
-
new URL(connStr);
|
|
47633
|
+
url = new URL(connStr);
|
|
47399
47634
|
} catch {
|
|
47400
|
-
return
|
|
47401
|
-
}
|
|
47402
|
-
|
|
47403
|
-
|
|
47635
|
+
return { error: CONN_STR_FORMAT };
|
|
47636
|
+
}
|
|
47637
|
+
if (!url.username)
|
|
47638
|
+
return { error: CONN_STR_FORMAT };
|
|
47639
|
+
if (/%3a/i.test(url.username))
|
|
47640
|
+
return { error: "Invalid connection string format: percent-encode the user name and the password one at a time, with a raw ':' between them" };
|
|
47641
|
+
if (!url.password)
|
|
47642
|
+
return { error: "Invalid connection string format: put the password in the URL: postgresql://user:password@host[:port]/database" };
|
|
47643
|
+
if (url.hostname.includes(":"))
|
|
47644
|
+
return { error: "Invalid connection string format: an IPv6 address is not supported as the host; use a host name" };
|
|
47645
|
+
const db = url.pathname.slice(1);
|
|
47646
|
+
if (!db)
|
|
47647
|
+
return { error: CONN_STR_FORMAT };
|
|
47648
|
+
return { name: `${url.hostname}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-") };
|
|
47404
47649
|
}
|
|
47405
47650
|
function parseEnvValue(content, key) {
|
|
47406
47651
|
const last = [...content.matchAll(new RegExp(`^[ \\t]*(?:export[ \\t]+)?${key}=(.*)$`, "gm"))].at(-1);
|
|
@@ -47445,7 +47690,7 @@ async function setRdsHostStats(projectDir, values) {
|
|
|
47445
47690
|
if (!cmd)
|
|
47446
47691
|
return true;
|
|
47447
47692
|
const { composeFile } = await resolveOrInitPaths();
|
|
47448
|
-
const ps =
|
|
47693
|
+
const ps = spawnSync3(cmd[0], [...cmd.slice(1), "-f", composeFile, "--profile", "rds", "ps", "-q", "--status", "running", "rds-host-stats"]);
|
|
47449
47694
|
if (ps.status !== 0) {
|
|
47450
47695
|
console.error("If rds-host-stats is running, recreate it: docker compose --profile rds up -d rds-host-stats");
|
|
47451
47696
|
return true;
|
|
@@ -47530,6 +47775,7 @@ async function syncVmalert(projectDir, apply, addOnly = false) {
|
|
|
47530
47775
|
return true;
|
|
47531
47776
|
}
|
|
47532
47777
|
var looksLikeConnStr = (value) => /^postgres(ql)?:\/\//i.test(value);
|
|
47778
|
+
var isPlainTargetName = (value) => /^[A-Za-z0-9._=-]+$/.test(value) && !/(password|pwd)\s*=/i.test(value);
|
|
47533
47779
|
async function addTarget(file, projectDir, connStr, name, env, { apply = true } = {}) {
|
|
47534
47780
|
if (!connStr) {
|
|
47535
47781
|
console.error("Connection string required: postgresql://user:pass@host:port/db");
|
|
@@ -47539,15 +47785,15 @@ async function addTarget(file, projectDir, connStr, name, env, { apply = true }
|
|
|
47539
47785
|
const channelBinding = splitChannelBinding2(connStr);
|
|
47540
47786
|
connStr = channelBinding.uri;
|
|
47541
47787
|
const defaultName = defaultTargetName(connStr);
|
|
47542
|
-
if (
|
|
47543
|
-
console.error(
|
|
47788
|
+
if ("error" in defaultName) {
|
|
47789
|
+
console.error(defaultName.error);
|
|
47544
47790
|
process.exitCode = 1;
|
|
47545
47791
|
return false;
|
|
47546
47792
|
}
|
|
47547
47793
|
if (channelBinding.value !== null) {
|
|
47548
47794
|
console.error("Note: removed channel_binding from the connection string; the collector does not support it (TLS is kept)");
|
|
47549
47795
|
}
|
|
47550
|
-
const instanceName = name && name.trim() ? name.trim() : defaultName;
|
|
47796
|
+
const instanceName = name && name.trim() ? name.trim() : defaultName.name;
|
|
47551
47797
|
if (detectProvider(connStr) !== "clickhouse" && extractProjectRefFromUrl(connStr) && supabaseHostMetricsOn(projectDir, env) && !SUPABASE_TARGET_NAME_RE.test(instanceName)) {
|
|
47552
47798
|
console.error(SUPABASE_TARGET_NAME_ERROR);
|
|
47553
47799
|
process.exitCode = 1;
|
|
@@ -47630,14 +47876,40 @@ Supabase: with PGAI_SUPABASE_HOST_METRICS=true (environment or .env), writes hos
|
|
|
47630
47876
|
RDS instance endpoint: writes RDS_DB_INSTANCE_IDENTIFIER, AWS_REGION, PGAI_CLUSTER and PGAI_NODE_NAME to .env for rds-host-stats.
|
|
47631
47877
|
|
|
47632
47878
|
Environment:
|
|
47633
|
-
PGAI_DB_URL the connection string,
|
|
47634
|
-
the only argument is then the name
|
|
47879
|
+
PGAI_DB_URL the connection string, unless a postgres:// or postgresql:// URL is given
|
|
47880
|
+
(keeps the password out of argv). The only argument is then the name, of
|
|
47881
|
+
ASCII letters, digits, '.', '_', '=' and '-', with no password= or pwd=;
|
|
47882
|
+
anything else is refused. Set it for this command only:
|
|
47883
|
+
PGAI_DB_URL='postgresql://user:pass@host:5432/db' postgres-ai mon targets add my-db
|
|
47884
|
+
Under sudo, pass it on stdin: sudo logs a variable kept with --preserve-env.
|
|
47885
|
+
printf '%s\\n' "$URL" | sudo sh -c \\
|
|
47886
|
+
'IFS= read -r PGAI_DB_URL; export PGAI_DB_URL; exec postgres-ai mon targets add my-db'
|
|
47887
|
+
sudo I/O logging (log_input in sudoers) records stdin: then read it from a
|
|
47888
|
+
file of mode 0600 in the root shell.
|
|
47889
|
+
sudo sh -c \\
|
|
47890
|
+
'IFS= read -r PGAI_DB_URL < /path/to/db-url; export PGAI_DB_URL; exec postgres-ai mon targets add my-db'
|
|
47635
47891
|
`).action(async (connStr, name) => {
|
|
47636
47892
|
const { instancesFile: file, projectDir } = await resolveOrInitPaths();
|
|
47637
47893
|
const envUrl = process.env.PGAI_DB_URL || undefined;
|
|
47894
|
+
delete process.env.PGAI_DB_URL;
|
|
47638
47895
|
if (envUrl && name === undefined && (connStr === undefined || !looksLikeConnStr(connStr))) {
|
|
47639
|
-
|
|
47896
|
+
const arg = connStr?.trim();
|
|
47897
|
+
if (arg && !isPlainTargetName(arg)) {
|
|
47898
|
+
console.error("PGAI_DB_URL is set: pass only the target name (ASCII letters, digits, '.', '_', '=', '-'; no password= or pwd=)");
|
|
47899
|
+
process.exitCode = 1;
|
|
47900
|
+
return;
|
|
47901
|
+
}
|
|
47902
|
+
name = arg;
|
|
47640
47903
|
connStr = envUrl;
|
|
47904
|
+
let user = "";
|
|
47905
|
+
if (!("error" in defaultTargetName(splitChannelBinding2(envUrl).uri))) {
|
|
47906
|
+
try {
|
|
47907
|
+
user = decodeURIComponent(new URL(envUrl).username);
|
|
47908
|
+
} catch {
|
|
47909
|
+
user = new URL(envUrl).username;
|
|
47910
|
+
}
|
|
47911
|
+
}
|
|
47912
|
+
console.error(`Using PGAI_DB_URL${user ? ` (user ${user})` : ""}`);
|
|
47641
47913
|
}
|
|
47642
47914
|
await addTarget(file, projectDir, connStr, name, process.env);
|
|
47643
47915
|
});
|
|
@@ -47838,7 +48110,7 @@ Opening browser for authentication...`);
|
|
|
47838
48110
|
${authUrl}
|
|
47839
48111
|
`);
|
|
47840
48112
|
const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
47841
|
-
const browserProcess =
|
|
48113
|
+
const browserProcess = spawn3(openCommand, [authUrl], { detached: true, stdio: "ignore" });
|
|
47842
48114
|
browserProcess.on("error", () => {});
|
|
47843
48115
|
browserProcess.unref();
|
|
47844
48116
|
console.log("Waiting for authorization...");
|
|
@@ -49547,10 +49819,9 @@ export {
|
|
|
49547
49819
|
monitoringHealthServices,
|
|
49548
49820
|
persistInstanceId,
|
|
49549
49821
|
planMonitoringRegistration,
|
|
49550
|
-
planUpdateTag,
|
|
49551
49822
|
program2 as program,
|
|
49552
49823
|
readComposeProfiles,
|
|
49553
|
-
|
|
49824
|
+
readDeployedTag,
|
|
49554
49825
|
refreshBundledComposeIfStale,
|
|
49555
49826
|
registerMonitoringInstance,
|
|
49556
49827
|
resolveAdoptedProject,
|
|
@@ -49558,6 +49829,5 @@ export {
|
|
|
49558
49829
|
selfRegisterAndPersist,
|
|
49559
49830
|
shouldScaleOutNodeExporter,
|
|
49560
49831
|
statOwner,
|
|
49561
|
-
updatePgwatchConfig
|
|
49562
|
-
writeEnvTag
|
|
49832
|
+
updatePgwatchConfig
|
|
49563
49833
|
};
|