postgresai 0.17.0-rc.3 → 0.17.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13466,8 +13466,11 @@ function resolveBaseUrls2(opts, cfg, defaults = {}) {
13466
13466
  storageBaseUrl: normalizeBaseUrl2(storageCandidate)
13467
13467
  };
13468
13468
  }
13469
- var DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000;
13470
- var init_util = () => {};
13469
+ var DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000, SENSITIVE_KEY_SRC2 = "(?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)", SENSITIVE_PAIR_SEPARATOR_SRC2 = `["']?\\s*[:=]`, CREDENTIAL_CHAR_SRC2 = "[A-Za-z0-9_+/=~.-]", TEXT_SENSITIVE_PAIR2, CREDENTIAL_RUN_ONLY2;
13470
+ var init_util = __esm(() => {
13471
+ TEXT_SENSITIVE_PAIR2 = new RegExp(`(${SENSITIVE_KEY_SRC2}${SENSITIVE_PAIR_SEPARATOR_SRC2}\\s*)` + `(?:("[^"]*"|'[^']*')` + `|([^\\s,;&]+)` + `((?:\\s+(?![^\\s,;&]*${SENSITIVE_KEY_SRC2}${SENSITIVE_PAIR_SEPARATOR_SRC2})[^\\s,;&]+)*)` + `)`, "gi");
13472
+ CREDENTIAL_RUN_ONLY2 = new RegExp(`^${CREDENTIAL_CHAR_SRC2}+$`);
13473
+ });
13471
13474
 
13472
13475
  // node_modules/commander/index.js
13473
13476
  var exports_commander = {};
