postgresai 0.14.0-dev.74 → 0.14.0-dev.76

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.
Files changed (32) hide show
  1. package/bin/postgres-ai.ts +312 -6
  2. package/dist/bin/postgres-ai.js +327 -21
  3. package/dist/sql/02.extensions.sql +8 -0
  4. package/dist/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
  5. package/dist/sql/sql/02.extensions.sql +8 -0
  6. package/dist/sql/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
  7. package/dist/sql/sql/uninit/01.helpers.sql +5 -0
  8. package/dist/sql/sql/uninit/02.permissions.sql +30 -0
  9. package/dist/sql/sql/uninit/03.role.sql +27 -0
  10. package/dist/sql/uninit/01.helpers.sql +5 -0
  11. package/dist/sql/uninit/02.permissions.sql +30 -0
  12. package/dist/sql/uninit/03.role.sql +27 -0
  13. package/lib/init.ts +109 -8
  14. package/lib/metrics-embedded.ts +1 -1
  15. package/lib/supabase.ts +2 -10
  16. package/package.json +1 -1
  17. package/sql/02.extensions.sql +8 -0
  18. package/sql/{02.permissions.sql → 03.permissions.sql} +1 -0
  19. package/sql/uninit/01.helpers.sql +5 -0
  20. package/sql/uninit/02.permissions.sql +30 -0
  21. package/sql/uninit/03.role.sql +27 -0
  22. package/test/init.test.ts +245 -11
  23. package/test/supabase.test.ts +0 -59
  24. /package/dist/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
  25. /package/dist/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
  26. /package/dist/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
  27. /package/dist/sql/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
  28. /package/dist/sql/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
  29. /package/dist/sql/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
  30. /package/sql/{03.optional_rds.sql → 04.optional_rds.sql} +0 -0
  31. /package/sql/{04.optional_self_managed.sql → 05.optional_self_managed.sql} +0 -0
  32. /package/sql/{05.helpers.sql → 06.helpers.sql} +0 -0
@@ -12,7 +12,7 @@ import { Client } from "pg";
12
12
  import { startMcpServer } from "../lib/mcp-server";
13
13
  import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment, fetchActionItem, fetchActionItems, createActionItem, updateActionItem, type ConfigChange } from "../lib/issues";
14
14
  import { resolveBaseUrls } from "../lib/util";
15
- import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
15
+ import { applyInitPlan, applyUninitPlan, buildInitPlan, buildUninitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, KNOWN_PROVIDERS, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, validateProvider, verifyInitSetup } from "../lib/init";
16
16
  import { SupabaseClient, resolveSupabaseConfig, extractProjectRefFromUrl, applyInitPlanViaSupabase, verifyInitSetupViaSupabase, fetchPoolerDatabaseUrl, type PgCompatibleError } from "../lib/supabase";
17
17
  import * as pkce from "../lib/pkce";
18
18
  import * as authServer from "../lib/auth-server";
@@ -1008,12 +1008,17 @@ program
1008
1008
  if (errAny.code === "42501") {
1009
1009
  if (failedStep === "01.role") {
1010
1010
  console.error(" Context: role creation/update requires CREATEROLE or superuser");
1011
- } else if (failedStep === "02.permissions") {
1011
+ } else if (failedStep === "03.permissions") {
1012
1012
  console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
1013
1013
  }
1014
1014
  console.error(" Fix: ensure your Supabase access token has sufficient permissions");
1015
1015
  console.error(" Tip: run with --print-sql to review the exact SQL plan");
1016
1016
  }
1017
+ // Schema already exists (42P06) or other duplicate object errors
1018
+ if (errAny.code === "42P06" || (message.includes("already exists") && failedStep === "03.permissions")) {
1019
+ console.error(" Hint: postgres_ai schema or objects already exist from a previous setup.");
1020
+ console.error(" Fix: run 'postgresai unprepare-db <connection>' first to clean up, then retry prepare-db.");
1021
+ }
1017
1022
  }
