postgresai 0.14.0-dev.69 → 0.14.0-dev.70
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/README.md +32 -0
- package/bin/postgres-ai.ts +486 -72
- package/dist/bin/postgres-ai.js +828 -75
- package/lib/metrics-embedded.ts +1 -1
- package/lib/supabase.ts +769 -0
- package/package.json +1 -1
- package/test/supabase.test.ts +568 -0
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13064,7 +13064,7 @@ var {
|
|
|
13064
13064
|
// package.json
|
|
13065
13065
|
var package_default = {
|
|
13066
13066
|
name: "postgresai",
|
|
13067
|
-
version: "0.14.0-dev.
|
|
13067
|
+
version: "0.14.0-dev.70",
|
|
13068
13068
|
description: "postgres_ai CLI",
|
|
13069
13069
|
license: "Apache-2.0",
|
|
13070
13070
|
private: false,
|
|
@@ -15887,7 +15887,7 @@ var Result = import_lib.default.Result;
|
|
|
15887
15887
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15888
15888
|
var defaults = import_lib.default.defaults;
|
|
15889
15889
|
// package.json
|
|
15890
|
-
var version = "0.14.0-dev.
|
|
15890
|
+
var version = "0.14.0-dev.70";
|
|
15891
15891
|
var package_default2 = {
|
|
15892
15892
|
name: "postgresai",
|
|
15893
15893
|
version,
|
|
@@ -24459,6 +24459,397 @@ async function verifyInitSetup(params) {
|
|
|
24459
24459
|
}
|
|
24460
24460
|
}
|
|
24461
24461
|
|
|
24462
|
+
// lib/supabase.ts
|
|
24463
|
+
var SUPABASE_API_BASE = "https://api.supabase.com";
|
|
24464
|
+
function isValidProjectRef(ref) {
|
|
24465
|
+
return /^[a-z0-9]{10,30}$/i.test(ref);
|
|
24466
|
+
}
|
|
24467
|
+
|
|
24468
|
+
class SupabaseClient {
|
|
24469
|
+
config;
|
|
24470
|
+
constructor(config2) {
|
|
24471
|
+
if (!config2.projectRef) {
|
|
24472
|
+
throw new Error("Supabase project reference is required");
|
|
24473
|
+
}
|
|
24474
|
+
if (!config2.accessToken) {
|
|
24475
|
+
throw new Error("Supabase access token is required");
|
|
24476
|
+
}
|
|
24477
|
+
if (!isValidProjectRef(config2.projectRef)) {
|
|
24478
|
+
throw new Error(`Invalid Supabase project reference format: "${config2.projectRef}". Expected 10-30 alphanumeric characters.`);
|
|
24479
|
+
}
|
|
24480
|
+
this.config = config2;
|
|
24481
|
+
}
|
|
24482
|
+
async query(sql, readOnly = false) {
|
|
24483
|
+
const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(this.config.projectRef)}/database/query`;
|
|
24484
|
+
const response = await fetch(url, {
|
|
24485
|
+
method: "POST",
|
|
24486
|
+
headers: {
|
|
24487
|
+
"Content-Type": "application/json",
|
|
24488
|
+
Authorization: `Bearer ${this.config.accessToken}`
|
|
24489
|
+
},
|
|
24490
|
+
body: JSON.stringify({
|
|
24491
|
+
query: sql,
|
|
24492
|
+
read_only: readOnly
|
|
24493
|
+
})
|
|
24494
|
+
});
|
|
24495
|
+
const body = await response.text();
|
|
24496
|
+
let data;
|
|
24497
|
+
try {
|
|
24498
|
+
data = JSON.parse(body);
|
|
24499
|
+
} catch {
|
|
24500
|
+
throw this.createPgError({
|
|
24501
|
+
message: `Supabase API returned non-JSON response: ${body.slice(0, 200)}`,
|
|
24502
|
+
httpStatus: response.status
|
|
24503
|
+
});
|
|
24504
|
+
}
|
|
24505
|
+
if (!response.ok) {
|
|
24506
|
+
throw this.parseApiError(data, response.status);
|
|
24507
|
+
}
|
|
24508
|
+
if (data && typeof data === "object" && "error" in data && data.error) {
|
|
24509
|
+
throw this.parseApiError(data, response.status);
|
|
24510
|
+
}
|
|
24511
|
+
const rows = Array.isArray(data) ? data : [];
|
|
24512
|
+
return {
|
|
24513
|
+
rows,
|
|
24514
|
+
rowCount: rows.length
|
|
24515
|
+
};
|
|
24516
|
+
}
|
|
24517
|
+
async testConnection() {
|
|
24518
|
+
const result = await this.query("SELECT current_database() as db, version() as version", true);
|
|
24519
|
+
const row = result.rows[0] ?? {};
|
|
24520
|
+
return {
|
|
24521
|
+
database: String(row.db ?? ""),
|
|
24522
|
+
version: String(row.version ?? "")
|
|
24523
|
+
};
|
|
24524
|
+
}
|
|
24525
|
+
async getCurrentDatabase() {
|
|
24526
|
+
const result = await this.query("SELECT current_database() as db", true);
|
|
24527
|
+
const row = result.rows[0] ?? {};
|
|
24528
|
+
return String(row.db ?? "");
|
|
24529
|
+
}
|
|
24530
|
+
parseApiError(data, httpStatus) {
|
|
24531
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
24532
|
+
const errObj = "error" in data && data.error ? data.error : data;
|
|
24533
|
+
const pgCode = this.extractPgErrorCode(errObj);
|
|
24534
|
+
const message = this.extractErrorMessage(errObj);
|
|
24535
|
+
const detail = this.extractField(errObj, ["details", "detail"]);
|
|
24536
|
+
const hint = this.extractField(errObj, ["hint"]);
|
|
24537
|
+
return this.createPgError({
|
|
24538
|
+
message,
|
|
24539
|
+
code: pgCode,
|
|
24540
|
+
detail,
|
|
24541
|
+
hint,
|
|
24542
|
+
httpStatus,
|
|
24543
|
+
supabaseErrorCode: typeof errObj === "object" && errObj && "code" in errObj ? String(errObj.code ?? "") : undefined
|
|
24544
|
+
});
|
|
24545
|
+
}
|
|
24546
|
+
return this.createPgError({
|
|
24547
|
+
message: `Supabase API error (HTTP ${httpStatus})`,
|
|
24548
|
+
httpStatus
|
|
24549
|
+
});
|
|
24550
|
+
}
|
|
24551
|
+
extractPgErrorCode(errObj) {
|
|
24552
|
+
if (!errObj || typeof errObj !== "object")
|
|
24553
|
+
return;
|
|
24554
|
+
const obj = errObj;
|
|
24555
|
+
if (typeof obj.code === "string") {
|
|
24556
|
+
const code = obj.code;
|
|
24557
|
+
if (/^\d{5}$/.test(code)) {
|
|
24558
|
+
return code;
|
|
24559
|
+
}
|
|
24560
|
+
return this.mapSupabaseCodeToPg(code);
|
|
24561
|
+
}
|
|
24562
|
+
return;
|
|
24563
|
+
}
|
|
24564
|
+
mapSupabaseCodeToPg(code) {
|
|
24565
|
+
const mapping = {
|
|
24566
|
+
PGRST301: "28000",
|
|
24567
|
+
PGRST302: "28P01",
|
|
24568
|
+
"42501": "42501",
|
|
24569
|
+
PGRST000: "42501",
|
|
24570
|
+
"42601": "42601",
|
|
24571
|
+
"42P01": "42P01",
|
|
24572
|
+
PGRST200: "42P01",
|
|
24573
|
+
"42883": "42883",
|
|
24574
|
+
"08000": "08000",
|
|
24575
|
+
"08003": "08003",
|
|
24576
|
+
"08006": "08006",
|
|
24577
|
+
"42710": "42710"
|
|
24578
|
+
};
|
|
24579
|
+
return mapping[code];
|
|
24580
|
+
}
|
|
24581
|
+
extractErrorMessage(errObj) {
|
|
24582
|
+
if (!errObj || typeof errObj !== "object") {
|
|
24583
|
+
return "Unknown Supabase API error";
|
|
24584
|
+
}
|
|
24585
|
+
const obj = errObj;
|
|
24586
|
+
for (const field of ["message", "error", "msg", "description"]) {
|
|
24587
|
+
if (typeof obj[field] === "string" && obj[field]) {
|
|
24588
|
+
return obj[field];
|
|
24589
|
+
}
|
|
24590
|
+
}
|
|
24591
|
+
if (obj.error && typeof obj.error === "object") {
|
|
24592
|
+
return this.extractErrorMessage(obj.error);
|
|
24593
|
+
}
|
|
24594
|
+
return "Unknown Supabase API error";
|
|
24595
|
+
}
|
|
24596
|
+
extractField(errObj, fieldNames) {
|
|
24597
|
+
if (!errObj || typeof errObj !== "object")
|
|
24598
|
+
return;
|
|
24599
|
+
const obj = errObj;
|
|
24600
|
+
for (const field of fieldNames) {
|
|
24601
|
+
if (typeof obj[field] === "string" && obj[field]) {
|
|
24602
|
+
return obj[field];
|
|
24603
|
+
}
|
|
24604
|
+
}
|
|
24605
|
+
return;
|
|
24606
|
+
}
|
|
24607
|
+
createPgError(opts) {
|
|
24608
|
+
const err = new Error(opts.message);
|
|
24609
|
+
if (opts.code)
|
|
24610
|
+
err.code = opts.code;
|
|
24611
|
+
if (opts.detail)
|
|
24612
|
+
err.detail = opts.detail;
|
|
24613
|
+
if (opts.hint)
|
|
24614
|
+
err.hint = opts.hint;
|
|
24615
|
+
if (opts.httpStatus)
|
|
24616
|
+
err.httpStatus = opts.httpStatus;
|
|
24617
|
+
if (opts.supabaseErrorCode)
|
|
24618
|
+
err.supabaseErrorCode = opts.supabaseErrorCode;
|
|
24619
|
+
return err;
|
|
24620
|
+
}
|
|
24621
|
+
}
|
|
24622
|
+
function resolveSupabaseConfig(opts) {
|
|
24623
|
+
const accessToken = opts.accessToken?.trim() || process.env.SUPABASE_ACCESS_TOKEN?.trim() || "";
|
|
24624
|
+
const projectRef = opts.projectRef?.trim() || process.env.SUPABASE_PROJECT_REF?.trim() || "";
|
|
24625
|
+
if (!accessToken) {
|
|
24626
|
+
throw new Error(`Supabase access token is required.
|
|
24627
|
+
` + `Provide it via --supabase-access-token or SUPABASE_ACCESS_TOKEN environment variable.
|
|
24628
|
+
` + "Generate a token at: https://supabase.com/dashboard/account/tokens");
|
|
24629
|
+
}
|
|
24630
|
+
if (!projectRef) {
|
|
24631
|
+
throw new Error(`Supabase project reference is required.
|
|
24632
|
+
` + `Provide it via --supabase-project-ref or SUPABASE_PROJECT_REF environment variable.
|
|
24633
|
+
` + "Find your project ref in the Supabase dashboard URL: https://supabase.com/dashboard/project/<ref>");
|
|
24634
|
+
}
|
|
24635
|
+
return { accessToken, projectRef };
|
|
24636
|
+
}
|
|
24637
|
+
function extractProjectRefFromUrl(dbUrl) {
|
|
24638
|
+
try {
|
|
24639
|
+
const url = new URL(dbUrl);
|
|
24640
|
+
const host = url.hostname;
|
|
24641
|
+
const match = host.match(/^(?:db\.)?([^.]+)\.supabase\.co$/i);
|
|
24642
|
+
if (match && match[1]) {
|
|
24643
|
+
return match[1];
|
|
24644
|
+
}
|
|
24645
|
+
if (host.includes("pooler.supabase.com")) {
|
|
24646
|
+
const username = url.username;
|
|
24647
|
+
const userMatch = username.match(/^postgres\.([a-z0-9]+)$/i);
|
|
24648
|
+
if (userMatch && userMatch[1]) {
|
|
24649
|
+
return userMatch[1];
|
|
24650
|
+
}
|
|
24651
|
+
}
|
|
24652
|
+
const poolerMatch = host.match(/^([a-z0-9]+)\.pooler\.supabase\.com$/i);
|
|
24653
|
+
if (poolerMatch && poolerMatch[1] && !poolerMatch[1].startsWith("aws-")) {
|
|
24654
|
+
return poolerMatch[1];
|
|
24655
|
+
}
|
|
24656
|
+
return;
|
|
24657
|
+
} catch {
|
|
24658
|
+
return;
|
|
24659
|
+
}
|
|
24660
|
+
}
|
|
24661
|
+
async function applyInitPlanViaSupabase(params) {
|
|
24662
|
+
const applied = [];
|
|
24663
|
+
const skippedOptional = [];
|
|
24664
|
+
const executeStep = async (step) => {
|
|
24665
|
+
const wrappedSql = `BEGIN;
|
|
24666
|
+
${step.sql}
|
|
24667
|
+
COMMIT;`;
|
|
24668
|
+
await params.client.query(wrappedSql, false);
|
|
24669
|
+
};
|
|
24670
|
+
for (const step of params.plan.steps.filter((s) => !s.optional)) {
|
|
24671
|
+
try {
|
|
24672
|
+
if (params.verbose) {
|
|
24673
|
+
console.log(`Executing step: ${step.name}`);
|
|
24674
|
+
}
|
|
24675
|
+
await executeStep(step);
|
|
24676
|
+
applied.push(step.name);
|
|
24677
|
+
} catch (e) {
|
|
24678
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
24679
|
+
const errAny = e;
|
|
24680
|
+
const wrapped = new Error(`Failed at step "${step.name}": ${msg}`);
|
|
24681
|
+
const pgErrorFields = [
|
|
24682
|
+
"code",
|
|
24683
|
+
"detail",
|
|
24684
|
+
"hint",
|
|
24685
|
+
"position",
|
|
24686
|
+
"internalPosition",
|
|
24687
|
+
"internalQuery",
|
|
24688
|
+
"where",
|
|
24689
|
+
"schema",
|
|
24690
|
+
"table",
|
|
24691
|
+
"column",
|
|
24692
|
+
"dataType",
|
|
24693
|
+
"constraint",
|
|
24694
|
+
"file",
|
|
24695
|
+
"line",
|
|
24696
|
+
"routine",
|
|
24697
|
+
"httpStatus",
|
|
24698
|
+
"supabaseErrorCode"
|
|
24699
|
+
];
|
|
24700
|
+
for (const field of pgErrorFields) {
|
|
24701
|
+
if (errAny[field] !== undefined) {
|
|
24702
|
+
wrapped[field] = errAny[field];
|
|
24703
|
+
}
|
|
24704
|
+
}
|
|
24705
|
+
if (e instanceof Error && e.stack) {
|
|
24706
|
+
wrapped.stack = e.stack;
|
|
24707
|
+
}
|
|
24708
|
+
throw wrapped;
|
|
24709
|
+
}
|
|
24710
|
+
}
|
|
24711
|
+
for (const step of params.plan.steps.filter((s) => s.optional)) {
|
|
24712
|
+
try {
|
|
24713
|
+
if (params.verbose) {
|
|
24714
|
+
console.log(`Executing optional step: ${step.name}`);
|
|
24715
|
+
}
|
|
24716
|
+
await executeStep(step);
|
|
24717
|
+
applied.push(step.name);
|
|
24718
|
+
} catch {
|
|
24719
|
+
skippedOptional.push(step.name);
|
|
24720
|
+
}
|
|
24721
|
+
}
|
|
24722
|
+
return { applied, skippedOptional };
|
|
24723
|
+
}
|
|
24724
|
+
async function verifyInitSetupViaSupabase(params) {
|
|
24725
|
+
const missingRequired = [];
|
|
24726
|
+
const missingOptional = [];
|
|
24727
|
+
const role = params.monitoringUser;
|
|
24728
|
+
const db = params.database;
|
|
24729
|
+
if (!isValidIdentifier(role)) {
|
|
24730
|
+
throw new Error(`Invalid monitoring user name: "${role}". Must be a valid PostgreSQL identifier (letters, digits, underscores, max 63 chars, starting with letter or underscore).`);
|
|
24731
|
+
}
|
|
24732
|
+
const roleRes = await params.client.query(`SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
|
|
24733
|
+
const roleExists = roleRes.rowCount > 0;
|
|
24734
|
+
if (!roleExists) {
|
|
24735
|
+
missingRequired.push(`role "${role}" does not exist`);
|
|
24736
|
+
return { ok: false, missingRequired, missingOptional };
|
|
24737
|
+
}
|
|
24738
|
+
const connectRes = await params.client.query(`SELECT has_database_privilege('${escapeLiteral2(role)}', '${escapeLiteral2(db)}', 'CONNECT') as ok`, true);
|
|
24739
|
+
if (!connectRes.rows?.[0]?.ok) {
|
|
24740
|
+
missingRequired.push(`CONNECT on database "${db}"`);
|
|
24741
|
+
}
|
|
24742
|
+
const pgMonitorRes = await params.client.query(`SELECT pg_has_role('${escapeLiteral2(role)}', 'pg_monitor', 'member') as ok`, true);
|
|
24743
|
+
if (!pgMonitorRes.rows?.[0]?.ok) {
|
|
24744
|
+
missingRequired.push("membership in role pg_monitor");
|
|
24745
|
+
}
|
|
24746
|
+
const pgIndexRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'pg_catalog.pg_index', 'SELECT') as ok`, true);
|
|
24747
|
+
if (!pgIndexRes.rows?.[0]?.ok) {
|
|
24748
|
+
missingRequired.push("SELECT on pg_catalog.pg_index");
|
|
24749
|
+
}
|
|
24750
|
+
const schemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'postgres_ai'", true);
|
|
24751
|
+
if (schemaExistsRes.rowCount === 0) {
|
|
24752
|
+
missingRequired.push("schema postgres_ai exists");
|
|
24753
|
+
} else {
|
|
24754
|
+
const schemaPrivRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'postgres_ai', 'USAGE') as ok`, true);
|
|
24755
|
+
if (!schemaPrivRes.rows?.[0]?.ok) {
|
|
24756
|
+
missingRequired.push("USAGE on schema postgres_ai");
|
|
24757
|
+
}
|
|
24758
|
+
}
|
|
24759
|
+
const viewExistsRes = await params.client.query("SELECT to_regclass('postgres_ai.pg_statistic') IS NOT NULL as ok", true);
|
|
24760
|
+
if (!viewExistsRes.rows?.[0]?.ok) {
|
|
24761
|
+
missingRequired.push("view postgres_ai.pg_statistic exists");
|
|
24762
|
+
} else {
|
|
24763
|
+
const viewPrivRes = await params.client.query(`SELECT has_table_privilege('${escapeLiteral2(role)}', 'postgres_ai.pg_statistic', 'SELECT') as ok`, true);
|
|
24764
|
+
if (!viewPrivRes.rows?.[0]?.ok) {
|
|
24765
|
+
missingRequired.push("SELECT on view postgres_ai.pg_statistic");
|
|
24766
|
+
}
|
|
24767
|
+
}
|
|
24768
|
+
const publicSchemaExistsRes = await params.client.query("SELECT nspname FROM pg_namespace WHERE nspname = 'public'", true);
|
|
24769
|
+
if (publicSchemaExistsRes.rowCount === 0) {
|
|
24770
|
+
missingRequired.push("schema public exists");
|
|
24771
|
+
} else {
|
|
24772
|
+
const schemaUsageRes = await params.client.query(`SELECT has_schema_privilege('${escapeLiteral2(role)}', 'public', 'USAGE') as ok`, true);
|
|
24773
|
+
if (!schemaUsageRes.rows?.[0]?.ok) {
|
|
24774
|
+
missingRequired.push("USAGE on schema public");
|
|
24775
|
+
}
|
|
24776
|
+
}
|
|
24777
|
+
const rolcfgRes = await params.client.query(`SELECT rolconfig FROM pg_catalog.pg_roles WHERE rolname = '${escapeLiteral2(role)}'`, true);
|
|
24778
|
+
const rolconfig = rolcfgRes.rows?.[0]?.rolconfig;
|
|
24779
|
+
const spLine = Array.isArray(rolconfig) ? rolconfig.find((v) => String(v).startsWith("search_path=")) : undefined;
|
|
24780
|
+
if (typeof spLine !== "string" || !spLine) {
|
|
24781
|
+
missingRequired.push("role search_path is set");
|
|
24782
|
+
} else {
|
|
24783
|
+
const sp = spLine.toLowerCase();
|
|
24784
|
+
if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
|
|
24785
|
+
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
24786
|
+
}
|
|
24787
|
+
}
|
|
24788
|
+
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);
|
|
24789
|
+
if (explainFnExistsRes.rowCount === 0) {
|
|
24790
|
+
missingRequired.push("function postgres_ai.explain_generic exists");
|
|
24791
|
+
} else {
|
|
24792
|
+
const explainFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok`, true);
|
|
24793
|
+
if (!explainFnRes.rows?.[0]?.ok) {
|
|
24794
|
+
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
24795
|
+
}
|
|
24796
|
+
}
|
|
24797
|
+
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);
|
|
24798
|
+
if (tableDescribeFnExistsRes.rowCount === 0) {
|
|
24799
|
+
missingRequired.push("function postgres_ai.table_describe exists");
|
|
24800
|
+
} else {
|
|
24801
|
+
const tableDescribeFnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'postgres_ai.table_describe(text)', 'EXECUTE') as ok`, true);
|
|
24802
|
+
if (!tableDescribeFnRes.rows?.[0]?.ok) {
|
|
24803
|
+
missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
|
|
24804
|
+
}
|
|
24805
|
+
}
|
|
24806
|
+
if (params.includeOptionalPermissions) {
|
|
24807
|
+
const extRes = await params.client.query("SELECT 1 FROM pg_extension WHERE extname = 'rds_tools'", true);
|
|
24808
|
+
if (extRes.rowCount === 0) {
|
|
24809
|
+
missingOptional.push("extension rds_tools");
|
|
24810
|
+
} else {
|
|
24811
|
+
try {
|
|
24812
|
+
const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', 'rds_tools.pg_ls_multixactdir()', 'EXECUTE') as ok`, true);
|
|
24813
|
+
if (!fnRes.rows?.[0]?.ok) {
|
|
24814
|
+
missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
|
|
24815
|
+
}
|
|
24816
|
+
} catch {
|
|
24817
|
+
missingOptional.push("EXECUTE on rds_tools.pg_ls_multixactdir()");
|
|
24818
|
+
}
|
|
24819
|
+
}
|
|
24820
|
+
const optionalFns = [
|
|
24821
|
+
"pg_catalog.pg_stat_file(text)",
|
|
24822
|
+
"pg_catalog.pg_stat_file(text, boolean)",
|
|
24823
|
+
"pg_catalog.pg_ls_dir(text)",
|
|
24824
|
+
"pg_catalog.pg_ls_dir(text, boolean, boolean)"
|
|
24825
|
+
];
|
|
24826
|
+
for (const fn of optionalFns) {
|
|
24827
|
+
try {
|
|
24828
|
+
const fnRes = await params.client.query(`SELECT has_function_privilege('${escapeLiteral2(role)}', '${fn}', 'EXECUTE') as ok`, true);
|
|
24829
|
+
if (!fnRes.rows?.[0]?.ok) {
|
|
24830
|
+
missingOptional.push(`EXECUTE on ${fn}`);
|
|
24831
|
+
}
|
|
24832
|
+
} catch {
|
|
24833
|
+
missingOptional.push(`EXECUTE on ${fn}`);
|
|
24834
|
+
}
|
|
24835
|
+
}
|
|
24836
|
+
}
|
|
24837
|
+
return {
|
|
24838
|
+
ok: missingRequired.length === 0,
|
|
24839
|
+
missingRequired,
|
|
24840
|
+
missingOptional
|
|
24841
|
+
};
|
|
24842
|
+
}
|
|
24843
|
+
function isValidIdentifier(name) {
|
|
24844
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(name);
|
|
24845
|
+
}
|
|
24846
|
+
function escapeLiteral2(value) {
|
|
24847
|
+
if (value.includes("\x00")) {
|
|
24848
|
+
throw new Error("SQL literal cannot contain null bytes");
|
|
24849
|
+
}
|
|
24850
|
+
return value.replace(/'/g, "''");
|
|
24851
|
+
}
|
|
24852
|
+
|
|
24462
24853
|
// lib/pkce.ts
|
|
24463
24854
|
import * as crypto from "crypto";
|
|
24464
24855
|
function generateRandomString(length = 64) {
|
|
@@ -26743,7 +27134,7 @@ program2.command("set-default-project <project>").description("store default pro
|
|
|
26743
27134
|
writeConfig({ defaultProject: value });
|
|
26744
27135
|
console.log(`Default project saved: ${value}`);
|
|
26745
27136
|
});
|
|
26746
|
-
program2.command("prepare-db [conn]").description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)").option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)").option("-h, --host <host>", "PostgreSQL host (psql-like)").option("-p, --port <port>", "PostgreSQL port (psql-like)").option("-U, --username <username>", "PostgreSQL user (psql-like)").option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)").option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)").option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER).option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)").option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false).option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false).option("--reset-password", "Reset monitoring role password only (no other changes)", false).option("--print-sql", "Print SQL plan and exit (no changes applied)", false).option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false).addHelpText("after", [
|
|
27137
|
+
program2.command("prepare-db [conn]").description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)").option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)").option("-h, --host <host>", "PostgreSQL host (psql-like)").option("-p, --port <port>", "PostgreSQL port (psql-like)").option("-U, --username <username>", "PostgreSQL user (psql-like)").option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)").option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)").option("--monitoring-user <name>", "Monitoring role name to create/update", DEFAULT_MONITORING_USER).option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)").option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false).option("--verify", "Verify that monitoring role/permissions are in place (no changes)", false).option("--reset-password", "Reset monitoring role password only (no other changes)", false).option("--print-sql", "Print SQL plan and exit (no changes applied)", false).option("--print-password", "Print generated monitoring password (DANGEROUS in CI logs)", false).option("--supabase", "Use Supabase Management API instead of direct PostgreSQL connection", false).option("--supabase-access-token <token>", "Supabase Management API access token (or SUPABASE_ACCESS_TOKEN env)").option("--supabase-project-ref <ref>", "Supabase project reference (or SUPABASE_PROJECT_REF env)").option("--json", "Output result as JSON (machine-readable)", false).addHelpText("after", [
|
|
26747
27138
|
"",
|
|
26748
27139
|
"Examples:",
|
|
26749
27140
|
" postgresai prepare-db postgresql://admin@host:5432/dbname",
|
|
@@ -26779,17 +27170,48 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26779
27170
|
" postgresai prepare-db <conn> --reset-password --password '...'",
|
|
26780
27171
|
"",
|
|
26781
27172
|
"Offline SQL plan (no DB connection):",
|
|
26782
|
-
" postgresai prepare-db --print-sql"
|
|
27173
|
+
" postgresai prepare-db --print-sql",
|
|
27174
|
+
"",
|
|
27175
|
+
"Supabase mode (use Management API instead of direct connection):",
|
|
27176
|
+
" postgresai prepare-db --supabase --supabase-project-ref <ref>",
|
|
27177
|
+
" SUPABASE_ACCESS_TOKEN=... postgresai prepare-db --supabase --supabase-project-ref <ref>",
|
|
27178
|
+
"",
|
|
27179
|
+
" Generate a token at: https://supabase.com/dashboard/account/tokens",
|
|
27180
|
+
" Find your project ref in: https://supabase.com/dashboard/project/<ref>"
|
|
26783
27181
|
].join(`
|
|
26784
27182
|
`)).action(async (conn, opts, cmd) => {
|
|
26785
|
-
|
|
26786
|
-
|
|
27183
|
+
const jsonOutput = opts.json;
|
|
27184
|
+
const outputJson = (data) => {
|
|
27185
|
+
console.log(JSON.stringify(data, null, 2));
|
|
27186
|
+
};
|
|
27187
|
+
const outputError = (error2) => {
|
|
27188
|
+
if (jsonOutput) {
|
|
27189
|
+
outputJson({
|
|
27190
|
+
success: false,
|
|
27191
|
+
mode: opts.supabase ? "supabase" : "direct",
|
|
27192
|
+
error: error2
|
|
27193
|
+
});
|
|
27194
|
+
} else {
|
|
27195
|
+
console.error(`Error: prepare-db${opts.supabase ? " (Supabase)" : ""}: ${error2.message}`);
|
|
27196
|
+
if (error2.step)
|
|
27197
|
+
console.error(` Step: ${error2.step}`);
|
|
27198
|
+
if (error2.code)
|
|
27199
|
+
console.error(` Code: ${error2.code}`);
|
|
27200
|
+
if (error2.detail)
|
|
27201
|
+
console.error(` Detail: ${error2.detail}`);
|
|
27202
|
+
if (error2.hint)
|
|
27203
|
+
console.error(` Hint: ${error2.hint}`);
|
|
27204
|
+
if (error2.httpStatus)
|
|
27205
|
+
console.error(` HTTP Status: ${error2.httpStatus}`);
|
|
27206
|
+
}
|
|
26787
27207
|
process.exitCode = 1;
|
|
27208
|
+
};
|
|
27209
|
+
if (opts.verify && opts.resetPassword) {
|
|
27210
|
+
outputError({ message: "Provide only one of --verify or --reset-password" });
|
|
26788
27211
|
return;
|
|
26789
27212
|
}
|
|
26790
27213
|
if (opts.verify && opts.printSql) {
|
|
26791
|
-
|
|
26792
|
-
process.exitCode = 1;
|
|
27214
|
+
outputError({ message: "--verify cannot be combined with --print-sql" });
|
|
26793
27215
|
return;
|
|
26794
27216
|
}
|
|
26795
27217
|
const shouldPrintSql = !!opts.printSql;
|
|
@@ -26822,6 +27244,266 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26822
27244
|
return;
|
|
26823
27245
|
}
|
|
26824
27246
|
}
|
|
27247
|
+
if (opts.supabase) {
|
|
27248
|
+
let supabaseConfig;
|
|
27249
|
+
try {
|
|
27250
|
+
let projectRef = opts.supabaseProjectRef;
|
|
27251
|
+
if (!projectRef && conn) {
|
|
27252
|
+
projectRef = extractProjectRefFromUrl(conn);
|
|
27253
|
+
}
|
|
27254
|
+
supabaseConfig = resolveSupabaseConfig({
|
|
27255
|
+
accessToken: opts.supabaseAccessToken,
|
|
27256
|
+
projectRef
|
|
27257
|
+
});
|
|
27258
|
+
} catch (e) {
|
|
27259
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
27260
|
+
outputError({ message: msg });
|
|
27261
|
+
return;
|
|
27262
|
+
}
|
|
27263
|
+
const includeOptionalPermissions2 = !opts.skipOptionalPermissions;
|
|
27264
|
+
if (!jsonOutput) {
|
|
27265
|
+
console.log(`Supabase mode: project ref ${supabaseConfig.projectRef}`);
|
|
27266
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
27267
|
+
console.log(`Optional permissions: ${includeOptionalPermissions2 ? "enabled" : "skipped"}`);
|
|
27268
|
+
}
|
|
27269
|
+
const supabaseClient = new SupabaseClient(supabaseConfig);
|
|
27270
|
+
try {
|
|
27271
|
+
const database = await supabaseClient.getCurrentDatabase();
|
|
27272
|
+
if (!database) {
|
|
27273
|
+
throw new Error("Failed to resolve current database name");
|
|
27274
|
+
}
|
|
27275
|
+
if (!jsonOutput) {
|
|
27276
|
+
console.log(`Database: ${database}`);
|
|
27277
|
+
}
|
|
27278
|
+
if (opts.verify) {
|
|
27279
|
+
const v = await verifyInitSetupViaSupabase({
|
|
27280
|
+
client: supabaseClient,
|
|
27281
|
+
database,
|
|
27282
|
+
monitoringUser: opts.monitoringUser,
|
|
27283
|
+
includeOptionalPermissions: includeOptionalPermissions2
|
|
27284
|
+
});
|
|
27285
|
+
if (v.ok) {
|
|
27286
|
+
if (jsonOutput) {
|
|
27287
|
+
outputJson({
|
|
27288
|
+
success: true,
|
|
27289
|
+
mode: "supabase",
|
|
27290
|
+
action: "verify",
|
|
27291
|
+
database,
|
|
27292
|
+
monitoringUser: opts.monitoringUser,
|
|
27293
|
+
verified: true,
|
|
27294
|
+
missingOptional: v.missingOptional
|
|
27295
|
+
});
|
|
27296
|
+
} else {
|
|
27297
|
+
console.log("\u2713 prepare-db verify: OK");
|
|
27298
|
+
if (v.missingOptional.length > 0) {
|
|
27299
|
+
console.log("\u26A0 Optional items missing:");
|
|
27300
|
+
for (const m of v.missingOptional)
|
|
27301
|
+
console.log(`- ${m}`);
|
|
27302
|
+
}
|
|
27303
|
+
}
|
|
27304
|
+
return;
|
|
27305
|
+
}
|
|
27306
|
+
if (jsonOutput) {
|
|
27307
|
+
outputJson({
|
|
27308
|
+
success: false,
|
|
27309
|
+
mode: "supabase",
|
|
27310
|
+
action: "verify",
|
|
27311
|
+
database,
|
|
27312
|
+
monitoringUser: opts.monitoringUser,
|
|
27313
|
+
verified: false,
|
|
27314
|
+
missingRequired: v.missingRequired,
|
|
27315
|
+
missingOptional: v.missingOptional
|
|
27316
|
+
});
|
|
27317
|
+
} else {
|
|
27318
|
+
console.error("\u2717 prepare-db verify failed: missing required items");
|
|
27319
|
+
for (const m of v.missingRequired)
|
|
27320
|
+
console.error(`- ${m}`);
|
|
27321
|
+
if (v.missingOptional.length > 0) {
|
|
27322
|
+
console.error("Optional items missing:");
|
|
27323
|
+
for (const m of v.missingOptional)
|
|
27324
|
+
console.error(`- ${m}`);
|
|
27325
|
+
}
|
|
27326
|
+
}
|
|
27327
|
+
process.exitCode = 1;
|
|
27328
|
+
return;
|
|
27329
|
+
}
|
|
27330
|
+
let monPassword;
|
|
27331
|
+
let passwordGenerated = false;
|
|
27332
|
+
try {
|
|
27333
|
+
const resolved = await resolveMonitoringPassword({
|
|
27334
|
+
passwordFlag: opts.password,
|
|
27335
|
+
passwordEnv: process.env.PGAI_MON_PASSWORD,
|
|
27336
|
+
monitoringUser: opts.monitoringUser
|
|
27337
|
+
});
|
|
27338
|
+
monPassword = resolved.password;
|
|
27339
|
+
passwordGenerated = resolved.generated;
|
|
27340
|
+
if (resolved.generated) {
|
|
27341
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
|
|
27342
|
+
if (canPrint) {
|
|
27343
|
+
if (!jsonOutput) {
|
|
27344
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
27345
|
+
console.error("");
|
|
27346
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
27347
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
27348
|
+
console.error("");
|
|
27349
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
27350
|
+
}
|
|
27351
|
+
} else {
|
|
27352
|
+
console.error([
|
|
27353
|
+
`\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
27354
|
+
"",
|
|
27355
|
+
"Provide it explicitly:",
|
|
27356
|
+
" --password <password> or PGAI_MON_PASSWORD=...",
|
|
27357
|
+
"",
|
|
27358
|
+
"Or (NOT recommended) print the generated password:",
|
|
27359
|
+
" --print-password"
|
|
27360
|
+
].join(`
|
|
27361
|
+
`));
|
|
27362
|
+
process.exitCode = 1;
|
|
27363
|
+
return;
|
|
27364
|
+
}
|
|
27365
|
+
}
|
|
27366
|
+
} catch (e) {
|
|
27367
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
27368
|
+
outputError({ message: msg });
|
|
27369
|
+
return;
|
|
27370
|
+
}
|
|
27371
|
+
const plan = await buildInitPlan({
|
|
27372
|
+
database,
|
|
27373
|
+
monitoringUser: opts.monitoringUser,
|
|
27374
|
+
monitoringPassword: monPassword,
|
|
27375
|
+
includeOptionalPermissions: includeOptionalPermissions2
|
|
27376
|
+
});
|
|
27377
|
+
const supabaseApplicableSteps = plan.steps.filter((s) => s.name !== "03.optional_rds" && s.name !== "04.optional_self_managed");
|
|
27378
|
+
const effectivePlan = opts.resetPassword ? { ...plan, steps: supabaseApplicableSteps.filter((s) => s.name === "01.role") } : { ...plan, steps: supabaseApplicableSteps };
|
|
27379
|
+
if (shouldPrintSql) {
|
|
27380
|
+
console.log(`
|
|
27381
|
+
--- SQL plan ---`);
|
|
27382
|
+
for (const step of effectivePlan.steps) {
|
|
27383
|
+
console.log(`
|
|
27384
|
+
-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
27385
|
+
console.log(redactPasswords(step.sql));
|
|
27386
|
+
}
|
|
27387
|
+
console.log(`
|
|
27388
|
+
--- end SQL plan ---
|
|
27389
|
+
`);
|
|
27390
|
+
console.log("Note: passwords are redacted in the printed SQL output.");
|
|
27391
|
+
return;
|
|
27392
|
+
}
|
|
27393
|
+
const { applied, skippedOptional } = await applyInitPlanViaSupabase({
|
|
27394
|
+
client: supabaseClient,
|
|
27395
|
+
plan: effectivePlan
|
|
27396
|
+
});
|
|
27397
|
+
if (jsonOutput) {
|
|
27398
|
+
const result = {
|
|
27399
|
+
success: true,
|
|
27400
|
+
mode: "supabase",
|
|
27401
|
+
action: opts.resetPassword ? "reset-password" : "apply",
|
|
27402
|
+
database,
|
|
27403
|
+
monitoringUser: opts.monitoringUser,
|
|
27404
|
+
applied,
|
|
27405
|
+
skippedOptional,
|
|
27406
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
27407
|
+
};
|
|
27408
|
+
if (passwordGenerated) {
|
|
27409
|
+
result.generatedPassword = monPassword;
|
|
27410
|
+
}
|
|
27411
|
+
outputJson(result);
|
|
27412
|
+
} else {
|
|
27413
|
+
console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
|
|
27414
|
+
if (skippedOptional.length > 0) {
|
|
27415
|
+
console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
|
|
27416
|
+
for (const s of skippedOptional)
|
|
27417
|
+
console.log(`- ${s}`);
|
|
27418
|
+
}
|
|
27419
|
+
if (process.stdout.isTTY) {
|
|
27420
|
+
console.log(`Applied ${applied.length} steps`);
|
|
27421
|
+
}
|
|
27422
|
+
}
|
|
27423
|
+
} catch (error2) {
|
|
27424
|
+
const errAny = error2;
|
|
27425
|
+
let message = "";
|
|
27426
|
+
if (error2 instanceof Error && error2.message) {
|
|
27427
|
+
message = error2.message;
|
|
27428
|
+
} else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
|
|
27429
|
+
message = errAny.message;
|
|
27430
|
+
} else {
|
|
27431
|
+
message = String(error2);
|
|
27432
|
+
}
|
|
27433
|
+
if (!message || message === "[object Object]") {
|
|
27434
|
+
message = "Unknown error";
|
|
27435
|
+
}
|
|
27436
|
+
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
27437
|
+
const failedStep = stepMatch?.[1];
|
|
27438
|
+
const errorObj = { message };
|
|
27439
|
+
if (failedStep)
|
|
27440
|
+
errorObj.step = failedStep;
|
|
27441
|
+
if (errAny && typeof errAny === "object") {
|
|
27442
|
+
if (typeof errAny.code === "string" && errAny.code)
|
|
27443
|
+
errorObj.code = errAny.code;
|
|
27444
|
+
if (typeof errAny.detail === "string" && errAny.detail)
|
|
27445
|
+
errorObj.detail = errAny.detail;
|
|
27446
|
+
if (typeof errAny.hint === "string" && errAny.hint)
|
|
27447
|
+
errorObj.hint = errAny.hint;
|
|
27448
|
+
if (typeof errAny.httpStatus === "number")
|
|
27449
|
+
errorObj.httpStatus = errAny.httpStatus;
|
|
27450
|
+
}
|
|
27451
|
+
if (jsonOutput) {
|
|
27452
|
+
outputJson({
|
|
27453
|
+
success: false,
|
|
27454
|
+
mode: "supabase",
|
|
27455
|
+
error: errorObj
|
|
27456
|
+
});
|
|
27457
|
+
process.exitCode = 1;
|
|
27458
|
+
} else {
|
|
27459
|
+
console.error(`Error: prepare-db (Supabase): ${message}`);
|
|
27460
|
+
if (failedStep) {
|
|
27461
|
+
console.error(` Step: ${failedStep}`);
|
|
27462
|
+
}
|
|
27463
|
+
if (errAny && typeof errAny === "object") {
|
|
27464
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
27465
|
+
console.error(` Code: ${errAny.code}`);
|
|
27466
|
+
}
|
|
27467
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
27468
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
27469
|
+
}
|
|
27470
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
27471
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
27472
|
+
}
|
|
27473
|
+
if (typeof errAny.httpStatus === "number") {
|
|
27474
|
+
console.error(` HTTP Status: ${errAny.httpStatus}`);
|
|
27475
|
+
}
|
|
27476
|
+
}
|
|
27477
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
27478
|
+
if (errAny.code === "42501") {
|
|
27479
|
+
if (failedStep === "01.role") {
|
|
27480
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
27481
|
+
} else if (failedStep === "02.permissions") {
|
|
27482
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
27483
|
+
}
|
|
27484
|
+
console.error(" Fix: ensure your Supabase access token has sufficient permissions");
|
|
27485
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
27486
|
+
}
|
|
27487
|
+
}
|
|
27488
|
+
if (errAny && typeof errAny === "object" && typeof errAny.httpStatus === "number") {
|
|
27489
|
+
if (errAny.httpStatus === 401) {
|
|
27490
|
+
console.error(" Hint: invalid or expired access token; generate a new one at https://supabase.com/dashboard/account/tokens");
|
|
27491
|
+
}
|
|
27492
|
+
if (errAny.httpStatus === 403) {
|
|
27493
|
+
console.error(" Hint: access denied; check your token permissions and project access");
|
|
27494
|
+
}
|
|
27495
|
+
if (errAny.httpStatus === 404) {
|
|
27496
|
+
console.error(" Hint: project not found; verify the project reference is correct");
|
|
27497
|
+
}
|
|
27498
|
+
if (errAny.httpStatus === 429) {
|
|
27499
|
+
console.error(" Hint: rate limited; wait a moment and try again");
|
|
27500
|
+
}
|
|
27501
|
+
}
|
|
27502
|
+
process.exitCode = 1;
|
|
27503
|
+
}
|
|
27504
|
+
}
|
|
27505
|
+
return;
|
|
27506
|
+
}
|
|
26825
27507
|
let adminConn;
|
|
26826
27508
|
try {
|
|
26827
27509
|
adminConn = resolveAdminConnection({
|
|
@@ -26836,18 +27518,24 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26836
27518
|
});
|
|
26837
27519
|
} catch (e) {
|
|
26838
27520
|
const msg = e instanceof Error ? e.message : String(e);
|
|
26839
|
-
|
|
26840
|
-
|
|
26841
|
-
|
|
26842
|
-
|
|
27521
|
+
if (jsonOutput) {
|
|
27522
|
+
outputError({ message: msg });
|
|
27523
|
+
} else {
|
|
27524
|
+
console.error(`Error: prepare-db: ${msg}`);
|
|
27525
|
+
if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
|
|
27526
|
+
console.error("");
|
|
27527
|
+
cmd.outputHelp({ error: true });
|
|
27528
|
+
}
|
|
27529
|
+
process.exitCode = 1;
|
|
26843
27530
|
}
|
|
26844
|
-
process.exitCode = 1;
|
|
26845
27531
|
return;
|
|
26846
27532
|
}
|
|
26847
27533
|
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
26848
|
-
|
|
26849
|
-
|
|
26850
|
-
|
|
27534
|
+
if (!jsonOutput) {
|
|
27535
|
+
console.log(`Connecting to: ${adminConn.display}`);
|
|
27536
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
27537
|
+
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
27538
|
+
}
|
|
26851
27539
|
let client;
|
|
26852
27540
|
try {
|
|
26853
27541
|
const connResult = await connectWithSslFallback(Client, adminConn);
|
|
@@ -26865,26 +27553,52 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26865
27553
|
includeOptionalPermissions
|
|
26866
27554
|
});
|
|
26867
27555
|
if (v.ok) {
|
|
26868
|
-
|
|
26869
|
-
|
|
26870
|
-
|
|
26871
|
-
|
|
26872
|
-
|
|
27556
|
+
if (jsonOutput) {
|
|
27557
|
+
outputJson({
|
|
27558
|
+
success: true,
|
|
27559
|
+
mode: "direct",
|
|
27560
|
+
action: "verify",
|
|
27561
|
+
database,
|
|
27562
|
+
monitoringUser: opts.monitoringUser,
|
|
27563
|
+
verified: true,
|
|
27564
|
+
missingOptional: v.missingOptional
|
|
27565
|
+
});
|
|
27566
|
+
} else {
|
|
27567
|
+
console.log("\u2713 prepare-db verify: OK");
|
|
27568
|
+
if (v.missingOptional.length > 0) {
|
|
27569
|
+
console.log("\u26A0 Optional items missing:");
|
|
27570
|
+
for (const m of v.missingOptional)
|
|
27571
|
+
console.log(`- ${m}`);
|
|
27572
|
+
}
|
|
26873
27573
|
}
|
|
26874
27574
|
return;
|
|
26875
27575
|
}
|
|
26876
|
-
|
|
26877
|
-
|
|
26878
|
-
|
|
26879
|
-
|
|
26880
|
-
|
|
26881
|
-
|
|
27576
|
+
if (jsonOutput) {
|
|
27577
|
+
outputJson({
|
|
27578
|
+
success: false,
|
|
27579
|
+
mode: "direct",
|
|
27580
|
+
action: "verify",
|
|
27581
|
+
database,
|
|
27582
|
+
monitoringUser: opts.monitoringUser,
|
|
27583
|
+
verified: false,
|
|
27584
|
+
missingRequired: v.missingRequired,
|
|
27585
|
+
missingOptional: v.missingOptional
|
|
27586
|
+
});
|
|
27587
|
+
} else {
|
|
27588
|
+
console.error("\u2717 prepare-db verify failed: missing required items");
|
|
27589
|
+
for (const m of v.missingRequired)
|
|
26882
27590
|
console.error(`- ${m}`);
|
|
27591
|
+
if (v.missingOptional.length > 0) {
|
|
27592
|
+
console.error("Optional items missing:");
|
|
27593
|
+
for (const m of v.missingOptional)
|
|
27594
|
+
console.error(`- ${m}`);
|
|
27595
|
+
}
|
|
26883
27596
|
}
|
|
26884
27597
|
process.exitCode = 1;
|
|
26885
27598
|
return;
|
|
26886
27599
|
}
|
|
26887
27600
|
let monPassword;
|
|
27601
|
+
let passwordGenerated = false;
|
|
26888
27602
|
try {
|
|
26889
27603
|
const resolved = await resolveMonitoringPassword({
|
|
26890
27604
|
passwordFlag: opts.password,
|
|
@@ -26892,15 +27606,18 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26892
27606
|
monitoringUser: opts.monitoringUser
|
|
26893
27607
|
});
|
|
26894
27608
|
monPassword = resolved.password;
|
|
27609
|
+
passwordGenerated = resolved.generated;
|
|
26895
27610
|
if (resolved.generated) {
|
|
26896
|
-
const canPrint = process.stdout.isTTY || !!opts.printPassword;
|
|
27611
|
+
const canPrint = process.stdout.isTTY || !!opts.printPassword || jsonOutput;
|
|
26897
27612
|
if (canPrint) {
|
|
26898
|
-
|
|
26899
|
-
|
|
26900
|
-
|
|
26901
|
-
|
|
26902
|
-
|
|
26903
|
-
|
|
27613
|
+
if (!jsonOutput) {
|
|
27614
|
+
const shellSafe = monPassword.replace(/'/g, "'\\''");
|
|
27615
|
+
console.error("");
|
|
27616
|
+
console.error(`Generated monitoring password for ${opts.monitoringUser} (copy/paste):`);
|
|
27617
|
+
console.error(`PGAI_MON_PASSWORD='${shellSafe}'`);
|
|
27618
|
+
console.error("");
|
|
27619
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
27620
|
+
}
|
|
26904
27621
|
} else {
|
|
26905
27622
|
console.error([
|
|
26906
27623
|
`\u2717 Monitoring password was auto-generated for ${opts.monitoringUser} but not printed in non-interactive mode.`,
|
|
@@ -26918,8 +27635,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26918
27635
|
}
|
|
26919
27636
|
} catch (e) {
|
|
26920
27637
|
const msg = e instanceof Error ? e.message : String(e);
|
|
26921
|
-
|
|
26922
|
-
process.exitCode = 1;
|
|
27638
|
+
outputError({ message: msg });
|
|
26923
27639
|
return;
|
|
26924
27640
|
}
|
|
26925
27641
|
const plan = await buildInitPlan({
|
|
@@ -26944,14 +27660,31 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26944
27660
|
return;
|
|
26945
27661
|
}
|
|
26946
27662
|
const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
|
|
26947
|
-
|
|
26948
|
-
|
|
26949
|
-
|
|
26950
|
-
|
|
26951
|
-
|
|
26952
|
-
|
|
26953
|
-
|
|
26954
|
-
|
|
27663
|
+
if (jsonOutput) {
|
|
27664
|
+
const result = {
|
|
27665
|
+
success: true,
|
|
27666
|
+
mode: "direct",
|
|
27667
|
+
action: opts.resetPassword ? "reset-password" : "apply",
|
|
27668
|
+
database,
|
|
27669
|
+
monitoringUser: opts.monitoringUser,
|
|
27670
|
+
applied,
|
|
27671
|
+
skippedOptional,
|
|
27672
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
27673
|
+
};
|
|
27674
|
+
if (passwordGenerated) {
|
|
27675
|
+
result.generatedPassword = monPassword;
|
|
27676
|
+
}
|
|
27677
|
+
outputJson(result);
|
|
27678
|
+
} else {
|
|
27679
|
+
console.log(opts.resetPassword ? "\u2713 prepare-db password reset completed" : "\u2713 prepare-db completed");
|
|
27680
|
+
if (skippedOptional.length > 0) {
|
|
27681
|
+
console.log("\u26A0 Some optional steps were skipped (not supported or insufficient privileges):");
|
|
27682
|
+
for (const s of skippedOptional)
|
|
27683
|
+
console.log(`- ${s}`);
|
|
27684
|
+
}
|
|
27685
|
+
if (process.stdout.isTTY) {
|
|
27686
|
+
console.log(`Applied ${applied.length} steps`);
|
|
27687
|
+
}
|
|
26955
27688
|
}
|
|
26956
27689
|
} catch (error2) {
|
|
26957
27690
|
const errAny = error2;
|
|
@@ -26966,45 +27699,65 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
26966
27699
|
if (!message || message === "[object Object]") {
|
|
26967
27700
|
message = "Unknown error";
|
|
26968
27701
|
}
|
|
26969
|
-
console.error(`Error: prepare-db: ${message}`);
|
|
26970
27702
|
const stepMatch = typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
|
|
26971
27703
|
const failedStep = stepMatch?.[1];
|
|
26972
|
-
|
|
26973
|
-
|
|
26974
|
-
|
|
27704
|
+
const errorObj = { message };
|
|
27705
|
+
if (failedStep)
|
|
27706
|
+
errorObj.step = failedStep;
|
|
26975
27707
|
if (errAny && typeof errAny === "object") {
|
|
26976
|
-
if (typeof errAny.code === "string" && errAny.code)
|
|
26977
|
-
|
|
26978
|
-
|
|
26979
|
-
|
|
26980
|
-
|
|
26981
|
-
|
|
26982
|
-
|
|
26983
|
-
|
|
27708
|
+
if (typeof errAny.code === "string" && errAny.code)
|
|
27709
|
+
errorObj.code = errAny.code;
|
|
27710
|
+
if (typeof errAny.detail === "string" && errAny.detail)
|
|
27711
|
+
errorObj.detail = errAny.detail;
|
|
27712
|
+
if (typeof errAny.hint === "string" && errAny.hint)
|
|
27713
|
+
errorObj.hint = errAny.hint;
|
|
27714
|
+
}
|
|
27715
|
+
if (jsonOutput) {
|
|
27716
|
+
outputJson({
|
|
27717
|
+
success: false,
|
|
27718
|
+
mode: "direct",
|
|
27719
|
+
error: errorObj
|
|
27720
|
+
});
|
|
27721
|
+
process.exitCode = 1;
|
|
27722
|
+
} else {
|
|
27723
|
+
console.error(`Error: prepare-db: ${message}`);
|
|
27724
|
+
if (failedStep) {
|
|
27725
|
+
console.error(` Step: ${failedStep}`);
|
|
26984
27726
|
}
|
|
26985
|
-
|
|
26986
|
-
|
|
26987
|
-
|
|
26988
|
-
|
|
26989
|
-
|
|
26990
|
-
|
|
26991
|
-
|
|
27727
|
+
if (errAny && typeof errAny === "object") {
|
|
27728
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
27729
|
+
console.error(` Code: ${errAny.code}`);
|
|
27730
|
+
}
|
|
27731
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
27732
|
+
console.error(` Detail: ${errAny.detail}`);
|
|
27733
|
+
}
|
|
27734
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
27735
|
+
console.error(` Hint: ${errAny.hint}`);
|
|
26992
27736
|
}
|
|
26993
|
-
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
26994
|
-
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
26995
|
-
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
26996
|
-
}
|
|
26997
|
-
if (errAny.code === "ECONNREFUSED") {
|
|
26998
|
-
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
26999
|
-
}
|
|
27000
|
-
if (errAny.code === "ENOTFOUND") {
|
|
27001
|
-
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
27002
27737
|
}
|
|
27003
|
-
if (errAny.code === "
|
|
27004
|
-
|
|
27738
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
27739
|
+
if (errAny.code === "42501") {
|
|
27740
|
+
if (failedStep === "01.role") {
|
|
27741
|
+
console.error(" Context: role creation/update requires CREATEROLE or superuser");
|
|
27742
|
+
} else if (failedStep === "02.permissions") {
|
|
27743
|
+
console.error(" Context: grants/view/search_path require sufficient GRANT/DDL privileges");
|
|
27744
|
+
}
|
|
27745
|
+
console.error(" Fix: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges)");
|
|
27746
|
+
console.error(" Fix: on managed Postgres, use the provider's admin/master user");
|
|
27747
|
+
console.error(" Tip: run with --print-sql to review the exact SQL plan");
|
|
27748
|
+
}
|
|
27749
|
+
if (errAny.code === "ECONNREFUSED") {
|
|
27750
|
+
console.error(" Hint: check host/port and ensure Postgres is reachable from this machine");
|
|
27751
|
+
}
|
|
27752
|
+
if (errAny.code === "ENOTFOUND") {
|
|
27753
|
+
console.error(" Hint: DNS resolution failed; double-check the host name");
|
|
27754
|
+
}
|
|
27755
|
+
if (errAny.code === "ETIMEDOUT") {
|
|
27756
|
+
console.error(" Hint: connection timed out; check network/firewall rules");
|
|
27757
|
+
}
|
|
27005
27758
|
}
|
|
27759
|
+
process.exitCode = 1;
|
|
27006
27760
|
}
|
|
27007
|
-
process.exitCode = 1;
|
|
27008
27761
|
} finally {
|
|
27009
27762
|
if (client) {
|
|
27010
27763
|
try {
|