postgresai 0.14.0-dev.36 → 0.14.0-dev.38

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 CHANGED
@@ -50,26 +50,26 @@ If you want `npx pgai ...` as a shorthand for `npx postgresai ...`, install the
50
50
  npx pgai --help
51
51
  ```
52
52
 
53
- ## init (create monitoring user in Postgres)
53
+ ## prepare-db (create monitoring user in Postgres)
54
54
 
55
55
  This command creates (or updates) the `postgres_ai_mon` user, creates the required view(s), and grants the permissions described in the root `README.md` (it is idempotent). Where supported, it also enables observability extensions described there.
56
56
 
57
57
  Run without installing (positional connection string):
58
58
 
59
59
  ```bash
60
- npx postgresai init postgresql://admin@host:5432/dbname
60
+ npx postgresai prepare-db postgresql://admin@host:5432/dbname
61
61
  ```
62
62
 
63
- It also accepts libpq conninfo syntax:
63
+ It also accepts libpq "conninfo" syntax:
64
64
 
65
65
  ```bash
66
- npx postgresai init "dbname=dbname host=host user=admin"
66
+ npx postgresai prepare-db "dbname=dbname host=host user=admin"
67
67
  ```
68
68
 
69
69
  And psql-like options:
70
70
 
71
71
  ```bash
72
- npx postgresai init -h host -p 5432 -U admin -d dbname
72
+ npx postgresai prepare-db -h host -p 5432 -U admin -d dbname
73
73
  ```
74
74
 
75
75
  Password input options (in priority order):
@@ -83,7 +83,7 @@ By default, the generated password is printed **only in interactive (TTY) mode**
83
83
  Optional permissions (RDS/self-managed extras from the root `README.md`) are enabled by default. To skip them:
84
84
 
85
85
  ```bash
86
- npx postgresai init postgresql://admin@host:5432/dbname --skip-optional-permissions
86
+ npx postgresai prepare-db postgresql://admin@host:5432/dbname --skip-optional-permissions
87
87
  ```
88
88
 
89
89
  ### Print SQL / dry run
@@ -91,7 +91,7 @@ npx postgresai init postgresql://admin@host:5432/dbname --skip-optional-permissi
91
91
  To see what SQL would be executed (passwords redacted by default):
92
92
 
93
93
  ```bash
94
- npx postgresai init postgresql://admin@host:5432/dbname --print-sql
94
+ npx postgresai prepare-db postgresql://admin@host:5432/dbname --print-sql
95
95
  ```
96
96
 
97
97
  ### Verify and password reset
@@ -99,13 +99,13 @@ npx postgresai init postgresql://admin@host:5432/dbname --print-sql
99
99
  Verify that everything is configured as expected (no changes):
100
100
 
101
101
  ```bash
102
- npx postgresai init postgresql://admin@host:5432/dbname --verify
102
+ npx postgresai prepare-db postgresql://admin@host:5432/dbname --verify
103
103
  ```
104
104
 
105
105
  Reset monitoring user password only (no other changes):
106
106
 
107
107
  ```bash
108
- npx postgresai init postgresql://admin@host:5432/dbname --reset-password --password 'new_password'
108
+ npx postgresai prepare-db postgresql://admin@host:5432/dbname --reset-password --password 'new_password'
109
109
  ```
110
110
 
111
111
  ## Quick start
@@ -126,17 +126,17 @@ This will:
126
126
 
127
127
  Start monitoring with demo database:
128
128
  ```bash
129
- postgres-ai mon quickstart --demo
129
+ postgres-ai mon local-install --demo
130
130
  ```
131
131
 
132
132
  Start monitoring with your own database:
133
133
  ```bash
134
- postgres-ai mon quickstart --db-url postgresql://user:pass@host:5432/db
134
+ postgres-ai mon local-install --db-url postgresql://user:pass@host:5432/db
135
135
  ```
136
136
 
137
137
  Complete automated setup with API key and database:
138
138
  ```bash
139
- postgres-ai mon quickstart --api-key your_key --db-url postgresql://user:pass@host:5432/db -y
139
+ postgres-ai mon local-install --api-key your_key --db-url postgresql://user:pass@host:5432/db -y
140
140
  ```
141
141
 
142
142
  This will:
@@ -153,12 +153,12 @@ This will:
153
153
  #### Service lifecycle
154
154
  ```bash
