postgresai 0.14.0-dev.50 → 0.14.0-dev.52
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/bin/postgres-ai.ts +0 -324
- package/bun.lock +1 -3
- package/dist/bin/postgres-ai.js +171 -1711
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.permissions.sql +37 -0
- package/dist/sql/03.optional_rds.sql +6 -0
- package/dist/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/05.helpers.sql +415 -0
- package/lib/auth-server.ts +75 -65
- package/lib/config.ts +0 -3
- package/package.json +2 -4
- package/test/init.integration.test.ts +6 -6
- package/lib/checkup-api.ts +0 -175
- package/lib/checkup.ts +0 -1139
- package/lib/metrics-loader.ts +0 -514
- package/test/checkup.test.ts +0 -1016
- package/test/schema-validation.test.ts +0 -260
package/bin/postgres-ai.ts
CHANGED
|
@@ -7,7 +7,6 @@ import * as yaml from "js-yaml";
|
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import * as path from "path";
|
|
9
9
|
import * as os from "os";
|
|
10
|
-
import * as crypto from "node:crypto";
|
|
11
10
|
import { Client } from "pg";
|
|
12
11
|
import { startMcpServer } from "../lib/mcp-server";
|
|
13
12
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
|
|
@@ -18,8 +17,6 @@ import * as authServer from "../lib/auth-server";
|
|
|
18
17
|
import { maskSecret } from "../lib/util";
|
|
19
18
|
import { createInterface } from "readline";
|
|
20
19
|
import * as childProcess from "child_process";
|
|
21
|
-
import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checkup";
|
|
22
|
-
import { createCheckupReport, uploadCheckupReportJson, RpcError, formatRpcErrorForDisplay } from "../lib/checkup-api";
|
|
23
20
|
|
|
24
21
|
// Singleton readline interface for stdin prompts
|
|
25
22
|
let rl: ReturnType<typeof createInterface> | null = null;
|
|
@@ -112,62 +109,6 @@ async function question(prompt: string): Promise<string> {
|
|
|
112
109
|
});
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
function expandHomePath(p: string): string {
|
|
116
|
-
const s = (p || "").trim();
|
|
117
|
-
if (!s) return s;
|
|
118
|
-
if (s === "~") return os.homedir();
|
|
119
|
-
if (s.startsWith("~/") || s.startsWith("~\\")) {
|
|
120
|
-
return path.join(os.homedir(), s.slice(2));
|
|
121
|
-
}
|
|
122
|
-
return s;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function createTtySpinner(
|
|
126
|
-
enabled: boolean,
|
|
127
|
-
initialText: string
|
|
128
|
-
): { update: (text: string) => void; stop: (finalText?: string) => void } {
|
|
129
|
-
if (!enabled) {
|
|
130
|
-
return {
|
|
131
|
-
update: () => {},
|
|
132
|
-
stop: () => {},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const frames = ["|", "/", "-", "\\"];
|
|
137
|
-
const startTs = Date.now();
|
|
138
|
-
let text = initialText;
|
|
139
|
-
let frameIdx = 0;
|
|
140
|
-
let stopped = false;
|
|
141
|
-
|
|
142
|
-
const render = (): void => {
|
|
143
|
-
if (stopped) return;
|
|
144
|
-
const elapsedSec = ((Date.now() - startTs) / 1000).toFixed(1);
|
|
145
|
-
const frame = frames[frameIdx % frames.length]!;
|
|
146
|
-
frameIdx += 1;
|
|
147
|
-
process.stdout.write(`\r\x1b[2K${frame} ${text} (${elapsedSec}s)`);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const timer = setInterval(render, 120);
|
|
151
|
-
render(); // immediate feedback
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
update: (t: string) => {
|
|
155
|
-
text = t;
|
|
156
|
-
render();
|
|
157
|
-
},
|
|
158
|
-
stop: (finalText?: string) => {
|
|
159
|
-
if (stopped) return;
|
|
160
|
-
stopped = true;
|
|
161
|
-
clearInterval(timer);
|
|
162
|
-
process.stdout.write("\r\x1b[2K");
|
|
163
|
-
if (finalText && finalText.trim()) {
|
|
164
|
-
process.stdout.write(finalText);
|
|
165
|
-
}
|
|
166
|
-
process.stdout.write("\n");
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
112
|
/**
|
|
172
113
|
* CLI configuration options
|
|
173
114
|
*/
|
|
@@ -345,20 +286,6 @@ program
|
|
|
345
286
|
"UI base URL for browser routes (overrides PGAI_UI_BASE_URL)"
|
|
346
287
|
);
|
|
347
288
|
|
|
348
|
-
program
|
|
349
|
-
.command("set-default-project <project>")
|
|
350
|
-
.description("store default project for checkup uploads")
|
|
351
|
-
.action(async (project: string) => {
|
|
352
|
-
const value = (project || "").trim();
|
|
353
|
-
if (!value) {
|
|
354
|
-
console.error("Error: project is required");
|
|
355
|
-
process.exitCode = 1;
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
config.writeConfig({ defaultProject: value });
|
|
359
|
-
console.log(`Default project saved: ${value}`);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
289
|
program
|
|
363
290
|
.command("prepare-db [conn]")
|
|
364
291
|
.description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)")
|
|
@@ -686,251 +613,6 @@ program
|
|
|
686
613
|
}
|
|
687
614
|
});
|
|
688
615
|
|
|
689
|
-
program
|
|
690
|
-
.command("checkup [conn]")
|
|
691
|
-
.description("generate health check reports directly from PostgreSQL (express mode)")
|
|
692
|
-
.option("--check-id <id>", `specific check to run: ${Object.keys(CHECK_INFO).join(", ")}, or ALL`, "ALL")
|
|
693
|
-
.option("--node-name <name>", "node name for reports", "node-01")
|
|
694
|
-
.option("--output <path>", "output directory for JSON files")
|
|
695
|
-
.option("--[no-]upload", "upload JSON results to PostgresAI (default: enabled; requires API key)", undefined)
|
|
696
|
-
.option(
|
|
697
|
-
"--project <project>",
|
|
698
|
-
"project name or ID for remote upload (used with --upload; defaults to config defaultProject; auto-generated on first run)"
|
|
699
|
-
)
|
|
700
|
-
.option("--json", "output JSON to stdout (implies --no-upload)")
|
|
701
|
-
.addHelpText(
|
|
702
|
-
"after",
|
|
703
|
-
[
|
|
704
|
-
"",
|
|
705
|
-
"Available checks:",
|
|
706
|
-
...Object.entries(CHECK_INFO).map(([id, title]) => ` ${id}: ${title}`),
|
|
707
|
-
"",
|
|
708
|
-
"Examples:",
|
|
709
|
-
" postgresai checkup postgresql://user:pass@host:5432/db",
|
|
710
|
-
" postgresai checkup postgresql://user:pass@host:5432/db --check-id A003",
|
|
711
|
-
" postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
|
|
712
|
-
" postgresai checkup postgresql://user:pass@host:5432/db --project my_project",
|
|
713
|
-
" postgresai set-default-project my_project",
|
|
714
|
-
" postgresai checkup postgresql://user:pass@host:5432/db",
|
|
715
|
-
" postgresai checkup postgresql://user:pass@host:5432/db --no-upload --json",
|
|
716
|
-
].join("\n")
|
|
717
|
-
)
|
|
718
|
-
.action(async (conn: string | undefined, opts: {
|
|
719
|
-
checkId: string;
|
|
720
|
-
nodeName: string;
|
|
721
|
-
output?: string;
|
|
722
|
-
upload?: boolean;
|
|
723
|
-
project?: string;
|
|
724
|
-
json?: boolean;
|
|
725
|
-
}, cmd: Command) => {
|
|
726
|
-
if (!conn) {
|
|
727
|
-
// No args — show help like other commands do (instead of a bare error).
|
|
728
|
-
cmd.outputHelp();
|
|
729
|
-
process.exitCode = 1;
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const shouldPrintJson = !!opts.json;
|
|
734
|
-
// `--json` implies "local output" mode — do not upload by default.
|
|
735
|
-
const shouldUpload = opts.upload !== false && !shouldPrintJson;
|
|
736
|
-
const generateDefaultProjectName = (): string => {
|
|
737
|
-
// Must start with a letter; use only letters/numbers/underscores.
|
|
738
|
-
return `project_${crypto.randomBytes(6).toString("hex")}`;
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
// Preflight: validate/create output directory BEFORE connecting / running checks.
|
|
742
|
-
// This avoids waiting on network/DB work only to fail at the very end.
|
|
743
|
-
let outputPath: string | undefined;
|
|
744
|
-
if (opts.output) {
|
|
745
|
-
const outputDir = expandHomePath(opts.output);
|
|
746
|
-
outputPath = path.isAbsolute(outputDir) ? outputDir : path.resolve(process.cwd(), outputDir);
|
|
747
|
-
if (!fs.existsSync(outputPath)) {
|
|
748
|
-
try {
|
|
749
|
-
fs.mkdirSync(outputPath, { recursive: true });
|
|
750
|
-
} catch (e) {
|
|
751
|
-
const errAny = e as any;
|
|
752
|
-
const code = typeof errAny?.code === "string" ? errAny.code : "";
|
|
753
|
-
const msg = errAny instanceof Error ? errAny.message : String(errAny);
|
|
754
|
-
if (code === "EACCES" || code === "EPERM" || code === "ENOENT") {
|
|
755
|
-
console.error(`Error: Failed to create output directory: ${outputPath}`);
|
|
756
|
-
console.error(`Reason: ${msg}`);
|
|
757
|
-
console.error("Tip: choose a writable path, e.g. --output ./reports or --output ~/reports");
|
|
758
|
-
process.exitCode = 1;
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
throw e;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Preflight: validate upload flags/credentials BEFORE connecting / running checks.
|
|
767
|
-
// This allows "fast-fail" for missing API key / project name.
|
|
768
|
-
let uploadCfg:
|
|
769
|
-
| { apiKey: string; apiBaseUrl: string; project: string; epoch: number }
|
|
770
|
-
| undefined;
|
|
771
|
-
let projectWasGenerated = false;
|
|
772
|
-
if (shouldUpload) {
|
|
773
|
-
const rootOpts = program.opts() as CliOptions;
|
|
774
|
-
const { apiKey } = getConfig(rootOpts);
|
|
775
|
-
if (!apiKey) {
|
|
776
|
-
console.error("Error: API key is required for upload");
|
|
777
|
-
console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
|
|
778
|
-
process.exitCode = 1;
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const cfg = config.readConfig();
|
|
783
|
-
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
784
|
-
let project = ((opts.project || cfg.defaultProject) || "").trim();
|
|
785
|
-
if (!project) {
|
|
786
|
-
project = generateDefaultProjectName();
|
|
787
|
-
projectWasGenerated = true;
|
|
788
|
-
try {
|
|
789
|
-
config.writeConfig({ defaultProject: project });
|
|
790
|
-
} catch (e) {
|
|
791
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
792
|
-
console.error(`Warning: Failed to save generated default project: ${message}`);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
uploadCfg = {
|
|
796
|
-
apiKey,
|
|
797
|
-
apiBaseUrl,
|
|
798
|
-
project,
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// Use the same SSL behavior as prepare-db:
|
|
803
|
-
// - Default: sslmode=prefer (try SSL first, fallback to non-SSL)
|
|
804
|
-
// - Respect PGSSLMODE env and ?sslmode=... in connection URI
|
|
805
|
-
const adminConn = resolveAdminConnection({
|
|
806
|
-
conn,
|
|
807
|
-
envPassword: process.env.PGPASSWORD,
|
|
808
|
-
});
|
|
809
|
-
let client: Client | undefined;
|
|
810
|
-
const spinnerEnabled = !!process.stdout.isTTY && !!shouldUpload;
|
|
811
|
-
const spinner = createTtySpinner(spinnerEnabled, "Connecting to Postgres");
|
|
812
|
-
|
|
813
|
-
try {
|
|
814
|
-
spinner.update("Connecting to Postgres");
|
|
815
|
-
const connResult = await connectWithSslFallback(Client, adminConn);
|
|
816
|
-
client = connResult.client as Client;
|
|
817
|
-
|
|
818
|
-
let reports: Record<string, any>;
|
|
819
|
-
let uploadSummary:
|
|
820
|
-
| { project: string; reportId: number; uploaded: Array<{ checkId: string; filename: string; chunkId: number }> }
|
|
821
|
-
| undefined;
|
|
822
|
-
|
|
823
|
-
if (opts.checkId === "ALL") {
|
|
824
|
-
reports = await generateAllReports(client, opts.nodeName, (p) => {
|
|
825
|
-
spinner.update(`Running ${p.checkId}: ${p.checkTitle} (${p.index}/${p.total})`);
|
|
826
|
-
});
|
|
827
|
-
} else {
|
|
828
|
-
const checkId = opts.checkId.toUpperCase();
|
|
829
|
-
const generator = REPORT_GENERATORS[checkId];
|
|
830
|
-
if (!generator) {
|
|
831
|
-
spinner.stop();
|
|
832
|
-
console.error(`Unknown check ID: ${opts.checkId}`);
|
|
833
|
-
console.error(`Available: ${Object.keys(CHECK_INFO).join(", ")}, ALL`);
|
|
834
|
-
process.exitCode = 1;
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
spinner.update(`Running ${checkId}: ${CHECK_INFO[checkId] || checkId}`);
|
|
838
|
-
reports = { [checkId]: await generator(client, opts.nodeName) };
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Optional: upload to PostgresAI API.
|
|
842
|
-
if (uploadCfg) {
|
|
843
|
-
spinner.update("Creating remote checkup report");
|
|
844
|
-
const created = await createCheckupReport({
|
|
845
|
-
apiKey: uploadCfg.apiKey,
|
|
846
|
-
apiBaseUrl: uploadCfg.apiBaseUrl,
|
|
847
|
-
project: uploadCfg.project,
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
const reportId = created.reportId;
|
|
851
|
-
// Keep upload progress out of stdout when JSON is printed to stdout.
|
|
852
|
-
const logUpload = (msg: string): void => {
|
|
853
|
-
if (shouldPrintJson) {
|
|
854
|
-
console.error(msg);
|
|
855
|
-
} else {
|
|
856
|
-
console.log(msg);
|
|
857
|
-
}
|
|
858
|
-
};
|
|
859
|
-
logUpload(`Created remote checkup report: ${reportId}`);
|
|
860
|
-
|
|
861
|
-
const uploaded: Array<{ checkId: string; filename: string; chunkId: number }> = [];
|
|
862
|
-
for (const [checkId, report] of Object.entries(reports)) {
|
|
863
|
-
spinner.update(`Uploading ${checkId}.json`);
|
|
864
|
-
const jsonText = JSON.stringify(report, null, 2);
|
|
865
|
-
const r = await uploadCheckupReportJson({
|
|
866
|
-
apiKey: uploadCfg.apiKey,
|
|
867
|
-
apiBaseUrl: uploadCfg.apiBaseUrl,
|
|
868
|
-
reportId,
|
|
869
|
-
filename: `${checkId}.json`,
|
|
870
|
-
checkId,
|
|
871
|
-
jsonText,
|
|
872
|
-
});
|
|
873
|
-
uploaded.push({ checkId, filename: `${checkId}.json`, chunkId: r.reportChunkId });
|
|
874
|
-
}
|
|
875
|
-
logUpload("Upload completed");
|
|
876
|
-
uploadSummary = { project: uploadCfg.project, reportId, uploaded };
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
spinner.stop();
|
|
880
|
-
// Output results
|
|
881
|
-
if (opts.output) {
|
|
882
|
-
// Write to files
|
|
883
|
-
// outputPath is preflight-validated above
|
|
884
|
-
const outDir = outputPath || path.resolve(process.cwd(), expandHomePath(opts.output));
|
|
885
|
-
for (const [checkId, report] of Object.entries(reports)) {
|
|
886
|
-
const filePath = path.join(outDir, `${checkId}.json`);
|
|
887
|
-
fs.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
|
|
888
|
-
console.log(`✓ ${checkId}: ${filePath}`);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
if (uploadSummary) {
|
|
893
|
-
const out = shouldPrintJson ? console.error : console.log;
|
|
894
|
-
out("\nCheckup report uploaded");
|
|
895
|
-
out("======================\n");
|
|
896
|
-
if (projectWasGenerated) {
|
|
897
|
-
out(`Project: ${uploadSummary.project} (generated and saved as default)`);
|
|
898
|
-
} else {
|
|
899
|
-
out(`Project: ${uploadSummary.project}`);
|
|
900
|
-
}
|
|
901
|
-
out(`Report ID: ${uploadSummary.reportId}`);
|
|
902
|
-
out("View in Console: console.postgres.ai → Support → checkup reports");
|
|
903
|
-
out("");
|
|
904
|
-
out("Files:");
|
|
905
|
-
for (const item of uploadSummary.uploaded) {
|
|
906
|
-
out(`- ${item.checkId}: ${item.filename}`);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (shouldPrintJson) {
|
|
911
|
-
console.log(JSON.stringify(reports, null, 2));
|
|
912
|
-
} else if (!shouldUpload && !opts.output) {
|
|
913
|
-
// Offline mode keeps historical behavior: print JSON when not uploading and no --output.
|
|
914
|
-
console.log(JSON.stringify(reports, null, 2));
|
|
915
|
-
}
|
|
916
|
-
} catch (error) {
|
|
917
|
-
spinner.stop();
|
|
918
|
-
if (error instanceof RpcError) {
|
|
919
|
-
for (const line of formatRpcErrorForDisplay(error)) {
|
|
920
|
-
console.error(line);
|
|
921
|
-
}
|
|
922
|
-
} else {
|
|
923
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
924
|
-
console.error(`Error: ${message}`);
|
|
925
|
-
}
|
|
926
|
-
process.exitCode = 1;
|
|
927
|
-
} finally {
|
|
928
|
-
if (client) {
|
|
929
|
-
await client.end();
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
});
|
|
933
|
-
|
|
934
616
|
/**
|
|
935
617
|
* Stub function for not implemented commands
|
|
936
618
|
*/
|
|
@@ -1915,9 +1597,6 @@ auth
|
|
|
1915
1597
|
}
|
|
1916
1598
|
|
|
1917
1599
|
config.writeConfig({ apiKey: trimmedKey });
|
|
1918
|
-
// When API key is set directly, invalidate org/project selection
|
|
1919
|
-
// as it likely belongs to a different account/session.
|
|
1920
|
-
config.deleteConfigKeys(["orgId", "defaultProject"]);
|
|
1921
1600
|
console.log(`API key saved to ${config.getConfigPath()}`);
|
|
1922
1601
|
return;
|
|
1923
1602
|
}
|
|
@@ -2096,9 +1775,6 @@ auth
|
|
|
2096
1775
|
baseUrl: apiBaseUrl,
|
|
2097
1776
|
orgId: orgId,
|
|
2098
1777
|
});
|
|
2099
|
-
// When re-authing via OAuth, orgId will be refreshed,
|
|
2100
|
-
// but defaultProject may no longer be valid in the new org.
|
|
2101
|
-
config.deleteConfigKeys(["defaultProject"]);
|
|
2102
1778
|
|
|
2103
1779
|
console.log("\nAuthentication successful!");
|
|
2104
1780
|
console.log(`API key saved to: ${config.getConfigPath()}`);
|
package/bun.lock
CHANGED
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
"@types/bun": "^1.1.14",
|
|
15
15
|
"@types/js-yaml": "^4.0.9",
|
|
16
16
|
"@types/pg": "^8.15.6",
|
|
17
|
-
"ajv": "^8.17.1",
|
|
18
|
-
"ajv-formats": "^3.0.1",
|
|
19
17
|
"typescript": "^5.3.3",
|
|
20
18
|
},
|
|
21
19
|
},
|
|
@@ -131,7 +129,7 @@
|
|
|
131
129
|
|
|
132
130
|
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
|
133
131
|
|
|
134
|
-
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin":
|
|
132
|
+
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
|
135
133
|
|
|
136
134
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
|
137
135
|
|