1018
1023
  if (errAny && typeof errAny === "object" && typeof errAny.httpStatus === "number") {
1019
1024
  if (errAny.httpStatus === 401) {
@@ -1305,13 +1310,313 @@ program
1305
1310
  if (errAny.code === "42501") {
1306
1311
  if (failedStep === "01.role") {
1307
1312
  console.error(" Context: role creation/update requires CREATEROLE or superuser");
1308
- } else if (failedStep === "02.permissions") {
1313
+ } else if (failedStep === "03.permissions") {
1309
1314
  console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
1310
1315
  }
1311
1316
  console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
1312
1317
  console.error(" Fix: on managed Postgres, use the provider's admin/master user");
1313
1318
  console.error(" Tip: run with --print-sql to review the exact SQL plan");
1314
1319
  }
1320
+ // Schema already exists (42P06) or other duplicate object errors
1321
+ if (errAny.code === "42P06" || (message.includes("already exists") && failedStep === "03.permissions")) {
1322
+ console.error(" Hint: postgres_ai schema or objects already exist from a previous setup.");
1323
+ console.error(" Fix: run 'postgresai unprepare-db <connection>' first to clean up, then retry prepare-db.");
1324
+ }
1325
+ if (errAny.code === "ECONNREFUSED") {
1326
+ console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
1327
+ }
1328
+ if (errAny.code === "ENOTFOUND") {
1329
+ console.error(" Hint: DNS resolution failed; double-check the host name");
1330
+ }
1331
+ if (errAny.code === "ETIMEDOUT") {
1332
+ console.error(" Hint: connection timed out; check network/firewall rules");
1333
+ }
1334
+ }
1335
+ process.exitCode = 1;
1336
+ }
1337
+ } finally {
1338
+ if (client) {
1339
+ try {
1340
+ await client.end();
1341
+ } catch {
1342
+ // ignore
1343
+ }
1344
+ }
1345
+ }
1346
+ });
1347
+
1348
+ program
1349
+ .command("unprepare-db [conn]")
1350
+ .description("remove monitoring setup: drop monitoring user, views, schema, and revoke permissions")
1351
+ .option("--db-url <url>", "PostgreSQL connection URL (admin) (deprecated; pass it as positional arg)")
1352
+ .option("-h, --host <host>", "PostgreSQL host (psql-like)")
1353
+ .option("-p, --port <port>", "PostgreSQL port (psql-like)")
1354
+ .option("-U, --username <username>", "PostgreSQL user (psql-like)")
1355
+ .option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
1356
+ .option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)")
1357
+ .option("--monitoring-user <name>", "Monitoring role name to remove", DEFAULT_MONITORING_USER)
1358
+ .option("--keep-role", "Keep the monitoring role (only revoke permissions and drop objects)", false)
1359
+ .option("--provider <provider>", "Database provider (e.g., supabase). Affects which steps are executed.")
1360
+ .option("--print-sql", "Print SQL plan and exit (no changes applied)", false)
1361
+ .option("--force", "Skip confirmation prompt", false)
1362
+ .option("--json", "Output result as JSON (machine-readable)", false)
1363
+ .addHelpText(
1364
+ "after",
1365
+ [
1366
+ "",
1367
+ "Examples:",
1368
+ " postgresai unprepare-db postgresql://admin@host:5432/dbname",
1369
+ " postgresai unprepare-db \"dbname=dbname host=host user=admin\"",
1370
+ " postgresai unprepare-db -h host -p 5432 -U admin -d dbname",
1371
+ "",
1372
+ "Admin password:",
1373
+ " --admin-password <password> or PGPASSWORD=... (libpq standard)",
1374
+ "",
1375
+ "Keep role but remove objects/permissions:",
1376
+ " postgresai unprepare-db <conn> --keep-role",
1377
+ "",
1378
+ "Inspect SQL without applying changes:",
1379
+ " postgresai unprepare-db <conn> --print-sql",
1380
+ "",
1381
+ "Offline SQL plan (no DB connection):",
1382
+ " postgresai unprepare-db --print-sql",
1383
+ "",
1384
+ "Skip confirmation prompt:",
1385
+ " postgresai unprepare-db <conn> --force",
1386
+ ].join("\n")
1387
+ )
1388
+ .action(async (conn: string | undefined, opts: {
1389
+ dbUrl?: string;
1390
+ host?: string;
1391
+ port?: string;
1392
+ username?: string;
1393
+ dbname?: string;
1394
+ adminPassword?: string;
1395
+ monitoringUser: string;
1396
+ keepRole?: boolean;
1397
+ provider?: string;
1398
+ printSql?: boolean;
1399
+ force?: boolean;
1400
+ json?: boolean;
1401
+ }, cmd: Command) => {
1402
+ // JSON output helper
1403
+ const jsonOutput = opts.json;
1404
+ const outputJson = (data: Record<string, unknown>) => {
1405
+ console.log(JSON.stringify(data, null, 2));
1406
+ };
1407
+ const outputError = (error: {
1408
+ message: string;
1409
+ step?: string;
1410
+ code?: string;
1411
+ detail?: string;
1412
+ hint?: string;
1413
+ }) => {
1414
+ if (jsonOutput) {
1415
+ outputJson({
1416
+ success: false,
1417
+ error,
1418
+ });
1419
+ } else {
1420
+ console.error(`Error: unprepare-db: ${error.message}`);
1421
+ if (error.step) console.error(` Step: ${error.step}`);
1422
+ if (error.code) console.error(` Code: ${error.code}`);
1423
+ if (error.detail) console.error(` Detail: ${error.detail}`);
1424
+ if (error.hint) console.error(` Hint: ${error.hint}`);
1425
+ }
1426
+ process.exitCode = 1;
1427
+ };
1428
+
1429
+ const shouldPrintSql = !!opts.printSql;
1430
+ const dropRole = !opts.keepRole;
1431
+
1432
+ // Validate provider and warn if unknown
1433
+ const providerWarning = validateProvider(opts.provider);
1434
+ if (providerWarning) {
1435
+ console.warn(`⚠ ${providerWarning}`);
1436
+ }
1437
+
1438
+ // Offline mode: allow printing SQL without providing/using an admin connection.
1439
+ if (!conn && !opts.dbUrl && !opts.host && !opts.port && !opts.username && !opts.adminPassword) {
1440
+ if (shouldPrintSql) {
1441
+ const database = (opts.dbname ?? process.env.PGDATABASE ?? "postgres").trim();
1442
+
1443
+ const plan = await buildUninitPlan({
1444
+ database,
1445
+ monitoringUser: opts.monitoringUser,
1446
+ dropRole,
1447
+ provider: opts.provider,
1448
+ });
1449
+
1450
+ console.log("\n--- SQL plan (offline; not connected) ---");
1451
+ console.log(`-- database: ${database}`);
1452
+ console.log(`-- monitoring user: ${opts.monitoringUser}`);
1453
+ console.log(`-- provider: ${opts.provider ?? "self-managed"}`);
1454
+ console.log(`-- drop role: ${dropRole}`);
1455
+ for (const step of plan.steps) {
1456
+ console.log(`\n-- ${step.name}`);
1457
+ console.log(step.sql);
1458
+ }
1459
+ console.log("\n--- end SQL plan ---\n");
1460
+ return;
1461
+ }
1462
+ }
1463
+
1464
+ let adminConn;
1465
+ try {
1466
+ adminConn = resolveAdminConnection({
1467
+ conn,
1468
+ dbUrlFlag: opts.dbUrl,
1469
+ host: opts.host ?? process.env.PGHOST,
1470
+ port: opts.port ?? process.env.PGPORT,
1471
+ username: opts.username ?? process.env.PGUSER,
1472
+ dbname: opts.dbname ?? process.env.PGDATABASE,
1473
+ adminPassword: opts.adminPassword,
1474
+ envPassword: process.env.PGPASSWORD,
1475
+ });
1476
+ } catch (e) {
1477
+ const msg = e instanceof Error ? e.message : String(e);
1478
+ if (jsonOutput) {
1479
+ outputError({ message: msg });
1480
+ } else {
1481
+ console.error(`Error: unprepare-db: ${msg}`);
1482
+ if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
1483
+ console.error("");
1484
+ cmd.outputHelp({ error: true });
1485
+ }
1486
+ process.exitCode = 1;
1487
+ }
1488
+ return;
1489
+ }
1490
+
1491
+ if (!jsonOutput) {
1492
+ console.log(`Connecting to: ${adminConn.display}`);
1493
+ console.log(`Monitoring user: ${opts.monitoringUser}`);
1494
+ console.log(`Drop role: ${dropRole}`);
1495
+ }
1496
+
1497
+ // Confirmation prompt (unless --force or --json)
1498
+ if (!opts.force && !jsonOutput && !shouldPrintSql) {
1499
+ const answer = await new Promise<string>((resolve) => {
1500
+ const readline = getReadline();
1501
+ readline.question(
1502
+ `This will remove the monitoring setup for user "${opts.monitoringUser}"${dropRole ? " and drop the role" : ""}. Continue? [y/N] `,
1503
+ (ans) => resolve(ans.trim().toLowerCase())
1504
+ );
1505
+ });
1506
+ if (answer !== "y" && answer !== "yes") {
1507
+ console.log("Aborted.");
1508
+ return;
1509
+ }
1510
+ }
1511
+
1512
+ let client: Client | undefined;
1513
+ try {
1514
+ const connResult = await connectWithSslFallback(Client, adminConn);
1515
+ client = connResult.client;
1516
+
1517
+ const dbRes = await client.query("select current_database() as db");
1518
+ const database = dbRes.rows?.[0]?.db;
1519
+ if (typeof database !== "string" || !database) {
1520
+ throw new Error("Failed to resolve current database name");
1521
+ }
1522
+
1523
+ const plan = await buildUninitPlan({
1524
+ database,
1525
+ monitoringUser: opts.monitoringUser,
1526
+ dropRole,
1527
+ provider: opts.provider,
1528
+ });
1529
+
1530
+ if (shouldPrintSql) {
1531
+ console.log("\n--- SQL plan ---");
1532
+ for (const step of plan.steps) {
1533
+ console.log(`\n-- ${step.name}`);
1534
+ console.log(step.sql);
1535
+ }
1536
+ console.log("\n--- end SQL plan ---\n");
1537
+ return;
1538
+ }
1539
+
1540
+ const { applied, errors } = await applyUninitPlan({ client, plan });
1541
+
1542
+ if (jsonOutput) {
1543
+ outputJson({
1544
+ success: errors.length === 0,
1545
+ action: "unprepare",
1546
+ database,
1547
+ monitoringUser: opts.monitoringUser,
1548
+ dropRole,
1549
+ applied,
1550
+ errors,
1551
+ });
1552
+ if (errors.length > 0) {
1553
+ process.exitCode = 1;
1554
+ }
1555
+ } else {
1556
+ if (errors.length === 0) {
1557
+ console.log("✓ unprepare-db completed");
1558
+ console.log(`Applied ${applied.length} steps`);
1559
+ } else {
1560
+ console.log("⚠ unprepare-db completed with errors");
1561
+ console.log(`Applied ${applied.length} steps`);
1562
+ console.log("Errors:");
1563
+ for (const err of errors) {
1564
+ console.log(` - ${err}`);
1565
+ }
1566
+ process.exitCode = 1;
1567
+ }
1568
+ }
1569
+ } catch (error) {
1570
+ const errAny = error as any;
1571
+ let message = "";
1572
+ if (error instanceof Error && error.message) {
1573
+ message = error.message;
1574
+ } else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
1575
+ message = errAny.message;
1576
+ } else {
1577
+ message = String(error);
1578
+ }
1579
+ if (!message || message === "[object Object]") {
1580
+ message = "Unknown error";
1581
+ }
1582
+
1583
+ const errorObj: {
1584
+ message: string;
1585
+ code?: string;
1586
+ detail?: string;
1587
+ hint?: string;
1588
+ } = { message };
1589
+
1590
+ if (errAny && typeof errAny === "object") {
1591
+ if (typeof errAny.code === "string" && errAny.code) errorObj.code = errAny.code;
1592
+ if (typeof errAny.detail === "string" && errAny.detail) errorObj.detail = errAny.detail;
1593
+ if (typeof errAny.hint === "string" && errAny.hint) errorObj.hint = errAny.hint;
1594
+ }
1595
+
1596
+ if (jsonOutput) {
1597
+ outputJson({
1598
+ success: false,
1599
+ error: errorObj,
1600
+ });
1601
+ process.exitCode = 1;
1602
+ } else {
1603
+ console.error(`Error: unprepare-db: ${message}`);
1604
+ if (errAny && typeof errAny === "object") {
1605
+ if (typeof errAny.code === "string" && errAny.code) {
1606
+ console.error(` Code: ${errAny.code}`);
1607
+ }
1608
+ if (typeof errAny.detail === "string" && errAny.detail) {
1609
+ console.error(` Detail: ${errAny.detail}`);
1610
+ }
1611
+ if (typeof errAny.hint === "string" && errAny.hint) {
1612
+ console.error(` Hint: ${errAny.hint}`);
1613
+ }
1614
+ }
1615
+ if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
1616
+ if (errAny.code === "42501") {
1617
+ console.error(" Context: dropping roles/objects requires sufficient privileges");
1618
+ console.error(" Fix: connect as a superuser (or a role with appropriate DROP privileges)");
1619
+ }
1315
1620
  if (errAny.code === "ECONNREFUSED") {
1316
1621
  console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
1317
1622
  }
@@ -1332,6 +1637,7 @@ program
1332
1637
  // ignore
1333
1638
  }
1334
1639
  }