155
155
  # Complete setup with various options
156
- postgres-ai mon quickstart # Interactive setup for production
157
- postgres-ai mon quickstart --demo # Demo mode with sample database
158
- postgres-ai mon quickstart --api-key <key> # Setup with API key
159
- postgres-ai mon quickstart --db-url <url> # Setup with database URL
160
- postgres-ai mon quickstart --api-key <key> --db-url <url> # Complete automated setup
161
- postgres-ai mon quickstart -y # Auto-accept all defaults
156
+ postgres-ai mon local-install # Interactive setup for production
157
+ postgres-ai mon local-install --demo # Demo mode with sample database
158
+ postgres-ai mon local-install --api-key <key> # Setup with API key
159
+ postgres-ai mon local-install --db-url <url> # Setup with database URL
160
+ postgres-ai mon local-install --api-key <key> --db-url <url> # Complete automated setup
161
+ postgres-ai mon local-install -y # Auto-accept all defaults
162
162
 
163
163
  # Service management
164
164
  postgres-ai mon start # Start monitoring services
@@ -168,7 +168,7 @@ postgres-ai mon status # Show monitoring services status
168
168
  postgres-ai mon health [--wait <sec>] # Check monitoring services health
169
169
  ```
170
170
 
171
- ##### Quickstart options
171
+ ##### local-install options
172
172
  - `--demo` - Demo mode with sample database (testing only, cannot use with --api-key)
173
173
  - `--api-key <key>` - Postgres AI API key for automated report uploads
174
174
  - `--db-url <url>` - PostgreSQL connection URL to monitor (format: `postgresql://user:pass@host:port/db`)
@@ -256,10 +256,10 @@ postgres-ai mon show-grafana-credentials # Show Grafana credentials
256
256
 
257
257
  ### Authentication and API key management
258
258
  ```bash
259
- postgres-ai auth # Authenticate via browser (recommended)
260
- postgres-ai add-key <key> # Manually store API key
261
- postgres-ai show-key # Show stored key (masked)
262
- postgres-ai remove-key # Remove stored key
259
+ postgres-ai auth # Authenticate via browser (OAuth)
260
+ postgres-ai auth --set-key <key> # Store API key directly
261
+ postgres-ai show-key # Show stored key (masked)
262
+ postgres-ai remove-key # Remove stored key
263
263
  ```
264
264
 
265
265
  ## Configuration
@@ -17,10 +17,68 @@ import { startMcpServer } from "../lib/mcp-server";
17
17
  import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
18
18
  import { resolveBaseUrls } from "../lib/util";
19
19
  import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
20
+ import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checkup";
21
+ import { createCheckupReport, uploadCheckupReportJson, RpcError, formatRpcErrorForDisplay } from "../lib/checkup-api";
20
22
 
21
23
  const execPromise = promisify(exec);
22
24
  const execFilePromise = promisify(execFile);
23
25
 
26
+ function expandHomePath(p: string): string {
27
+ const s = (p || "").trim();
28
+ if (!s) return s;
29
+ if (s === "~") return os.homedir();
30
+ if (s.startsWith("~/") || s.startsWith("~\\")) {
31
+ return path.join(os.homedir(), s.slice(2));
32
+ }
33
+ return s;
34
+ }
35
+
36
+ function createTtySpinner(
37
+ enabled: boolean,
38
+ initialText: string
39
+ ): { update: (text: string) => void; stop: (finalText?: string) => void } {
40
+ if (!enabled) {
41
+ return {
42
+ update: () => {},
43
+ stop: () => {},
44
+ };
45
+ }
46
+
47
+ const frames = ["|", "/", "-", "\\"];
48
+ const startTs = Date.now();
49
+ let text = initialText;
50
+ let frameIdx = 0;
51
+ let stopped = false;
52
+
53
+ const render = (): void => {
54
+ if (stopped) return;
55
+ const elapsedSec = ((Date.now() - startTs) / 1000).toFixed(1);
56
+ const frame = frames[frameIdx % frames.length]!;
57
+ frameIdx += 1;
58
+ process.stdout.write(`\r\x1b[2K${frame} ${text} (${elapsedSec}s)`);
59
+ };
60
+
61
+ const timer = setInterval(render, 120);
62
+ render(); // immediate feedback
63
+
64
+ return {
65
+ update: (t: string) => {
66
+ text = t;
67
+ render();
68
+ },
69
+ stop: (finalText?: string) => {
70
+ if (stopped) return;
71
+ stopped = true;
72
+ clearInterval(timer);
73
+ process.stdout.write("\r\x1b[2K");
74
+ if (finalText && finalText.trim()) {
75
+ process.stdout.write(finalText);
76
+ }
77
+ process.stdout.write("\n");
78
+ },
79
+ };
80
+ }
81
+
24
82
  /**
25
83
  * CLI configuration options
26
84
  */
