postgresai 0.11.0-alpha.1

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 ADDED
@@ -0,0 +1,31 @@
1
+ ## CLI developer quickstart
2
+
3
+ ### run without install
4
+ ```bash
5
+ npm --prefix cli install --no-audit --no-fund
6
+ node ./cli/bin/postgres-ai.js --help
7
+ ```
8
+
9
+ ### use aliases locally (no ./)
10
+ ```bash
11
+ # install from repo into global prefix
12
+ npm install -g ./cli
13
+
14
+ # ensure global npm bin is in PATH (zsh)
15
+ echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> ~/.zshrc
16
+ exec zsh -l
17
+
18
+ # aliases
19
+ postgres-ai --help
20
+ pgai --help
21
+ ```
22
+
23
+ ### one‑off run (no install)
24
+ ```bash
25
+ npx -y -p file:cli postgres-ai --help
26
+ ```
27
+
28
+ ### env vars for integration tests
29
+ - `PGAI_API_KEY`
30
+ - `PGAI_BASE_URL` (default `https://v2.postgres.ai/api/general/`)
31
+
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { Command } = require("commander");
5
+ const pkg = require("../package.json");
6
+
7
+ function getConfig(opts) {
8
+ const apiKey = opts.apiKey || process.env.PGAI_API_KEY || "";
9
+ const baseUrl =
10
+ opts.baseUrl || process.env.PGAI_BASE_URL || "https://v2.postgres.ai/api/general/";
11
+ return { apiKey, baseUrl };
12
+ }
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name("postgres-ai")
18
+ .description("PostgresAI CLI")
19
+ .version(pkg.version)
20
+ .option("--api-key <key>", "API key (overrides PGAI_API_KEY)")
21
+ .option(
22
+ "--base-url <url>",
23
+ "API base URL (overrides PGAI_BASE_URL)",
24
+ "https://v2.postgres.ai/api/general/"
25
+ );
26
+
27
+ const stub = (name) => async () => {
28
+ // Temporary stubs until Node parity is implemented
29
+ console.error(`${name}: not implemented in Node CLI yet; use bash CLI for now`);
30
+ process.exitCode = 2;
31
+ };
32
+
33
+ function resolvePaths() {
34
+ const path = require("path");
35
+ const fs = require("fs");
36
+ const projectDir = process.cwd();
37
+ const composeFile = path.resolve(projectDir, "docker-compose.yml");
38
+ const instancesFile = path.resolve(projectDir, "instances.yml");
39
+ return { fs, path, projectDir, composeFile, instancesFile };
40
+ }
41
+
42
+ function getComposeCmd() {
43
+ const { spawnSync } = require("child_process");
44
+ const tryCmd = (cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }).status === 0;
45
+ if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
46
+ if (tryCmd("docker", ["compose", "version"])) return ["docker", "compose"];
47
+ return null;
48
+ }
49
+
50
+ async function runCompose(args) {
51
+ const { composeFile } = resolvePaths();
52
+ const cmd = getComposeCmd();
53
+ if (!cmd) {
54
+ console.error("docker compose not found (need docker-compose or docker compose)");
55
+ process.exitCode = 1;
56
+ return 1;
57
+ }
58
+ const { spawn } = require("child_process");
59
+ return new Promise((resolve) => {
60
+ const child = spawn(cmd[0], [...cmd.slice(1), "-f", composeFile, ...args], { stdio: "inherit" });
61
+ child.on("close", (code) => resolve(code));
62
+ });
63
+ }
64
+
65
+ program.command("help", { isDefault: true }).description("show help").action(() => {
66
+ program.outputHelp();
67
+ });
68
+
69
+ // Service lifecycle
70
+ program
71
+ .command("quickstart")
72
+ .description("complete setup (generate config, start services)")
73
+ .option("--demo", "demo mode", false)
74
+ .action(async () => {
75
+ const code1 = await runCompose(["run", "--rm", "sources-generator"]);
76
+ if (code1 !== 0) {
77
+ process.exitCode = code1;
78
+ return;
79
+ }
80
+ const code2 = await runCompose(["up", "-d"]);
81
+ if (code2 !== 0) process.exitCode = code2;
82
+ });
83
+ program
84
+ .command("install")
85
+ .description("prepare project (no-op in repo checkout)")
86
+ .action(async () => {
87
+ console.log("Project files present; nothing to install.");
88
+ });
89
+ program
90
+ .command("start")
91
+ .description("start services")
92
+ .action(async () => {
93
+ const code = await runCompose(["up", "-d"]);
94
+ if (code !== 0) process.exitCode = code;
95
+ });
96
+ program
97
+ .command("stop")
98
+ .description("stop services")
99
+ .action(async () => {
100
+ const code = await runCompose(["down"]);
101
+ if (code !== 0) process.exitCode = code;
102
+ });
103
+ program
104
+ .command("restart")
105
+ .description("restart services")
106
+ .action(async () => {
107
+ const code = await runCompose(["restart"]);
108
+ if (code !== 0) process.exitCode = code;
109
+ });
110
+ program
111
+ .command("status")
112
+ .description("show service status")
113
+ .action(async () => {
114
+ const code = await runCompose(["ps"]);
115
+ if (code !== 0) process.exitCode = code;
116
+ });
117
+ program
118
+ .command("logs [service]")
119
+ .option("-f, --follow", "follow logs", false)
120
+ .description("show logs for all or specific service")
121
+ .action(async (service, opts) => {
122
+ const args = ["logs"]; if (opts.follow) args.push("-f"); if (service) args.push(service);
123
+ const code = await runCompose(args);
124
+ if (code !== 0) process.exitCode = code;
125
+ });
126
+ program.command("health").description("health check").action(stub("health"));
127
+ program
128
+ .command("config")
129
+ .description("show configuration")
130
+ .action(async () => {
131
+ const { fs, projectDir, composeFile, instancesFile } = resolvePaths();
132
+ console.log(`Project Directory: ${projectDir}`);
133
+ console.log(`Docker Compose File: ${composeFile}`);
134
+ console.log(`Instances File: ${instancesFile}`);
135
+ if (fs.existsSync(instancesFile)) {
136
+ console.log("\nInstances configuration:\n");
137
+ const text = fs.readFileSync(instancesFile, "utf8");
138
+ process.stdout.write(text);
139
+ if (!/\n$/.test(text)) console.log();
140
+ }
141
+ });
142
+ program
143
+ .command("update-config")
144
+ .description("apply configuration (generate sources)")
145
+ .action(async () => {
146
+ const code = await runCompose(["run", "--rm", "sources-generator"]);
147
+ if (code !== 0) process.exitCode = code;
148
+ });
149
+ program.command("update").description("update project").action(stub("update"));
150
+ program
151
+ .command("reset [service]")
152
+ .description("reset all or specific service")
153
+ .action(stub("reset"));
154
+ program.command("clean").description("cleanup artifacts").action(stub("clean"));
155
+ program
156
+ .command("shell <service>")
157
+ .description("open service shell")
158
+ .action(async (service) => {
159
+ const code = await runCompose(["exec", "-T", service, "/bin/sh"]);
160
+ if (code !== 0) process.exitCode = code;
161
+ });
162
+ program
163
+ .command("check")
164
+ .description("system readiness check")
165
+ .action(async () => {
166
+ const code = await runCompose(["ps"]);
167
+ if (code !== 0) process.exitCode = code;
168
+ });
169
+
170
+ // Instance management
171
+ program
172
+ .command("list-instances")
173
+ .description("list instances")
174
+ .action(async () => {
175
+ const fs = require("fs");
176
+ const path = require("path");
177
+ const instancesPath = path.resolve(process.cwd(), "instances.yml");
178
+ if (!fs.existsSync(instancesPath)) {
179
+ console.error(`instances.yml not found in ${process.cwd()}`);
180
+ process.exitCode = 1;
181
+ return;
182
+ }
183
+ const content = fs.readFileSync(instancesPath, "utf8");
184
+ const lines = content.split(/\r?\n/);
185
+ let currentName = "";
186
+ let printed = false;
187
+ const collected = [];
188
+ for (const line of lines) {
189
+ const m = line.match(/^-[\t ]*name:[\t ]*(.+)$/);
190
+ if (m) {
191
+ currentName = m[1].trim();
192
+ collected.push(currentName);
193
+ printed = true;
194
+ }
195
+ }
196
+ // Hide demo placeholder if that's the only entry
197
+ if (printed) {
198
+ const filtered = collected.filter((n) => n !== "target-database");
199
+ const list = filtered.length > 0 ? filtered : [];
200
+ if (list.length === 0) {
201
+ console.log("No instances configured");
202
+ return;
203
+ }
204
+ for (const n of list) console.log(`Instance: ${n}`);
205
+ } else {
206
+ console.log("No instances found");
207
+ }
208
+ });
209
+ program
210
+ .command("add-instance [connStr] [name]")
211
+ .description("add instance")
212
+ .action(async (connStr, name) => {
213
+ const fs = require("fs");
214
+ const path = require("path");
215
+ const file = path.resolve(process.cwd(), "instances.yml");
216
+ if (!connStr) {
217
+ console.error("Connection string required: postgresql://user:pass@host:port/db");
218
+ process.exitCode = 1;
219
+ return;
220
+ }
221
+ const m = connStr.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:\/]+)(?::(\d+))?\/(.+)$/);
222
+ if (!m) {
223
+ console.error("Invalid connection string format");
224
+ process.exitCode = 1;
225
+ return;
226
+ }
227
+ const host = m[3];
228
+ const db = m[5];
229
+ const instanceName = name && name.trim() ? name.trim() : `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
230
+ const lineStart = `- name: ${instanceName}`;
231
+ 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`;
232
+ const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
233
+ if (new RegExp(`^${lineStart}$`, "m").test(content)) {
234
+ console.error(`Instance '${instanceName}' already exists`);
235
+ process.exitCode = 1;
236
+ return;
237
+ }
238
+ fs.appendFileSync(file, (content && !/\n$/.test(content) ? "\n" : "") + body, "utf8");
239
+ console.log(`Instance '${instanceName}' added`);
240
+ });
241
+ program
242
+ .command("remove-instance <name>")
243
+ .description("remove instance")
244
+ .action(async (name) => {
245
+ const fs = require("fs");
246
+ const path = require("path");
247
+ const file = path.resolve(process.cwd(), "instances.yml");
248
+ if (!fs.existsSync(file)) {
249
+ console.error("instances.yml not found");
250
+ process.exitCode = 1;
251
+ return;
252
+ }
253
+ const text = fs.readFileSync(file, "utf8");
254
+ const lines = text.split(/\r?\n/);
255
+ const out = [];
256
+ let skip = false;
257
+ for (let i = 0; i < lines.length; i++) {
258
+ const line = lines[i];
259
+ const isStart = /^-[\t ]*name:[\t ]*(.+)$/.test(line);
260
+ if (isStart) {
261
+ const n = line.replace(/^-[\t ]*name:[\t ]*/, "").trim();
262
+ if (n === name) {
263
+ skip = true;
264
+ continue;
265
+ } else if (skip) {
266
+ skip = false;
267
+ }
268
+ }
269
+ if (!skip) out.push(line);
270
+ }
271
+ if (out.join("\n") === text) {
272
+ console.error(`Instance '${name}' not found`);
273
+ process.exitCode = 1;
274
+ return;
275
+ }
276
+ fs.writeFileSync(file, out.join("\n"), "utf8");
277
+ console.log(`Instance '${name}' removed`);
278
+ });
279
+ program
280
+ .command("test-instance <name>")
281
+ .description("test instance connectivity")
282
+ .action(stub("test-instance"));
283
+
284
+ // API key and grafana
285
+ program
286
+ .command("add-key <apiKey>")
287
+ .description("store API key")
288
+ .action(async (apiKey) => {
289
+ const fs = require("fs");
290
+ const path = require("path");
291
+ const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
292
+ const existing = fs.existsSync(cfgPath) ? fs.readFileSync(cfgPath, "utf8") : "";
293
+ const filtered = existing
294
+ .split(/\r?\n/)
295
+ .filter((l) => !/^api_key=/.test(l))
296
+ .join("\n")
297
+ .replace(/\n+$/g, "");
298
+ const next = filtered.length ? `${filtered}\napi_key=${apiKey}\n` : `api_key=${apiKey}\n`;
299
+ fs.writeFileSync(cfgPath, next, "utf8");
300
+ console.log("API key saved to .pgwatch-config");
301
+ });
302
+
303
+ program
304
+ .command("show-key")
305
+ .description("show API key (masked)")
306
+ .action(async () => {
307
+ const fs = require("fs");
308
+ const path = require("path");
309
+ const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
310
+ if (!fs.existsSync(cfgPath)) {
311
+ console.log("No API key configured");
312
+ return;
313
+ }
314
+ const content = fs.readFileSync(cfgPath, "utf8");
315
+ const m = content.match(/^api_key=(.+)$/m);
316
+ if (!m) {
317
+ console.log("No API key configured");
318
+ return;
319
+ }
320
+ const key = m[1].trim();
321
+ if (!key) {
322
+ console.log("No API key configured");
323
+ return;
324
+ }
325
+ const mask = (k) => (k.length <= 8 ? "****" : `${k.slice(0, 4)}${"*".repeat(k.length - 8)}${k.slice(-4)}`);
326
+ console.log(`Current API key: ${mask(key)}`);
327
+ });
328
+
329
+ program
330
+ .command("remove-key")
331
+ .description("remove API key")
332
+ .action(async () => {
333
+ const fs = require("fs");
334
+ const path = require("path");
335
+ const cfgPath = path.resolve(process.cwd(), ".pgwatch-config");
336
+ if (!fs.existsSync(cfgPath)) {
337
+ console.log("No API key configured");
338
+ return;
339
+ }
340
+ const content = fs.readFileSync(cfgPath, "utf8");
341
+ const filtered = content
342
+ .split(/\r?\n/)
343
+ .filter((l) => !/^api_key=/.test(l))
344
+ .join("\n")
345
+ .replace(/\n+$/g, "\n");
346
+ fs.writeFileSync(cfgPath, filtered, "utf8");
347
+ console.log("API key removed");
348
+ });
349
+ program
350
+ .command("generate-grafana-password")
351
+ .description("generate Grafana password")
352
+ .action(stub("generate-grafana-password"));
353
+ program
354
+ .command("show-grafana-credentials")
355
+ .description("show Grafana credentials")
356
+ .action(stub("show-grafana-credentials"));
357
+
358
+ program.parseAsync(process.argv);
359
+
360
+
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "postgresai",
3
+ "version": "0.11.0-alpha.1",
4
+ "description": "PostgresAI CLI (Node.js)",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://gitlab.com/postgres-ai/postgres_ai.git"
10
+ },
11
+ "homepage": "https://gitlab.com/postgres-ai/postgres_ai",
12
+ "bugs": {
13
+ "url": "https://gitlab.com/postgres-ai/postgres_ai/-/issues"
14
+ },
15
+ "bin": {
16
+ "postgres-ai": "./bin/postgres-ai.js",
17
+ "pgai": "./bin/postgres-ai.js"
18
+ },
19
+ "type": "commonjs",
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "scripts": {
24
+ "start": "node ./bin/postgres-ai.js --help"
25
+ },
26
+ "dependencies": {
27
+ "commander": "^12.1.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }
33
+
34
+