@@ -12,7 +12,7 @@ import { Client } from "pg";
1212import { startMcpServer } from "../lib/mcp-server" ;
1313import { fetchIssues , fetchIssueComments , createIssueComment , fetchIssue , createIssue , updateIssue , updateIssueComment , fetchActionItem , fetchActionItems , createActionItem , updateActionItem , type ConfigChange } from "../lib/issues" ;
1414import { 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" ;
1616import { SupabaseClient , resolveSupabaseConfig , extractProjectRefFromUrl , applyInitPlanViaSupabase , verifyInitSetupViaSupabase , fetchPoolerDatabaseUrl , type PgCompatibleError } from "../lib/supabase" ;
1717import * as pkce from "../lib/pkce" ;
1818import * as authServer from "../lib/auth-server" ;
@@ -1335,6 +1335,302 @@ program
13351335 }
13361336 } ) ;
13371337
1338+ program
1339+ . command ( "unprepare-db [conn]" )
1340+ . description ( "remove monitoring setup: drop monitoring user, views, schema, and revoke permissions" )
1341+ . option ( "--db-url <url>" , "PostgreSQL connection URL (admin) (deprecated; pass it as positional arg)" )
1342+ . option ( "-h, --host <host>" , "PostgreSQL host (psql-like)" )
1343+ . option ( "-p, --port <port>" , "PostgreSQL port (psql-like)" )
1344+ . option ( "-U, --username <username>" , "PostgreSQL user (psql-like)" )
1345+ . option ( "-d, --dbname <dbname>" , "PostgreSQL database name (psql-like)" )
1346+ . option ( "--admin-password <password>" , "Admin connection password (otherwise uses PGPASSWORD if set)" )
1347+ . option ( "--monitoring-user <name>" , "Monitoring role name to remove" , DEFAULT_MONITORING_USER )
1348+ . option ( "--keep-role" , "Keep the monitoring role (only revoke permissions and drop objects)" , false )
1349+ . option ( "--provider <provider>" , "Database provider (e.g., supabase). Affects which steps are executed." )
1350+ . option ( "--print-sql" , "Print SQL plan and exit (no changes applied)" , false )
1351+ . option ( "--force" , "Skip confirmation prompt" , false )
1352+ . option ( "--json" , "Output result as JSON (machine-readable)" , false )
1353+ . addHelpText (
1354+ "after" ,
1355+ [
1356+ "" ,
1357+ "Examples:" ,
1358+ " postgresai unprepare-db postgresql://admin@host:5432/dbname" ,
1359+ " postgresai unprepare-db \"dbname=dbname host=host user=admin\"" ,
1360+ " postgresai unprepare-db -h host -p 5432 -U admin -d dbname" ,
1361+ "" ,
1362+ "Admin password:" ,
1363+ " --admin-password <password> or PGPASSWORD=... (libpq standard)" ,
1364+ "" ,
1365+ "Keep role but remove objects/permissions:" ,
1366+ " postgresai unprepare-db <conn> --keep-role" ,
1367+ "" ,
1368+ "Inspect SQL without applying changes:" ,
1369+ " postgresai unprepare-db <conn> --print-sql" ,
1370+ "" ,
1371+ "Offline SQL plan (no DB connection):" ,
1372+ " postgresai unprepare-db --print-sql" ,
1373+ "" ,
1374+ "Skip confirmation prompt:" ,
1375+ " postgresai unprepare-db <conn> --force" ,
1376+ ] . join ( "\n" )
1377+ )
1378+ . action ( async ( conn : string | undefined , opts : {
1379+ dbUrl ?: string ;
1380+ host ?: string ;
1381+ port ?: string ;
1382+ username ?: string ;
1383+ dbname ?: string ;
1384+ adminPassword ?: string ;
1385+ monitoringUser : string ;
1386+ keepRole ?: boolean ;
1387+ provider ?: string ;
1388+ printSql ?: boolean ;
1389+ force ?: boolean ;
1390+ json ?: boolean ;
1391+ } , cmd : Command ) => {
1392+ // JSON output helper
1393+ const jsonOutput = opts . json ;
1394+ const outputJson = ( data : Record < string , unknown > ) => {
1395+ console . log ( JSON . stringify ( data , null , 2 ) ) ;
1396+ } ;
1397+ const outputError = ( error : {
1398+ message : string ;
1399+ step ?: string ;
1400+ code ?: string ;
1401+ detail ?: string ;
1402+ hint ?: string ;
1403+ } ) => {
1404+ if ( jsonOutput ) {
1405+ outputJson ( {
1406+ success : false ,
1407+ error,
1408+ } ) ;
1409+ } else {
1410+ console . error ( `Error: unprepare-db: ${ error . message } ` ) ;
1411+ if ( error . step ) console . error ( ` Step: ${ error . step } ` ) ;
1412+ if ( error . code ) console . error ( ` Code: ${ error . code } ` ) ;
1413+ if ( error . detail ) console . error ( ` Detail: ${ error . detail } ` ) ;
1414+ if ( error . hint ) console . error ( ` Hint: ${ error . hint } ` ) ;
1415+ }
1416+ process . exitCode = 1 ;
1417+ } ;
1418+
1419+ const shouldPrintSql = ! ! opts . printSql ;
1420+ const dropRole = ! opts . keepRole ;
1421+
1422+ // Validate provider and warn if unknown
1423+ const providerWarning = validateProvider ( opts . provider ) ;
1424+ if ( providerWarning ) {
1425+ console . warn ( `⚠ ${ providerWarning } ` ) ;
1426+ }
1427+
1428+ // Offline mode: allow printing SQL without providing/using an admin connection.
1429+ if ( ! conn && ! opts . dbUrl && ! opts . host && ! opts . port && ! opts . username && ! opts . adminPassword ) {
1430+ if ( shouldPrintSql ) {
1431+ const database = ( opts . dbname ?? process . env . PGDATABASE ?? "postgres" ) . trim ( ) ;
1432+
1433+ const plan = await buildUninitPlan ( {
1434+ database,
1435+ monitoringUser : opts . monitoringUser ,
1436+ dropRole,
1437+ provider : opts . provider ,
1438+ } ) ;
1439+
1440+ console . log ( "\n--- SQL plan (offline; not connected) ---" ) ;
1441+ console . log ( `-- database: ${ database } ` ) ;
1442+ console . log ( `-- monitoring user: ${ opts . monitoringUser } ` ) ;
1443+ console . log ( `-- provider: ${ opts . provider ?? "self-managed" } ` ) ;
1444+ console . log ( `-- drop role: ${ dropRole } ` ) ;
1445+ for ( const step of plan . steps ) {
1446+ console . log ( `\n-- ${ step . name } ` ) ;
1447+ console . log ( step . sql ) ;
1448+ }
1449+ console . log ( "\n--- end SQL plan ---\n" ) ;
1450+ return ;
1451+ }
1452+ }
1453+
1454+ let adminConn ;
1455+ try {
1456+ adminConn = resolveAdminConnection ( {
1457+ conn,
1458+ dbUrlFlag : opts . dbUrl ,
1459+ host : opts . host ?? process . env . PGHOST ,
1460+ port : opts . port ?? process . env . PGPORT ,
1461+ username : opts . username ?? process . env . PGUSER ,
1462+ dbname : opts . dbname ?? process . env . PGDATABASE ,
1463+ adminPassword : opts . adminPassword ,
1464+ envPassword : process . env . PGPASSWORD ,
1465+ } ) ;
1466+ } catch ( e ) {
1467+ const msg = e instanceof Error ? e . message : String ( e ) ;
1468+ if ( jsonOutput ) {
1469+ outputError ( { message : msg } ) ;
1470+ } else {
1471+ console . error ( `Error: unprepare-db: ${ msg } ` ) ;
1472+ if ( typeof msg === "string" && msg . startsWith ( "Connection is required." ) ) {
1473+ console . error ( "" ) ;
1474+ cmd . outputHelp ( { error : true } ) ;
1475+ }
1476+ process . exitCode = 1 ;
1477+ }
1478+ return ;
1479+ }
1480+
1481+ if ( ! jsonOutput ) {
1482+ console . log ( `Connecting to: ${ adminConn . display } ` ) ;
1483+ console . log ( `Monitoring user: ${ opts . monitoringUser } ` ) ;
1484+ console . log ( `Drop role: ${ dropRole } ` ) ;
1485+ }
1486+
1487+ // Confirmation prompt (unless --force or --json)
1488+ if ( ! opts . force && ! jsonOutput && ! shouldPrintSql ) {
1489+ const answer = await new Promise < string > ( ( resolve ) => {
1490+ const readline = getReadline ( ) ;
1491+ readline . question (
1492+ `This will remove the monitoring setup for user "${ opts . monitoringUser } "${ dropRole ? " and drop the role" : "" } . Continue? [y/N] ` ,
1493+ ( ans ) => resolve ( ans . trim ( ) . toLowerCase ( ) )
1494+ ) ;
1495+ } ) ;
1496+ if ( answer !== "y" && answer !== "yes" ) {
1497+ console . log ( "Aborted." ) ;
1498+ return ;
1499+ }
1500+ }
1501+
1502+ let client : Client | undefined ;
1503+ try {
1504+ const connResult = await connectWithSslFallback ( Client , adminConn ) ;
1505+ client = connResult . client ;
1506+
1507+ const dbRes = await client . query ( "select current_database() as db" ) ;
1508+ const database = dbRes . rows ?. [ 0 ] ?. db ;
1509+ if ( typeof database !== "string" || ! database ) {
1510+ throw new Error ( "Failed to resolve current database name" ) ;
1511+ }
1512+
1513+ const plan = await buildUninitPlan ( {
1514+ database,
1515+ monitoringUser : opts . monitoringUser ,
1516+ dropRole,
1517+ provider : opts . provider ,
1518+ } ) ;
1519+
1520+ if ( shouldPrintSql ) {
1521+ console . log ( "\n--- SQL plan ---" ) ;
1522+ for ( const step of plan . steps ) {
1523+ console . log ( `\n-- ${ step . name } ` ) ;
1524+ console . log ( step . sql ) ;
1525+ }
1526+ console . log ( "\n--- end SQL plan ---\n" ) ;
1527+ return ;
1528+ }
1529+
1530+ const { applied, errors } = await applyUninitPlan ( { client, plan } ) ;
1531+
1532+ if ( jsonOutput ) {
1533+ outputJson ( {
1534+ success : errors . length === 0 ,
1535+ action : "unprepare" ,
1536+ database,
1537+ monitoringUser : opts . monitoringUser ,
1538+ dropRole,
1539+ applied,
1540+ errors,
1541+ } ) ;
1542+ if ( errors . length > 0 ) {
1543+ process . exitCode = 1 ;
1544+ }
1545+ } else {
1546+ if ( errors . length === 0 ) {
1547+ console . log ( "✓ unprepare-db completed" ) ;
1548+ console . log ( `Applied ${ applied . length } steps` ) ;
1549+ } else {
1550+ console . log ( "⚠ unprepare-db completed with errors" ) ;
1551+ console . log ( `Applied ${ applied . length } steps` ) ;
1552+ console . log ( "Errors:" ) ;
1553+ for ( const err of errors ) {
1554+ console . log ( ` - ${ err } ` ) ;
1555+ }
1556+ process . exitCode = 1 ;
1557+ }
1558+ }
1559+ } catch ( error ) {
1560+ const errAny = error as any ;
1561+ let message = "" ;
1562+ if ( error instanceof Error && error . message ) {
1563+ message = error . message ;
1564+ } else if ( errAny && typeof errAny === "object" && typeof errAny . message === "string" && errAny . message ) {
1565+ message = errAny . message ;
1566+ } else {
1567+ message = String ( error ) ;
1568+ }
1569+ if ( ! message || message === "[object Object]" ) {
1570+ message = "Unknown error" ;
1571+ }
1572+
1573+ const errorObj : {
1574+ message : string ;
1575+ code ?: string ;
1576+ detail ?: string ;
1577+ hint ?: string ;
1578+ } = { message } ;
1579+
1580+ if ( errAny && typeof errAny === "object" ) {
1581+ if ( typeof errAny . code === "string" && errAny . code ) errorObj . code = errAny . code ;
1582+ if ( typeof errAny . detail === "string" && errAny . detail ) errorObj . detail = errAny . detail ;
1583+ if ( typeof errAny . hint === "string" && errAny . hint ) errorObj . hint = errAny . hint ;
1584+ }
1585+
1586+ if ( jsonOutput ) {
1587+ outputJson ( {
1588+ success : false ,
1589+ error : errorObj ,
1590+ } ) ;
1591+ process . exitCode = 1 ;
1592+ } else {
1593+ console . error ( `Error: unprepare-db: ${ message } ` ) ;
1594+ if ( errAny && typeof errAny === "object" ) {
1595+ if ( typeof errAny . code === "string" && errAny . code ) {
1596+ console . error ( ` Code: ${ errAny . code } ` ) ;
1597+ }
1598+ if ( typeof errAny . detail === "string" && errAny . detail ) {
1599+ console . error ( ` Detail: ${ errAny . detail } ` ) ;
1600+ }
1601+ if ( typeof errAny . hint === "string" && errAny . hint ) {
1602+ console . error ( ` Hint: ${ errAny . hint } ` ) ;
1603+ }
1604+ }
1605+ if ( errAny && typeof errAny === "object" && typeof errAny . code === "string" ) {
1606+ if ( errAny . code === "42501" ) {
1607+ console . error ( " Context: dropping roles/objects requires sufficient privileges" ) ;
1608+ console . error ( " Fix: connect as a superuser (or a role with appropriate DROP privileges)" ) ;
1609+ }
1610+ if ( errAny . code === "ECONNREFUSED" ) {
1611+ console . error ( " Hint: check host/port and ensure Postgres is reachable from this machine" ) ;
1612+ }
1613+ if ( errAny . code === "ENOTFOUND" ) {
1614+ console . error ( " Hint: DNS resolution failed; double-check the host name" ) ;
1615+ }
1616+ if ( errAny . code === "ETIMEDOUT" ) {
1617+ console . error ( " Hint: connection timed out; check network/firewall rules" ) ;
1618+ }
1619+ }
1620+ process . exitCode = 1 ;
1621+ }
1622+ } finally {
1623+ if ( client ) {
1624+ try {
1625+ await client . end ( ) ;
1626+ } catch {
1627+ // ignore
1628+ }
1629+ }
1630+ closeReadline ( ) ;
1631+ }
1632+ } ) ;
1633+
13381634program
13391635 . command ( "checkup [conn]" )
13401636 . description ( "generate health check reports directly from PostgreSQL (express mode)" )
0 commit comments