@@ -203,8 +261,22 @@ program
203
261
  );
204
262
 
205
263
  program
206
- .command("init [conn]")
207
- .description("Create a monitoring user, required view(s), and grant required permissions (idempotent)")
264
+ .command("set-default-project <project>")
265
+ .description("store default project for checkup uploads")
266
+ .action(async (project: string) => {
267
+ const value = (project || "").trim();
268
+ if (!value) {
269
+ console.error("Error: project is required");
270
+ process.exitCode = 1;
271
+ return;
272
+ }
273
+ config.writeConfig({ defaultProject: value });
274
+ console.log(`Default project saved: ${value}`);
275
+ });
276
+
277
+ program
278
+ .command("prepare-db [conn]")
279
+ .description("Prepare database for monitoring: create monitoring user, required view(s), and grant permissions (idempotent)")
208
280
  .option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)")
209
281
  .option("-h, --host <host>", "PostgreSQL host (psql-like)")
210
282
  .option("-p, --port <port>", "PostgreSQL port (psql-like)")
@@ -223,9 +295,9 @@ program
223
295
  [
224
296
  "",
225
297
  "Examples:",
226
- " postgresai init postgresql://admin@host:5432/dbname",
227
- " postgresai init \"dbname=dbname host=host user=admin\"",
228
- " postgresai init -h host -p 5432 -U admin -d dbname",
298
+ " postgresai prepare-db postgresql://admin@host:5432/dbname",
299
+ " postgresai prepare-db \"dbname=dbname host=host user=admin\"",
300
+ " postgresai prepare-db -h host -p 5432 -U admin -d dbname",
229
301
  "",
230
302
  "Admin password:",
231
303
  " --admin-password <password> or PGPASSWORD=... (libpq standard)",
@@ -247,16 +319,16 @@ program
247
319
  " PGAI_MON_PASSWORD — monitoring password",
248
320
  "",
249
321
  "Inspect SQL without applying changes:",
250
- " postgresai init <conn> --print-sql",
322
+ " postgresai prepare-db <conn> --print-sql",
251
323
  "",
252
324
  "Verify setup (no changes):",
253
- " postgresai init <conn> --verify",
325
+ " postgresai prepare-db <conn> --verify",
254
326
  "",
255
327
  "Reset monitoring password only:",
256
- " postgresai init <conn> --reset-password --password '...'",
328
+ " postgresai prepare-db <conn> --reset-password --password '...'",
257
329
  "",
258
330
  "Offline SQL plan (no DB connection):",
259
- " postgresai init --print-sql",
331
+ " postgresai prepare-db --print-sql",
260
332
  ].join("\n")
261
333
  )
262
334
  .action(async (conn: string | undefined, opts: {
@@ -336,7 +408,7 @@ program
336
408
  });
337
409
  } catch (e) {
338
410
  const msg = e instanceof Error ? e.message : String(e);
339
- console.error(`Error: init: ${msg}`);
411
+ console.error(`Error: prepare-db: ${msg}`);
340
412
  // When connection details are missing, show full init help (options + examples).
341
413
  if (typeof msg === "string" && msg.startsWith("Connection is required.")) {
342
414
  console.error("");
@@ -372,14 +444,14 @@ program
372
444
  includeOptionalPermissions,
373
445
  });
374
446
  if (v.ok) {
375
- console.log("✓ init verify: OK");
447
+ console.log("✓ prepare-db verify: OK");
376
448
  if (v.missingOptional.length > 0) {
377
449
  console.log("⚠ Optional items missing:");
378
450
  for (const m of v.missingOptional) console.log(`- ${m}`);
379
451
  }
380
452
  return;
381
453
  }
382
- console.error("✗ init verify failed: missing required items");
454
+ console.error("✗ prepare-db verify failed: missing required items");
383
455
  for (const m of v.missingRequired) console.error(`- ${m}`);
