postgresai 0.16.0-rc.2 → 0.16.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ### Fixed
6
+
7
+ - `checkup --markdown` previously performed server-side conversion and sent the
8
+ full report JSON to the PostgresAI API even when `--no-upload` was set. The
9
+ flags are now mutually exclusive, and `--no-upload` prevents report data from
10
+ being sent to the PostgresAI API. Use `--json` or `--output` for local-only
11
+ output.
@@ -335,12 +335,18 @@ function prepareUploadConfig(
335
335
  console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
336
336
  return null; // Signal to exit
337
337
  }
338
- // No credentials and upload not explicitly requested: fall back to
339
- // local-only mode, but say so prominently skipping the upload silently
340
- // hides the fact that results never reach the Console.
341
- console.error("Notice: no API key configured results will NOT be uploaded to PostgresAI.");
342
- console.error(" To upload: run 'postgresai auth login' or pass --api-key / set PGAI_API_KEY.");
343
- console.error(" To run locally without this notice, pass --no-upload.");
338
+ if (opts.markdown) {
339
+ console.error("Notice: no API key configuredregular report upload is disabled.");
340
+ console.error(" The full report JSON will still be sent to the PostgresAI API for markdown conversion.");
341
+ console.error(" To avoid sending report data, replace --markdown with --no-upload and --json or --output.");
342
+ } else {
343
+ // No credentials and upload not explicitly requested: fall back to
344
+ // local-only mode, but say so prominently — skipping the upload silently
345
+ // hides the fact that results never reach the Console.
346
+ console.error("Notice: no API key configured — results will NOT be uploaded to PostgresAI.");
347
+ console.error(" To upload: run 'postgresai auth login' or pass --api-key / set PGAI_API_KEY.");
348
+ console.error(" To run locally without this notice, pass --no-upload.");
349
+ }
344
350
  return undefined; // Skip upload, run checks locally
345
351
  }
346
352
 
@@ -1996,7 +2002,7 @@ program
1996
2002
  "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)"
1997
2003
  )
1998
2004
  .option("--json", "output JSON to stdout")
1999
- .option("--markdown", "output markdown to stdout")
2005
+ .option("--markdown", "output markdown via PostgresAI API (transmits the full report JSON)")
2000
2006
  .addHelpText(
2001
2007
  "after",
2002
2008
  [
@@ -2010,7 +2016,7 @@ program
2010
2016
  " postgresai checkup postgresql://user:pass@host:5432/db --check-id H002",
2011
2017
  " postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
2012
2018
  " postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json",
2013
- " postgresai checkup postgresql://user:pass@host:5432/db --no-upload --markdown",
2019
+ " postgresai checkup postgresql://user:pass@host:5432/db --markdown",
2014
2020
  ].join("\n")
2015
2021
  )
2016
2022
  .action(async (checkIdOrConn: string | undefined, connArg: string | undefined, opts: CheckupOptions, cmd: Command) => {
@@ -2060,9 +2066,14 @@ program
2060
2066
  process.exitCode = 1;
2061
2067
  return;
2062
2068
  }
2063
- // Note: --json, --markdown and --upload/--no-upload are independent flags.
2064
- // Use --no-upload to explicitly disable upload when using --json or --markdown.
2065
2069
  const uploadExplicitlyDisabled = opts.upload === false;
2070
+ if (uploadExplicitlyDisabled && shouldConvertMarkdown) {
2071
+ console.error("Error: --no-upload and --markdown are mutually exclusive");
2072
+ console.error("Markdown conversion is performed by the PostgresAI API and transmits the full report JSON.");
2073
+ console.error("Drop --no-upload to allow transmission, or use --json or --output for local-only output.");
2074
+ process.exitCode = 1;
2075
+ return;
2076
+ }
2066
2077
  let shouldUpload = !uploadExplicitlyDisabled;
2067
2078
 
2068
2079
  // Preflight: validate/create output directory BEFORE connecting / running checks.
@@ -2308,7 +2319,9 @@ program
2308
2319
 
2309
2320
  console.log('\nFor details:');
2310
2321
  console.log(' --json Output JSON');
2311
- console.log(' --markdown Output markdown');
2322
+ if (!uploadExplicitlyDisabled) {
2323
+ console.log(' --markdown Output markdown via PostgresAI API');
2324
+ }
2312
2325
  console.log(' --output <dir> Save to directory');
2313
2326
  }
