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.
@@ -13533,7 +13533,7 @@ var {
13533
13533
  // package.json
13534
13534
  var package_default = {
13535
13535
  name: "postgresai",
13536
- version: "0.17.0-dev.5",
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.5";
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.1.0";
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 box (${q.plan} plan)${q.subscription ? `, box ${q.quantity + 1} on the subscription` : ""}`;
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
- var errorText = (err) => err instanceof Error ? err.message : String(err);
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 billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${deps.billingUrl(q.org_alias)}, then re-run` };
28091
- const stop = (next) => ({ billing, billingPage, accepted: false, stop: { ...billing, next } });
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
- if (q.billed && !opts.yes && !await deps.confirm(`${name} is billed: ${billing.price}. Provision it? (y/N): `)) {
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 '<postgres_ai_mon URL>'" };
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 othersOnServer = (list) => list.filter((d) => d.name !== name && !disconnecting(d.status) && serverOf(d.name) === serverOf(name));
28164
- const cutOff = (others) => ({ status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} would cut off the monitoring of ${others.map((d) => d.name).join(", ")} on this server: set PGAI_MON_PASSWORD to its password instead` });
28165
- if (opts.resetPassword) {
28166
- if (othersOnServer(rows).length)
28167
- return cutOff(othersOnServer(rows));
28168
- }
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 b = await billingFor(await deps.quote(opts.coupon), name, opts, deps);
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
- lockId = (await deps.resetLock(serverOf(name))).lock_id;
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, { resetPassword: opts.resetPassword });
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
- checkup = await deps.checkup(withLocalTls(prepared.monitoringUrl, url), created.name).catch((err) => ({ error: errorText(err) }));
28247
- progress("checkup", checkupLines(checkup).join(`
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
- row = (await deps.list().catch(() => [])).find((d) => d.id === row.id) ?? row;
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
- return connectWithSslFallback(opts.Client ?? Client2, opts.agent ? { ...conn, sslFallbackEnabled: false } : conn);
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 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)`;
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
- 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` };
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. ${setPassword}` };
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 idOf = (fn, answer, ...keys) => {
28408
- const a = answer ?? {};
28409
- const id = Number(keys.map((k) => a[k]).find((v) => v !== undefined));
28410
- if (!(id > 0))
28411
- throw new Error(typeof a.message === "string" ? a.message : `Unexpected ${fn} answer: ${JSON.stringify(answer)}`);
28412
- return id;
28413
- };
28414
- const reportId = idOf("checkup_report_create", await rpc("checkup_report_create", { access_token: accessToken, project }), "report_id");
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
- idOf("checkup_report_file_post", await rpc("checkup_report_file_post", {
28419
- access_token: accessToken,
28420
- checkup_report_id: reportId,
28552
+ await uploadCheckupReportJson(rpc, {
28553
+ apiKey: accessToken,
28554
+ reportId,
28421
28555
  filename: `${checkId}.json`,
28422
- check_id: checkId,
28423
- data: JSON.stringify(report, null, 2),
28424
- type: "json",
28425
- generate_issue: true
28426
- }), "report_chunck_id", "report_chunk_id");
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 box (${q.plan} plan)${q.subscription ? `, box ${q.quantity + 1} on the subscription` : ""}`;
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
- var errorText2 = (err) => err instanceof Error ? err.message : String(err);
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 billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${deps.billingUrl(q.org_alias)}, then re-run` };
36822
- const stop = (next) => ({ billing, billingPage, accepted: false, stop: { ...billing, next } });
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
- if (q.billed && !opts.yes && !await deps.confirm(`${name} is billed: ${billing.price}. Provision it? (y/N): `)) {
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 '<postgres_ai_mon URL>'" };
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 othersOnServer = (list) => list.filter((d) => d.name !== name && !disconnecting2(d.status) && serverOf2(d.name) === serverOf2(name));
36895
- const cutOff = (others) => ({ status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} would cut off the monitoring of ${others.map((d) => d.name).join(", ")} on this server: set PGAI_MON_PASSWORD to its password instead` });
36896
- if (opts.resetPassword) {
36897
- if (othersOnServer(rows).length)
36898
- return cutOff(othersOnServer(rows));
36899
- }
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 b = await billingFor2(await deps.quote(opts.coupon), name, opts, deps);
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
- lockId = (await deps.resetLock(serverOf2(name))).lock_id;
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, { resetPassword: opts.resetPassword });
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
- checkup = await deps.checkup(withLocalTls2(prepared.monitoringUrl, url), created.name).catch((err) => ({ error: errorText2(err) }));
36978
- progress("checkup", checkupLines2(checkup).join(`
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
- row = (await deps.list().catch(() => [])).find((d) => d.id === row.id) ?? row;
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
- return connectWithSslFallback(opts.Client ?? Client2, opts.agent ? { ...conn, sslFallbackEnabled: false } : conn);
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 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)`;
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
- 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` };
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. ${setPassword}` };
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 idOf = (fn, answer, ...keys) => {
37139
- const a = answer ?? {};
37140
- const id = Number(keys.map((k) => a[k]).find((v) => v !== undefined));
37141
- if (!(id > 0))
37142
- throw new Error(typeof a.message === "string" ? a.message : `Unexpected ${fn} answer: ${JSON.stringify(answer)}`);
37143
- return id;
37144
- };
37145
- const reportId = idOf("checkup_report_create", await rpc("checkup_report_create", { access_token: accessToken, project }), "report_id");
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
- idOf("checkup_report_file_post", await rpc("checkup_report_file_post", {
37150
- access_token: accessToken,
37151
- checkup_report_id: reportId,
37383
+ await uploadCheckupReportJson(rpc, {
37384
+ apiKey: accessToken,
37385
+ reportId,
37152
37386
  filename: `${checkId}.json`,
37153
- check_id: checkId,
37154
- data: JSON.stringify(report, null, 2),
37155
- type: "json",
37156
- generate_issue: true
37157
- }), "report_chunck_id", "report_chunk_id");
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.1.0";
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 createCheckupReport(params) {
43094
- const { apiKey, apiBaseUrl, project, status } = params;
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 uploadCheckupReportJson(params) {
43114
- const { apiKey, apiBaseUrl, reportId, filename, checkId, jsonText } = params;
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
- async function withJsonStderr(fn) {
43742
+ function jsonConsole() {
43534
43743
  const { error, warn } = console;
43535
- console.error = (...args) => writeEvent({ event: "log", level: "error", message: format(...args) });
43536
- console.warn = (...args) => writeEvent({ event: "log", level: "warn", message: format(...args) });
43537
- try {
43538
- return await fn();
43539
- } finally {
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 || undefined,
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 = createInterface({ input: process.stdin, output: process.stdout });
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 spawnSync2(cmd, args, options) {
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 spawn2(cmd, args, options) {
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 = createInterface({ input: process.stdin, output: new Writable({ write: (_chunk, _encoding, done) => done() }), terminal: true });
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(() => createCheckupReport({
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(() => uploadCheckupReportJson({
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 planUpdateTag(current, cliVersion) {
44294
- const semver = (v) => {
44295
- const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(v);
44296
- return m ? { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] } : null;
44297
- };
44298
- const cli = semver(cliVersion);
44299
- if (!cli || cli.pre) {
44300
- return { tag: null, note: `PGAI_TAG stays ${current ?? "unset"}: this CLI (${cliVersion}) is not a release. To pick a stack version, set PGAI_TAG=<version> in .env and re-run 'postgresai mon update'` };
44301
- }
44302
- if (current === cliVersion)
44303
- return { tag: null, note: `PGAI_TAG is ${cliVersion}, matching this CLI` };
44304
- const cur = current ? semver(current) : null;
44305
- if (cur) {
44306
- const i = cur.core.findIndex((part, j) => part !== cli.core[j]);
44307
- if (i >= 0 && cur.core[i] > cli.core[i]) {
44308
- return { tag: null, note: `PGAI_TAG stays ${current}: it is newer than this CLI (${cliVersion}). Upgrade the CLI to move the stack: npm install -g postgresai@latest` };
44309
- }
44310
- }
44311
- return { tag: cliVersion, note: `PGAI_TAG: ${current ?? "unset"} -> ${cliVersion}` };
44312
- }
44313
- var ENV_TAG_LINE = /^([ \t]*(?:export[ \t]+)?PGAI_TAG[ \t]*=[ \t]*)(["']?)([A-Za-z0-9._-]*)\2((?:[ \t]+#[^\r\n]*)?[ \t]*\r?)$/gm;
44314
- var ENV_TAG_ANY = /^[ \t]*(?:export[ \t]+)?PGAI_TAG[ \t]*[=:]/gm;
44315
- function readEnvTag(projectDir) {
44534
+ function readDeployedTag(projectDir) {
44316
44535
  const envFile = path9.resolve(projectDir, ".env");
44317
44536
  if (!fs11.existsSync(envFile))
44318
- return { tag: null, plain: true };
44319
- const content = fs11.readFileSync(envFile, "utf8");
44320
- const plainLines = [...content.matchAll(ENV_TAG_LINE)];
44321
- if (plainLines.length !== [...content.matchAll(ENV_TAG_ANY)].length)
44322
- return { tag: null, plain: false };
44323
- const last = plainLines.at(-1);
44324
- return { tag: last && last[3] ? last[3] : null, plain: true };
44325
- }
44326
- function writeEnvTag(projectDir, tag) {
44327
- const envFile = path9.resolve(projectDir, ".env");
44328
- const existing = fs11.existsSync(envFile) ? fs11.readFileSync(envFile, "utf8") : "";
44329
- const content = new RegExp(ENV_TAG_LINE.source, "m").test(existing) ? existing.replace(ENV_TAG_LINE, (_match, prefix, quote, _value, rest) => `${prefix}${quote}${tag}${quote}${rest}`) : `${existing}${existing && !existing.endsWith(`
44330
- `) ? `
44331
- ` : ""}PGAI_TAG=${tag}
44332
- `;
44333
- writeEnvFile(envFile, content);
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 ?? readEnvTag(projectDir).tag);
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
- function jsonConnect(command) {
44456
- return command.parent === program2 && command.name() === "connect" && (!!command.opts().json || !process.stdout.isTTY);
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
- if (jsonConnect(actionCommand))
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
- spawn2(openCommand, [FEEDBACK_URL], { detached: true, stdio: "ignore" }).unref();
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 = spawnSync2("docker", ["info"], { stdio: "pipe", timeout: 5000 });
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) => spawnSync2(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
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 = spawnSync2("docker", ["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf8", timeout: 5000 });
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 = spawn2(cmd[0], [...cmd.slice(1), "-f", composeFile, ...finalArgs], {
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: err instanceof Error ? err.message : String(err) }, json);
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
- return opts.json || !process.stdout.isTTY ? withJsonStderr(() => connectCommand(url, opts, true)) : connectCommand(url, opts, false);
46357
- }
46358
- function runLocalInstall(args, env, json) {
46359
- if (!json)
46360
- return Promise.resolve(childProcess.spawnSync(process.execPath, args, { stdio: ["ignore", 2, 2], env }).status);
46361
- const child = childProcess.spawn(process.execPath, args, { stdio: ["ignore", "pipe", "pipe"], env });
46362
- for (const stream of [child.stdout, child.stderr]) {
46363
- createInterface({ input: stream }).on("line", (line) => writeEvent({ event: "log", level: "info", source: "mon local-install", message: line }));
46364
- }
46365
- return new Promise((resolve) => {
46366
- child.on("error", () => resolve(null));
46367
- child.on("close", (code) => resolve(code));
46368
- });
46369
- }
46370
- async function connectCommand(url, opts, json) {
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 runLocalInstall([process.argv[1], "mon", "local-install", "-y", ...org, "--project", project], { ...process.env, ...env, PGAI_DB_URL: monitoringUrl, PGAI_API_BASE_URL: api.apiBaseUrl, ...api.apiKey ? { PGAI_API_KEY: api.apiKey } : {} }, json);
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: err instanceof Error ? err.message : String(err) }, opts.json);
46646
+ emitConnect({ status: "failed", provider, name, next: errorText2(err) }, opts.json);
46416
46647
  }
46417
46648
  }
46418
- withOrgOptions(program2.command("connect <database-url>")).configureOutput({
46419
- writeErr: (text) => {
46420
- if (!(process.argv.includes("--json") || !process.stdout.isTTY))
46421
- return void process.stderr.write(text);
46422
- for (const line of text.split(`
46423
- `).filter((l) => l.trim() !== ""))
46424
- writeEvent({ event: "log", level: "error", message: line });
46425
- }
46426
- }).description("put a database under PostgresAI's care: prepare it, provision monitoring, print the dashboard").option("--provider <provider>", "clickhouse | rds | supabase | self-managed (default: detected from the host)").option("--clickhouse-key <id:secret>", "ClickHouse Cloud API key (Basic Service API Reader) for CPU, memory and disk; or CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET").option("--self-hosted", "run the monitoring stack on this machine (mon local-install) instead of PostgresAI Cloud").option("--reset-password", "postgres_ai_mon exists and its password is lost: set a new one (admin URL; refused while another database on the server is monitored)").option("--wait <minutes>", "how long to wait for the monitoring box (0 = do not wait)", "20").option("--coupon <code>", "promotion code for the organization's monitoring subscription (applies when it is first created)").option("-y, --yes", "never prompt: accept the price of a billed box").option("--json", "JSON output (default when stdout is not a TTY)").option("--debug", "print HTTP requests (secrets masked)").addHelpText("after", [
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
- 'Billing: before a new box, connect shows its price (or "free (N of M free slots)"). A billed',
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 = spawnSync2("docker", ["inspect", "-f", "{{.State.Status}}", service.container], { stdio: "pipe" });
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 ? (spawnSync2("docker", ["inspect", "-f", "{{if .State.Health}}{{.State.Health.Status}}{{end}}", service.container], { stdio: "pipe" }).stdout || "").trim() : "";
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, deployedTag);
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(" postgresai mon stop && postgresai mon start");
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 = spawnSync2(cmd[0], [...cmd.slice(1), "-f", composeFile, "ps", "-a", "-q", "--status", "running", "--status", "paused", "sink-prometheus"]);
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 null;
47401
- }
47402
- const m = connStr.match(/^postgres(?:ql)?:\/\/[^:]+:[^@]+@([^:\/]+)(?::\d+)?\/([^?]+)/);
47403
- return m ? `${m[1]}-${m[2]}`.replace(/[^a-zA-Z0-9-]/g, "-") : null;
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 = spawnSync2(cmd[0], [...cmd.slice(1), "-f", composeFile, "--profile", "rds", "ps", "-q", "--status", "running", "rds-host-stats"]);
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 (!defaultName) {
47543
- console.error("Invalid connection string format");
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, when none is given (keeps the password out of argv);
47634
- the only argument is then the name: PGAI_DB_URL=... postgres-ai mon targets add my-db
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
- name = connStr;
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 = spawn2(openCommand, [authUrl], { detached: true, stdio: "ignore" });
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
- readEnvTag,
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
  };