384
456
  if (v.missingOptional.length > 0) {
385
457
  console.error("Optional items missing:");
@@ -455,7 +527,7 @@ program
455
527
 
456
528
  const { applied, skippedOptional } = await applyInitPlan({ client, plan: effectivePlan });
457
529
 
458
- console.log(opts.resetPassword ? "✓ init password reset completed" : "✓ init completed");
530
+ console.log(opts.resetPassword ? "✓ prepare-db password reset completed" : "✓ prepare-db completed");
459
531
  if (skippedOptional.length > 0) {
460
532
  console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
461
533
  for (const s of skippedOptional) console.log(`- ${s}`);
@@ -477,7 +549,7 @@ program
477
549
  if (!message || message === "[object Object]") {
478
550
  message = "Unknown error";
479
551
  }
480
- console.error(`Error: init: ${message}`);
552
+ console.error(`Error: prepare-db: ${message}`);
481
553
  // If this was a plan step failure, surface the step name explicitly to help users diagnose quickly.
482
554
  const stepMatch =
483
555
  typeof message === "string" ? message.match(/Failed at step "([^"]+)":/i) : null;
@@ -529,6 +601,250 @@ program
529
601
  }
530
602
  });
531
603
 
604
+ program
605
+ .command("checkup [conn]")
606
+ .description("generate health check reports directly from PostgreSQL (express mode)")
607
+ .option("--check-id <id>", `specific check to run: ${Object.keys(CHECK_INFO).join(", ")}, or ALL`, "ALL")
608
+ .option("--node-name <name>", "node name for reports", "node-01")
609
+ .option("--output <path>", "output directory for JSON files")
610
+ .option("--json", "output to stdout as JSON instead of files")
611
+ .option("--upload", "create a remote checkup report and upload JSON results (requires API key)", false)
612
+ .option("--project <project>", "project name or ID for remote upload (used with --upload; defaults to config defaultProject)")
613
+ .addHelpText(
614
+ "after",
615
+ [
616
+ "",
617
+ "Available checks:",
618
+ ...Object.entries(CHECK_INFO).map(([id, title]) => ` ${id}: ${title}`),
619
+ "",
620
+ "Examples:",
621
+ " postgresai checkup postgresql://user:pass@host:5432/db",
622
+ " postgresai checkup postgresql://user:pass@host:5432/db --check-id A003",
623
+ " postgresai checkup postgresql://user:pass@host:5432/db --json",
624
+ " postgresai checkup postgresql://user:pass@host:5432/db --output ./reports",
625
+ " postgresai checkup postgresql://user:pass@host:5432/db --upload --project my_project",
626
+ " postgresai set-default-project my_project",
627
+ " postgresai checkup postgresql://user:pass@host:5432/db --upload",
628
+ ].join("\n")
629
+ )
630
+ .action(async (conn: string | undefined, opts: {
631
+ checkId: string;
632
+ nodeName: string;
633
+ output?: string;
634
+ json?: boolean;
635
+ upload?: boolean;
636
+ project?: string;
637
+ }, cmd: Command) => {
638
+ if (!conn) {
639
+ // No args — show help like other commands do (instead of a bare error).
640
+ cmd.outputHelp();
641
+ process.exitCode = 1;
642
+ return;
643
+ }
644
+
645
+ // Preflight: validate/create output directory BEFORE connecting / running checks.
646
+ // This avoids waiting on network/DB work only to fail at the very end.
647
+ let outputPath: string | undefined;
648
+ if (opts.output && !opts.json) {
649
+ const outputDir = expandHomePath(opts.output);
650
+ outputPath = path.isAbsolute(outputDir) ? outputDir : path.resolve(process.cwd(), outputDir);
651
+ if (!fs.existsSync(outputPath)) {
652
+ try {
653
+ fs.mkdirSync(outputPath, { recursive: true });
654
+ } catch (e) {
655
+ const errAny = e as any;
656
+ const code = typeof errAny?.code === "string" ? errAny.code : "";
657
+ const msg = errAny instanceof Error ? errAny.message : String(errAny);
658
+ if (code === "EACCES" || code === "EPERM" || code === "ENOENT") {
659
+ console.error(`Error: Failed to create output directory: ${outputPath}`);
660
+ console.error(`Reason: ${msg}`);
661
+ console.error("Tip: choose a writable path, e.g. --output ./reports or --output ~/reports");
662
+ process.exitCode = 1;
663
+ return;
664
+ }
665
+ throw e;
666
+ }
667
+ }
668
+ }
669
+
670
+ // Preflight: validate upload flags/credentials BEFORE connecting / running checks.
671
+ // This allows "fast-fail" for missing API key / project name.
672
+ let uploadCfg:
673
+ | { apiKey: string; apiBaseUrl: string; project: string; epoch: number }
674
+ | undefined;
675
+ if (opts.upload) {
676
+ const rootOpts = program.opts() as CliOptions;
677
+ const { apiKey } = getConfig(rootOpts);
678
+ if (!apiKey) {
679
+ console.error("Error: API key is required for --upload");
680
+ console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
681
+ process.exitCode = 1;
682
+ return;
683
+ }
684
+
685
+ const cfg = config.readConfig();
686
+ const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
687
+ const project = ((opts.project || cfg.defaultProject) || "").trim();
688
+ if (!project) {
689
+ console.error("Error: --project is required (or set a default via 'postgresai set-default-project <project>')");
690
+ process.exitCode = 1;
691
+ return;
692
+ }
693
+ const epoch = Math.floor(Date.now() / 1000);
694
+ uploadCfg = {
695
+ apiKey,
696
+ apiBaseUrl,
697
+ project,
698
+ epoch,
699
+ };
700
+ }
701
+
702
+ // Use the same SSL behavior as prepare-db:
703
+ // - Default: sslmode=prefer (try SSL first, fallback to non-SSL)
704
+ // - Respect PGSSLMODE env and ?sslmode=... in connection URI
705
+ const adminConn = resolveAdminConnection({
706
+ conn,
707
+ envPassword: process.env.PGPASSWORD,
708
+ });
709
+ let client: Client | undefined;
710
+ const spinnerEnabled = !!process.stdout.isTTY && !opts.json;
711
+ const spinner = createTtySpinner(spinnerEnabled, "Connecting to Postgres");
712
+
713
+ try {
714
+ spinner.update("Connecting to Postgres");
715
+ const connResult = await connectWithSslFallback(Client, adminConn);
716
+ client = connResult.client as Client;
717
+
718
+ let reports: Record<string, any>;
719
+ let uploadSummary:
720
+ | { project: string; reportId: number; uploaded: Array<{ checkId: string; filename: string; chunkId: number }> }
721
+ | undefined;
722
+
723
+ if (opts.checkId === "ALL") {
724
+ reports = await generateAllReports(client, opts.nodeName, (p) => {
725
+ spinner.update(`Running ${p.checkId}: ${p.checkTitle} (${p.index}/${p.total})`);
726
+ });
727
+ } else {
728
+ const checkId = opts.checkId.toUpperCase();
729
+ const generator = REPORT_GENERATORS[checkId];
730
+ if (!generator) {
731
+ spinner.stop();
732
+ console.error(`Unknown check ID: ${opts.checkId}`);
733
+ console.error(`Available: ${Object.keys(CHECK_INFO).join(", ")}, ALL`);
734
+ process.exitCode = 1;
735
+ return;
736
+ }
737
+ spinner.update(`Running ${checkId}: ${CHECK_INFO[checkId] || checkId}`);
738
+ reports = { [checkId]: await generator(client, opts.nodeName) };
739
+ }
740
+
741
+ // Optional: upload to PostgresAI API.
742
+ if (uploadCfg) {
743
+ spinner.update("Creating remote checkup report");
744
+ const created = await createCheckupReport({
745
+ apiKey: uploadCfg.apiKey,
746
+ apiBaseUrl: uploadCfg.apiBaseUrl,
747
+ project: uploadCfg.project,
748
+ epoch: uploadCfg.epoch,
749
+ });
750
+
751
+ const reportId = created.reportId;
752
+ // Keep upload progress out of stdout when --json is used.
753
+ const logUpload = (msg: string): void => {
754
+ if (opts.json) console.error(msg);
755
+ };
756
+ logUpload(`Created remote checkup report: ${reportId}`);
757
+
758
+ const uploaded: Array<{ checkId: string; filename: string; chunkId: number }> = [];
759
+ for (const [checkId, report] of Object.entries(reports)) {
760
+ spinner.update(`Uploading ${checkId}.json`);
761
+ const jsonText = JSON.stringify(report, null, 2);
762
+ const r = await uploadCheckupReportJson({
763
+ apiKey: uploadCfg.apiKey,
764
+ apiBaseUrl: uploadCfg.apiBaseUrl,
765
+ reportId,
766
+ filename: `${checkId}.json`,
767
+ checkId,
768
+ jsonText,
769
+ });
770
+ uploaded.push({ checkId, filename: `${checkId}.json`, chunkId: r.reportChunkId });
771
+ }
772
+ logUpload("Upload completed");
773
+ uploadSummary = { project: uploadCfg.project, reportId, uploaded };
774
+ }
775
+
776
+ spinner.stop();
777
+ // Output results
778
+ if (opts.json) {
779
+ console.log(JSON.stringify(reports, null, 2));
780
+ } else if (opts.output) {
781
+ // Write to files
782
+ // outputPath is preflight-validated above
783
+ const outDir = outputPath || path.resolve(process.cwd(), expandHomePath(opts.output));
784
+ for (const [checkId, report] of Object.entries(reports)) {
785
+ const filePath = path.join(outDir, `${checkId}.json`);
786
+ fs.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
787
+ console.log(`✓ ${checkId}: ${filePath}`);
788
+ }
789
+ } else if (uploadSummary) {
790
+ // Default with --upload: show upload result instead of local-only summary.
791
+ console.log("\nCheckup report uploaded");
792
+ console.log("======================\n");
793
+ console.log(`Project: ${uploadSummary.project}`);
794
+ console.log(`Report ID: ${uploadSummary.reportId}`);
795
+ console.log("View in Console: console.postgres.ai → Support → checkup reports");
796
+ console.log("");
797
+ console.log("Files:");
798
+ for (const item of uploadSummary.uploaded) {
799
+ console.log(`- ${item.checkId}: ${item.filename}`);
800
+ }
801
+ } else {
802
+ // Default: print summary
803
+ console.log("\nHealth Check Reports Generated:");
804
+ console.log("================================\n");
805
+ for (const [checkId, report] of Object.entries(reports)) {
806
+ const r = report as any;
807
+ console.log(`${checkId}: ${r.checkTitle}`);
808
+ if (r.results && r.results[opts.nodeName]) {
809
+ const nodeData = r.results[opts.nodeName];
810
+ if (nodeData.postgres_version) {
811
+ console.log(` PostgreSQL: ${nodeData.postgres_version.version}`);
812
+ }
813
+ if (checkId === "A007" && nodeData.data) {
814
+ const count = Object.keys(nodeData.data).length;
815
+ console.log(` Altered settings: ${count}`);
816
+ }
817
+ if (checkId === "A004" && nodeData.data) {
818
+ if (nodeData.data.database_sizes) {
819
+ const dbCount = Object.keys(nodeData.data.database_sizes).length;
820
+ console.log(` Databases: ${dbCount}`);
821
+ }
822
+ if (nodeData.data.general_info?.cache_hit_ratio) {
823
+ console.log(` Cache hit ratio: ${nodeData.data.general_info.cache_hit_ratio.value}%`);
824
+ }
825
+ }
826
+ }
827
+ }
828
+ console.log("\nUse --json for full output or --output <dir> to save files");
829
+ }
830
+ } catch (error) {
831
+ spinner.stop();
832
+ if (error instanceof RpcError) {
833
+ for (const line of formatRpcErrorForDisplay(error)) {
834
+ console.error(line);
835
+ }
836
+ } else {
837
+ const message = error instanceof Error ? error.message : String(error);
838
+ console.error(`Error: ${message}`);
839
+ }
840
+ process.exitCode = 1;
841
+ } finally {
842
+ if (client) {
843
+ await client.end();
844
+ }
845
+ }
846
+ });
847
+
532
848
  /**
533
849
  * Stub function for not implemented commands
534
850
  */
