postgresai 0.14.0-dev.43 → 0.14.0-dev.45
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 +649 -310
- package/bun.lock +258 -0
- package/dist/bin/postgres-ai.js +29491 -1910
- 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 +58 -97
- package/lib/checkup-api.ts +175 -0
- package/lib/checkup.ts +837 -0
- package/lib/config.ts +3 -0
- package/lib/init.ts +106 -74
- package/lib/issues.ts +121 -194
- package/lib/mcp-server.ts +6 -17
- package/lib/metrics-loader.ts +156 -0
- package/package.json +13 -9
- package/sql/02.permissions.sql +9 -5
- package/sql/05.helpers.sql +415 -0
- package/test/checkup.test.ts +953 -0
- package/test/init.integration.test.ts +396 -0
- package/test/init.test.ts +345 -0
- package/test/schema-validation.test.ts +188 -0
- package/tsconfig.json +12 -20
- package/dist/bin/postgres-ai.d.ts +0 -3
- package/dist/bin/postgres-ai.d.ts.map +0 -1
- package/dist/bin/postgres-ai.js.map +0 -1
- package/dist/lib/auth-server.d.ts +0 -31
- package/dist/lib/auth-server.d.ts.map +0 -1
- package/dist/lib/auth-server.js +0 -263
- package/dist/lib/auth-server.js.map +0 -1
- package/dist/lib/config.d.ts +0 -45
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js +0 -181
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/init.d.ts +0 -85
- package/dist/lib/init.d.ts.map +0 -1
- package/dist/lib/init.js +0 -644
- package/dist/lib/init.js.map +0 -1
- package/dist/lib/issues.d.ts +0 -75
- package/dist/lib/issues.d.ts.map +0 -1
- package/dist/lib/issues.js +0 -336
- package/dist/lib/issues.js.map +0 -1
- package/dist/lib/mcp-server.d.ts +0 -9
- package/dist/lib/mcp-server.d.ts.map +0 -1
- package/dist/lib/mcp-server.js +0 -168
- package/dist/lib/mcp-server.js.map +0 -1
- package/dist/lib/pkce.d.ts +0 -32
- package/dist/lib/pkce.d.ts.map +0 -1
- package/dist/lib/pkce.js +0 -101
- package/dist/lib/pkce.js.map +0 -1
- package/dist/lib/util.d.ts +0 -27
- package/dist/lib/util.d.ts.map +0 -1
- package/dist/lib/util.js +0 -46
- package/dist/lib/util.js.map +0 -1
- package/dist/package.json +0 -46
- package/test/init.integration.test.cjs +0 -382
- package/test/init.test.cjs +0 -392
package/bin/postgres-ai.ts
CHANGED
|
@@ -1,25 +1,172 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
-
import
|
|
4
|
+
import pkg from "../package.json";
|
|
5
5
|
import * as config from "../lib/config";
|
|
6
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
|
|
11
|
-
import { promisify } from "util";
|
|
12
|
-
import * as readline from "readline";
|
|
13
|
-
import * as http from "https";
|
|
14
|
-
import { URL } from "url";
|
|
10
|
+
import * as crypto from "node:crypto";
|
|
15
11
|
import { Client } from "pg";
|
|
16
12
|
import { startMcpServer } from "../lib/mcp-server";
|
|
17
13
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
|
|
18
14
|
import { resolveBaseUrls } from "../lib/util";
|
|
19
15
|
import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
|
|
16
|
+
import * as pkce from "../lib/pkce";
|
|
17
|
+
import * as authServer from "../lib/auth-server";
|
|
18
|
+
import { maskSecret } from "../lib/util";
|
|
19
|
+
import { createInterface } from "readline";
|
|
20
|
+
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
|
+
|
|
24
|
+
// Singleton readline interface for stdin prompts
|
|
25
|
+
let rl: ReturnType<typeof createInterface> | null = null;
|
|
26
|
+
function getReadline() {
|
|
27
|
+
if (!rl) {
|
|
28
|
+
rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
29
|
+
}
|
|
30
|
+
return rl;
|
|
31
|
+
}
|
|
32
|
+
function closeReadline() {
|
|
33
|
+
if (rl) {
|
|
34
|
+
rl.close();
|
|
35
|
+
rl = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Helper functions for spawning processes - use Node.js child_process for compatibility
|
|
40
|
+
async function execPromise(command: string): Promise<{ stdout: string; stderr: string }> {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
childProcess.exec(command, (error, stdout, stderr) => {
|
|
43
|
+
if (error) {
|
|
44
|
+
const err = error as Error & { code: number };
|
|
45
|
+
err.code = error.code ?? 1;
|
|
46
|
+
reject(err);
|
|
47
|
+
} else {
|
|
48
|
+
resolve({ stdout, stderr });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function execFilePromise(file: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
childProcess.execFile(file, args, (error, stdout, stderr) => {
|
|
57
|
+
if (error) {
|
|
58
|
+
const err = error as Error & { code: number };
|
|
59
|
+
err.code = error.code ?? 1;
|
|
60
|
+
reject(err);
|
|
61
|
+
} else {
|
|
62
|
+
resolve({ stdout, stderr });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function spawnSync(cmd: string, args: string[], options?: { stdio?: "pipe" | "ignore" | "inherit"; encoding?: string; env?: Record<string, string | undefined>; cwd?: string }): { status: number | null; stdout: string; stderr: string } {
|
|
69
|
+
const result = childProcess.spawnSync(cmd, args, {
|
|
70
|
+
stdio: options?.stdio === "inherit" ? "inherit" : "pipe",
|
|
71
|
+
env: options?.env as NodeJS.ProcessEnv,
|
|
72
|
+
cwd: options?.cwd,
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
status: result.status,
|
|
77
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
78
|
+
stderr: typeof result.stderr === "string" ? result.stderr : "",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function spawn(cmd: string, args: string[], options?: { stdio?: "pipe" | "ignore" | "inherit"; env?: Record<string, string | undefined>; cwd?: string; detached?: boolean }): { on: (event: string, cb: (code: number | null, signal?: string) => void) => void; unref: () => void; pid?: number } {
|
|
83
|
+
const proc = childProcess.spawn(cmd, args, {
|
|
84
|
+
stdio: options?.stdio ?? "pipe",
|
|
85
|
+
env: options?.env as NodeJS.ProcessEnv,
|
|
86
|
+
cwd: options?.cwd,
|
|
87
|
+
detached: options?.detached,
|
|
88
|
+
});
|
|
20
89
|
|
|
21
|
-
|
|
22
|
-
|
|
90
|
+
return {
|
|
91
|
+
on(event: string, cb: (code: number | null, signal?: string) => void) {
|
|
92
|
+
if (event === "close" || event === "exit") {
|
|
93
|
+
proc.on(event, (code, signal) => cb(code, signal ?? undefined));
|
|
94
|
+
} else if (event === "error") {
|
|
95
|
+
proc.on("error", (err) => cb(null, String(err)));
|
|
96
|
+
}
|
|
97
|
+
return this;
|
|
98
|
+
},
|
|
99
|
+
unref() {
|
|
100
|
+
proc.unref();
|
|
101
|
+
},
|
|
102
|
+
pid: proc.pid,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Simple readline-like interface for prompts using Bun
|
|
107
|
+
async function question(prompt: string): Promise<string> {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
getReadline().question(prompt, (answer) => {
|
|
110
|
+
resolve(answer);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
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
|
+
}
|
|
23
170
|
|
|
24
171
|
/**
|
|
25
172
|
* CLI configuration options
|
|
@@ -69,21 +216,17 @@ function getDefaultMonitoringProjectDir(): string {
|
|
|
69
216
|
}
|
|
70
217
|
|
|
71
218
|
async function downloadText(url: string): Promise<string> {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
84
|
-
req.on("error", reject);
|
|
85
|
-
req.setTimeout(15_000, () => req.destroy(new Error(`Timeout fetching ${url}`)));
|
|
86
|
-
});
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
throw new Error(`HTTP ${response.status} for ${url}`);
|
|
225
|
+
}
|
|
226
|
+
return await response.text();
|
|
227
|
+
} finally {
|
|
228
|
+
clearTimeout(timeout);
|
|
229
|
+
}
|
|
87
230
|
}
|
|
88
231
|
|
|
89
232
|
async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
|
|
@@ -202,6 +345,20 @@ program
|
|
|
202
345
|
"UI base URL for browser routes (overrides PGAI_UI_BASE_URL)"
|
|
203
346
|
);
|
|
204
347
|
|
|
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
|
+
|
|
205
362
|
program
|
|
206
363
|
.command("prepare-db [conn]")
|
|
207
364
|
.description("prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)")
|
|
@@ -529,6 +686,251 @@ program
|
|
|
529
686
|
}
|
|
530
687
|
});
|
|
531
688
|
|
|
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
|
+
|
|
532
934
|
/**
|
|
533
935
|
* Stub function for not implemented commands
|
|
534
936
|
*/
|
|
@@ -760,48 +1162,36 @@ mon
|
|
|
760
1162
|
console.log("⚠ Reports will be generated locally only");
|
|
761
1163
|
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
762
1164
|
} else {
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
config.writeConfig({ apiKey: trimmedKey });
|
|
782
|
-
// Keep reporter compatibility (docker-compose mounts .pgwatch-config)
|
|
783
|
-
fs.writeFileSync(path.resolve(projectDir, ".pgwatch-config"), `api_key=${trimmedKey}\n`, {
|
|
784
|
-
encoding: "utf8",
|
|
785
|
-
mode: 0o600
|
|
786
|
-
});
|
|
787
|
-
console.log("✓ API key saved\n");
|
|
788
|
-
break;
|
|
789
|
-
}
|
|
1165
|
+
const answer = await question("Do you have a Postgres AI API key? (Y/n): ");
|
|
1166
|
+
const proceedWithApiKey = !answer || answer.toLowerCase() === "y";
|
|
1167
|
+
|
|
1168
|
+
if (proceedWithApiKey) {
|
|
1169
|
+
while (true) {
|
|
1170
|
+
const inputApiKey = await question("Enter your Postgres AI API key: ");
|
|
1171
|
+
const trimmedKey = inputApiKey.trim();
|
|
1172
|
+
|
|
1173
|
+
if (trimmedKey) {
|
|
1174
|
+
config.writeConfig({ apiKey: trimmedKey });
|
|
1175
|
+
// Keep reporter compatibility (docker-compose mounts .pgwatch-config)
|
|
1176
|
+
fs.writeFileSync(path.resolve(projectDir, ".pgwatch-config"), `api_key=${trimmedKey}\n`, {
|
|
1177
|
+
encoding: "utf8",
|
|
1178
|
+
mode: 0o600
|
|
1179
|
+
});
|
|
1180
|
+
console.log("✓ API key saved\n");
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
790
1183
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
1184
|
+
console.log("⚠ API key cannot be empty");
|
|
1185
|
+
const retry = await question("Try again or skip API key setup, retry? (Y/n): ");
|
|
1186
|
+
if (retry.toLowerCase() === "n") {
|
|
1187
|
+
console.log("⚠ Skipping API key setup - reports will be generated locally only");
|
|
1188
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
1189
|
+
break;
|
|
798
1190
|
}
|
|
799
|
-
} else {
|
|
800
|
-
console.log("⚠ Skipping API key setup - reports will be generated locally only");
|
|
801
|
-
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
802
1191
|
}
|
|
803
|
-
}
|
|
804
|
-
|
|
1192
|
+
} else {
|
|
1193
|
+
console.log("⚠ Skipping API key setup - reports will be generated locally only");
|
|
1194
|
+
console.log("You can add an API key later with: postgres-ai add-key <api_key>\n");
|
|
805
1195
|
}
|
|
806
1196
|
}
|
|
807
1197
|
} else {
|
|
@@ -847,7 +1237,6 @@ mon
|
|
|
847
1237
|
// Test connection
|
|
848
1238
|
console.log("Testing connection to the added instance...");
|
|
849
1239
|
try {
|
|
850
|
-
const { Client } = require("pg");
|
|
851
1240
|
const client = new Client({ connectionString: connStr });
|
|
852
1241
|
await client.connect();
|
|
853
1242
|
const result = await client.query("select version();");
|
|
@@ -864,63 +1253,50 @@ mon
|
|
|
864
1253
|
console.log("⚠ No PostgreSQL instance added");
|
|
865
1254
|
console.log("You can add one later with: postgres-ai mon targets add\n");
|
|
866
1255
|
} else {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
console.log(" 2. Press Enter to skip for now\n");
|
|
884
|
-
|
|
885
|
-
const connStr = await question("Enter connection string (or press Enter to skip): ");
|
|
886
|
-
|
|
887
|
-
if (connStr.trim()) {
|
|
888
|
-
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
889
|
-
if (!m) {
|
|
890
|
-
console.error("✗ Invalid connection string format");
|
|
891
|
-
console.log("⚠ Continuing without adding instance\n");
|
|
892
|
-
} else {
|
|
893
|
-
const host = m[3];
|
|
894
|
-
const db = m[5];
|
|
895
|
-
const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
896
|
-
|
|
897
|
-
const body = `- name: ${instanceName}\n conn_str: ${connStr}\n preset_metrics: full\n custom_metrics:\n is_enabled: true\n group: default\n custom_tags:\n env: production\n cluster: default\n node_name: ${instanceName}\n sink_type: ~sink_type~\n`;
|
|
898
|
-
fs.appendFileSync(instancesPath, body, "utf8");
|
|
899
|
-
console.log(`✓ Monitoring target '${instanceName}' added\n`);
|
|
900
|
-
|
|
901
|
-
// Test connection
|
|
902
|
-
console.log("Testing connection to the added instance...");
|
|
903
|
-
try {
|
|
904
|
-
const { Client } = require("pg");
|
|
905
|
-
const client = new Client({ connectionString: connStr });
|
|
906
|
-
await client.connect();
|
|
907
|
-
const result = await client.query("select version();");
|
|
908
|
-
console.log("✓ Connection successful");
|
|
909
|
-
console.log(`${result.rows[0].version}\n`);
|
|
910
|
-
await client.end();
|
|
911
|
-
} catch (error) {
|
|
912
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
913
|
-
console.error(`✗ Connection failed: ${message}\n`);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
1256
|
+
console.log("You need to add at least one PostgreSQL instance to monitor");
|
|
1257
|
+
const answer = await question("Do you want to add a PostgreSQL instance now? (Y/n): ");
|
|
1258
|
+
const proceedWithInstance = !answer || answer.toLowerCase() === "y";
|
|
1259
|
+
|
|
1260
|
+
if (proceedWithInstance) {
|
|
1261
|
+
console.log("\nYou can provide either:");
|
|
1262
|
+
console.log(" 1. A full connection string: postgresql://user:pass@host:port/database");
|
|
1263
|
+
console.log(" 2. Press Enter to skip for now\n");
|
|
1264
|
+
|
|
1265
|
+
const connStr = await question("Enter connection string (or press Enter to skip): ");
|
|
1266
|
+
|
|
1267
|
+
if (connStr.trim()) {
|
|
1268
|
+
const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
|
|
1269
|
+
if (!m) {
|
|
1270
|
+
console.error("✗ Invalid connection string format");
|
|
1271
|
+
console.log("⚠ Continuing without adding instance\n");
|
|
916
1272
|
} else {
|
|
917
|
-
|
|
1273
|
+
const host = m[3];
|
|
1274
|
+
const db = m[5];
|
|
1275
|
+
const instanceName = `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
1276
|
+
|
|
1277
|
+
const body = `- name: ${instanceName}\n conn_str: ${connStr}\n preset_metrics: full\n custom_metrics:\n is_enabled: true\n group: default\n custom_tags:\n env: production\n cluster: default\n node_name: ${instanceName}\n sink_type: ~sink_type~\n`;
|
|
1278
|
+
fs.appendFileSync(instancesPath, body, "utf8");
|
|
1279
|
+
console.log(`✓ Monitoring target '${instanceName}' added\n`);
|
|
1280
|
+
|
|
1281
|
+
// Test connection
|
|
1282
|
+
console.log("Testing connection to the added instance...");
|
|
1283
|
+
try {
|
|
1284
|
+
const client = new Client({ connectionString: connStr });
|
|
1285
|
+
await client.connect();
|
|
1286
|
+
const result = await client.query("select version();");
|
|
1287
|
+
console.log("✓ Connection successful");
|
|
1288
|
+
console.log(`${result.rows[0].version}\n`);
|
|
1289
|
+
await client.end();
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1292
|
+
console.error(`✗ Connection failed: ${message}\n`);
|
|
1293
|
+
}
|
|
918
1294
|
}
|
|
919
1295
|
} else {
|
|
920
1296
|
console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
921
1297
|
}
|
|
922
|
-
}
|
|
923
|
-
|
|
1298
|
+
} else {
|
|
1299
|
+
console.log("⚠ No PostgreSQL instance added - you can add one later with: postgres-ai mon targets add\n");
|
|
924
1300
|
}
|
|
925
1301
|
}
|
|
926
1302
|
} else {
|
|
@@ -1106,17 +1482,17 @@ mon
|
|
|
1106
1482
|
allHealthy = true;
|
|
1107
1483
|
for (const service of services) {
|
|
1108
1484
|
try {
|
|
1109
|
-
const
|
|
1110
|
-
const status =
|
|
1111
|
-
encoding: 'utf8',
|
|
1112
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
1113
|
-
}).trim();
|
|
1485
|
+
const result = spawnSync("docker", ["inspect", "-f", "{{.State.Status}}", service.container], { stdio: "pipe" });
|
|
1486
|
+
const status = result.stdout.trim();
|
|
1114
1487
|
|
|
1115
|
-
if (status === 'running') {
|
|
1488
|
+
if (result.status === 0 && status === 'running') {
|
|
1116
1489
|
console.log(`✓ ${service.name}: healthy`);
|
|
1117
|
-
} else {
|
|
1490
|
+
} else if (result.status === 0) {
|
|
1118
1491
|
console.log(`✗ ${service.name}: unhealthy (status: ${status})`);
|
|
1119
1492
|
allHealthy = false;
|
|
1493
|
+
} else {
|
|
1494
|
+
console.log(`✗ ${service.name}: unreachable`);
|
|
1495
|
+
allHealthy = false;
|
|
1120
1496
|
}
|
|
1121
1497
|
} catch (error) {
|
|
1122
1498
|
console.log(`✗ ${service.name}: unreachable`);
|
|
@@ -1220,14 +1596,6 @@ mon
|
|
|
1220
1596
|
.command("reset [service]")
|
|
1221
1597
|
.description("reset all or specific monitoring service")
|
|
1222
1598
|
.action(async (service?: string) => {
|
|
1223
|
-
const rl = readline.createInterface({
|
|
1224
|
-
input: process.stdin,
|
|
1225
|
-
output: process.stdout,
|
|
1226
|
-
});
|
|
1227
|
-
|
|
1228
|
-
const question = (prompt: string): Promise<string> =>
|
|
1229
|
-
new Promise((resolve) => rl.question(prompt, resolve));
|
|
1230
|
-
|
|
1231
1599
|
try {
|
|
1232
1600
|
if (service) {
|
|
1233
1601
|
// Reset specific service
|
|
@@ -1237,7 +1605,6 @@ mon
|
|
|
1237
1605
|
const answer = await question("Continue? (y/N): ");
|
|
1238
1606
|
if (answer.toLowerCase() !== "y") {
|
|
1239
1607
|
console.log("Cancelled");
|
|
1240
|
-
rl.close();
|
|
1241
1608
|
return;
|
|
1242
1609
|
}
|
|
1243
1610
|
|
|
@@ -1264,7 +1631,6 @@ mon
|
|
|
1264
1631
|
const answer = await question("Continue? (y/N): ");
|
|
1265
1632
|
if (answer.toLowerCase() !== "y") {
|
|
1266
1633
|
console.log("Cancelled");
|
|
1267
|
-
rl.close();
|
|
1268
1634
|
return;
|
|
1269
1635
|
}
|
|
1270
1636
|
|
|
@@ -1278,10 +1644,7 @@ mon
|
|
|
1278
1644
|
process.exitCode = 1;
|
|
1279
1645
|
}
|
|
1280
1646
|
}
|
|
1281
|
-
|
|
1282
|
-
rl.close();
|
|
1283
1647
|
} catch (error) {
|
|
1284
|
-
rl.close();
|
|
1285
1648
|
const message = error instanceof Error ? error.message : String(error);
|
|
1286
1649
|
console.error(`Reset failed: ${message}`);
|
|
1287
1650
|
process.exitCode = 1;
|
|
@@ -1515,7 +1878,6 @@ targets
|
|
|
1515
1878
|
console.log(`Testing connection to monitoring target '${name}'...`);
|
|
1516
1879
|
|
|
1517
1880
|
// Use native pg client instead of requiring psql to be installed
|
|
1518
|
-
const { Client } = require('pg');
|
|
1519
1881
|
const client = new Client({ connectionString: instance.conn_str });
|
|
1520
1882
|
|
|
1521
1883
|
try {
|
|
@@ -1553,14 +1915,14 @@ auth
|
|
|
1553
1915
|
}
|
|
1554
1916
|
|
|
1555
1917
|
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"]);
|
|
1556
1921
|
console.log(`API key saved to ${config.getConfigPath()}`);
|
|
1557
1922
|
return;
|
|
1558
1923
|
}
|
|
1559
1924
|
|
|
1560
1925
|
// Otherwise, proceed with OAuth flow
|
|
1561
|
-
const pkce = require("../lib/pkce");
|
|
1562
|
-
const authServer = require("../lib/auth-server");
|
|
1563
|
-
|
|
1564
1926
|
console.log("Starting authentication flow...\n");
|
|
1565
1927
|
|
|
1566
1928
|
// Generate PKCE parameters
|
|
@@ -1606,173 +1968,166 @@ auth
|
|
|
1606
1968
|
console.log(`Debug: Request data: ${initData}`);
|
|
1607
1969
|
}
|
|
1608
1970
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1971
|
+
// Step 2: Initialize OAuth session on backend using fetch
|
|
1972
|
+
let initResponse: Response;
|
|
1973
|
+
try {
|
|
1974
|
+
initResponse = await fetch(initUrl.toString(), {
|
|
1612
1975
|
method: "POST",
|
|
1613
1976
|
headers: {
|
|
1614
1977
|
"Content-Type": "application/json",
|
|
1615
|
-
"Content-Length": Buffer.byteLength(initData),
|
|
1616
1978
|
},
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
if (data.trim().startsWith("<!") || data.trim().startsWith("<html")) {
|
|
1627
|
-
console.error("Error: Received HTML response instead of JSON. This usually means:");
|
|
1628
|
-
console.error(" 1. The API endpoint URL is incorrect");
|
|
1629
|
-
console.error(" 2. The endpoint does not exist (404)");
|
|
1630
|
-
console.error(`\nAPI URL attempted: ${initUrl.toString()}`);
|
|
1631
|
-
console.error("\nPlease verify the --api-base-url parameter.");
|
|
1632
|
-
} else {
|
|
1633
|
-
console.error(data);
|
|
1634
|
-
}
|
|
1979
|
+
body: initData,
|
|
1980
|
+
});
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1983
|
+
console.error(`Failed to connect to API: ${message}`);
|
|
1984
|
+
callbackServer.server.stop();
|
|
1985
|
+
process.exit(1);
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1635
1988
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1989
|
+
if (!initResponse.ok) {
|
|
1990
|
+
const data = await initResponse.text();
|
|
1991
|
+
console.error(`Failed to initialize auth session: ${initResponse.status}`);
|
|
1992
|
+
|
|
1993
|
+
// Check if response is HTML (common for 404 pages)
|
|
1994
|
+
if (data.trim().startsWith("<!") || data.trim().startsWith("<html")) {
|
|
1995
|
+
console.error("Error: Received HTML response instead of JSON. This usually means:");
|
|
1996
|
+
console.error(" 1. The API endpoint URL is incorrect");
|
|
1997
|
+
console.error(" 2. The endpoint does not exist (404)");
|
|
1998
|
+
console.error(`\nAPI URL attempted: ${initUrl.toString()}`);
|
|
1999
|
+
console.error("\nPlease verify the --api-base-url parameter.");
|
|
2000
|
+
} else {
|
|
2001
|
+
console.error(data);
|
|
2002
|
+
}
|
|
1639
2003
|
|
|
1640
|
-
|
|
1641
|
-
|
|
2004
|
+
callbackServer.server.stop();
|
|
2005
|
+
process.exit(1);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
1642
2008
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
}
|
|
2009
|
+
// Step 3: Open browser
|
|
2010
|
+
const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
1646
2011
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
// Open browser (cross-platform)
|
|
1651
|
-
const openCommand = process.platform === "darwin" ? "open" :
|
|
1652
|
-
process.platform === "win32" ? "start" :
|
|
1653
|
-
"xdg-open";
|
|
1654
|
-
spawn(openCommand, [authUrl], { detached: true, stdio: "ignore" }).unref();
|
|
1655
|
-
|
|
1656
|
-
// Step 4: Wait for callback
|
|
1657
|
-
console.log("Waiting for authorization...");
|
|
1658
|
-
console.log("(Press Ctrl+C to cancel)\n");
|
|
1659
|
-
|
|
1660
|
-
// Handle Ctrl+C gracefully
|
|
1661
|
-
const cancelHandler = () => {
|
|
1662
|
-
console.log("\n\nAuthentication cancelled by user.");
|
|
1663
|
-
callbackServer.server.close();
|
|
1664
|
-
process.exit(130); // Standard exit code for SIGINT
|
|
1665
|
-
};
|
|
1666
|
-
process.on("SIGINT", cancelHandler);
|
|
1667
|
-
|
|
1668
|
-
try {
|
|
1669
|
-
const { code } = await callbackServer.promise;
|
|
1670
|
-
|
|
1671
|
-
// Remove the cancel handler after successful auth
|
|
1672
|
-
process.off("SIGINT", cancelHandler);
|
|
1673
|
-
|
|
1674
|
-
// Step 5: Exchange code for token
|
|
1675
|
-
console.log("\nExchanging authorization code for API token...");
|
|
1676
|
-
const exchangeData = JSON.stringify({
|
|
1677
|
-
authorization_code: code,
|
|
1678
|
-
code_verifier: params.codeVerifier,
|
|
1679
|
-
state: params.state,
|
|
1680
|
-
});
|
|
1681
|
-
const exchangeUrl = new URL(`${apiBaseUrl}/rpc/oauth_token_exchange`);
|
|
1682
|
-
const exchangeReq = http.request(
|
|
1683
|
-
exchangeUrl,
|
|
1684
|
-
{
|
|
1685
|
-
method: "POST",
|
|
1686
|
-
headers: {
|
|
1687
|
-
"Content-Type": "application/json",
|
|
1688
|
-
"Content-Length": Buffer.byteLength(exchangeData),
|
|
1689
|
-
},
|
|
1690
|
-
},
|
|
1691
|
-
(exchangeRes) => {
|
|
1692
|
-
let exchangeBody = "";
|
|
1693
|
-
exchangeRes.on("data", (chunk) => (exchangeBody += chunk));
|
|
1694
|
-
exchangeRes.on("end", () => {
|
|
1695
|
-
if (exchangeRes.statusCode !== 200) {
|
|
1696
|
-
console.error(`Failed to exchange code for token: ${exchangeRes.statusCode}`);
|
|
1697
|
-
|
|
1698
|
-
// Check if response is HTML (common for 404 pages)
|
|
1699
|
-
if (exchangeBody.trim().startsWith("<!") || exchangeBody.trim().startsWith("<html")) {
|
|
1700
|
-
console.error("Error: Received HTML response instead of JSON. This usually means:");
|
|
1701
|
-
console.error(" 1. The API endpoint URL is incorrect");
|
|
1702
|
-
console.error(" 2. The endpoint does not exist (404)");
|
|
1703
|
-
console.error(`\nAPI URL attempted: ${exchangeUrl.toString()}`);
|
|
1704
|
-
console.error("\nPlease verify the --api-base-url parameter.");
|
|
1705
|
-
} else {
|
|
1706
|
-
console.error(exchangeBody);
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
process.exit(1);
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
try {
|
|
1714
|
-
const result = JSON.parse(exchangeBody);
|
|
1715
|
-
const apiToken = result.api_token || result?.[0]?.result?.api_token; // There is a bug with PostgREST Caching that may return an array, not single object, it's a workaround to support both cases.
|
|
1716
|
-
const orgId = result.org_id || result?.[0]?.result?.org_id; // There is a bug with PostgREST Caching that may return an array, not single object, it's a workaround to support both cases.
|
|
1717
|
-
|
|
1718
|
-
// Step 6: Save token to config
|
|
1719
|
-
config.writeConfig({
|
|
1720
|
-
apiKey: apiToken,
|
|
1721
|
-
baseUrl: apiBaseUrl,
|
|
1722
|
-
orgId: orgId,
|
|
1723
|
-
});
|
|
1724
|
-
|
|
1725
|
-
console.log("\nAuthentication successful!");
|
|
1726
|
-
console.log(`API key saved to: ${config.getConfigPath()}`);
|
|
1727
|
-
console.log(`Organization ID: ${orgId}`);
|
|
1728
|
-
console.log(`\nYou can now use the CLI without specifying an API key.`);
|
|
1729
|
-
process.exit(0);
|
|
1730
|
-
} catch (err) {
|
|
1731
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1732
|
-
console.error(`Failed to parse response: ${message}`);
|
|
1733
|
-
process.exit(1);
|
|
1734
|
-
}
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
);
|
|
1738
|
-
|
|
1739
|
-
exchangeReq.on("error", (err: Error) => {
|
|
1740
|
-
console.error(`Exchange request failed: ${err.message}`);
|
|
1741
|
-
process.exit(1);
|
|
1742
|
-
});
|
|
2012
|
+
if (opts.debug) {
|
|
2013
|
+
console.log(`Debug: Auth URL: ${authUrl}`);
|
|
2014
|
+
}
|
|
1743
2015
|
|
|
1744
|
-
|
|
1745
|
-
|
|
2016
|
+
console.log(`\nOpening browser for authentication...`);
|
|
2017
|
+
console.log(`If browser does not open automatically, visit:\n${authUrl}\n`);
|
|
1746
2018
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2019
|
+
// Open browser (cross-platform)
|
|
2020
|
+
const openCommand = process.platform === "darwin" ? "open" :
|
|
2021
|
+
process.platform === "win32" ? "start" :
|
|
2022
|
+
"xdg-open";
|
|
2023
|
+
spawn(openCommand, [authUrl], { detached: true, stdio: "ignore" }).unref();
|
|
1750
2024
|
|
|
1751
|
-
|
|
2025
|
+
// Step 4: Wait for callback
|
|
2026
|
+
console.log("Waiting for authorization...");
|
|
2027
|
+
console.log("(Press Ctrl+C to cancel)\n");
|
|
1752
2028
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
}
|
|
2029
|
+
// Handle Ctrl+C gracefully
|
|
2030
|
+
const cancelHandler = () => {
|
|
2031
|
+
console.log("\n\nAuthentication cancelled by user.");
|
|
2032
|
+
callbackServer.server.stop();
|
|
2033
|
+
process.exit(130); // Standard exit code for SIGINT
|
|
2034
|
+
};
|
|
2035
|
+
process.on("SIGINT", cancelHandler);
|
|
1761
2036
|
|
|
1762
|
-
|
|
1763
|
-
|
|
2037
|
+
try {
|
|
2038
|
+
const { code } = await callbackServer.promise;
|
|
2039
|
+
|
|
2040
|
+
// Remove the cancel handler after successful auth
|
|
2041
|
+
process.off("SIGINT", cancelHandler);
|
|
2042
|
+
|
|
2043
|
+
// Step 5: Exchange code for token using fetch
|
|
2044
|
+
console.log("\nExchanging authorization code for API token...");
|
|
2045
|
+
const exchangeData = JSON.stringify({
|
|
2046
|
+
authorization_code: code,
|
|
2047
|
+
code_verifier: params.codeVerifier,
|
|
2048
|
+
state: params.state,
|
|
2049
|
+
});
|
|
2050
|
+
const exchangeUrl = new URL(`${apiBaseUrl}/rpc/oauth_token_exchange`);
|
|
2051
|
+
|
|
2052
|
+
let exchangeResponse: Response;
|
|
2053
|
+
try {
|
|
2054
|
+
exchangeResponse = await fetch(exchangeUrl.toString(), {
|
|
2055
|
+
method: "POST",
|
|
2056
|
+
headers: {
|
|
2057
|
+
"Content-Type": "application/json",
|
|
2058
|
+
},
|
|
2059
|
+
body: exchangeData,
|
|
1764
2060
|
});
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2063
|
+
console.error(`Exchange request failed: ${message}`);
|
|
2064
|
+
process.exit(1);
|
|
2065
|
+
return;
|
|
1765
2066
|
}
|
|
1766
|
-
);
|
|
1767
2067
|
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
});
|
|
2068
|
+
const exchangeBody = await exchangeResponse.text();
|
|
2069
|
+
|
|
2070
|
+
if (!exchangeResponse.ok) {
|
|
2071
|
+
console.error(`Failed to exchange code for token: ${exchangeResponse.status}`);
|
|
1773
2072
|
|
|
1774
|
-
|
|
1775
|
-
|
|
2073
|
+
// Check if response is HTML (common for 404 pages)
|
|
2074
|
+
if (exchangeBody.trim().startsWith("<!") || exchangeBody.trim().startsWith("<html")) {
|
|
2075
|
+
console.error("Error: Received HTML response instead of JSON. This usually means:");
|
|
2076
|
+
console.error(" 1. The API endpoint URL is incorrect");
|
|
2077
|
+
console.error(" 2. The endpoint does not exist (404)");
|
|
2078
|
+
console.error(`\nAPI URL attempted: ${exchangeUrl.toString()}`);
|
|
2079
|
+
console.error("\nPlease verify the --api-base-url parameter.");
|
|
2080
|
+
} else {
|
|
2081
|
+
console.error(exchangeBody);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
process.exit(1);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
try {
|
|
2089
|
+
const result = JSON.parse(exchangeBody);
|
|
2090
|
+
const apiToken = result.api_token || result?.[0]?.result?.api_token; // There is a bug with PostgREST Caching that may return an array, not single object, it's a workaround to support both cases.
|
|
2091
|
+
const orgId = result.org_id || result?.[0]?.result?.org_id; // There is a bug with PostgREST Caching that may return an array, not single object, it's a workaround to support both cases.
|
|
2092
|
+
|
|
2093
|
+
// Step 6: Save token to config
|
|
2094
|
+
config.writeConfig({
|
|
2095
|
+
apiKey: apiToken,
|
|
2096
|
+
baseUrl: apiBaseUrl,
|
|
2097
|
+
orgId: orgId,
|
|
2098
|
+
});
|
|
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
|
+
|
|
2103
|
+
console.log("\nAuthentication successful!");
|
|
2104
|
+
console.log(`API key saved to: ${config.getConfigPath()}`);
|
|
2105
|
+
console.log(`Organization ID: ${orgId}`);
|
|
2106
|
+
console.log(`\nYou can now use the CLI without specifying an API key.`);
|
|
2107
|
+
process.exit(0);
|
|
2108
|
+
} catch (err) {
|
|
2109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2110
|
+
console.error(`Failed to parse response: ${message}`);
|
|
2111
|
+
process.exit(1);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
} catch (err) {
|
|
2115
|
+
// Remove the cancel handler in error case too
|
|
2116
|
+
process.off("SIGINT", cancelHandler);
|
|
2117
|
+
|
|
2118
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2119
|
+
|
|
2120
|
+
// Provide more helpful error messages
|
|
2121
|
+
if (message.includes("timeout")) {
|
|
2122
|
+
console.error(`\nAuthentication timed out.`);
|
|
2123
|
+
console.error(`This usually means you closed the browser window without completing authentication.`);
|
|
2124
|
+
console.error(`Please try again and complete the authentication flow.`);
|
|
2125
|
+
} else {
|
|
2126
|
+
console.error(`\nAuthentication failed: ${message}`);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
process.exit(1);
|
|
2130
|
+
}
|
|
1776
2131
|
|
|
1777
2132
|
} catch (err) {
|
|
1778
2133
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1781,15 +2136,6 @@ auth
|
|
|
1781
2136
|
}
|
|
1782
2137
|
});
|
|
1783
2138
|
|
|
1784
|
-
auth
|
|
1785
|
-
.command("add-key <apiKey>")
|
|
1786
|
-
.description("store API key (deprecated: use 'auth --set-key' instead)")
|
|
1787
|
-
.action(async (apiKey: string) => {
|
|
1788
|
-
console.warn("Warning: 'add-key' is deprecated. Use 'auth --set-key <key>' instead.\n");
|
|
1789
|
-
config.writeConfig({ apiKey });
|
|
1790
|
-
console.log(`API key saved to ${config.getConfigPath()}`);
|
|
1791
|
-
});
|
|
1792
|
-
|
|
1793
2139
|
auth
|
|
1794
2140
|
.command("show-key")
|
|
1795
2141
|
.description("show API key (masked)")
|
|
@@ -1800,7 +2146,6 @@ auth
|
|
|
1800
2146
|
console.log(`\nTo authenticate, run: pgai auth`);
|
|
1801
2147
|
return;
|
|
1802
2148
|
}
|
|
1803
|
-
const { maskSecret } = require("../lib/util");
|
|
1804
2149
|
console.log(`Current API key: ${maskSecret(cfg.apiKey)}`);
|
|
1805
2150
|
if (cfg.orgId) {
|
|
1806
2151
|
console.log(`Organization ID: ${cfg.orgId}`);
|
|
@@ -2110,15 +2455,7 @@ mcp
|
|
|
2110
2455
|
console.log(" 4. Codex");
|
|
2111
2456
|
console.log("");
|
|
2112
2457
|
|
|
2113
|
-
const
|
|
2114
|
-
input: process.stdin,
|
|
2115
|
-
output: process.stdout
|
|
2116
|
-
});
|
|
2117
|
-
|
|
2118
|
-
const answer = await new Promise<string>((resolve) => {
|
|
2119
|
-
rl.question("Select your AI coding tool (1-4): ", resolve);
|
|
2120
|
-
});
|
|
2121
|
-
rl.close();
|
|
2458
|
+
const answer = await question("Select your AI coding tool (1-4): ");
|
|
2122
2459
|
|
|
2123
2460
|
const choices: Record<string, string> = {
|
|
2124
2461
|
"1": "cursor",
|
|
@@ -2253,5 +2590,7 @@ mcp
|
|
|
2253
2590
|
}
|
|
2254
2591
|
});
|
|
2255
2592
|
|
|
2256
|
-
program.parseAsync(process.argv)
|
|
2593
|
+
program.parseAsync(process.argv).finally(() => {
|
|
2594
|
+
closeReadline();
|
|
2595
|
+
});
|
|
2257
2596
|
|