2314
2327
  } catch (error) {
@@ -3931,13 +3944,9 @@ targets
3931
3944
  // Authentication and API key management
3932
3945
  const auth = program.command("auth").description("authentication and API key management");
3933
3946
 
3934
- auth
3935
- .command("login", { isDefault: true })
3936
- .description("authenticate via browser (OAuth) or store API key directly")
3937
- .option("--set-key <key>", "store API key directly without OAuth flow")
3938
- .option("--port <port>", "local callback server port (default: random)", parseInt)
3939
- .option("--debug", "enable debug output")
3940
- .action(async (opts: { setKey?: string; port?: number; debug?: boolean }) => {
3947
+ type AuthLoginOptions = { setKey?: string; port?: number; debug?: boolean };
3948
+
3949
+ async function runAuthLogin(opts: AuthLoginOptions) {
3941
3950
  // If --set-key is provided, store it directly without OAuth
3942
3951
  if (opts.setKey) {
3943
3952
  const trimmedKey = opts.setKey.trim();
@@ -4191,7 +4200,21 @@ auth
4191
4200
  console.error(`Authentication error: ${message}`);
4192
4201
  process.exit(1);
4193
4202
  }
4194
- });
4203
+ }
4204
+
4205
+ function configureLoginCommand(command: Command): Command {
4206
+ return command
4207
+ .description("authenticate via browser (OAuth) or store API key directly")
4208
+ .option("--set-key <key>", "store API key directly without OAuth flow")
4209
+ .option("--port <port>", "local callback server port (default: random)", parseInt)
4210
+ .option("--debug", "enable debug output");
4211
+ }
4212
+
4213
+ configureLoginCommand(auth.command("login", { isDefault: true }))
4214
+ .action(runAuthLogin);
4215
+
4216
+ configureLoginCommand(program.command("login"))
4217
+ .action(runAuthLogin);
4195
4218
 
4196
4219
  auth
4197
4220
  .command("show-key")
@@ -13425,7 +13425,7 @@ var {
13425
13425
  // package.json
13426
13426
  var package_default = {
13427
13427
  name: "postgresai",
13428
- version: "0.16.0-rc.2",
13428
+ version: "0.16.0-rc.4",
13429
13429
  description: "postgres_ai CLI",
13430
13430
  license: "Apache-2.0",
13431
13431
  private: false,
@@ -16256,7 +16256,7 @@ var Result = import_lib.default.Result;
16256
16256
  var TypeOverrides = import_lib.default.TypeOverrides;
16257
16257
  var defaults = import_lib.default.defaults;
16258
16258
  // package.json
16259
- var version = "0.16.0-rc.2";
16259
+ var version = "0.16.0-rc.4";
16260
16260
  var package_default2 = {
16261
16261
  name: "postgresai",
16262
16262
  version,
@@ -29452,10 +29452,6 @@ async function verifyInitSetup(params) {
29452
29452
  }
29453
29453
  }
29454
29454
  }
29455
- const explainFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok", [role]);
29456
- if (!explainFnRes.rows?.[0]?.ok) {
29457
- missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
29458
- }
29459
29455
  const tableDescribeFnRes = await params.client.query("select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok", [role]);
29460
29456
  if (!tableDescribeFnRes.rows?.[0]?.ok) {
29461
29457
  missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
@@ -29522,10 +29518,28 @@ async function checkCurrentUserPermissions(client) {
29522
29518
 
29523
29519
  union all
29524
29520
 
29521
+ select
29522
+ 'postgres_ai schema exists' as permission_name,
29523
+ 'optional' as status,
29524
+ to_regnamespace('postgres_ai') is not null as granted
29525
+
29526
+ union all
29527
+
29528
+ select
29529
+ 'usage on postgres_ai schema' as permission_name,
29530
+ 'optional' as status,
29531
+ case
29532
+ when to_regnamespace('postgres_ai') is null then null
29533
+ else has_schema_privilege(current_user, 'postgres_ai', 'USAGE')
29534
+ end as granted
29535
+
29536
+ union all
29537
+
29525
29538
  select
29526
29539
  'postgres_ai.pg_statistic view exists' as permission_name,
29527
29540
  'optional' as status,
29528
29541
  case
29542
+ when to_regnamespace('postgres_ai') is null then null
29529
29543
  when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
29530
29544
  else to_regclass('postgres_ai.pg_statistic') is not null
29531
29545
  end as granted
@@ -29536,6 +29550,7 @@ async function checkCurrentUserPermissions(client) {
29536
29550
  'select on postgres_ai.pg_statistic' as permission_name,
29537
29551
  'optional' as status,
29538
29552
  case
29553
+ when to_regnamespace('postgres_ai') is null then null
29539
29554
  when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
29540
29555
  when to_regclass('postgres_ai.pg_statistic') is null then null
29541
29556
  else has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'select')
@@ -29555,6 +29570,10 @@ async function checkCurrentUserPermissions(client) {
29555
29570
  when permission_name like 'select on pg_catalog.pg_index' then
29556
29571
  format('grant select on pg_catalog.pg_index to %I;', current_user)
29557
29572
  end
29573
+ when permission_name = 'postgres_ai schema exists' and granted = false then
29574
+ '-- run postgresai prepare-db or create the postgres_ai schema and pg_statistic view manually'
29575
+ when permission_name = 'usage on postgres_ai schema' and granted = false then
29576
+ format('grant usage on schema postgres_ai to %I;', current_user)
29558
29577
  when permission_name = 'postgres_ai.pg_statistic view exists' and granted = false then
29559
29578
  '-- create postgres_ai.pg_statistic view (see setup script)'
29560
29579
  when permission_name = 'select on postgres_ai.pg_statistic' and granted = false then
@@ -29581,6 +29600,10 @@ function formatPermissionCheckMessages(result) {
29581
29600
  const warnings = [];
29582
29601
  const errors3 = [];
29583
29602
  for (const row of result.missingOptional) {
29603
+ if (row.permission_name === "postgres_ai schema exists") {
29604
+ warnings.push("Warning: optional: postgres_ai schema not found — F004/F005 (bloat estimates) will be skipped; run prepare-db or create the view manually to enable them.");
29605
+ continue;
29606
+ }
29584
29607
  const fix = row.fix_command ? ` Fix: ${row.fix_command}` : "";
29585
29608
  warnings.push(`Warning: optional permission missing — ${row.permission_name}.${fix}`);
29586
29609
  }
@@ -29974,15 +29997,6 @@ async function verifyInitSetupViaSupabase(params) {
29974
29997
  missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
29975
29998
  }
29976
29999
  }
29977
- const explainFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'explain_generic' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
29978
- if (explainFnExistsRes.rowCount === 0) {
29979
- missingRequired.push("function postgres_ai.explain_generic exists");
29980
- } else {
29981
- const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
29982
- if (!explainFnRes.rows?.[0]?.ok) {
29983
- missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
29984
- }
29985
- }
29986
30000
  const tableDescribeFnExistsRes = await params.client.query("SELECT oid FROM pg_proc WHERE proname = 'table_describe' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'postgres_ai')", true);
29987
30001
  if (tableDescribeFnExistsRes.rowCount === 0) {
29988
30002
  missingRequired.push("function postgres_ai.table_describe exists");
@@ -32778,12 +32792,70 @@ async function generateF003(client, nodeName) {
32778
32792
  };
32779
32793
  return report;
32780
32794
  }
32795
+ function bloatErrorStatus(err) {
32796
+ const error2 = err instanceof Error ? err.message : String(err);
32797
+ const code = typeof err === "object" && err !== null && "code" in err ? String(err.code || "") : "";
32798
+ const normalized = error2.toLowerCase();
32799
+ let reason = "query_error";
32800
+ if (code === "3F000" || normalized.includes('schema "postgres_ai" does not exist')) {
32801
+ reason = "missing_schema";
32802
+ } else if (code === "42P01" || normalized.includes('relation "postgres_ai.pg_statistic" does not exist')) {
32803
+ reason = "missing_view";
32804
+ } else if (code === "42501" || normalized.includes("permission denied")) {
32805
+ reason = "missing_grant";
32806
+ }
32807
+ return { ok: false, reason, error: error2 };
32808
+ }
32809
+ async function getBloatCheckStatus(client) {
32810
+ try {
32811
+ const result = await client.query(`
32812
+ select
32813
+ to_regnamespace('postgres_ai') is not null as schema_exists,
32814
+ case
32815
+ when to_regnamespace('postgres_ai') is null then false
32816
+ else has_schema_privilege(current_user, 'postgres_ai', 'USAGE')
32817
+ end as schema_usage,
32818
+ case
32819
+ when to_regnamespace('postgres_ai') is null then false
32820
+ when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then false
32821
+ else to_regclass('postgres_ai.pg_statistic') is not null
32822
+ end as view_exists,
32823
+ case
32824
+ when to_regnamespace('postgres_ai') is null then false
32825
+ when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then false
32826
+ when to_regclass('postgres_ai.pg_statistic') is null then false
32827
+ else has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'SELECT')
32828
+ end as view_select
32829
+ `);
32830
+ const capability = result.rows[0] || {};
32831
+ if (!capability.schema_exists) {
32832
+ return { ok: false, reason: "missing_schema", error: 'schema "postgres_ai" does not exist' };
32833
+ }
32834
+ if (!capability.schema_usage) {
32835
+ return { ok: false, reason: "missing_grant", error: "permission denied for schema postgres_ai" };
32836
+ }
32837
+ if (!capability.view_exists) {
32838
+ return { ok: false, reason: "missing_view", error: 'relation "postgres_ai.pg_statistic" does not exist' };
32839
+ }
32840
+ if (!capability.view_select) {
32841
+ return { ok: false, reason: "missing_grant", error: "permission denied for relation postgres_ai.pg_statistic" };
32842
+ }
32843
+ return { ok: true, reason: null, error: null };
32844
+ } catch (err) {
32845
+ return bloatErrorStatus(err);
32846
+ }
32847
+ }
32781
32848
  async function generateF004(client, nodeName) {
32782
32849
  const report = createBaseReport("F004", "Autovacuum: heap bloat (estimated)", nodeName);
32783
32850
  const postgresVersion = await getPostgresVersion(client);
32784
32851
  const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10);
32785
32852
  let bloatedTables = [];
32853
+ let status = await getBloatCheckStatus(client);
32786
32854
  try {
32855
+ if (!status.ok)
32856
+ throw Object.assign(new Error(status.error || "Bloat prerequisites unavailable"), {
32857
+ code: status.reason === "missing_schema" ? "3F000" : status.reason === "missing_view" ? "42P01" : status.reason === "missing_grant" ? "42501" : undefined
32858
+ });
32787
32859
  const sql = getMetricSql(METRIC_NAMES.F004, pgMajorVersion);
32788
32860
  const bloatResult = await client.query(sql);
32789
32861
  const vacuumStatsResult = await client.query(`
@@ -32827,7 +32899,8 @@ async function generateF004(client, nodeName) {
32827
32899
  };
32828
32900
  });
32829
32901
  } catch (err) {
32830
- const errorMsg = err instanceof Error ? err.message : String(err);
32902
+ status = bloatErrorStatus(err);
32903
+ const errorMsg = status.error || "Unknown error";
32831
32904
  console.error(`[F004] Error estimating table bloat: ${errorMsg}`);
32832
32905
  if (errorMsg.includes("postgres_ai.")) {
32833
32906
  console.error(` Hint: Run "postgresai prepare-db <connection>" to create required objects.`);
@@ -32837,6 +32910,7 @@ async function generateF004(client, nodeName) {
32837
32910
  const totalCount = bloatedTables.length;
32838
32911
  const totalBloatSizeBytes = bloatedTables.reduce((sum, t) => sum + t.bloat_size, 0);
32839
32912
  const dbEntry = {
32913
+ status,
32840
32914
  bloated_tables: bloatedTables,
32841
32915
  total_count: totalCount,
32842
32916
  total_bloat_size_bytes: totalBloatSizeBytes,
@@ -32855,7 +32929,12 @@ async function generateF005(client, nodeName) {
32855
32929
  const postgresVersion = await getPostgresVersion(client);
32856
32930
  const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10);
32857
32931
  let bloatedIndexes = [];
32932
+ let status = await getBloatCheckStatus(client);
32858
32933
  try {
32934
+ if (!status.ok)
32935
+ throw Object.assign(new Error(status.error || "Bloat prerequisites unavailable"), {
32936
+ code: status.reason === "missing_schema" ? "3F000" : status.reason === "missing_view" ? "42P01" : status.reason === "missing_grant" ? "42501" : undefined
32937
+ });
32859
32938
  const sql = getMetricSql(METRIC_NAMES.F005, pgMajorVersion);
32860
32939
  const bloatResult = await client.query(sql);
32861
32940
  const vacuumStatsResult = await client.query(`
@@ -32904,7 +32983,8 @@ async function generateF005(client, nodeName) {
32904
32983
  };
32905
32984
  });
32906
32985
  } catch (err) {
32907
- const errorMsg = err instanceof Error ? err.message : String(err);
32986
+ status = bloatErrorStatus(err);
32987
+ const errorMsg = status.error || "Unknown error";
32908
32988
  console.error(`[F005] Error estimating index bloat: ${errorMsg}`);
32909
32989
  if (errorMsg.includes("postgres_ai.")) {
32910
32990
  console.error(` Hint: Run "postgresai prepare-db <connection>" to create required objects.`);
@@ -32914,6 +32994,7 @@ async function generateF005(client, nodeName) {
32914
32994
  const totalCount = bloatedIndexes.length;
32915
32995
  const totalBloatSizeBytes = bloatedIndexes.reduce((sum, idx) => sum + idx.bloat_size, 0);
32916
32996
  const dbEntry = {
32997
+ status,
32917
32998
  bloated_indexes: bloatedIndexes,
32918
32999
  total_count: totalCount,
32919
33000
  total_bloat_size_bytes: totalBloatSizeBytes,
@@ -33586,6 +33667,10 @@ function generateCheckSummary(checkId, report) {
33586
33667
  return summarizeF001(nodeData);
33587
33668
  case "F003":
33588
33669
  return summarizeF003(nodeData);
33670
+ case "F004":
33671
+ return summarizeBloat(nodeData, "table");
33672
+ case "F005":
33673
+ return summarizeBloat(nodeData, "index");
33589
33674
  case "G001":
33590
33675
  return summarizeG001(nodeData);
33591
33676
  case "G003":
@@ -33771,6 +33856,25 @@ function summarizeF003(nodeData) {
33771
33856
  }
33772
33857
  return { status: "warning", message: parts.join(", ") };
33773
33858
  }
33859
+ function summarizeBloat(nodeData, kind) {
33860
+ const data = nodeData?.data || {};
33861
+ let totalCount = 0;
33862
+ for (const dbData of Object.values(data)) {
33863
+ const dbEntry = dbData;
33864
+ if (dbEntry?.status?.ok === false) {
33865
+ const reason = String(dbEntry.status.reason || "query_error").replaceAll("_", " ");
33866
+ return { status: "warning", message: `Bloat estimate degraded: ${reason}` };
33867
+ }
33868
+ totalCount += dbEntry?.total_count || 0;
33869
+ }
33870
+ if (totalCount === 0) {
33871
+ return { status: "ok", message: `No bloated ${kind}${kind === "index" ? "es" : "s"} found` };
33872
+ }
33873
+ return {
33874
+ status: "warning",
33875
+ message: `Found ${totalCount} bloated ${kind}${totalCount === 1 ? "" : kind === "index" ? "es" : "s"}`
33876
+ };
33877
+ }
33774
33878
  function summarizeG001(nodeData) {
33775
33879
  const data = nodeData?.data || {};
33776
33880
  const settingsCount = Object.keys(data).length;
@@ -34127,9 +34231,15 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
34127
34231
  console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
34128
34232
  return null;
34129
34233
  }
34130
- console.error("Notice: no API key configured \u2014 results will NOT be uploaded to PostgresAI.");
34131
- console.error(" To upload: run 'postgresai auth login' or pass --api-key / set PGAI_API_KEY.");
34132
- console.error(" To run locally without this notice, pass --no-upload.");
34234
+ if (opts.markdown) {
34235
+ console.error("Notice: no API key configured \u2014 regular report upload is disabled.");
34236
+ console.error(" The full report JSON will still be sent to the PostgresAI API for markdown conversion.");
34237
+ console.error(" To avoid sending report data, replace --markdown with --no-upload and --json or --output.");
34238
+ } else {
34239
+ console.error("Notice: no API key configured \u2014 results will NOT be uploaded to PostgresAI.");
34240
+ console.error(" To upload: run 'postgresai auth login' or pass --api-key / set PGAI_API_KEY.");
34241
+ console.error(" To run locally without this notice, pass --no-upload.");
34242
+ }
34133
34243
  return;
34134
34244
  }
34135
34245
  const cfg = readConfig();
@@ -35354,7 +35464,7 @@ program2.command("unprepare-db [conn]").description("remove monitoring setup: dr
35354
35464
  closeReadline();
35355
35465
  }
35356
35466
  });
35357
- program2.command("checkup [checkIdOrConn] [conn]").description("generate health check reports directly from PostgreSQL (express mode)").option("--check-id <id>", `specific check to run (see list below), or ALL`).option("--node-name <name>", "node name for reports", "node-01").option("--output <path>", "output directory for JSON files").option("--upload", "upload JSON results to PostgresAI (requires API key)").option("--no-upload", "disable upload to PostgresAI").option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)").option("--json", "output JSON to stdout").option("--markdown", "output markdown to stdout").addHelpText("after", [
35467
+ program2.command("checkup [checkIdOrConn] [conn]").description("generate health check reports directly from PostgreSQL (express mode)").option("--check-id <id>", `specific check to run (see list below), or ALL`).option("--node-name <name>", "node name for reports", "node-01").option("--output <path>", "output directory for JSON files").option("--upload", "upload JSON results to PostgresAI (requires API key)").option("--no-upload", "disable upload to PostgresAI").option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)").option("--json", "output JSON to stdout").option("--markdown", "output markdown via PostgresAI API (transmits the full report JSON)").addHelpText("after", [
35358
35468
  "",
35359
35469
  "Available checks:",
35360
35470
  ...Object.entries(CHECK_INFO).map(([id, title]) => ` ${id}: ${title}`),
@@ -35365,7 +35475,7 @@ program2.command("checkup [checkIdOrConn] [conn]").description("generate health
35365
35475
  " postgresai checkup postgresql://user:pass@host:5432/db --check-id H002",
35366
35476
  " postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
35367
35477
  " postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json",
35368
- " postgresai checkup postgresql://user:pass@host:5432/db --no-upload --markdown"
35478
+ " postgresai checkup postgresql://user:pass@host:5432/db --markdown"
35369
35479
  ].join(`
35370
35480
  `)).action(async (checkIdOrConn, connArg, opts, cmd) => {
35371
35481
  const checkIdPattern = /^[A-Z]\d{3}$/i;
@@ -35405,6 +35515,13 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
35405
35515
  return;
35406
35516
  }
35407
35517
  const uploadExplicitlyDisabled = opts.upload === false;
35518
+ if (uploadExplicitlyDisabled && shouldConvertMarkdown) {
35519
+ console.error("Error: --no-upload and --markdown are mutually exclusive");
35520
+ console.error("Markdown conversion is performed by the PostgresAI API and transmits the full report JSON.");
35521
+ console.error("Drop --no-upload to allow transmission, or use --json or --output for local-only output.");
35522
+ process.exitCode = 1;
35523
+ return;
35524
+ }
35408
35525
  let shouldUpload = !uploadExplicitlyDisabled;
35409
35526
  const outputPath = prepareOutputDirectory(opts.output);
35410
35527
  if (outputPath === null) {
@@ -35587,7 +35704,9 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
35587
35704
  console.log(`
35588
35705
  For details:`);
35589
35706
  console.log(" --json Output JSON");
35590
- console.log(" --markdown Output markdown");
35707
+ if (!uploadExplicitlyDisabled) {
35708
+ console.log(" --markdown Output markdown via PostgresAI API");
35709
+ }
35591
35710
  console.log(" --output <dir> Save to directory");
35592
35711
  }
35593
35712
  } catch (error2) {
@@ -36802,7 +36921,7 @@ targets.command("test <name>").description("test monitoring target database conn
36802
36921
  }
36803
36922
  });
36804
36923
  var auth = program2.command("auth").description("authentication and API key management");
36805
- auth.command("login", { isDefault: true }).description("authenticate via browser (OAuth) or store API key directly").option("--set-key <key>", "store API key directly without OAuth flow").option("--port <port>", "local callback server port (default: random)", parseInt).option("--debug", "enable debug output").action(async (opts) => {
36924
+ async function runAuthLogin(opts) {
36806
36925
  if (opts.setKey) {
36807
36926
  const trimmedKey = opts.setKey.trim();
36808
36927
  if (!trimmedKey) {
@@ -37003,7 +37122,12 @@ Authentication failed: ${message}`);
37003
37122
  console.error(`Authentication error: ${message}`);
37004
37123
  process.exit(1);
37005
37124
  }
37006
- });
37125
+ }
37126
+ function configureLoginCommand(command) {
37127
+ return command.description("authenticate via browser (OAuth) or store API key directly").option("--set-key <key>", "store API key directly without OAuth flow").option("--port <port>", "local callback server port (default: random)", parseInt).option("--debug", "enable debug output");
37128
+ }
37129
+ configureLoginCommand(auth.command("login", { isDefault: true })).action(runAuthLogin);
37130
+ configureLoginCommand(program2.command("login")).action(runAuthLogin);
37007
37131
  auth.command("show-key").description("show API key (masked)").action(async () => {
37008
37132
  const cfg = readConfig();
37009
37133
  if (!cfg.apiKey) {
@@ -2,126 +2,6 @@
2
2
  -- These functions use SECURITY DEFINER to allow the monitoring user to perform
3
3
  -- operations they don't have direct permissions for.
4
4
 
5
- /*
6
- * explain_generic
7
- *
8
- * Function to get generic explain plans with optional HypoPG index testing.
9
- * Requires: PostgreSQL 16+ (for generic_plan option), HypoPG extension (optional).
10
- *
11
- * Security notes:
12
- * - EXPLAIN without ANALYZE is read-only (plans but doesn't execute the query)
13
- * - PostgreSQL's EXPLAIN only accepts a single statement (primary protection)
14
- * - Input validation uses a simple heuristic to detect multiple statements
15
- * (Note: may reject valid queries containing semicolons in string literals)
16
- *
17
- * Usage examples:
18
- * -- Basic generic plan
19
- * select postgres_ai.explain_generic('select * from users where id = $1');
20
- *
21
- * -- JSON format
22
- * select postgres_ai.explain_generic('select * from users where id = $1', 'json');
23
- *
24
- * -- Test a hypothetical index
25
- * select postgres_ai.explain_generic(
26
- * 'select * from users where email = $1',
27
- * 'text',
28
- * 'create index on users (email)'
29
- * );
30
- */
31
- create or replace function postgres_ai.explain_generic(
32
- in query text,
33
- in format text default 'text',
34
- in hypopg_index text default null,
35
- out result text
36
- )
37
- language plpgsql
38
- security definer
39
- set search_path = pg_catalog, public
40
- as $$
41
- declare
42
- v_line record;
43
- v_lines text[] := '{}';
44
- v_explain_query text;
45
- v_hypo_result record;
46
- v_version int;
47
- v_hypopg_available boolean;
48
- v_clean_query text;
49
- begin
50
- -- Check PostgreSQL version (generic_plan requires 16+)
51
- select current_setting('server_version_num')::int into v_version;
52
-
53
- if v_version < 160000 then
54
- raise exception 'generic_plan requires PostgreSQL 16+, current version: %',
55
- current_setting('server_version');
56
- end if;
57
-
58
- -- Input validation: reject empty queries
59
- if query is null or trim(query) = '' then
60
- raise exception 'query cannot be empty';
61
- end if;
62
-
63
- -- Input validation: detect multiple statements (defense-in-depth)
64
- -- Note: This is a simple heuristic - EXPLAIN itself only accepts single statements
65
- -- Limitation: Queries with semicolons inside string literals will be rejected
66
- v_clean_query := trim(query);
67
- if v_clean_query like '%;%' then
68
- -- Strip trailing semicolon if present (common user convenience)
69
- v_clean_query := regexp_replace(v_clean_query, ';\s*$', '');
70
- -- If there's still a semicolon, reject (likely multiple statements or semicolon in string)
71
- if v_clean_query like '%;%' then
72
- raise exception 'query contains semicolon (multiple statements not allowed; note: semicolons in string literals are also not supported)';
73
- end if;
74
- end if;
75
-
76
- -- Check if HypoPG extension is available
77
- if hypopg_index is not null then
78
- select exists(
79
- select 1 from pg_extension where extname = 'hypopg'
80
- ) into v_hypopg_available;
81
-
82
- if not v_hypopg_available then
83
- raise exception 'HypoPG extension is required for hypothetical index testing but is not installed';
84
- end if;
85
-
86
- -- Create hypothetical index
87
- select * into v_hypo_result from hypopg_create_index(hypopg_index);
88
- raise notice 'Created hypothetical index: % (oid: %)',
89
- v_hypo_result.indexname, v_hypo_result.indexrelid;
90
- end if;
91
-
92
- -- Build and execute EXPLAIN query
93
- -- Note: EXPLAIN is read-only (plans but doesn't execute), making this safe
94
- begin
95
- if lower(format) = 'json' then
96
- execute 'explain (verbose, settings, generic_plan, format json) ' || v_clean_query
97
- into result;
98
- else
99
- for v_line in execute 'explain (verbose, settings, generic_plan) ' || v_clean_query loop
100
- v_lines := array_append(v_lines, v_line."QUERY PLAN");
101
- end loop;
102
- result := array_to_string(v_lines, e'\n');
103
- end if;
104
- exception when others then
105
- -- Clean up hypothetical index before re-raising
106
- if hypopg_index is not null then
107
- perform hypopg_reset();
108
- end if;
109
- raise;
110
- end;
111
-
112
- -- Clean up hypothetical index
113
- if hypopg_index is not null then
114
- perform hypopg_reset();
115
- end if;
116
- end;
117
- $$;
118
-
119
- comment on function postgres_ai.explain_generic(text, text, text) is
120
- 'Returns generic EXPLAIN plan with optional HypoPG index testing (requires PG16+)';
121
-
122
- -- Grant execute to the monitoring user
123
- grant execute on function postgres_ai.explain_generic(text, text, text) to {{ROLE_IDENT}};
124
-
125
5
  /*
126
6
  * table_describe
127
7
  *
@@ -435,5 +315,3 @@ comment on function postgres_ai.table_describe(text) is
435
315
  'Returns comprehensive table information in compact text format for LLM analysis';
436
316
 
437
317
  grant execute on function postgres_ai.table_describe(text) to {{ROLE_IDENT}};
438
-
439
-