@@ -679,8 +995,8 @@ program.command("help", { isDefault: true }).description("show help").action(()
679
995
  const mon = program.command("mon").description("monitoring services management");
680
996
 
681
997
  mon
682
- .command("quickstart")
683
- .description("complete setup (generate config, start monitoring services)")
998
+ .command("local-install")
999
+ .description("install local monitoring stack (generate config, start services)")
684
1000
  .option("--demo", "demo mode with sample database", false)
685
1001
  .option("--api-key <key>", "Postgres AI API key for automated report uploads")
686
1002
  .option("--db-url <url>", "PostgreSQL connection URL to monitor")
@@ -688,7 +1004,7 @@ mon
688
1004
  .option("-y, --yes", "accept all defaults and skip interactive prompts", false)
689
1005
  .action(async (opts: { demo: boolean; apiKey?: string; dbUrl?: string; tag?: string; yes: boolean }) => {
690
1006
  console.log("\n=================================");
691
- console.log(" PostgresAI Monitoring Quickstart");
1007
+ console.log(" PostgresAI monitoring local install");
692
1008
  console.log("=================================\n");
693
1009
  console.log("This will install, configure, and start the monitoring system\n");
694
1010
 
@@ -726,8 +1042,8 @@ mon
726
1042
  if (opts.demo && opts.apiKey) {
727
1043
  console.error("✗ Cannot use --api-key with --demo mode");
728
1044
  console.error("✗ Demo mode is for testing only and does not support API key integration");
729
- console.error("\nUse demo mode without API key: postgres-ai mon quickstart --demo");
730
- console.error("Or use production mode with API key: postgres-ai mon quickstart --api-key=your_key");
1045
+ console.error("\nUse demo mode without API key: postgres-ai mon local-install --demo");
1046
+ console.error("Or use production mode with API key: postgres-ai mon local-install --api-key=your_key");
731
1047
  process.exitCode = 1;
732
1048
  return;
733
1049
  }
@@ -989,7 +1305,7 @@ mon
989
1305
 
990
1306
  // Final summary
991
1307
  console.log("=================================");
992
- console.log(" 🎉 Quickstart setup completed!");
1308
+ console.log(" Local install completed!");
993
1309
  console.log("=================================\n");
994
1310
 
995
1311
  console.log("What's running:");
@@ -1536,10 +1852,26 @@ targets
1536
1852
  // Authentication and API key management
1537
1853
  program
1538
1854
  .command("auth")
1539
- .description("authenticate via browser and obtain API key")
1855
+ .description("authenticate via browser (OAuth) or store API key directly")
1856
+ .option("--set-key <key>", "store API key directly without OAuth flow")
1540
1857
  .option("--port <port>", "local callback server port (default: random)", parseInt)
1541
1858
  .option("--debug", "enable debug output")
1542
- .action(async (opts: { port?: number; debug?: boolean }) => {
1859
+ .action(async (opts: { setKey?: string; port?: number; debug?: boolean }) => {
1860
+ // If --set-key is provided, store it directly without OAuth
1861
+ if (opts.setKey) {
1862
+ const trimmedKey = opts.setKey.trim();
1863
+ if (!trimmedKey) {
1864
+ console.error("Error: API key cannot be empty");
1865
+ process.exitCode = 1;
1866
+ return;
1867
+ }
1868
+
1869
+ config.writeConfig({ apiKey: trimmedKey });
1870
+ console.log(`API key saved to ${config.getConfigPath()}`);
1871
+ return;
1872
+ }
1873
+
1874
+ // Otherwise, proceed with OAuth flow
1543
1875
  const pkce = require("../lib/pkce");
1544
1876
  const authServer = require("../lib/auth-server");
1545
1877
 
@@ -1765,8 +2097,9 @@ program
1765
2097
 
1766
2098
  program
1767
2099
  .command("add-key <apiKey>")
1768
- .description("store API key")
2100
+ .description("store API key (deprecated: use 'auth --set-key' instead)")
1769
2101
  .action(async (apiKey: string) => {
2102
+ console.warn("Warning: 'add-key' is deprecated. Use 'auth --set-key <key>' instead.\n");
1770
2103
  config.writeConfig({ apiKey });
1771
2104
  console.log(`API key saved to ${config.getConfigPath()}`);
1772
2105
  });
@@ -1893,7 +2226,7 @@ mon
1893
2226
  const { projectDir } = await resolveOrInitPaths();
1894
2227
  const cfgPath = path.resolve(projectDir, ".pgwatch-config");
1895
2228
  if (!fs.existsSync(cfgPath)) {
1896
- console.error("Configuration file not found. Run 'postgres-ai mon quickstart' first.");
2229
+ console.error("Configuration file not found. Run 'postgres-ai mon local-install' first.");
1897
2230
  process.exitCode = 1;
1898
2231
  return;
1899
2232
  }