@@ -13530,7 +13533,7 @@ var {
13530
13533
  // package.json
13531
13534
  var package_default = {
13532
13535
  name: "postgresai",
13533
- version: "0.17.0-rc.3",
13536
+ version: "0.17.0-rc.5",
13534
13537
  description: "postgres_ai CLI",
13535
13538
  license: "Apache-2.0",
13536
13539
  private: false,
@@ -16890,7 +16893,7 @@ var Result = import_lib.default.Result;
16890
16893
  var TypeOverrides = import_lib.default.TypeOverrides;
16891
16894
  var defaults = import_lib.default.defaults;
16892
16895
  // package.json
16893
- var version = "0.17.0-rc.3";
16896
+ var version = "0.17.0-rc.5";
16894
16897
  var package_default2 = {
16895
16898
  name: "postgresai",
16896
16899
  version,
@@ -17166,6 +17169,22 @@ class HttpStatusError extends Error {
17166
17169
  function isRetryableHttpStatus(status) {
17167
17170
  return status >= 500 || status === 429;
17168
17171
  }
17172
+ function isUnknownRpcSignature(status, text, rpc) {
17173
+ if (status !== 404)
17174
+ return false;
17175
+ try {
17176
+ const parsed = JSON.parse(text);
17177
+ return typeof parsed.message === "string" && /Could not find the .* function/.test(parsed.message) && parsed.message.includes(`${rpc}(`);
17178
+ } catch {
17179
+ return false;
17180
+ }
17181
+ }
17182
+ function positiveSeconds(v) {
17183
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
17184
+ }
17185
+ function asyncPollDelayMs(attempt) {
17186
+ return Math.min(1000 * 2 ** attempt, 15000);
17187
+ }
17169
17188
  var DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 25000;
17170
17189
 
17171
17190
  class HttpRequestTimeoutError extends Error {
@@ -17219,9 +17238,120 @@ function redactSecretsForLog(text) {
17219
17238
  return JSON.stringify(redactSecrets(parsed));
17220
17239
  }
17221
17240
  var TEXT_URL_USERINFO = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+):[^\s/@]+@/gi;
17222
- var TEXT_SENSITIVE_PAIR = /((?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;&]+)/gi;
17241
+ var SENSITIVE_KEY_SRC = "(?:password|passwd|db[-_]?pass|connstr|secret|token|api[-_]?key|private[-_]?key|access[-_]?key|cred(?:ential)?s?|dsn)";
17242
+ var SENSITIVE_PAIR_SEPARATOR_SRC = `["']?\\s*[:=]`;
17243
+ var CREDENTIAL_CHAR_SRC = "[A-Za-z0-9_+/=~.-]";
17244
+ var MIN_CREDENTIAL_RUN = 12;
17245
+ var TEXT_SENSITIVE_PAIR = new RegExp(`(${SENSITIVE_KEY_SRC}${SENSITIVE_PAIR_SEPARATOR_SRC}\\s*)` + `(?:("[^"]*"|'[^']*')` + `|([^\\s,;&]+)` + `((?:\\s+(?![^\\s,;&]*${SENSITIVE_KEY_SRC}${SENSITIVE_PAIR_SEPARATOR_SRC})[^\\s,;&]+)*)` + `)`, "gi");
17246
+ var CREDENTIAL_RUN_ONLY = new RegExp(`^${CREDENTIAL_CHAR_SRC}+$`);
17247
+ var RUN_LEADING_PUNCTUATION = /^[("'\[{<]+/;
17248
+ var RUN_TRAILING_PUNCTUATION = /[)"'\]}>!?.,;:]+$/;
17249
+ var LONG_CONSONANT_RUN = /[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]{5,}/;
17250
+ var UUID_SHAPE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
17251
+ function isWordShaped(s) {
17252
+ if (!/^[A-Za-z]+$/.test(s))
17253
+ return false;
17254
+ if (s.length <= 2)
17255
+ return true;
17256
+ if (hasInternalCapital(s))
17257
+ return false;
17258
+ if (!/[aeiouyAEIOUY]/.test(s))
17259
+ return false;
17260
+ return !LONG_CONSONANT_RUN.test(s);
17261
+ }
17262
+ function hasInternalCapital(s) {
17263
+ return /[a-z]/.test(s) && /.[A-Z]/.test(s);
17264
+ }
17265
+ function hasHardStructure(core) {
17266
+ if (/=/.test(core.replace(/={1,2}$/, "")))
17267
+ return true;
17268
+ return UUID_SHAPE.test(core);
17269
+ }
17270
+ function isSeparatedName(core) {
17271
+ const segments = core.split(/[/_.-]/).filter((part) => part.length > 0);
17272
+ if (segments.length < 2)
17273
+ return false;
17274
+ if (!segments.some(isNameSegment))
17275
+ return false;
17276
+ return segments.every((part) => isNameSegment(part) || /^[0-9]{1,4}$/.test(part));
17277
+ }
17278
+ function isNameSegment(part) {
17279
+ return isWordShaped(part) || /^[A-Z]+$/.test(part) && part.length <= 6;
17280
+ }
17281
+ function hasTokenSignal(core) {
17282
+ if (/[0-9_+]/.test(core))
17283
+ return true;
17284
+ if (/=$/.test(core))
17285
+ return true;
17286
+ if (hasInternalCapital(core))
17287
+ return true;
17288
+ return LONG_CONSONANT_RUN.test(core);
17289
+ }
17290
+ function runCore(run) {
17291
+ return run.replace(RUN_LEADING_PUNCTUATION, "").replace(RUN_TRAILING_PUNCTUATION, "");
17292
+ }
17293
+ function looksLikeCredentialRun(core) {
17294
+ return core.length >= MIN_CREDENTIAL_RUN && !hasHardStructure(core) && hasTokenSignal(core);
17295
+ }
17296
+ function isChainableFragment(core) {
17297
+ return core.length > 0 && CREDENTIAL_RUN_ONLY.test(core) && !hasHardStructure(core) && !isWordShaped(core);
17298
+ }
17299
+ function replaceSensitivePair(_match, keyPrefix, quotedValue, _bareValue, tail) {
17300
+ if (quotedValue !== undefined)
17301
+ return `${keyPrefix}[REDACTED]`;
17302
+ const runs = [...(tail ?? "").matchAll(/(\s+)(\S+)/g)].map((m) => {
17303
+ const run = m[2] ?? "";
17304
+ return { gap: m[1] ?? "", run, core: runCore(run) };
17305
+ });
17306
+ const absorbed = runs.map(() => false);
17307
+ for (let i = 0;i < runs.length; ) {
17308
+ if (!isChainableFragment(runs[i].core)) {
17309
+ i++;
17310
+ continue;
17311
+ }
17312
+ let end = i;
17313
+ while (end < runs.length && isChainableFragment(runs[end].core))
17314
+ end++;
17315
+ const members = runs.slice(i, end);
17316
+ const chain = members.map((r) => r.core).join("");
17317
+ const allNames = members.every((r) => isSeparatedName(r.core));
17318
+ if (!allNames && looksLikeCredentialRun(chain))
17319
+ for (let k = i;k < end; k++)
17320
+ absorbed[k] = true;
17321
+ i = end;
17322
+ }
17323
+ let kept = "";
17324
+ for (let k = 0;k < runs.length; k++) {
17325
+ const r = runs[k];
17326
+ if (!absorbed[k]) {
17327
+ kept += r.gap + r.run;
17328
+ continue;
17329
+ }
17330
+ const lead = r.run.match(RUN_LEADING_PUNCTUATION)?.[0] ?? "";
17331
+ const trail = r.run.match(RUN_TRAILING_PUNCTUATION)?.[0] ?? "";
17332
+ if (lead || trail)
17333
+ kept += r.gap + lead + trail;
17334
+ }
17335
+ return `${keyPrefix}[REDACTED]${kept}`;
17336
+ }
17223
17337
  function redactTextSecrets(text) {
17224
- return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR, "$1[REDACTED]");
17338
+ return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR, replaceSensitivePair);
17339
+ }
17340
+ function isControlCodePoint(cp) {
17341
+ return cp < 32 || cp === 127 || cp >= 128 && cp <= 159 || cp === 8232 || cp === 8233;
17342
+ }
17343
+ function stripControlsToSpace(text) {
17344
+ let out = "";
17345
+ for (const ch of text) {
17346
+ const cp = ch.codePointAt(0);
17347
+ out += cp !== undefined && isControlCodePoint(cp) ? " " : ch;
17348
+ }
17349
+ return out;
17350
+ }
17351
+ function whitespaceTolerantSecretPattern(secret) {
17352
+ const chars = Array.from(secret.replace(/\s+/g, ""));
17353
+ const body = chars.map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s*");
17354
+ return new RegExp(body, "g");
17225
17355
  }
17226
17356
  function normalizeBaseUrl(value) {
17227
17357
  const trimmed = (value || "").replace(/\/$/, "");
@@ -26457,68 +26587,103 @@ function renderMarkdownForTerminal(md) {
26457
26587
  // lib/joe.ts
26458
26588
  var DEFAULT_BUDGET_MS = 25000;
26459
26589
  var DEFAULT_POLL_INTERVAL_MS = 800;
26590
+ var JOE_ASYNC_FALLBACK_WAIT_MS = 10 * 60 * 1000;
26591
+ var JOE_ASYNC_MAX_WAIT_MS = 60 * 60 * 1000;
26592
+ var JOE_ASYNC_MAX_POLL_FAILURES = 5;
26460
26593
  async function callRpc(params) {
26594
+ return (await callRpcRaw(params)).data;
26595
+ }
26596
+ async function callRpcRaw(params) {
26461
26597
  const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
26462
26598
  if (!apiKey) {
26463
26599
  throw new Error("API key is required");
26464
26600
  }
26465
26601
  const base = normalizeBaseUrl(apiBaseUrl);
26466
26602
  const url = new URL(`${base}/rpc/${fn}`);
26467
- const payload = JSON.stringify(body);
26468
26603
  const headers = buildAuthHeaders(apiKey);
26469
- if (debug) {
26470
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26471
- console.error(`Debug: POST URL: ${url.toString()}`);
26472
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26473
- console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
26474
- }
26475
- let response;
26476
- const requestTimeout = requestTimeoutSignal(params.timeoutMs);
26477
- try {
26478
- response = await fetch(url.toString(), {
26479
- method: "POST",
26480
- headers,
26481
- body: payload,
26482
- signal: requestTimeout.signal
26483
- });
26484
- } catch (err) {
26485
- if (isFetchTimeout(err)) {
26486
- throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
26604
+ const send = async (bodyObj) => {
26605
+ const payload = JSON.stringify(bodyObj);
26606
+ if (debug) {
26607
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26608
+ console.error(`Debug: POST URL: ${url.toString()}`);
26609
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26610
+ console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
26611
+ }
26612
+ let response;
26613
+ const requestTimeout = requestTimeoutSignal(params.timeoutMs);
26614
+ try {
26615
+ response = await fetch(url.toString(), {
26616
+ method: "POST",
26617
+ headers,
26618
+ body: payload,
26619
+ signal: requestTimeout.signal
26620
+ });
26621
+ } catch (err) {
26622
+ if (isFetchTimeout(err)) {
26623
+ throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
26624
+ }
26625
+ throw new Error(describeFetchError(operation, base, err));
26487
26626
  }
26488
- throw new Error(describeFetchError(operation, base, err));
26489
- }
26490
- const text = await response.text();
26491
- if (debug) {
26492
- console.error(`Debug: Response status: ${response.status}`);
26493
- console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
26627
+ const text = await response.text();
26628
+ if (debug) {
26629
+ console.error(`Debug: Response status: ${response.status}`);
26630
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
26631
+ }
26632
+ return { response, text };
26633
+ };
26634
+ let { response, text } = await send(body);
26635
+ if (params.fallbackBody && isUnknownRpcSignature(response.status, text, fn)) {
26636
+ ({ response, text } = await send(params.fallbackBody));
26494
26637
  }
26495
26638
  if (!response.ok) {
26496
26639
  throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
26497
26640
  }
26498
26641
  try {
26499
- return JSON.parse(text);
26642
+ return { data: JSON.parse(text), status: response.status };
26500
26643
  } catch {
26501
26644
  throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
26502
26645
  }
26503
26646
  }
26504
- async function startCommand(params) {
26647
+ function joeAsyncWaitMs(handle) {
26648
+ const expires = positiveSeconds(handle.expires_in_s);
26649
+ return Math.min(expires ? expires * 1000 : JOE_ASYNC_FALLBACK_WAIT_MS, JOE_ASYNC_MAX_WAIT_MS);
26650
+ }
26651
+ function asyncHandleCommandId(reply, operation) {
26652
+ const commandId = reply?.command_id;
26653
+ if (typeof commandId !== "string" || !/^[0-9]+$/.test(commandId)) {
26654
+ throw new Error(`${operation}: the platform accepted the command over the job channel but ` + `returned no command id: ${redactSecretsForLog(JSON.stringify(reply))}`);
26655
+ }
26656
+ return commandId;
26657
+ }
26658
+ async function startCommandDetailed(params) {
26505
26659
  const { apiKey, apiBaseUrl, instanceId, command, debug, timeoutMs } = params;
26506
26660
  if (!String(command ?? "").trim()) {
26507
26661
  throw new Error("command text is required");
26508
26662
  }
26509
- const commandId = await callRpc({
26663
+ const operation = "Failed to run Joe command";
26664
+ const body = { instance_id: instanceId, command };
26665
+ const { data, status } = await callRpcRaw({
26510
26666
  apiKey,
26511
26667
  apiBaseUrl,
26512
26668
  fn: "joe_command_run",
26513
- body: { instance_id: instanceId, command },
26514
- operation: "Failed to run Joe command",
26669
+ body: { ...body, accept_async: true },
26670
+ fallbackBody: body,
26671
+ operation,
26515
26672
  debug,
26516
26673
  timeoutMs
26517
26674
  });
26518
- if (typeof commandId !== "string" || !/^[0-9]+$/.test(commandId)) {
26519
- throw new Error(`Failed to run Joe command: expected a command id string, got: ${redactSecretsForLog(JSON.stringify(commandId))}`);
26675
+ if (status === 202) {
26676
+ const handle = data ?? {};
26677
+ return {
26678
+ commandId: asyncHandleCommandId(data, operation),
26679
+ asyncWaitMs: joeAsyncWaitMs(handle),
26680
+ firstAnswerEstimateS: positiveSeconds(handle.first_answer_estimate_s)
26681
+ };
26520
26682
  }
26521
- return commandId;
26683
+ if (typeof data !== "string" || !/^[0-9]+$/.test(data)) {
26684
+ throw new Error(`${operation}: expected a command id string, got: ${redactSecretsForLog(JSON.stringify(data))}`);
26685
+ }
26686
+ return { commandId: data, asyncWaitMs: null, firstAnswerEstimateS: null };
26522
26687
  }
26523
26688
  async function getCommandOutput(params) {
26524
26689
  const { apiKey, apiBaseUrl, commandId, debug, timeoutMs } = params;
@@ -26549,7 +26714,10 @@ function normalizeProjectRow(row) {
26549
26714
  joe_ready: Boolean(row.joe_ready ?? false),
26550
26715
  tunnel: Boolean(row.tunnel ?? false),
26551
26716
  instance_id: row.instance_id == null ? null : preserveIntegerId(row.instance_id),
26552
- dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId(row.dblab_instance_id)
26717
+ dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId(row.dblab_instance_id),
26718
+ ...row.monitoring_instance_ids !== undefined && {
26719
+ monitoring_instance_ids: row.monitoring_instance_ids ?? []
26720
+ }
26553
26721
  };
26554
26722
  }
26555
26723
  async function listProjects(params) {
@@ -26645,18 +26813,29 @@ function buildJoeCommandText(command, input = {}) {
26645
26813
  var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
26646
26814
  async function runCommand(params) {
26647
26815
  const { apiKey, apiBaseUrl, instanceId, command, debug } = params;
26648
- const budgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) && params.budgetMs >= 0 ? params.budgetMs : DEFAULT_BUDGET_MS;
26649
- const pollIntervalMs = params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
26816
+ const explicitBudgetMs = typeof params.budgetMs === "number" && Number.isFinite(params.budgetMs) && params.budgetMs >= 0 ? params.budgetMs : null;
26650
26817
  const now = params.now ?? Date.now;
26651
26818
  const sleep = params.sleep ?? defaultSleep;
26652
- const commandId = await startCommand({ apiKey, apiBaseUrl, instanceId, command, debug });
26819
+ const started = await startCommandDetailed({ apiKey, apiBaseUrl, instanceId, command, debug });
26820
+ const commandId = started.commandId;
26821
+ const budgetMs = explicitBudgetMs ?? started.asyncWaitMs ?? DEFAULT_BUDGET_MS;
26822
+ const pollDelayMs = (attempt) => params.pollIntervalMs ?? (started.asyncWaitMs === null ? DEFAULT_POLL_INTERVAL_MS : asyncPollDelayMs(attempt));
26653
26823
  const deadline = now() + budgetMs;
26654
26824
  let status = "pending";
26655
26825
  const remainingRequestMs = () => Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS, deadline - now()));
26656
26826
  if (now() >= deadline) {
26657
- return { commandId, status, output: null, budgetExpired: true };
26827
+ return { commandId, status, output: null, budgetExpired: true, budgetMs };
26658
26828
  }
26659
- for (;; ) {
26829
+ let announced = false;
26830
+ const announce = () => {
26831
+ if (announced || started.asyncWaitMs === null)
26832
+ return;
26833
+ announced = true;
26834
+ const estimate = started.firstAnswerEstimateS;
26835
+ console.error(`Queued on the Joe job channel (command ${commandId}` + (estimate ? `, reaches Joe in about ${estimate}s` : "") + `); waiting up to ${Math.round(budgetMs / 1000)}s...`);
26836
+ };
26837
+ let transientFailures = 0;
26838
+ for (let attempt = 0;; attempt++) {
26660
26839
  let output;
26661
26840
  try {
26662
26841
  output = await getCommandOutput({
@@ -26668,25 +26847,28 @@ async function runCommand(params) {
26668
26847
  });
26669
26848
  } catch (err) {
26670
26849
  if (err instanceof HttpRequestTimeoutError) {
26671
- return { commandId, status, output: null, budgetExpired: true };
26850
+ return { commandId, status, output: null, budgetExpired: true, budgetMs };
26672
26851
  }
26673
26852
  if (err instanceof HttpStatusError && isRetryableHttpStatus(err.status)) {
26674
- if (now() >= deadline) {
26675
- return { commandId, status, output: null, budgetExpired: true };
26853
+ transientFailures += 1;
26854
+ if (now() >= deadline || started.asyncWaitMs !== null && transientFailures >= JOE_ASYNC_MAX_POLL_FAILURES) {
26855
+ return { commandId, status, output: null, budgetExpired: true, budgetMs };
26676
26856
  }
26677
- await sleep(pollIntervalMs);
26857
+ await sleep(pollDelayMs(attempt));
26678
26858
  continue;
26679
26859
  }
26680
26860
  throw err;
26681
26861
  }
26862
+ transientFailures = 0;
26682
26863
  status = output.status;
26683
26864
  if (status === "ok" || status === "error") {
26684
- return { commandId, status, output, budgetExpired: false };
26865
+ return { commandId, status, output, budgetExpired: false, budgetMs };
26685
26866
  }
26686
26867
  if (now() >= deadline) {
26687
- return { commandId, status, output: null, budgetExpired: true };
26868
+ return { commandId, status, output: null, budgetExpired: true, budgetMs };
26688
26869
  }
26689
- await sleep(pollIntervalMs);
26870
+ announce();
26871
+ await sleep(pollDelayMs(attempt));
26690
26872
  }
26691
26873
  }
26692
26874
  async function executeJoeCommand(params) {
@@ -26797,46 +26979,58 @@ function formatProjectsTable(projects) {
26797
26979
  }
26798
26980
 
26799
26981
  // lib/joe.ts
26982
+ var JOE_ASYNC_FALLBACK_WAIT_MS2 = 10 * 60 * 1000;
26983
+ var JOE_ASYNC_MAX_WAIT_MS2 = 60 * 60 * 1000;
26800
26984
  async function callRpc2(params) {
26985
+ return (await callRpcRaw2(params)).data;
26986
+ }
26987
+ async function callRpcRaw2(params) {
26801
26988
  const { apiKey, apiBaseUrl, fn, body, operation, debug } = params;
26802
26989
  if (!apiKey) {
26803
26990
  throw new Error("API key is required");
26804
26991
  }
26805
26992
  const base = normalizeBaseUrl(apiBaseUrl);
26806
26993
  const url = new URL(`${base}/rpc/${fn}`);
26807
- const payload = JSON.stringify(body);
26808
26994
  const headers = buildAuthHeaders(apiKey);
26809
- if (debug) {
26810
- const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26811
- console.error(`Debug: POST URL: ${url.toString()}`);
26812
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
26813
- console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
26814
- }
26815
- let response;
26816
- const requestTimeout = requestTimeoutSignal(params.timeoutMs);
26817
- try {
26818
- response = await fetch(url.toString(), {
26819
- method: "POST",
26820
- headers,
26821
- body: payload,
26822
- signal: requestTimeout.signal
26823
- });
26824
- } catch (err) {
26825
- if (isFetchTimeout(err)) {
26826
- throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
26995
+ const send = async (bodyObj) => {
26996
+ const payload = JSON.stringify(bodyObj);
26997
+ if (debug) {
26998
+ const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
26999
+ console.error(`Debug: POST URL: ${url.toString()}`);
27000
+ console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
27001
+ console.error(`Debug: Request body: ${redactSecretsForLog(payload)}`);
27002
+ }
27003
+ let response;
27004
+ const requestTimeout = requestTimeoutSignal(params.timeoutMs);
27005
+ try {
27006
+ response = await fetch(url.toString(), {
27007
+ method: "POST",
27008
+ headers,
27009
+ body: payload,
27010
+ signal: requestTimeout.signal
27011
+ });
27012
+ } catch (err) {
27013
+ if (isFetchTimeout(err)) {
27014
+ throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
27015
+ }
27016
+ throw new Error(describeFetchError(operation, base, err));
26827
27017
  }
26828
- throw new Error(describeFetchError(operation, base, err));
26829
- }
26830
- const text = await response.text();
26831
- if (debug) {
26832
- console.error(`Debug: Response status: ${response.status}`);
26833
- console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
27018
+ const text = await response.text();
27019
+ if (debug) {
27020
+ console.error(`Debug: Response status: ${response.status}`);
27021
+ console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
27022
+ }
27023
+ return { response, text };
27024
+ };
27025
+ let { response, text } = await send(body);
27026
+ if (params.fallbackBody && isUnknownRpcSignature(response.status, text, fn)) {
27027
+ ({ response, text } = await send(params.fallbackBody));
26834
27028
  }
26835
27029
  if (!response.ok) {
26836
27030
  throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
26837
27031
  }
26838
27032
  try {
26839
- return JSON.parse(text);
27033
+ return { data: JSON.parse(text), status: response.status };
26840
27034
  } catch {
26841
27035
  throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
26842
27036
  }
@@ -26855,7 +27049,10 @@ function normalizeProjectRow2(row) {
26855
27049
  joe_ready: Boolean(row.joe_ready ?? false),
26856
27050
  tunnel: Boolean(row.tunnel ?? false),
26857
27051
  instance_id: row.instance_id == null ? null : preserveIntegerId2(row.instance_id),
26858
- dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId2(row.dblab_instance_id)
27052
+ dblab_instance_id: row.dblab_instance_id == null ? null : preserveIntegerId2(row.dblab_instance_id),
27053
+ ...row.monitoring_instance_ids !== undefined && {
27054
+ monitoring_instance_ids: row.monitoring_instance_ids ?? []
27055
+ }
26859
27056
  };
26860
27057
  }
26861
27058
  async function listProjects2(params) {
@@ -26914,24 +27111,10 @@ async function resolveDblabInstanceId(params) {
26914
27111
  }
26915
27112
  return String(match.dblab_instance_id);
26916
27113
  }
26917
- async function callDblabApi(params) {
26918
- const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
26919
- if (!apiKey) {
26920
- throw new Error("API key is required");
26921
- }
26922
- if (!instanceId) {
26923
- throw new Error("instanceId is required");
26924
- }
27114
+ async function postDblabRpc(rpc, bodyObj, params) {
27115
+ const { apiKey, apiBaseUrl, operation, debug } = params;
26925
27116
  const base = normalizeBaseUrl(apiBaseUrl);
26926
- const url = new URL(`${base}/rpc/dblab_api_call`);
26927
- const bodyObj = {
26928
- instance_id: instanceId,
26929
- action,
26930
- method
26931
- };
26932
- if (data !== undefined) {
26933
- bodyObj.data = data;
26934
- }
27117
+ const url = new URL(`${base}/rpc/${rpc}`);
26935
27118
  const body = JSON.stringify(bodyObj);
26936
27119
  const headers = buildAuthHeaders(apiKey);
26937
27120
  if (debug) {
@@ -26955,13 +27138,163 @@ async function callDblabApi(params) {
26955
27138
  }
26956
27139
  throw new Error(describeFetchError(operation, base, err));
26957
27140
  }
26958
- const text = await response.text();
27141
+ let text;
27142
+ try {
27143
+ text = await response.text();
27144
+ } catch (err) {
27145
+ if (isFetchTimeout(err)) {
27146
+ throw new HttpRequestTimeoutError(operation, requestTimeout.timeoutMs);
27147
+ }
27148
+ throw new Error(describeFetchError(operation, base, err));
27149
+ }
26959
27150
  if (debug) {
26960
27151
  console.error(`Debug: Response status: ${response.status}`);
26961
27152
  console.error(`Debug: Response body: ${redactSecretsForLog(text)}`);
26962
27153
  }
27154
+ return { response, text };
27155
+ }
27156
+ var DBLAB_ASYNC_FALLBACK_WAIT_MS = 15 * 60 * 1000;
27157
+ var DBLAB_ASYNC_MAX_WAIT_MS = 60 * 60 * 1000;
27158
+ var DBLAB_POLL_MAX_CONSECUTIVE_FAILURES = 5;
27159
+ var DBLAB_JOB_PENDING = new Set(["queued", "running"]);
27160
+ function dblabMayHaveLandedAdvice() {
27161
+ return "The platform may have accepted this call even though the reply was lost — " + "check the instance's current state before running this command again.";
27162
+ }
27163
+ function dblabNotResentAdvice(jobId) {
27164
+ return `The call (job ${jobId}) was NOT re-sent and may still complete on the instance — ` + "check its current state (e.g. `pgai dblab clone list`) before running this command again.";
27165
+ }
27166
+ function stripControlsKeepingLines(text) {
27167
+ return text.split(`
27168
+ `).map(stripControlsToSpace).join(`
27169
+ `);
27170
+ }
27171
+ function scrubBoxText(text) {
27172
+ return stripControlsToSpace(redactTextSecrets(redactSecretsForLog(text))).replace(/\s+/g, " ").trim();
27173
+ }
27174
+ async function pollDblabCallOnce(jobIdRaw, jobId, params) {
27175
+ const { operation } = params;
27176
+ const advice = dblabNotResentAdvice(jobId);
27177
+ let response;
27178
+ let text;
27179
+ try {
27180
+ ({ response, text } = await postDblabRpc("dblab_call_result", { p_job_id: jobIdRaw }, params));
27181
+ } catch (err) {
27182
+ return { kind: "transient", why: err instanceof Error ? err.message : String(err) };
27183
+ }
26963
27184
  if (!response.ok) {
26964
- throw new HttpStatusError(formatHttpError(operation, response.status, text, response.statusText), response.status);
27185
+ if (isRetryableHttpStatus(response.status)) {
27186
+ return { kind: "transient", why: `HTTP ${response.status}` };
27187
+ }
27188
+ throw new HttpStatusError(`${stripControlsKeepingLines(formatHttpError(operation, response.status, text, response.statusText))}
27189
+ ${advice}`, response.status);
27190
+ }
27191
+ let res;
27192
+ try {
27193
+ res = JSON.parse(text);
27194
+ } catch {
27195
+ throw new Error(`${operation}: failed to parse response: ${stripControlsKeepingLines(redactSecretsForLog(text))}. ${advice}`);
27196
+ }
27197
+ if (!res || typeof res.status !== "string") {
27198
+ throw new Error(`${operation}: dblab_call_result returned an unexpected reply: ${stripControlsKeepingLines(redactSecretsForLog(text))}. ${advice}`);
27199
+ }
27200
+ return { kind: "result", res };
27201
+ }
27202
+ async function awaitDblabCall(handle, params) {
27203
+ const { operation } = params;
27204
+ const jobIdRaw = typeof handle.job_id === "string" ? handle.job_id : "";
27205
+ const jobId = jobIdRaw ? scrubBoxText(jobIdRaw) : "";
27206
+ if (!jobId) {
27207
+ throw new Error(`${operation}: the platform accepted the call but returned no job_id. ` + dblabMayHaveLandedAdvice());
27208
+ }
27209
+ const now = params.now ?? (() => Date.now());
27210
+ const sleep = params.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
27211
+ const waitMs = Math.min((positiveSeconds(handle.expires_in_s) ?? 0) * 1000 || DBLAB_ASYNC_FALLBACK_WAIT_MS, DBLAB_ASYNC_MAX_WAIT_MS);
27212
+ const deadline = now() + waitMs;
27213
+ const estimate = positiveSeconds(handle.first_answer_estimate_s);
27214
+ let announced = false;
27215
+ let failures = 0;
27216
+ let lastStatus = "unknown";
27217
+ for (let attempt = 0;; attempt++) {
27218
+ const outcome = await pollDblabCallOnce(jobIdRaw, jobId, params);
27219
+ if (outcome.kind === "transient") {
27220
+ failures += 1;
27221
+ if (failures >= DBLAB_POLL_MAX_CONSECUTIVE_FAILURES) {
27222
+ throw new Error(`${operation}: lost contact with the platform while waiting (${failures} polls in a row failed, ` + `last: ${outcome.why}). ${dblabNotResentAdvice(jobId)}`);
27223
+ }
27224
+ } else {
27225
+ failures = 0;
27226
+ const res = outcome.res;
27227
+ lastStatus = scrubBoxText(res.status);
27228
+ if (res.status === "done" && res.outcome === "ok") {
27229
+ return res.result ?? null;
27230
+ }
27231
+ if (!DBLAB_JOB_PENDING.has(res.status)) {
27232
+ const why = typeof res.error === "string" && res.error ? res.error : res.status === "expired" ? "no agent claimed it before it expired" : `outcome ${String(res.outcome ?? "unknown")}`;
27233
+ const klass = typeof res.failure_class === "string" && res.failure_class ? ` [${scrubBoxText(res.failure_class)}]` : "";
27234
+ const advice = res.status === "expired" ? "" : ` ${dblabNotResentAdvice(jobId)}`;
27235
+ throw new Error(`${operation}: the DBLab instance did not complete the call (status ${scrubBoxText(res.status)}) — ` + `${scrubBoxText(why)}${klass}.${advice}`);
27236
+ }
27237
+ }
27238
+ if (!announced && outcome.kind === "result") {
27239
+ announced = true;
27240
+ console.error(`Waiting for the DBLab instance to pick up the call (job ${jobId}` + (estimate ? `, typically ~${estimate}s` : "") + ")...");
27241
+ }
27242
+ const delay = asyncPollDelayMs(attempt);
27243
+ if (now() + delay >= deadline) {
27244
+ throw new Error(`${operation}: timed out waiting for the DBLab instance (job ${jobId}, last status ${lastStatus}). ` + dblabNotResentAdvice(jobId));
27245
+ }
27246
+ await sleep(delay);
27247
+ }
27248
+ }
27249
+ async function enqueueDblabCall(bodyObj, params, isWrite) {
27250
+ try {
27251
+ return await postDblabRpc("dblab_api_call", bodyObj, params);
27252
+ } catch (err) {
27253
+ if (!isWrite)
27254
+ throw err;
27255
+ const message = err instanceof Error ? err.message : String(err);
27256
+ throw new Error(`${message}
27257
+ ${dblabMayHaveLandedAdvice()}`, { cause: err });
27258
+ }
27259
+ }
27260
+ async function callDblabApi(params) {
27261
+ const { apiKey, apiBaseUrl, instanceId, action, method, data, operation, debug } = params;
27262
+ if (!apiKey) {
27263
+ throw new Error("API key is required");
27264
+ }
27265
+ if (!instanceId) {
27266
+ throw new Error("instanceId is required");
27267
+ }
27268
+ const bodyObj = {
27269
+ instance_id: instanceId,
27270
+ action,
27271
+ method
27272
+ };
27273
+ if (data !== undefined) {
27274
+ bodyObj.data = data;
27275
+ }
27276
+ const rpcParams = { apiKey, apiBaseUrl, operation, debug };
27277
+ const isWrite = method.toLowerCase() !== "get";
27278
+ let { response, text } = await enqueueDblabCall({ ...bodyObj, accept_async: true }, rpcParams, isWrite);
27279
+ if (isUnknownRpcSignature(response.status, text, "dblab_api_call")) {
27280
+ ({ response, text } = await enqueueDblabCall(bodyObj, rpcParams, isWrite));
27281
+ }
27282
+ if (response.status === 202) {
27283
+ let handle;
27284
+ try {
27285
+ handle = JSON.parse(text);
27286
+ } catch {
27287
+ throw new Error(`${operation}: failed to parse response: ${stripControlsKeepingLines(redactSecretsForLog(text))}. ` + dblabMayHaveLandedAdvice());
27288
+ }
27289
+ if (!handle || typeof handle !== "object") {
27290
+ throw new Error(`${operation}: the platform accepted the call but returned no handle. ` + dblabMayHaveLandedAdvice());
27291
+ }
27292
+ return await awaitDblabCall(handle, rpcParams);
27293
+ }
27294
+ if (!response.ok) {
27295
+ const lost = isWrite && response.status >= 500;
27296
+ throw new HttpStatusError(stripControlsKeepingLines(formatHttpError(operation, response.status, text, response.statusText)) + (lost ? `
27297
+ ${dblabMayHaveLandedAdvice()}` : ""), response.status);
26965
27298
  }
26966
27299
  if (text.trim() === "") {
26967
27300
  return null;
@@ -26969,7 +27302,7 @@ async function callDblabApi(params) {
26969
27302
  try {
26970
27303
  return JSON.parse(text);
26971
27304
  } catch {
26972
- throw new Error(`${operation}: failed to parse response: ${redactSecretsForLog(text)}`);
27305
+ throw new Error(`${operation}: failed to parse response: ${stripControlsKeepingLines(redactSecretsForLog(text))}`);
26973
27306
  }
26974
27307
  }
26975
27308
  async function createClone(params) {
@@ -27206,6 +27539,37 @@ async function callRpc3(name, apiKey, body, opts) {
27206
27539
  throw new Error(`${name} returned a body that is not JSON`);
27207
27540
  }
27208
27541
  }
27542
+ async function resolveMonitoringInstanceId(params) {
27543
+ const ref = String(params.project ?? "").trim();
27544
+ if (!ref) {
27545
+ throw new Error("project is required (--project <id|alias>)");
27546
+ }
27547
+ const projects = await listProjects2({
27548
+ apiKey: params.apiKey,
27549
+ apiBaseUrl: params.apiBaseUrl,
27550
+ orgId: params.orgId
27551
+ });
27552
+ const needle = ref.toLowerCase();
27553
+ const matches = isNumericProjectRef2(ref) ? projects.filter((p) => String(p.project_id) === BigInt(ref).toString()) : projects.filter((p) => p.alias !== null && p.alias.toLowerCase() === needle || p.name !== null && p.name.toLowerCase() === needle);
27554
+ if (matches.length === 0) {
27555
+ throw new Error(`Project not found for id/alias/name '${ref}'. Run 'pgai projects' to see available projects.`);
27556
+ }
27557
+ if (matches.length > 1) {
27558
+ throw new Error(`'${ref}' matches ${matches.length} projects (ids ${matches.map((p) => p.project_id).join(", ")}). ` + "Pass --project <id> instead.");
27559
+ }
27560
+ const match = matches[0];
27561
+ const ids = match.monitoring_instance_ids;
27562
+ if (ids === undefined) {
27563
+ throw new Error("This platform does not report monitoring instances per project yet. Pass --instance <uuid> instead.");
27564
+ }
27565
+ if (ids.length === 0) {
27566
+ throw new Error(`Project '${ref}' has no active monitoring instance.`);
27567
+ }
27568
+ if (ids.length > 1) {
27569
+ throw new Error(`Project '${ref}' has ${ids.length} active monitoring instances: ${ids.join(", ")}. Pass --instance <uuid>.`);
27570
+ }
27571
+ return ids[0];
27572
+ }
27209
27573
  async function enqueueQuery(apiKey, args, opts) {
27210
27574
  const body = {
27211
27575
  p_instance_id: args.instanceId,
@@ -27224,9 +27588,9 @@ async function enqueueQuery(apiKey, args, opts) {
27224
27588
  if (!jobId) {
27225
27589
  throw new Error("instance_query_enqueue returned no job_id");
27226
27590
  }
27227
- return { jobId, firstAnswerEstimateS: positiveSeconds(reply?.first_answer_estimate_s) };
27591
+ return { jobId, firstAnswerEstimateS: positiveSeconds2(reply?.first_answer_estimate_s) };
27228
27592
  }
27229
- function positiveSeconds(v) {
27593
+ function positiveSeconds2(v) {
27230
27594
  return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
27231
27595
  }
27232
27596
  async function readQueryResult(apiKey, jobId, opts) {
@@ -30566,6 +30930,38 @@ async function resolveDatasourceId(adminPassword, debug = false) {
30566
30930
  return null;
30567
30931
  }
30568
30932
  }
30933
+ function aasSuccessMessage(r) {
30934
+ const armed = r.armedFor === "pull" ? " (armed for pull: the platform will query this instance's Grafana)" : r.armedFor === "jobs" ? " (armed for the job channel: this instance answers its own queries)" : "";
30935
+ const base = `AAS auto-collection registered${armed}`;
30936
+ if (r.vcpus === 0) {
30937
+ return `${base} — collection stays OFF until a source-DB vCPU count is known (the platform stamps it at provision time; --vcpus <n> sets it for a manual install)`;
30938
+ }
30939
+ if (r.urlPending) {
30940
+ return `${base} — collection starts once this deploy reports the Grafana url to the platform`;
30941
+ }
30942
+ return base;
30943
+ }
30944
+ var PLATFORM_ERROR_MAX = 300;
30945
+ function formatPlatformError(body, secrets = []) {
30946
+ if (!body || typeof body !== "object")
30947
+ return "";
30948
+ const b = body;
30949
+ const str = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : "";
30950
+ const text = str(b.details) || str(b.message);
30951
+ if (!text)
30952
+ return "";
30953
+ const code = str(b.code);
30954
+ let out = stripControlsToSpace(code ? `${code}: ${text}` : text).replace(/\s+/g, " ").trim();
30955
+ for (const secret of secrets) {
30956
+ const compact = secret.replace(/\s+/g, "");
30957
+ if (compact.length < 8)
30958
+ continue;
30959
+ out = out.replace(whitespaceTolerantSecretPattern(compact), "[redacted]");
30960
+ }
30961
+ out = redactTextSecrets(out);
30962
+ out = out.replace(/glsa_[A-Za-z0-9_-]+/g, "[redacted]");
30963
+ return out.length > PLATFORM_ERROR_MAX ? `${out.slice(0, PLATFORM_ERROR_MAX - 1).trimEnd()}…` : out;
30964
+ }
30569
30965
  async function registerAasCollection(apiKey, instanceId, opts) {
30570
30966
  const debug = !!opts.debug;
30571
30967
  try {
@@ -30615,11 +31011,19 @@ async function registerAasCollection(apiKey, instanceId, opts) {
30615
31011
  })
30616
31012
  });
30617
31013
  if (!res.ok) {
31014
+ const detail = formatPlatformError(await res.json().catch(() => null), [apiKey, saToken]);
31015
+ const suffix = detail ? ` — ${detail}` : "";
30618
31016
  if (debug)
30619
- console.error(`Debug: AAS register failed: HTTP ${res.status}`);
30620
- return { ok: false, reason: `platform returned HTTP ${res.status}` };
31017
+ console.error(`Debug: AAS register failed: HTTP ${res.status}${suffix}`);
31018
+ return { ok: false, reason: `platform returned HTTP ${res.status}${suffix}` };
30621
31019
  }
30622
- return { ok: true };
31020
+ const reply = await res.json().catch(() => null);
31021
+ return {
31022
+ ok: true,
31023
+ vcpus: typeof reply?.vcpus === "number" ? reply.vcpus : undefined,
31024
+ urlPending: typeof reply?.url_pending === "boolean" ? reply.url_pending : undefined,
31025
+ armedFor: typeof reply?.armed_for === "string" ? reply.armed_for : undefined
31026
+ };
30623
31027
  } catch (err) {
30624
31028
  if (debug)
30625
31029
  console.error(`Debug: AAS register error: ${err.message}`);
@@ -38950,8 +39354,41 @@ var LOCAL_INSTALL_MANAGED_ENV_KEYS = [
38950
39354
  "VM_FORCE_MERGE_AUTH_KEY",
38951
39355
  "VM_PPROF_AUTH_KEY",
38952
39356
  "PGAI_INSTANCE_ID",
38953
- "INSTANCE_JOBS_USER"
39357
+ "INSTANCE_JOBS_USER",
39358
+ "COMPOSE_PROFILES"
38954
39359
  ];
39360
+ var INSTANCE_JOBS_PROFILE = "instance-jobs";
39361
+ function composeProfilesValue(existing, want) {
39362
+ const profiles = [];
39363
+ for (const raw of (existing ?? "").split(",")) {
39364
+ const token = raw.trim();
39365
+ if (token && !profiles.includes(token))
39366
+ profiles.push(token);
39367
+ }
39368
+ const at = profiles.indexOf(INSTANCE_JOBS_PROFILE);
39369
+ if (want === true && at === -1)
39370
+ profiles.push(INSTANCE_JOBS_PROFILE);
39371
+ if (want === false && at !== -1)
39372
+ profiles.splice(at, 1);
39373
+ return profiles.length > 0 ? profiles.join(",") : null;
39374
+ }
39375
+ function instanceJobsProfileEnabled(composeProfiles) {
39376
+ return (composeProfiles ?? "").split(",").map((p) => p.trim()).includes(INSTANCE_JOBS_PROFILE);
39377
+ }
39378
+ function resolveInstanceJobsRequest(flag, envValue) {
39379
+ if (flag !== undefined)
39380
+ return { enabled: flag };
39381
+ const value = (envValue ?? "").trim().toLowerCase();
39382
+ if (value === "")
39383
+ return {};
39384
+ if (["1", "true", "yes", "on"].includes(value))
39385
+ return { enabled: true };
39386
+ if (["0", "false", "no", "off"].includes(value))
39387
+ return { enabled: false };
39388
+ return {
39389
+ error: `PGAI_INSTANCE_JOBS=${envValue} is not a boolean ` + "(use true/false, 1/0, yes/no, on/off, or leave it empty to keep the current setting)"
39390
+ };
39391
+ }
38955
39392
  function statOwner(file) {
38956
39393
  if (!file)
38957
39394
  return;
@@ -38970,7 +39407,7 @@ function processOwner() {
38970
39407
  return null;
38971
39408
  return `${process.getuid()}:${process.getgid()}`;
38972
39409
  }
38973
- function buildLocalInstallEnv(existingEnv, imageTag, instanceId, credentialFile) {
39410
+ function buildLocalInstallEnv(existingEnv, imageTag, instanceId, credentialFile, instanceJobs) {
38974
39411
  const readRaw = (key) => {
38975
39412
  const re = new RegExp(`^[ ]*(?:export[ ]+)?${key}=(.*)$`, "gm");
38976
39413
  let value = null;
@@ -39002,6 +39439,10 @@ function buildLocalInstallEnv(existingEnv, imageTag, instanceId, credentialFile)
39002
39439
  const credentialOwner = owner === undefined ? processOwner() : owner;
39003
39440
  if (credentialOwner)
39004
39441
  envLines.push(`INSTANCE_JOBS_USER=${credentialOwner}`);
39442
+ const existingProfiles = readRaw("COMPOSE_PROFILES");
39443
+ const composeProfiles = composeProfilesValue(existingProfiles === null ? null : stripMatchingQuotes(existingProfiles), instanceJobs);
39444
+ if (composeProfiles)
39445
+ envLines.push(`COMPOSE_PROFILES=${composeProfiles}`);
39005
39446
  for (const key of VM_ADMIN_AUTH_KEYS) {
39006
39447
  const existing = readRaw(key);
39007
39448
  envLines.push(`${key}=${existing ? stripMatchingQuotes(existing) : crypto2.randomBytes(32).toString("hex")}`);
@@ -41053,7 +41494,7 @@ async function applyMonitoringTargetsConfig() {
41053
41494
  console.log("Restarting pgwatch collectors to pick up target changes...");
41054
41495
  return runCompose(["up", "-d", "--force-recreate", "pgwatch-prometheus", "pgwatch-postgres"]);
41055
41496
  }
41056
- async function runCompose(args, grafanaPassword) {
41497
+ async function runCompose(args, grafanaPassword, extraEnv) {
41057
41498
  let composeFile;
41058
41499
  let projectDir;
41059
41500
  try {
@@ -41117,6 +41558,8 @@ async function runCompose(args, grafanaPassword) {
41117
41558
  }
41118
41559
  }
41119
41560
  }
41561
+ if (extraEnv)
41562
+ Object.assign(env, extraEnv);
41120
41563
  const finalArgs = [...args];
41121
41564
  if (process.platform === "darwin" && shouldScaleOutNodeExporter(args)) {
41122
41565
  finalArgs.push("--scale", "self-node-exporter=0");
@@ -41175,6 +41618,127 @@ Applying sink-prometheus config (VictoriaMetrics admin keys, #359)...`);
41175
41618
  console.log("\u2713 sink-prometheus is running the current config");
41176
41619
  return true;
41177
41620
  }
41621
+ function readComposeProfiles(knownProjectDir) {
41622
+ const fromProcess = process.env.COMPOSE_PROFILES;
41623
+ if (fromProcess !== undefined && fromProcess.trim() !== "")
41624
+ return fromProcess;
41625
+ let projectDir;
41626
+ if (knownProjectDir) {
41627
+ projectDir = knownProjectDir;
41628
+ } else {
41629
+ try {
41630
+ ({ projectDir } = resolvePaths());
41631
+ } catch {
41632
+ projectDir = getDefaultMonitoringProjectDir();
41633
+ }
41634
+ }
41635
+ const envFile = path7.resolve(projectDir, ".env");
41636
+ let content;
41637
+ try {
41638
+ content = fs9.readFileSync(envFile, "utf8");
41639
+ } catch (err) {
41640
+ if (err?.code !== "ENOENT") {
41641
+ console.error(`\u26A0 Could not read ${envFile}: ${err instanceof Error ? err.message : String(err)}`);
41642
+ console.error(" Treating the instance-jobs profile as disabled; if it is in fact enabled,");
41643
+ console.error(" an absent container will be reported as '- not enabled' rather than a fault.");
41644
+ }
41645
+ return null;
41646
+ }
41647
+ let value = null;
41648
+ for (const m of content.matchAll(/^[ \t]*(?:export[ \t]+)?COMPOSE_PROFILES=(.*)$/gm)) {
41649
+ value = stripMatchingQuotes(m[1].trim());
41650
+ }
41651
+ return value;
41652
+ }
41653
+ function instanceJobsRemovalOutcome(before, after, removeExitCode) {
41654
+ if (before === "unknown" || after === "unknown") {
41655
+ return {
41656
+ ok: false,
41657
+ message: `\u2717 Could not determine whether the instance-jobs container is present.
41658
+ ` + " The profile is off in .env, but a container left behind keeps polling."
41659
+ };
41660
+ }
41661
+ if (before === "absent") {
41662
+ return { ok: true, message: "\u2713 instance-jobs is disabled (no container was present)" };
41663
+ }
41664
+ if (removeExitCode !== 0 || after !== "absent") {
41665
+ return {
41666
+ ok: false,
41667
+ message: `\u2717 Could not remove the instance-jobs container.
41668
+ ` + ` The profile is off in .env, but the container is still there and still polling. To retry:
41669
+ ` + ` COMPOSE_PROFILES=${INSTANCE_JOBS_PROFILE} docker compose rm -sf ${INSTANCE_JOBS_PROFILE}`
41670
+ };
41671
+ }
41672
+ return before === "running" ? {
41673
+ ok: true,
41674
+ message: "\u26A0 instance-jobs was RUNNING and has been removed \u2014 outbound collection on this machine is now OFF"
41675
+ } : { ok: true, message: "\u2713 instance-jobs container removed (it was not running)" };
41676
+ }
41677
+ function instanceJobsStartSkipReason(storeRunning) {
41678
+ if (storeRunning === true)
41679
+ return null;
41680
+ return storeRunning === false ? "the stack is not running, so instance-jobs was not started - it comes up with the rest on `mon start`" : "could not tell whether the stack is running, so instance-jobs was not started - run `mon start`, then `mon health`";
41681
+ }
41682
+ async function composeServiceRunning(projectDir, service) {
41683
+ const composeCmd = getComposeCmd();
41684
+ if (!composeCmd)
41685
+ return null;
41686
+ try {
41687
+ const { stdout } = await execFilePromise(composeCmd[0], [...composeCmd.slice(1), "-f", path7.resolve(projectDir, "docker-compose.yml"), "ps", "-q", service], { cwd: projectDir });
41688
+ return stdout.trim().length > 0;
41689
+ } catch {
41690
+ return null;
41691
+ }
41692
+ }
41693
+ async function applyInstanceJobsProfile(projectDir, enabled) {
41694
+ if (enabled === undefined)
41695
+ return true;
41696
+ if (enabled) {
41697
+ const skip = instanceJobsStartSkipReason(await composeServiceRunning(projectDir, "sink-prometheus"));
41698
+ if (skip) {
41699
+ console.log(`
41700
+ (${skip})`);
41701
+ return true;
41702
+ }
41703
+ console.log(`
41704
+ Starting the instance-jobs container (compose profile enabled)...`);
41705
+ const code = await runCompose(["up", "-d", "--no-deps", INSTANCE_JOBS_PROFILE]);
41706
+ if (code !== 0) {
41707
+ console.error("\u2717 Could not start the instance-jobs container.");
41708
+ console.error(" The channel is enabled in .env but nothing is polling. To retry:");
41709
+ console.error(` docker compose up -d --no-deps ${INSTANCE_JOBS_PROFILE}`);
41710
+ return false;
41711
+ }
41712
+ console.log("\u2713 instance-jobs is running");
41713
+ return true;
41714
+ }
41715
+ const before = await inspectInstanceJobsState();
41716
+ if (before === "absent" || before === "unknown") {
41717
+ const verdict = instanceJobsRemovalOutcome(before, before, 0);
41718
+ (verdict.ok ? console.log : console.error)(`
41719
+ ${verdict.message}`);
41720
+ return verdict.ok;
41721
+ }
41722
+ const code = await runCompose(["rm", "-sf", INSTANCE_JOBS_PROFILE], undefined, { COMPOSE_PROFILES: INSTANCE_JOBS_PROFILE });
41723
+ const verdict = instanceJobsRemovalOutcome(before, await inspectInstanceJobsState(), code);
41724
+ (verdict.ok ? console.log : console.error)(`
41725
+ ${verdict.message}`);
41726
+ return verdict.ok;
41727
+ }
41728
+ async function inspectInstanceJobsState() {
41729
+ try {
41730
+ const { stdout } = await execFilePromise("docker", [
41731
+ "inspect",
41732
+ "-f",
41733
+ "{{.State.Running}}",
41734
+ INSTANCE_JOBS_PROFILE
41735
+ ]);
41736
+ return stdout.trim() === "true" ? "running" : "stopped";
41737
+ } catch (error) {
41738
+ const text = error instanceof Error ? error.message : String(error);
41739
+ return /no such (object|container)/i.test(text) ? "absent" : "unknown";
41740
+ }
41741
+ }
41178
41742
  program2.command("help").description("show help").action(() => {
41179
41743
  program2.outputHelp();
41180
41744
  });
@@ -41188,7 +41752,7 @@ mon.command("local-install").description("install local monitoring stack (genera
41188
41752
  " PGAI_ENABLE_IPV6=false (accepted: true|false|yes|no, lowercase)",
41189
41753
  ""
41190
41754
  ].join(`
41191
- `)).option("--demo", "demo mode with sample database", false).option("--api-key <key>", "Postgres AI API key for automated report uploads").option("--org <alias>", `organization alias (or ${ORG_ENV}); required to register with a global token`).option("--org-id <id>", `organization id (or ${ORG_ID_ENV}); alternative to --org`).option("--db-url <url>", "PostgreSQL connection URL to monitor").option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)").option("--project <name>", "Docker Compose project name (default: postgres_ai)").option("--instance-id <uuid>", "adopt a console-provisioned monitoring instance instead of self-registering a new one (set automatically by the provisioning flow; PGAI_INSTANCE_ID env also works)").option("--vcpus <n>", "source DB vCPU count used for AAS zone thresholds (PGAI_VCPUS env also works). Omit or 0 = unknown: the platform keeps the value it stamped at provision time. An explicit non-zero value overwrites the stored one; changing it re-arms the AAS backfill.").option("-y, --yes", "accept all defaults and skip interactive prompts", false).action(async (opts) => {
41755
+ `)).option("--demo", "demo mode with sample database", false).option("--api-key <key>", "Postgres AI API key for automated report uploads").option("--org <alias>", `organization alias (or ${ORG_ENV}); required to register with a global token`).option("--org-id <id>", `organization id (or ${ORG_ID_ENV}); alternative to --org`).option("--db-url <url>", "PostgreSQL connection URL to monitor").option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)").option("--project <name>", "Docker Compose project name (default: postgres_ai)").option("--instance-id <uuid>", "adopt a console-provisioned monitoring instance instead of self-registering a new one (set automatically by the provisioning flow; PGAI_INSTANCE_ID env also works)").option("--vcpus <n>", "source DB vCPU count used for AAS zone thresholds (PGAI_VCPUS env also works). Omit or 0 = unknown: the platform keeps the value it stamped at provision time. An explicit non-zero value overwrites the stored one; changing it re-arms the AAS backfill.").option("--instance-jobs", "enable the instance-jobs compose profile (outbound collection channel; PGAI_INSTANCE_JOBS env also works). Writes COMPOSE_PROFILES into the stack .env, so every later mon command covers the container").option("--no-instance-jobs", "disable the instance-jobs compose profile and remove its container").option("-y, --yes", "accept all defaults and skip interactive prompts", false).action(async (opts) => {
41192
41756
  const globalOpts = program2.opts();
41193
41757
  let apiKey = opts.apiKey || globalOpts.apiKey;
41194
41758
  console.log(`
@@ -41210,7 +41774,14 @@ mon.command("local-install").description("install local monitoring stack (genera
41210
41774
  const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
41211
41775
  const previousTag = previousTagMatch ? previousTagMatch[1].trim() : null;
41212
41776
  const imageTag = opts.tag || package_default.version;
41213
- const { content: envContent, preservedKeys } = buildLocalInstallEnv(existingEnv, imageTag, opts.instanceId || process.env.PGAI_INSTANCE_ID || null, path7.resolve(projectDir, ".pgwatch-config"));
41777
+ const instanceJobsRequest = resolveInstanceJobsRequest(opts.instanceJobs, process.env.PGAI_INSTANCE_JOBS);
41778
+ if (instanceJobsRequest.error) {
41779
+ console.error(`\u2717 ${instanceJobsRequest.error}`);
41780
+ process.exitCode = 1;
41781
+ return;
41782
+ }
41783
+ const instanceJobs = instanceJobsRequest.enabled;
41784
+ const { content: envContent, preservedKeys } = buildLocalInstallEnv(existingEnv, imageTag, opts.instanceId || process.env.PGAI_INSTANCE_ID || null, path7.resolve(projectDir, ".pgwatch-config"), instanceJobs);
41214
41785
  writeEnvFile(envFile, envContent);
41215
41786
  if (preservedKeys.length > 0) {
41216
41787
  console.log(`Preserved existing .env settings: ${preservedKeys.join(", ")}
@@ -41241,6 +41812,8 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
41241
41812
  console.error(`\u26A0 Monitoring services are already running: ${containers.join(", ")}`);
41242
41813
  console.log(`Use 'postgres-ai mon restart' to restart them
41243
41814
  `);
41815
+ if (!await applyInstanceJobsProfile(projectDir, instanceJobs))
41816
+ process.exitCode = 1;
41244
41817
  return;
41245
41818
  }
41246
41819
  if (!opts.demo) {
@@ -41530,6 +42103,9 @@ Searched: ${demoCandidates.join(", ")}
41530
42103
  }
41531
42104
  console.log(`\u2713 Services started
41532
42105
  `);
42106
+ const instanceJobsApplied = await applyInstanceJobsProfile(projectDir, instanceJobs);
42107
+ if (!instanceJobsApplied)
42108
+ process.exitCode = 1;
41533
42109
  if (apiKey && !opts.demo) {
41534
42110
  let regOrgScope;
41535
42111
  if (isGlobalTokenValue(apiKey)) {
@@ -41571,7 +42147,7 @@ Searched: ${demoCandidates.join(", ")}
41571
42147
  debug: !!process.env.DEBUG
41572
42148
  });
41573
42149
  if (aas.ok) {
41574
- console.log(`\u2713 AAS auto-collection registered
42150
+ console.log(`\u2713 ${aasSuccessMessage(aas)}
41575
42151
  `);
41576
42152
  } else {
41577
42153
  console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
@@ -41590,9 +42166,15 @@ Searched: ${demoCandidates.join(", ")}
41590
42166
  }
41591
42167
  }
41592
42168
  console.log("=================================");
41593
- console.log(" Local install completed!");
42169
+ console.log(instanceJobsApplied ? " Local install completed!" : " Local install completed WITH ONE FAILURE");
41594
42170
  console.log(`=================================
41595
42171
  `);
42172
+ if (!instanceJobsApplied) {
42173
+ console.log(instanceJobs ? `\u2717 The instance-jobs container did not start. Everything below is running; the outbound
42174
+ ` + " collection channel is not. See the error above, then `postgres-ai mon health`.\n" : `\u2717 The instance-jobs container could not be removed. Everything below is running, and so
42175
+ ` + ` is a container that keeps polling. See the error above.
42176
+ `);
42177
+ }
41596
42178
  console.log("What's running:");
41597
42179
  if (opts.demo) {
41598
42180
  console.log(" \u2705 Demo PostgreSQL database (monitoring target)");
@@ -41707,7 +42289,7 @@ mon.command("logs [service]").option("-f, --follow", "follow logs", false).optio
41707
42289
  });
41708
42290
  function judgeServiceHealth(service, present, status, health) {
41709
42291
  if (!present) {
41710
- return service.optional ? { ok: true, mark: "-", label: "not enabled" } : { ok: false, mark: "\u2717", label: "unreachable" };
42292
+ return service.optional ? { ok: true, mark: "-", label: "not enabled" } : { ok: false, mark: "\u2717", label: service.absentLabel ?? "unreachable" };
41711
42293
  }
41712
42294
  if (status !== "running") {
41713
42295
  return { ok: false, mark: "\u2717", label: `unhealthy (status: ${status})` };
@@ -41717,7 +42299,7 @@ function judgeServiceHealth(service, present, status, health) {
41717
42299
  }
41718
42300
  return { ok: true, mark: "\u2713", label: "healthy" };
41719
42301
  }
41720
- function monitoringHealthServices() {
42302
+ function monitoringHealthServices(instanceJobsEnabled = false) {
41721
42303
  return [
41722
42304
  { name: "Grafana", container: "grafana-with-datasources" },
41723
42305
  { name: "Prometheus", container: "sink-prometheus" },
@@ -41725,11 +42307,17 @@ function monitoringHealthServices() {
41725
42307
  { name: "PGWatch (Prometheus)", container: "pgwatch-prometheus" },
41726
42308
  { name: "Target DB", container: "target-db" },
41727
42309
  { name: "Sink Postgres", container: "sink-postgres" },
41728
- { name: "Instance jobs", container: "instance-jobs", optional: true, readHealth: true }
42310
+ {
42311
+ name: "Instance jobs",
42312
+ container: "instance-jobs",
42313
+ optional: !instanceJobsEnabled,
42314
+ readHealth: true,
42315
+ absentLabel: "enabled but not running"
42316
+ }
41729
42317
  ];
41730
42318
  }
41731
42319
  mon.command("health").description("health check for monitoring services").option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0).action(async (opts) => {
41732
- const services = monitoringHealthServices();
42320
+ const services = monitoringHealthServices(instanceJobsProfileEnabled(readComposeProfiles()));
41733
42321
  const waitTime = opts.wait || 0;
41734
42322
  const maxAttempts = waitTime > 0 ? Math.ceil(waitTime / 5) : 1;
41735
42323
  console.log(`Checking service health...
@@ -41863,6 +42451,12 @@ Updating Docker images...`);
41863
42451
  process.exitCode = 1;
41864
42452
  return;
41865
42453
  }
42454
+ if (instanceJobsProfileEnabled(readComposeProfiles(projectDir))) {
42455
+ if (!await applyInstanceJobsProfile(projectDir, true)) {
42456
+ process.exitCode = 1;
42457
+ return;
42458
+ }
42459
+ }
41866
42460
  console.log(`
41867
42461
  \u2713 Update completed successfully`);
41868
42462
  console.log(`
@@ -42564,7 +43158,7 @@ program2.command("orgs").description("list organizations this credential can rea
42564
43158
  process.exitCode = 1;
42565
43159
  }
42566
43160
  });
42567
- withOrgOptions(program2.command("promql")).description("run a PromQL query on a monitoring instance's own metric store").argument("<expr>", "PromQL expression").option("--instance <uuid>", "monitoring instance id (default: this box's .pgwatch-config)").option("--at <rfc3339>", "evaluate an instant query at this time (default: now; ignored with --range)").option("--range", "run a range query (requires --start, --end, --step)").option("--start <rfc3339>", "range start").option("--end <rfc3339>", "range end").option("--step <duration>", "range step, e.g. 60s or 5m").option("--timeout <duration>", "how long to wait for an answer (default: sized from the instance's poll pacing; 11m if the platform does not report it)").option("--json", "output the raw result payload").action(async (expr, opts) => {
43161
+ withOrgOptions(program2.command("promql")).description("run a PromQL query on a monitoring instance's own metric store").argument("<expr>", "PromQL expression").option("--instance <uuid>", "monitoring instance id (default: this box's .pgwatch-config); not with --project").option("--project <id|alias>", "the project's monitoring instance, by project id, alias or name; not with --instance").option("--at <rfc3339>", "evaluate an instant query at this time (default: now; ignored with --range)").option("--range", "run a range query (requires --start, --end, --step)").option("--start <rfc3339>", "range start").option("--end <rfc3339>", "range end").option("--step <duration>", "range step, e.g. 60s or 5m").option("--timeout <duration>", "how long to wait for an answer (default: sized from the instance's poll pacing; 11m if the platform does not report it)").option("--json", "output the raw result payload").action(async (expr, opts) => {
42568
43162
  try {
42569
43163
  const rootOpts = program2.opts();
42570
43164
  const cfg = readConfig();
@@ -42575,9 +43169,19 @@ withOrgOptions(program2.command("promql")).description("run a PromQL query on a
42575
43169
  return;
42576
43170
  }
42577
43171
  const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
42578
- const instanceId = opts.instance || readInstanceIdFromProject();
43172
+ if (opts.instance && opts.project) {
43173
+ console.error("Pass --instance or --project, not both.");
43174
+ process.exitCode = 1;
43175
+ return;
43176
+ }
43177
+ const instanceId = opts.instance || (opts.project ? await resolveMonitoringInstanceId({
43178
+ apiKey,
43179
+ apiBaseUrl,
43180
+ project: opts.project,
43181
+ orgId: configOrgIdForBody(apiKey, cfg.orgId)
43182
+ }) : readInstanceIdFromProject());
42579
43183
  if (!instanceId) {
42580
- console.error("No monitoring instance. Pass --instance <uuid>, or run this on a box whose");
43184
+ console.error("No monitoring instance. Pass --project <id|alias> or --instance <uuid>, or run this on a box whose");
42581
43185
  console.error(".pgwatch-config has an instance_id (written by 'pgai mon local-install').");
42582
43186
  process.exitCode = 1;
42583
43187
  return;
@@ -42617,7 +43221,7 @@ withOrgOptions(program2.command("promql")).description("run a PromQL query on a
42617
43221
  });
42618
43222
  if (!isTerminal2(res.status)) {
42619
43223
  console.error(`Timed out after ${timeoutS}s. Job ${jobId} is still ${res.status}.`);
42620
- console.error(res.status === "queued" ? "Nothing has claimed it: check the instance-jobs container is running (it is behind " + "a compose profile), or wait \u2014 a first collection can queue ~20 periods ahead of it." : "The instance is working on it \u2014 wait, or re-run with a longer --timeout.");
43224
+ console.error(res.status === "queued" ? "Nothing has claimed it: check the instance-jobs container is running on that instance " + "(`postgresai mon health`; it is behind a compose profile, enabled with " + "`mon local-install --instance-jobs`), or wait \u2014 a first collection can queue " + "~20 periods ahead of it." : "The instance is working on it \u2014 wait, or re-run with a longer --timeout.");
42621
43225
  process.exitCode = 1;
42622
43226
  return;
42623
43227
  }
@@ -43429,7 +44033,7 @@ function tryParseJson(s) {
43429
44033
  }
43430
44034
  }
43431
44035
  function printJoeOutcome(outcome, json, budgetMs) {
43432
- const effectiveBudgetMs = typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
44036
+ const effectiveBudgetMs = Number.isFinite(outcome.budgetMs) ? outcome.budgetMs : typeof budgetMs === "number" && Number.isFinite(budgetMs) ? budgetMs : DEFAULT_BUDGET_MS;
43433
44037
  const budgetSeconds = Math.round(effectiveBudgetMs / 1000);
43434
44038
  if (outcome.budgetExpired) {
43435
44039
  if (json) {
@@ -43927,17 +44531,23 @@ export {
43927
44531
  applyApiKey,
43928
44532
  applyProjectName,
43929
44533
  buildLocalInstallEnv,
44534
+ composeProfilesValue,
43930
44535
  instanceIdToPersist,
44536
+ instanceJobsProfileEnabled,
44537
+ instanceJobsRemovalOutcome,
44538
+ instanceJobsStartSkipReason,
43931
44539
  isValidComposeYaml,
43932
44540
  judgeServiceHealth,
43933
44541
  monitoringHealthServices,
43934
44542
  persistInstanceId,
43935
44543
  planMonitoringRegistration,
43936
44544
  program2 as program,
44545
+ readComposeProfiles,
43937
44546
  readDeployedTag,
43938
44547
  refreshBundledComposeIfStale,
43939
44548
  registerMonitoringInstance,
43940
44549
  resolveAdoptedProject,
44550
+ resolveInstanceJobsRequest,
43941
44551
  selfRegisterAndPersist,
43942
44552
  shouldScaleOutNodeExporter,
43943
44553
  statOwner,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.17.0-rc.3",
3
+ "version": "0.17.0-rc.5",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,