1640
+ closeReadline();
1335
1641
  }
1336
1642
  });
1337
1643
 
@@ -1518,7 +1824,7 @@ async function resolveOrInitPaths(): Promise<PathResolution> {
1518
1824
  */
1519
1825
  function isDockerRunning(): boolean {
1520
1826
  try {
1521
- const result = spawnSync("docker", ["info"], { stdio: "pipe" });
1827
+ const result = spawnSync("docker", ["info"], { stdio: "pipe", timeout: 5000 });
1522
1828
  return result.status === 0;
1523
1829
  } catch {
1524
1830
  return false;
@@ -1530,7 +1836,7 @@ function isDockerRunning(): boolean {
1530
1836
  */
1531
1837
  function getComposeCmd(): string[] | null {
1532
1838
  const tryCmd = (cmd: string, args: string[]): boolean =>
1533
- spawnSync(cmd, args, { stdio: "ignore" }).status === 0;
1839
+ spawnSync(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
1534
1840
  if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
1535
1841
  if (tryCmd("docker", ["compose", "version"])) return ["docker", "compose"];
1536
1842
  return null;
@@ -1544,7 +1850,7 @@ function checkRunningContainers(): { running: boolean; containers: string[] } {
1544
1850
  const result = spawnSync(
1545
1851
  "docker",
1546
1852
  ["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"],
1547
- { stdio: "pipe", encoding: "utf8" }
1853
+ { stdio: "pipe", encoding: "utf8", timeout: 5000 }
1548
1854
  );
1549
1855
 
1550
1856
  if (result.status === 0 && result.stdout) {