postgresai 0.15.0-dev.4 → 0.15.0-dev.5
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 +121 -10
- package/dist/bin/postgres-ai.js +107 -11
- package/package.json +1 -1
- package/test/compose-cmd.test.ts +120 -0
package/bin/postgres-ai.ts
CHANGED
|
@@ -477,14 +477,14 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
|
|
|
477
477
|
fs.mkdirSync(projectDir, { recursive: true, mode: 0o700 });
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
].filter((v): v is string => Boolean(v && v.trim()));
|
|
480
|
+
const refs = [
|
|
481
|
+
process.env.PGAI_PROJECT_REF,
|
|
482
|
+
pkg.version,
|
|
483
|
+
`v${pkg.version}`,
|
|
484
|
+
"main",
|
|
485
|
+
].filter((v): v is string => Boolean(v && v.trim()));
|
|
487
486
|
|
|
487
|
+
if (!fs.existsSync(composeFile)) {
|
|
488
488
|
let lastErr: unknown;
|
|
489
489
|
for (const ref of refs) {
|
|
490
490
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
@@ -503,6 +503,21 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
|
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
// Download instances.demo.yml (demo target template) if not present
|
|
507
|
+
const demoFile = path.resolve(projectDir, "instances.demo.yml");
|
|
508
|
+
if (!fs.existsSync(demoFile)) {
|
|
509
|
+
for (const ref of refs) {
|
|
510
|
+
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/instances.demo.yml`;
|
|
511
|
+
try {
|
|
512
|
+
const text = await downloadText(url);
|
|
513
|
+
fs.writeFileSync(demoFile, text, { encoding: "utf8", mode: 0o600 });
|
|
514
|
+
break;
|
|
515
|
+
} catch {
|
|
516
|
+
// non-fatal — demo file is optional
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
506
521
|
// Ensure instances.yml exists as a FILE (avoid Docker creating a directory)
|
|
507
522
|
if (!fs.existsSync(instancesFile)) {
|
|
508
523
|
const header =
|
|
@@ -2071,13 +2086,16 @@ function isDockerRunning(): boolean {
|
|
|
2071
2086
|
}
|
|
2072
2087
|
|
|
2073
2088
|
/**
|
|
2074
|
-
* Get docker compose command
|
|
2089
|
+
* Get docker compose command.
|
|
2090
|
+
* Prefer "docker compose" (V2 plugin) over legacy "docker-compose" (V1 standalone)
|
|
2091
|
+
* because docker-compose V1 (<=1.29) is incompatible with modern Docker engines
|
|
2092
|
+
* (KeyError: 'ContainerConfig' on container recreation).
|
|
2075
2093
|
*/
|
|
2076
2094
|
function getComposeCmd(): string[] | null {
|
|
2077
2095
|
const tryCmd = (cmd: string, args: string[]): boolean =>
|
|
2078
2096
|
spawnSync(cmd, args, { stdio: "ignore", timeout: 5000 } as Parameters<typeof spawnSync>[2]).status === 0;
|
|
2079
|
-
if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
|
|
2080
2097
|
if (tryCmd("docker", ["compose", "version"])) return ["docker", "compose"];
|
|
2098
|
+
if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
|
|
2081
2099
|
return null;
|
|
2082
2100
|
}
|
|
2083
2101
|
|
|
@@ -2235,6 +2253,26 @@ async function runCompose(args: string[], grafanaPassword?: string): Promise<num
|
|
|
2235
2253
|
}
|
|
2236
2254
|
}
|
|
2237
2255
|
|
|
2256
|
+
// Load VM auth credentials from .env if not already set
|
|
2257
|
+
const envFilePath = path.resolve(projectDir, ".env");
|
|
2258
|
+
if (fs.existsSync(envFilePath)) {
|
|
2259
|
+
try {
|
|
2260
|
+
const envContent = fs.readFileSync(envFilePath, "utf8");
|
|
2261
|
+
if (!env.VM_AUTH_USERNAME) {
|
|
2262
|
+
const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
2263
|
+
if (m) env.VM_AUTH_USERNAME = m[1].trim().replace(/^["']|["']$/g, '');
|
|
2264
|
+
}
|
|
2265
|
+
if (!env.VM_AUTH_PASSWORD) {
|
|
2266
|
+
const m = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
2267
|
+
if (m) env.VM_AUTH_PASSWORD = m[1].trim().replace(/^["']|["']$/g, '');
|
|
2268
|
+
}
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
if (process.env.DEBUG) {
|
|
2271
|
+
console.warn(`Warning: Could not read VM auth from .env: ${err instanceof Error ? err.message : String(err)}`);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2238
2276
|
// On macOS, self-node-exporter can't mount host root filesystem - skip it
|
|
2239
2277
|
const finalArgs = [...args];
|
|
2240
2278
|
if (process.platform === "darwin" && args.includes("up")) {
|
|
@@ -2502,7 +2540,16 @@ mon
|
|
|
2502
2540
|
}
|
|
2503
2541
|
}
|
|
2504
2542
|
} else {
|
|
2505
|
-
|
|
2543
|
+
// Demo mode: copy instances.demo.yml → instances.yml so the demo target is active
|
|
2544
|
+
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
2545
|
+
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
2546
|
+
const demoSrc = path.resolve(projectDir, "instances.demo.yml");
|
|
2547
|
+
if (fs.existsSync(demoSrc)) {
|
|
2548
|
+
fs.copyFileSync(demoSrc, instancesPath);
|
|
2549
|
+
console.log("✓ Demo monitoring target configured\n");
|
|
2550
|
+
} else {
|
|
2551
|
+
console.error("⚠ instances.demo.yml not found — demo target not configured\n");
|
|
2552
|
+
}
|
|
2506
2553
|
}
|
|
2507
2554
|
|
|
2508
2555
|
// Step 3: Update configuration
|
|
@@ -2518,6 +2565,8 @@ mon
|
|
|
2518
2565
|
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
2519
2566
|
const cfgPath = path.resolve(projectDir, ".pgwatch-config");
|
|
2520
2567
|
let grafanaPassword = "";
|
|
2568
|
+
let vmAuthUsername = "";
|
|
2569
|
+
let vmAuthPassword = "";
|
|
2521
2570
|
|
|
2522
2571
|
try {
|
|
2523
2572
|
if (fs.existsSync(cfgPath)) {
|
|
@@ -2556,7 +2605,53 @@ mon
|
|
|
2556
2605
|
grafanaPassword = "demo";
|
|
2557
2606
|
}
|
|
2558
2607
|
|
|
2608
|
+
// Generate VictoriaMetrics auth credentials
|
|
2609
|
+
try {
|
|
2610
|
+
const envFile = path.resolve(projectDir, ".env");
|
|
2611
|
+
|
|
2612
|
+
// Read existing VM auth from .env if present
|
|
2613
|
+
if (fs.existsSync(envFile)) {
|
|
2614
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
2615
|
+
const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
2616
|
+
const passMatch = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
2617
|
+
if (userMatch) vmAuthUsername = userMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
2618
|
+
if (passMatch) vmAuthPassword = passMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
if (!vmAuthUsername || !vmAuthPassword) {
|
|
2622
|
+
console.log("Generating VictoriaMetrics auth credentials...");
|
|
2623
|
+
vmAuthUsername = vmAuthUsername || "vmauth";
|
|
2624
|
+
if (!vmAuthPassword) {
|
|
2625
|
+
const { stdout: vmPass } = await execPromise("openssl rand -base64 12 | tr -d '\n'");
|
|
2626
|
+
vmAuthPassword = vmPass.trim();
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// Update .env file with VM auth credentials
|
|
2630
|
+
let envContent = "";
|
|
2631
|
+
if (fs.existsSync(envFile)) {
|
|
2632
|
+
envContent = fs.readFileSync(envFile, "utf8");
|
|
2633
|
+
}
|
|
2634
|
+
const envLines = envContent.split(/\r?\n/)
|
|
2635
|
+
.filter((l) => !/^VM_AUTH_USERNAME=/.test(l) && !/^VM_AUTH_PASSWORD=/.test(l))
|
|
2636
|
+
.filter((l, i, arr) => !(i === arr.length - 1 && l === ''));
|
|
2637
|
+
envLines.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
|
|
2638
|
+
envLines.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
|
|
2639
|
+
fs.writeFileSync(envFile, envLines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
console.log("✓ VictoriaMetrics auth configured\n");
|
|
2643
|
+
} catch (error) {
|
|
2644
|
+
console.error("⚠ Could not generate VictoriaMetrics auth credentials automatically");
|
|
2645
|
+
if (process.env.DEBUG) {
|
|
2646
|
+
console.warn(` ${error instanceof Error ? error.message : String(error)}`);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2559
2650
|
// Step 5: Start services
|
|
2651
|
+
// Remove stopped containers left by "run --rm" dependencies (e.g. config-init)
|
|
2652
|
+
// to avoid docker-compose v1 'ContainerConfig' error on recreation.
|
|
2653
|
+
// Best-effort: ignore exit code — container may not exist, failure here is non-fatal.
|
|
2654
|
+
await runCompose(["rm", "-f", "-s", "config-init"]);
|
|
2560
2655
|
console.log("Step 5: Starting monitoring services...");
|
|
2561
2656
|
const code2 = await runCompose(["up", "-d", "--force-recreate"], grafanaPassword);
|
|
2562
2657
|
if (code2 !== 0) {
|
|
@@ -2606,6 +2701,9 @@ mon
|
|
|
2606
2701
|
console.log("🚀 MAIN ACCESS POINT - Start here:");
|
|
2607
2702
|
console.log(" Grafana Dashboard: http://localhost:3000");
|
|
2608
2703
|
console.log(` Login: monitor / ${grafanaPassword}`);
|
|
2704
|
+
if (vmAuthUsername && vmAuthPassword) {
|
|
2705
|
+
console.log(` VictoriaMetrics Auth: ${vmAuthUsername} / ${vmAuthPassword}`);
|
|
2706
|
+
}
|
|
2609
2707
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
|
2610
2708
|
});
|
|
2611
2709
|
|
|
@@ -3625,6 +3723,19 @@ mon
|
|
|
3625
3723
|
console.log(" URL: http://localhost:3000");
|
|
3626
3724
|
console.log(" Username: monitor");
|
|
3627
3725
|
console.log(` Password: ${password}`);
|
|
3726
|
+
|
|
3727
|
+
// Show VM auth credentials from .env
|
|
3728
|
+
const envFile = path.resolve(projectDir, ".env");
|
|
3729
|
+
if (fs.existsSync(envFile)) {
|
|
3730
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
3731
|
+
const vmUser = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
3732
|
+
const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
3733
|
+
if (vmUser && vmPass) {
|
|
3734
|
+
console.log("\nVictoriaMetrics credentials:");
|
|
3735
|
+
console.log(` Username: ${vmUser[1].trim().replace(/^["']|["']$/g, '')}`);
|
|
3736
|
+
console.log(` Password: ${vmPass[1].trim().replace(/^["']|["']$/g, '')}`);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3628
3739
|
console.log("");
|
|
3629
3740
|
});
|
|
3630
3741
|
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13151,7 +13151,7 @@ var {
|
|
|
13151
13151
|
// package.json
|
|
13152
13152
|
var package_default = {
|
|
13153
13153
|
name: "postgresai",
|
|
13154
|
-
version: "0.15.0-dev.
|
|
13154
|
+
version: "0.15.0-dev.5",
|
|
13155
13155
|
description: "postgres_ai CLI",
|
|
13156
13156
|
license: "Apache-2.0",
|
|
13157
13157
|
private: false,
|
|
@@ -15981,7 +15981,7 @@ var Result = import_lib.default.Result;
|
|
|
15981
15981
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15982
15982
|
var defaults = import_lib.default.defaults;
|
|
15983
15983
|
// package.json
|
|
15984
|
-
var version = "0.15.0-dev.
|
|
15984
|
+
var version = "0.15.0-dev.5";
|
|
15985
15985
|
var package_default2 = {
|
|
15986
15986
|
name: "postgresai",
|
|
15987
15987
|
version,
|
|
@@ -29867,13 +29867,13 @@ async function ensureDefaultMonitoringProject() {
|
|
|
29867
29867
|
if (!fs6.existsSync(projectDir)) {
|
|
29868
29868
|
fs6.mkdirSync(projectDir, { recursive: true, mode: 448 });
|
|
29869
29869
|
}
|
|
29870
|
+
const refs = [
|
|
29871
|
+
process.env.PGAI_PROJECT_REF,
|
|
29872
|
+
package_default.version,
|
|
29873
|
+
`v${package_default.version}`,
|
|
29874
|
+
"main"
|
|
29875
|
+
].filter((v) => Boolean(v && v.trim()));
|
|
29870
29876
|
if (!fs6.existsSync(composeFile)) {
|
|
29871
|
-
const refs = [
|
|
29872
|
-
process.env.PGAI_PROJECT_REF,
|
|
29873
|
-
package_default.version,
|
|
29874
|
-
`v${package_default.version}`,
|
|
29875
|
-
"main"
|
|
29876
|
-
].filter((v) => Boolean(v && v.trim()));
|
|
29877
29877
|
let lastErr;
|
|
29878
29878
|
for (const ref of refs) {
|
|
29879
29879
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
@@ -29890,6 +29890,17 @@ async function ensureDefaultMonitoringProject() {
|
|
|
29890
29890
|
throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
|
|
29891
29891
|
}
|
|
29892
29892
|
}
|
|
29893
|
+
const demoFile = path6.resolve(projectDir, "instances.demo.yml");
|
|
29894
|
+
if (!fs6.existsSync(demoFile)) {
|
|
29895
|
+
for (const ref of refs) {
|
|
29896
|
+
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/instances.demo.yml`;
|
|
29897
|
+
try {
|
|
29898
|
+
const text = await downloadText(url);
|
|
29899
|
+
fs6.writeFileSync(demoFile, text, { encoding: "utf8", mode: 384 });
|
|
29900
|
+
break;
|
|
29901
|
+
} catch {}
|
|
29902
|
+
}
|
|
29903
|
+
}
|
|
29893
29904
|
if (!fs6.existsSync(instancesFile)) {
|
|
29894
29905
|
const header = `# PostgreSQL instances to monitor
|
|
29895
29906
|
` + `# Add your instances using: pgai mon targets add <connection-string> <name>
|
|
@@ -31138,10 +31149,10 @@ function isDockerRunning() {
|
|
|
31138
31149
|
}
|
|
31139
31150
|
function getComposeCmd() {
|
|
31140
31151
|
const tryCmd = (cmd, args) => spawnSync2(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
|
|
31141
|
-
if (tryCmd("docker-compose", ["version"]))
|
|
31142
|
-
return ["docker-compose"];
|
|
31143
31152
|
if (tryCmd("docker", ["compose", "version"]))
|
|
31144
31153
|
return ["docker", "compose"];
|
|
31154
|
+
if (tryCmd("docker-compose", ["version"]))
|
|
31155
|
+
return ["docker-compose"];
|
|
31145
31156
|
return null;
|
|
31146
31157
|
}
|
|
31147
31158
|
function checkRunningContainers() {
|
|
@@ -31259,6 +31270,26 @@ async function runCompose(args, grafanaPassword) {
|
|
|
31259
31270
|
}
|
|
31260
31271
|
}
|
|
31261
31272
|
}
|
|
31273
|
+
const envFilePath = path6.resolve(projectDir, ".env");
|
|
31274
|
+
if (fs6.existsSync(envFilePath)) {
|
|
31275
|
+
try {
|
|
31276
|
+
const envContent = fs6.readFileSync(envFilePath, "utf8");
|
|
31277
|
+
if (!env.VM_AUTH_USERNAME) {
|
|
31278
|
+
const m = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
31279
|
+
if (m)
|
|
31280
|
+
env.VM_AUTH_USERNAME = m[1].trim().replace(/^["']|["']$/g, "");
|
|
31281
|
+
}
|
|
31282
|
+
if (!env.VM_AUTH_PASSWORD) {
|
|
31283
|
+
const m = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
31284
|
+
if (m)
|
|
31285
|
+
env.VM_AUTH_PASSWORD = m[1].trim().replace(/^["']|["']$/g, "");
|
|
31286
|
+
}
|
|
31287
|
+
} catch (err) {
|
|
31288
|
+
if (process.env.DEBUG) {
|
|
31289
|
+
console.warn(`Warning: Could not read VM auth from .env: ${err instanceof Error ? err.message : String(err)}`);
|
|
31290
|
+
}
|
|
31291
|
+
}
|
|
31292
|
+
}
|
|
31262
31293
|
const finalArgs = [...args];
|
|
31263
31294
|
if (process.platform === "darwin" && args.includes("up")) {
|
|
31264
31295
|
finalArgs.push("--scale", "self-node-exporter=0");
|
|
@@ -31517,8 +31548,17 @@ You can provide either:`);
|
|
|
31517
31548
|
}
|
|
31518
31549
|
}
|
|
31519
31550
|
} else {
|
|
31520
|
-
console.log(
|
|
31551
|
+
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
31552
|
+
const { instancesFile: instancesPath, projectDir: projectDir2 } = await resolveOrInitPaths();
|
|
31553
|
+
const demoSrc = path6.resolve(projectDir2, "instances.demo.yml");
|
|
31554
|
+
if (fs6.existsSync(demoSrc)) {
|
|
31555
|
+
fs6.copyFileSync(demoSrc, instancesPath);
|
|
31556
|
+
console.log(`\u2713 Demo monitoring target configured
|
|
31557
|
+
`);
|
|
31558
|
+
} else {
|
|
31559
|
+
console.error(`\u26A0 instances.demo.yml not found \u2014 demo target not configured
|
|
31521
31560
|
`);
|
|
31561
|
+
}
|
|
31522
31562
|
}
|
|
31523
31563
|
console.log(opts.demo ? "Step 3: Updating configuration..." : "Step 3: Updating configuration...");
|
|
31524
31564
|
const code1 = await runCompose(["run", "--rm", "sources-generator"]);
|
|
@@ -31531,6 +31571,8 @@ You can provide either:`);
|
|
|
31531
31571
|
console.log(opts.demo ? "Step 4: Configuring Grafana security..." : "Step 4: Configuring Grafana security...");
|
|
31532
31572
|
const cfgPath = path6.resolve(projectDir, ".pgwatch-config");
|
|
31533
31573
|
let grafanaPassword = "";
|
|
31574
|
+
let vmAuthUsername = "";
|
|
31575
|
+
let vmAuthPassword = "";
|
|
31534
31576
|
try {
|
|
31535
31577
|
if (fs6.existsSync(cfgPath)) {
|
|
31536
31578
|
const stats = fs6.statSync(cfgPath);
|
|
@@ -31568,6 +31610,45 @@ You can provide either:`);
|
|
|
31568
31610
|
`);
|
|
31569
31611
|
grafanaPassword = "demo";
|
|
31570
31612
|
}
|
|
31613
|
+
try {
|
|
31614
|
+
const envFile2 = path6.resolve(projectDir, ".env");
|
|
31615
|
+
if (fs6.existsSync(envFile2)) {
|
|
31616
|
+
const envContent = fs6.readFileSync(envFile2, "utf8");
|
|
31617
|
+
const userMatch = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
31618
|
+
const passMatch = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
31619
|
+
if (userMatch)
|
|
31620
|
+
vmAuthUsername = userMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
31621
|
+
if (passMatch)
|
|
31622
|
+
vmAuthPassword = passMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
31623
|
+
}
|
|
31624
|
+
if (!vmAuthUsername || !vmAuthPassword) {
|
|
31625
|
+
console.log("Generating VictoriaMetrics auth credentials...");
|
|
31626
|
+
vmAuthUsername = vmAuthUsername || "vmauth";
|
|
31627
|
+
if (!vmAuthPassword) {
|
|
31628
|
+
const { stdout: vmPass } = await execPromise(`openssl rand -base64 12 | tr -d '
|
|
31629
|
+
'`);
|
|
31630
|
+
vmAuthPassword = vmPass.trim();
|
|
31631
|
+
}
|
|
31632
|
+
let envContent = "";
|
|
31633
|
+
if (fs6.existsSync(envFile2)) {
|
|
31634
|
+
envContent = fs6.readFileSync(envFile2, "utf8");
|
|
31635
|
+
}
|
|
31636
|
+
const envLines2 = envContent.split(/\r?\n/).filter((l) => !/^VM_AUTH_USERNAME=/.test(l) && !/^VM_AUTH_PASSWORD=/.test(l)).filter((l, i2, arr) => !(i2 === arr.length - 1 && l === ""));
|
|
31637
|
+
envLines2.push(`VM_AUTH_USERNAME=${vmAuthUsername}`);
|
|
31638
|
+
envLines2.push(`VM_AUTH_PASSWORD=${vmAuthPassword}`);
|
|
31639
|
+
fs6.writeFileSync(envFile2, envLines2.join(`
|
|
31640
|
+
`) + `
|
|
31641
|
+
`, { encoding: "utf8", mode: 384 });
|
|
31642
|
+
}
|
|
31643
|
+
console.log(`\u2713 VictoriaMetrics auth configured
|
|
31644
|
+
`);
|
|
31645
|
+
} catch (error2) {
|
|
31646
|
+
console.error("\u26A0 Could not generate VictoriaMetrics auth credentials automatically");
|
|
31647
|
+
if (process.env.DEBUG) {
|
|
31648
|
+
console.warn(` ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
31649
|
+
}
|
|
31650
|
+
}
|
|
31651
|
+
await runCompose(["rm", "-f", "-s", "config-init"]);
|
|
31571
31652
|
console.log("Step 5: Starting monitoring services...");
|
|
31572
31653
|
const code2 = await runCompose(["up", "-d", "--force-recreate"], grafanaPassword);
|
|
31573
31654
|
if (code2 !== 0) {
|
|
@@ -31615,6 +31696,9 @@ You can provide either:`);
|
|
|
31615
31696
|
console.log("\uD83D\uDE80 MAIN ACCESS POINT - Start here:");
|
|
31616
31697
|
console.log(" Grafana Dashboard: http://localhost:3000");
|
|
31617
31698
|
console.log(` Login: monitor / ${grafanaPassword}`);
|
|
31699
|
+
if (vmAuthUsername && vmAuthPassword) {
|
|
31700
|
+
console.log(` VictoriaMetrics Auth: ${vmAuthUsername} / ${vmAuthPassword}`);
|
|
31701
|
+
}
|
|
31618
31702
|
console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
31619
31703
|
`);
|
|
31620
31704
|
});
|
|
@@ -32426,6 +32510,18 @@ Grafana credentials:`);
|
|
|
32426
32510
|
console.log(" URL: http://localhost:3000");
|
|
32427
32511
|
console.log(" Username: monitor");
|
|
32428
32512
|
console.log(` Password: ${password}`);
|
|
32513
|
+
const envFile = path6.resolve(projectDir, ".env");
|
|
32514
|
+
if (fs6.existsSync(envFile)) {
|
|
32515
|
+
const envContent = fs6.readFileSync(envFile, "utf8");
|
|
32516
|
+
const vmUser = envContent.match(/^VM_AUTH_USERNAME=([^\r\n]+)/m);
|
|
32517
|
+
const vmPass = envContent.match(/^VM_AUTH_PASSWORD=([^\r\n]+)/m);
|
|
32518
|
+
if (vmUser && vmPass) {
|
|
32519
|
+
console.log(`
|
|
32520
|
+
VictoriaMetrics credentials:`);
|
|
32521
|
+
console.log(` Username: ${vmUser[1].trim().replace(/^["']|["']$/g, "")}`);
|
|
32522
|
+
console.log(` Password: ${vmPass[1].trim().replace(/^["']|["']$/g, "")}`);
|
|
32523
|
+
}
|
|
32524
|
+
}
|
|
32429
32525
|
console.log("");
|
|
32430
32526
|
});
|
|
32431
32527
|
function interpretEscapes2(str2) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test getComposeCmd() selection logic.
|
|
5
|
+
* WARNING: This replicates the logic from postgres-ai.ts. If the production
|
|
6
|
+
* function changes, this replica must be updated to match.
|
|
7
|
+
* Since the function is internal to postgres-ai.ts, we replicate its logic
|
|
8
|
+
* with an injectable command checker (same pattern as monitoring.test.ts).
|
|
9
|
+
*/
|
|
10
|
+
function getComposeCmd(
|
|
11
|
+
tryCmd: (cmd: string, args: string[]) => boolean,
|
|
12
|
+
): string[] | null {
|
|
13
|
+
if (tryCmd("docker", ["compose", "version"])) return ["docker", "compose"];
|
|
14
|
+
if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("getComposeCmd", () => {
|
|
19
|
+
test("prefers docker compose V2 when both are available", () => {
|
|
20
|
+
const result = getComposeCmd(() => true);
|
|
21
|
+
expect(result).toEqual(["docker", "compose"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("falls back to docker-compose V1 when V2 is unavailable", () => {
|
|
25
|
+
const result = getComposeCmd((cmd, args) => {
|
|
26
|
+
// V2 plugin fails, V1 standalone succeeds
|
|
27
|
+
if (cmd === "docker" && args[0] === "compose") return false;
|
|
28
|
+
if (cmd === "docker-compose") return true;
|
|
29
|
+
return false;
|
|
30
|
+
});
|
|
31
|
+
expect(result).toEqual(["docker-compose"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("returns null when neither is available", () => {
|
|
35
|
+
const result = getComposeCmd(() => false);
|
|
36
|
+
expect(result).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("does not check V1 when V2 succeeds", () => {
|
|
40
|
+
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
41
|
+
getComposeCmd((cmd, args) => {
|
|
42
|
+
calls.push({ cmd, args });
|
|
43
|
+
return cmd === "docker" && args[0] === "compose";
|
|
44
|
+
});
|
|
45
|
+
expect(calls).toHaveLength(1);
|
|
46
|
+
expect(calls[0]).toEqual({ cmd: "docker", args: ["compose", "version"] });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("checks V2 first, then V1", () => {
|
|
50
|
+
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
51
|
+
getComposeCmd((cmd, args) => {
|
|
52
|
+
calls.push({ cmd, args });
|
|
53
|
+
return false;
|
|
54
|
+
});
|
|
55
|
+
expect(calls).toHaveLength(2);
|
|
56
|
+
expect(calls[0]).toEqual({ cmd: "docker", args: ["compose", "version"] });
|
|
57
|
+
expect(calls[1]).toEqual({ cmd: "docker-compose", args: ["version"] });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Test the monitoring startup sequence's container cleanup logic.
|
|
63
|
+
* Before "up --force-recreate", stopped containers from "run --rm" dependencies
|
|
64
|
+
* (e.g. config-init) must be removed to avoid docker-compose v1's
|
|
65
|
+
* KeyError: 'ContainerConfig' bug.
|
|
66
|
+
*
|
|
67
|
+
* We replicate the relevant sequence from the monitoring start command
|
|
68
|
+
* with an injectable runCompose to verify ordering and error tolerance.
|
|
69
|
+
*/
|
|
70
|
+
async function monitoringStartSequence(
|
|
71
|
+
runCompose: (args: string[]) => Promise<number>,
|
|
72
|
+
): Promise<number> {
|
|
73
|
+
// Best-effort: remove stopped containers left by "run --rm" dependencies
|
|
74
|
+
await runCompose(["rm", "-f", "-s", "config-init"]);
|
|
75
|
+
// Start services
|
|
76
|
+
const code = await runCompose(["up", "-d", "--force-recreate"]);
|
|
77
|
+
return code;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("monitoring start: config-init cleanup", () => {
|
|
81
|
+
test("calls rm before up", async () => {
|
|
82
|
+
const calls: string[][] = [];
|
|
83
|
+
await monitoringStartSequence(async (args) => {
|
|
84
|
+
calls.push(args);
|
|
85
|
+
return 0;
|
|
86
|
+
});
|
|
87
|
+
expect(calls).toHaveLength(2);
|
|
88
|
+
expect(calls[0]).toEqual(["rm", "-f", "-s", "config-init"]);
|
|
89
|
+
expect(calls[1]).toEqual(["up", "-d", "--force-recreate"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("continues to up even when rm fails", async () => {
|
|
93
|
+
const calls: string[][] = [];
|
|
94
|
+
await monitoringStartSequence(async (args) => {
|
|
95
|
+
calls.push(args);
|
|
96
|
+
// rm returns non-zero (container doesn't exist)
|
|
97
|
+
if (args[0] === "rm") return 1;
|
|
98
|
+
return 0;
|
|
99
|
+
});
|
|
100
|
+
expect(calls).toHaveLength(2);
|
|
101
|
+
expect(calls[0][0]).toBe("rm");
|
|
102
|
+
expect(calls[1][0]).toBe("up");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("returns up exit code, not rm exit code", async () => {
|
|
106
|
+
// rm fails but up succeeds → overall success
|
|
107
|
+
const result1 = await monitoringStartSequence(async (args) => {
|
|
108
|
+
if (args[0] === "rm") return 1;
|
|
109
|
+
return 0;
|
|
110
|
+
});
|
|
111
|
+
expect(result1).toBe(0);
|
|
112
|
+
|
|
113
|
+
// rm succeeds but up fails → overall failure
|
|
114
|
+
const result2 = await monitoringStartSequence(async (args) => {
|
|
115
|
+
if (args[0] === "up") return 2;
|
|
116
|
+
return 0;
|
|
117
|
+
});
|
|
118
|
+
expect(result2).toBe(2);
|
|
119
|
+
});
|
|
120
|
+
});
|