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 +31 -0
- package/bin/postgres-ai.js +360 -0
- package/package.json +34 -0
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
|
+
|