postgresai 0.15.0-dev.5 → 0.15.0-dev.7
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 +51 -35
- package/dist/bin/postgres-ai.js +40 -34
- package/package.json +1 -1
- package/test/init.test.ts +64 -1
- package/test/monitoring.test.ts +78 -0
package/bin/postgres-ai.ts
CHANGED
|
@@ -7,6 +7,7 @@ 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 { fileURLToPath } from "url";
|
|
10
11
|
import * as crypto from "node:crypto";
|
|
11
12
|
import { Client } from "pg";
|
|
12
13
|
import { startMcpServer } from "../lib/mcp-server";
|
|
@@ -477,14 +478,14 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
|
|
|
477
478
|
fs.mkdirSync(projectDir, { recursive: true, mode: 0o700 });
|
|
478
479
|
}
|
|
479
480
|
|
|
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()));
|
|
486
|
-
|
|
487
481
|
if (!fs.existsSync(composeFile)) {
|
|
482
|
+
const refs = [
|
|
483
|
+
process.env.PGAI_PROJECT_REF,
|
|
484
|
+
pkg.version,
|
|
485
|
+
`v${pkg.version}`,
|
|
486
|
+
"main",
|
|
487
|
+
].filter((v): v is string => Boolean(v && v.trim()));
|
|
488
|
+
|
|
488
489
|
let lastErr: unknown;
|
|
489
490
|
for (const ref of refs) {
|
|
490
491
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
@@ -503,22 +504,11 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
|
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
506
|
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
if (
|
|
509
|
-
|
|
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
|
-
}
|
|
507
|
+
// Ensure instances.yml exists as a FILE (avoid Docker creating a directory).
|
|
508
|
+
// Docker bind-mounts create missing paths as directories; replace if so.
|
|
509
|
+
if (fs.existsSync(instancesFile) && fs.lstatSync(instancesFile).isDirectory()) {
|
|
510
|
+
fs.rmSync(instancesFile, { recursive: true, force: true });
|
|
519
511
|
}
|
|
520
|
-
|
|
521
|
-
// Ensure instances.yml exists as a FILE (avoid Docker creating a directory)
|
|
522
512
|
if (!fs.existsSync(instancesFile)) {
|
|
523
513
|
const header =
|
|
524
514
|
"# PostgreSQL instances to monitor\n" +
|
|
@@ -2317,7 +2307,7 @@ mon
|
|
|
2317
2307
|
console.log("This will install, configure, and start the monitoring system\n");
|
|
2318
2308
|
|
|
2319
2309
|
// Ensure we have a project directory with docker-compose.yml even if running from elsewhere
|
|
2320
|
-
const { projectDir } = await resolveOrInitPaths();
|
|
2310
|
+
const { projectDir, instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
2321
2311
|
console.log(`Project directory: ${projectDir}\n`);
|
|
2322
2312
|
|
|
2323
2313
|
// Save project name to .pgwatch-config if provided (used by reporter container)
|
|
@@ -2540,15 +2530,37 @@ mon
|
|
|
2540
2530
|
}
|
|
2541
2531
|
}
|
|
2542
2532
|
} else {
|
|
2543
|
-
// Demo mode:
|
|
2533
|
+
// Demo mode: configure instances.yml from the bundled demo template.
|
|
2534
|
+
//
|
|
2535
|
+
// Side effects:
|
|
2536
|
+
// - Writes instancesPath (instances.yml next to docker-compose.yml)
|
|
2537
|
+
// - If Docker previously bind-mounted instances.yml as a directory, removes it first.
|
|
2538
|
+
//
|
|
2539
|
+
// Failure modes:
|
|
2540
|
+
// - Exits with code 1 if instances.demo.yml is not found in any candidate path.
|
|
2541
|
+
// This is fatal because starting without a target produces empty dashboards that
|
|
2542
|
+
// look like a bug rather than a misconfiguration.
|
|
2543
|
+
//
|
|
2544
|
+
// Template search order (import.meta.url is resolved at runtime, not baked in at build):
|
|
2545
|
+
// 1. npm layout: dist/bin/../../instances.demo.yml → package-root/instances.demo.yml
|
|
2546
|
+
// 2. dev layout: cli/bin/../../../instances.demo.yml → repo-root/instances.demo.yml
|
|
2544
2547
|
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
2545
|
-
const
|
|
2546
|
-
const
|
|
2547
|
-
|
|
2548
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
2549
|
+
const demoCandidates = [
|
|
2550
|
+
path.resolve(currentDir, "..", "..", "instances.demo.yml"), // npm: dist/bin -> package root
|
|
2551
|
+
path.resolve(currentDir, "..", "..", "..", "instances.demo.yml"), // dev: cli/bin -> repo root
|
|
2552
|
+
];
|
|
2553
|
+
const demoSrc = demoCandidates.find(p => fs.existsSync(p));
|
|
2554
|
+
if (demoSrc) {
|
|
2555
|
+
// Remove directory artifact left by Docker bind-mounts before copying
|
|
2556
|
+
if (fs.existsSync(instancesPath) && fs.lstatSync(instancesPath).isDirectory()) {
|
|
2557
|
+
fs.rmSync(instancesPath, { recursive: true, force: true });
|
|
2558
|
+
}
|
|
2548
2559
|
fs.copyFileSync(demoSrc, instancesPath);
|
|
2549
2560
|
console.log("✓ Demo monitoring target configured\n");
|
|
2550
2561
|
} else {
|
|
2551
|
-
console.error(
|
|
2562
|
+
console.error(`Error: instances.demo.yml not found — cannot configure demo target.\nSearched: ${demoCandidates.join(", ")}\n`);
|
|
2563
|
+
process.exit(1);
|
|
2552
2564
|
}
|
|
2553
2565
|
}
|
|
2554
2566
|
|
|
@@ -2904,7 +2916,7 @@ mon
|
|
|
2904
2916
|
console.log(`Project Directory: ${projectDir}`);
|
|
2905
2917
|
console.log(`Docker Compose File: ${composeFile}`);
|
|
2906
2918
|
console.log(`Instances File: ${instancesFile}`);
|
|
2907
|
-
if (fs.existsSync(instancesFile)) {
|
|
2919
|
+
if (fs.existsSync(instancesFile) && !fs.lstatSync(instancesFile).isDirectory()) {
|
|
2908
2920
|
console.log("\nInstances configuration:\n");
|
|
2909
2921
|
const text = fs.readFileSync(instancesFile, "utf8");
|
|
2910
2922
|
process.stdout.write(text);
|
|
@@ -3120,7 +3132,7 @@ targets
|
|
|
3120
3132
|
.description("list monitoring target databases")
|
|
3121
3133
|
.action(async () => {
|
|
3122
3134
|
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
3123
|
-
if (!fs.existsSync(instancesPath)) {
|
|
3135
|
+
if (!fs.existsSync(instancesPath) || fs.lstatSync(instancesPath).isDirectory()) {
|
|
3124
3136
|
console.error(`instances.yml not found in ${projectDir}`);
|
|
3125
3137
|
process.exitCode = 1;
|
|
3126
3138
|
return;
|
|
@@ -3186,7 +3198,7 @@ targets
|
|
|
3186
3198
|
|
|
3187
3199
|
// Check if instance already exists
|
|
3188
3200
|
try {
|
|
3189
|
-
if (fs.existsSync(file)) {
|
|
3201
|
+
if (fs.existsSync(file) && !fs.lstatSync(file).isDirectory()) {
|
|
3190
3202
|
const content = fs.readFileSync(file, "utf8");
|
|
3191
3203
|
const instances = yaml.load(content) as Instance[] | null || [];
|
|
3192
3204
|
if (Array.isArray(instances)) {
|
|
@@ -3200,7 +3212,8 @@ targets
|
|
|
3200
3212
|
}
|
|
3201
3213
|
} catch (err) {
|
|
3202
3214
|
// If YAML parsing fails, fall back to simple check
|
|
3203
|
-
const
|
|
3215
|
+
const isFile = fs.existsSync(file) && !fs.lstatSync(file).isDirectory();
|
|
3216
|
+
const content = isFile ? fs.readFileSync(file, "utf8") : "";
|
|
3204
3217
|
if (new RegExp(`^- name: ${instanceName}$`, "m").test(content)) {
|
|
3205
3218
|
console.error(`Monitoring target '${instanceName}' already exists`);
|
|
3206
3219
|
process.exitCode = 1;
|
|
@@ -3208,7 +3221,10 @@ targets
|
|
|
3208
3221
|
}
|
|
3209
3222
|
}
|
|
3210
3223
|
|
|
3211
|
-
// Add new instance
|
|
3224
|
+
// Add new instance — if instances.yml is a directory (Docker artifact), replace it with a file
|
|
3225
|
+
if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
|
|
3226
|
+
fs.rmSync(file, { recursive: true, force: true });
|
|
3227
|
+
}
|
|
3212
3228
|
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`;
|
|
3213
3229
|
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
3214
3230
|
fs.appendFileSync(file, (content && !/\n$/.test(content) ? "\n" : "") + body, "utf8");
|
|
@@ -3219,7 +3235,7 @@ targets
|
|
|
3219
3235
|
.description("remove monitoring target database")
|
|
3220
3236
|
.action(async (name: string) => {
|
|
3221
3237
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
3222
|
-
if (!fs.existsSync(file)) {
|
|
3238
|
+
if (!fs.existsSync(file) || fs.lstatSync(file).isDirectory()) {
|
|
3223
3239
|
console.error("instances.yml not found");
|
|
3224
3240
|
process.exitCode = 1;
|
|
3225
3241
|
return;
|
|
@@ -3256,7 +3272,7 @@ targets
|
|
|
3256
3272
|
.description("test monitoring target database connectivity")
|
|
3257
3273
|
.action(async (name: string) => {
|
|
3258
3274
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
3259
|
-
if (!fs.existsSync(instancesPath)) {
|
|
3275
|
+
if (!fs.existsSync(instancesPath) || fs.lstatSync(instancesPath).isDirectory()) {
|
|
3260
3276
|
console.error("instances.yml not found");
|
|
3261
3277
|
process.exitCode = 1;
|
|
3262
3278
|
return;
|
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.7",
|
|
13155
13155
|
description: "postgres_ai CLI",
|
|
13156
13156
|
license: "Apache-2.0",
|
|
13157
13157
|
private: false,
|
|
@@ -15965,6 +15965,7 @@ var safeDump = renamed("safeDump", "dump");
|
|
|
15965
15965
|
import * as fs6 from "fs";
|
|
15966
15966
|
import * as path6 from "path";
|
|
15967
15967
|
import * as os3 from "os";
|
|
15968
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15968
15969
|
import * as crypto2 from "crypto";
|
|
15969
15970
|
|
|
15970
15971
|
// node_modules/pg/esm/index.mjs
|
|
@@ -15981,7 +15982,7 @@ var Result = import_lib.default.Result;
|
|
|
15981
15982
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15982
15983
|
var defaults = import_lib.default.defaults;
|
|
15983
15984
|
// package.json
|
|
15984
|
-
var version = "0.15.0-dev.
|
|
15985
|
+
var version = "0.15.0-dev.7";
|
|
15985
15986
|
var package_default2 = {
|
|
15986
15987
|
name: "postgresai",
|
|
15987
15988
|
version,
|
|
@@ -29867,13 +29868,13 @@ async function ensureDefaultMonitoringProject() {
|
|
|
29867
29868
|
if (!fs6.existsSync(projectDir)) {
|
|
29868
29869
|
fs6.mkdirSync(projectDir, { recursive: true, mode: 448 });
|
|
29869
29870
|
}
|
|
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()));
|
|
29876
29871
|
if (!fs6.existsSync(composeFile)) {
|
|
29872
|
+
const refs = [
|
|
29873
|
+
process.env.PGAI_PROJECT_REF,
|
|
29874
|
+
package_default.version,
|
|
29875
|
+
`v${package_default.version}`,
|
|
29876
|
+
"main"
|
|
29877
|
+
].filter((v) => Boolean(v && v.trim()));
|
|
29877
29878
|
let lastErr;
|
|
29878
29879
|
for (const ref of refs) {
|
|
29879
29880
|
const url = `https://gitlab.com/postgres-ai/postgres_ai/-/raw/${encodeURIComponent(ref)}/docker-compose.yml`;
|
|
@@ -29890,16 +29891,8 @@ async function ensureDefaultMonitoringProject() {
|
|
|
29890
29891
|
throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
|
|
29891
29892
|
}
|
|
29892
29893
|
}
|
|
29893
|
-
|
|
29894
|
-
|
|
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
|
-
}
|
|
29894
|
+
if (fs6.existsSync(instancesFile) && fs6.lstatSync(instancesFile).isDirectory()) {
|
|
29895
|
+
fs6.rmSync(instancesFile, { recursive: true, force: true });
|
|
29903
29896
|
}
|
|
29904
29897
|
if (!fs6.existsSync(instancesFile)) {
|
|
29905
29898
|
const header = `# PostgreSQL instances to monitor
|
|
@@ -31317,7 +31310,7 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
31317
31310
|
`);
|
|
31318
31311
|
console.log(`This will install, configure, and start the monitoring system
|
|
31319
31312
|
`);
|
|
31320
|
-
const { projectDir } = await resolveOrInitPaths();
|
|
31313
|
+
const { projectDir, instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
31321
31314
|
console.log(`Project directory: ${projectDir}
|
|
31322
31315
|
`);
|
|
31323
31316
|
if (opts.project) {
|
|
@@ -31428,13 +31421,13 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
31428
31421
|
if (!opts.demo) {
|
|
31429
31422
|
console.log(`Step 2: Add PostgreSQL Instance to Monitor
|
|
31430
31423
|
`);
|
|
31431
|
-
const { instancesFile:
|
|
31424
|
+
const { instancesFile: instancesPath2, projectDir: projectDir2 } = await resolveOrInitPaths();
|
|
31432
31425
|
const emptyInstancesContent = `# PostgreSQL instances to monitor
|
|
31433
31426
|
# Add your instances using: postgres-ai mon targets add
|
|
31434
31427
|
|
|
31435
31428
|
`;
|
|
31436
|
-
fs6.writeFileSync(
|
|
31437
|
-
console.log(`Instances file: ${
|
|
31429
|
+
fs6.writeFileSync(instancesPath2, emptyInstancesContent, "utf8");
|
|
31430
|
+
console.log(`Instances file: ${instancesPath2}`);
|
|
31438
31431
|
console.log(`Project directory: ${projectDir2}
|
|
31439
31432
|
`);
|
|
31440
31433
|
if (opts.dbUrl) {
|
|
@@ -31465,7 +31458,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
31465
31458
|
node_name: ${instanceName}
|
|
31466
31459
|
sink_type: ~sink_type~
|
|
31467
31460
|
`;
|
|
31468
|
-
fs6.appendFileSync(
|
|
31461
|
+
fs6.appendFileSync(instancesPath2, body, "utf8");
|
|
31469
31462
|
console.log(`\u2713 Monitoring target '${instanceName}' added
|
|
31470
31463
|
`);
|
|
31471
31464
|
console.log("Testing connection to the added instance...");
|
|
@@ -31520,7 +31513,7 @@ You can provide either:`);
|
|
|
31520
31513
|
node_name: ${instanceName}
|
|
31521
31514
|
sink_type: ~sink_type~
|
|
31522
31515
|
`;
|
|
31523
|
-
fs6.appendFileSync(
|
|
31516
|
+
fs6.appendFileSync(instancesPath2, body, "utf8");
|
|
31524
31517
|
console.log(`\u2713 Monitoring target '${instanceName}' added
|
|
31525
31518
|
`);
|
|
31526
31519
|
console.log("Testing connection to the added instance...");
|
|
@@ -31549,15 +31542,24 @@ You can provide either:`);
|
|
|
31549
31542
|
}
|
|
31550
31543
|
} else {
|
|
31551
31544
|
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
31552
|
-
const
|
|
31553
|
-
const
|
|
31554
|
-
|
|
31545
|
+
const currentDir = path6.dirname(fileURLToPath2(import.meta.url));
|
|
31546
|
+
const demoCandidates = [
|
|
31547
|
+
path6.resolve(currentDir, "..", "..", "instances.demo.yml"),
|
|
31548
|
+
path6.resolve(currentDir, "..", "..", "..", "instances.demo.yml")
|
|
31549
|
+
];
|
|
31550
|
+
const demoSrc = demoCandidates.find((p) => fs6.existsSync(p));
|
|
31551
|
+
if (demoSrc) {
|
|
31552
|
+
if (fs6.existsSync(instancesPath) && fs6.lstatSync(instancesPath).isDirectory()) {
|
|
31553
|
+
fs6.rmSync(instancesPath, { recursive: true, force: true });
|
|
31554
|
+
}
|
|
31555
31555
|
fs6.copyFileSync(demoSrc, instancesPath);
|
|
31556
31556
|
console.log(`\u2713 Demo monitoring target configured
|
|
31557
31557
|
`);
|
|
31558
31558
|
} else {
|
|
31559
|
-
console.error(
|
|
31559
|
+
console.error(`Error: instances.demo.yml not found \u2014 cannot configure demo target.
|
|
31560
|
+
Searched: ${demoCandidates.join(", ")}
|
|
31560
31561
|
`);
|
|
31562
|
+
process.exit(1);
|
|
31561
31563
|
}
|
|
31562
31564
|
}
|
|
31563
31565
|
console.log(opts.demo ? "Step 3: Updating configuration..." : "Step 3: Updating configuration...");
|
|
@@ -31845,7 +31847,7 @@ mon.command("config").description("show monitoring services configuration").acti
|
|
|
31845
31847
|
console.log(`Project Directory: ${projectDir}`);
|
|
31846
31848
|
console.log(`Docker Compose File: ${composeFile}`);
|
|
31847
31849
|
console.log(`Instances File: ${instancesFile}`);
|
|
31848
|
-
if (fs6.existsSync(instancesFile)) {
|
|
31850
|
+
if (fs6.existsSync(instancesFile) && !fs6.lstatSync(instancesFile).isDirectory()) {
|
|
31849
31851
|
console.log(`
|
|
31850
31852
|
Instances configuration:
|
|
31851
31853
|
`);
|
|
@@ -32020,7 +32022,7 @@ mon.command("check").description("monitoring services system readiness check").a
|
|
|
32020
32022
|
var targets = mon.command("targets").description("manage databases to monitor");
|
|
32021
32023
|
targets.command("list").description("list monitoring target databases").action(async () => {
|
|
32022
32024
|
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
32023
|
-
if (!fs6.existsSync(instancesPath)) {
|
|
32025
|
+
if (!fs6.existsSync(instancesPath) || fs6.lstatSync(instancesPath).isDirectory()) {
|
|
32024
32026
|
console.error(`instances.yml not found in ${projectDir}`);
|
|
32025
32027
|
process.exitCode = 1;
|
|
32026
32028
|
return;
|
|
@@ -32075,7 +32077,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
32075
32077
|
const db = m[5];
|
|
32076
32078
|
const instanceName = name && name.trim() ? name.trim() : `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
32077
32079
|
try {
|
|
32078
|
-
if (fs6.existsSync(file)) {
|
|
32080
|
+
if (fs6.existsSync(file) && !fs6.lstatSync(file).isDirectory()) {
|
|
32079
32081
|
const content2 = fs6.readFileSync(file, "utf8");
|
|
32080
32082
|
const instances = load(content2) || [];
|
|
32081
32083
|
if (Array.isArray(instances)) {
|
|
@@ -32088,13 +32090,17 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
32088
32090
|
}
|
|
32089
32091
|
}
|
|
32090
32092
|
} catch (err) {
|
|
32091
|
-
const
|
|
32093
|
+
const isFile = fs6.existsSync(file) && !fs6.lstatSync(file).isDirectory();
|
|
32094
|
+
const content2 = isFile ? fs6.readFileSync(file, "utf8") : "";
|
|
32092
32095
|
if (new RegExp(`^- name: ${instanceName}$`, "m").test(content2)) {
|
|
32093
32096
|
console.error(`Monitoring target '${instanceName}' already exists`);
|
|
32094
32097
|
process.exitCode = 1;
|
|
32095
32098
|
return;
|
|
32096
32099
|
}
|
|
32097
32100
|
}
|
|
32101
|
+
if (fs6.existsSync(file) && fs6.lstatSync(file).isDirectory()) {
|
|
32102
|
+
fs6.rmSync(file, { recursive: true, force: true });
|
|
32103
|
+
}
|
|
32098
32104
|
const body = `- name: ${instanceName}
|
|
32099
32105
|
conn_str: ${connStr}
|
|
32100
32106
|
preset_metrics: full
|
|
@@ -32114,7 +32120,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
32114
32120
|
});
|
|
32115
32121
|
targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
|
|
32116
32122
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
32117
|
-
if (!fs6.existsSync(file)) {
|
|
32123
|
+
if (!fs6.existsSync(file) || fs6.lstatSync(file).isDirectory()) {
|
|
32118
32124
|
console.error("instances.yml not found");
|
|
32119
32125
|
process.exitCode = 1;
|
|
32120
32126
|
return;
|
|
@@ -32143,7 +32149,7 @@ targets.command("remove <name>").description("remove monitoring target database"
|
|
|
32143
32149
|
});
|
|
32144
32150
|
targets.command("test <name>").description("test monitoring target database connectivity").action(async (name) => {
|
|
32145
32151
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
32146
|
-
if (!fs6.existsSync(instancesPath)) {
|
|
32152
|
+
if (!fs6.existsSync(instancesPath) || fs6.lstatSync(instancesPath).isDirectory()) {
|
|
32147
32153
|
console.error("instances.yml not found");
|
|
32148
32154
|
process.exitCode = 1;
|
|
32149
32155
|
return;
|
package/package.json
CHANGED
package/test/init.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
-
import { resolve } from "path";
|
|
2
|
+
import path, { resolve } from "path";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as os from "os";
|
|
5
5
|
|
|
@@ -1070,6 +1070,69 @@ describe("CLI commands", () => {
|
|
|
1070
1070
|
expect(r.stderr).toMatch(/Reports will be generated locally only/);
|
|
1071
1071
|
});
|
|
1072
1072
|
|
|
1073
|
+
test("cli: mon local-install --demo configures demo monitoring target", () => {
|
|
1074
|
+
// --demo should copy instances.demo.yml to instances.yml and print confirmation.
|
|
1075
|
+
// The command will fail later (no Docker), but we verify the demo target step succeeded.
|
|
1076
|
+
// resolvePaths() walks cwd() up to find docker-compose.yml, so instances.yml
|
|
1077
|
+
// is written next to docker-compose.yml in the repo root.
|
|
1078
|
+
const repoRoot = resolve(import.meta.dir, "..", "..");
|
|
1079
|
+
const instancesPath = path.join(repoRoot, "instances.yml");
|
|
1080
|
+
// Remove instances.yml if it exists — use rmSync to handle both files and
|
|
1081
|
+
// directories (the EISDIR test may have left a directory here if it failed).
|
|
1082
|
+
if (fs.existsSync(instancesPath)) fs.rmSync(instancesPath, { recursive: true, force: true });
|
|
1083
|
+
try {
|
|
1084
|
+
const r = runCli(["mon", "local-install", "--demo"]);
|
|
1085
|
+
expect(r.stdout).toMatch(/Demo mode enabled/);
|
|
1086
|
+
expect(r.stdout).toMatch(/Demo monitoring target configured/);
|
|
1087
|
+
// Verify instances.yml was actually written with the demo target
|
|
1088
|
+
expect(fs.existsSync(instancesPath)).toBe(true);
|
|
1089
|
+
const content = fs.readFileSync(instancesPath, "utf8");
|
|
1090
|
+
expect(content).toContain("name: target_database");
|
|
1091
|
+
expect(content).toContain("conn_str: postgresql://monitor:monitor_pass@target-db:5432/target_database");
|
|
1092
|
+
} finally {
|
|
1093
|
+
// Clean up — instances.yml is gitignored so safe to remove
|
|
1094
|
+
if (fs.existsSync(instancesPath)) fs.rmSync(instancesPath, { recursive: true, force: true });
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
test("cli: mon local-install --demo exits with code 1 when instances.demo.yml is missing", () => {
|
|
1099
|
+
// Regression: if instances.demo.yml cannot be found in any candidate path, the CLI
|
|
1100
|
+
// must exit with a non-zero code and a descriptive error (not silently create empty dashboards).
|
|
1101
|
+
const repoRoot = resolve(import.meta.dir, "..", "..");
|
|
1102
|
+
const demoFile = path.join(repoRoot, "instances.demo.yml");
|
|
1103
|
+
const tempBackup = path.join(os.tmpdir(), `instances.demo.yml.test-backup-${Date.now()}`);
|
|
1104
|
+
// Temporarily move instances.demo.yml so neither candidate path resolves
|
|
1105
|
+
fs.copyFileSync(demoFile, tempBackup);
|
|
1106
|
+
fs.unlinkSync(demoFile);
|
|
1107
|
+
try {
|
|
1108
|
+
const r = runCli(["mon", "local-install", "--demo"]);
|
|
1109
|
+
expect(r.status).not.toBe(0);
|
|
1110
|
+
expect(r.stderr).toContain("instances.demo.yml not found");
|
|
1111
|
+
} finally {
|
|
1112
|
+
// Restore the file — critical to do before any assertion can throw
|
|
1113
|
+
if (!fs.existsSync(demoFile)) fs.copyFileSync(tempBackup, demoFile);
|
|
1114
|
+
fs.rmSync(tempBackup, { force: true });
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
test("cli: mon local-install --demo with EISDIR recovers instances.yml", () => {
|
|
1119
|
+
// Docker bind-mounts create missing paths as directories; the CLI must handle this.
|
|
1120
|
+
const repoRoot = resolve(import.meta.dir, "..", "..");
|
|
1121
|
+
const instancesPath = path.join(repoRoot, "instances.yml");
|
|
1122
|
+
// Create instances.yml as a directory (simulating Docker artifact)
|
|
1123
|
+
if (fs.existsSync(instancesPath)) fs.rmSync(instancesPath, { recursive: true, force: true });
|
|
1124
|
+
fs.mkdirSync(instancesPath);
|
|
1125
|
+
try {
|
|
1126
|
+
const r = runCli(["mon", "local-install", "--demo"]);
|
|
1127
|
+
expect(r.stdout).toMatch(/Demo monitoring target configured/);
|
|
1128
|
+
expect(fs.statSync(instancesPath).isFile()).toBe(true);
|
|
1129
|
+
const content = fs.readFileSync(instancesPath, "utf8");
|
|
1130
|
+
expect(content).toContain("name: target_database");
|
|
1131
|
+
} finally {
|
|
1132
|
+
if (fs.existsSync(instancesPath)) fs.rmSync(instancesPath, { recursive: true, force: true });
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1073
1136
|
test("cli: mon local-install --demo with global --api-key shows error", () => {
|
|
1074
1137
|
// When --demo is used with global --api-key, it should still be detected and error
|
|
1075
1138
|
const r = runCli([
|
package/test/monitoring.test.ts
CHANGED
|
@@ -259,3 +259,81 @@ describe("registerMonitoringInstance", () => {
|
|
|
259
259
|
expect(fetchCalls[0].url).toBe("https://custom.api.com/v2/rpc/monitoring_instance_register");
|
|
260
260
|
});
|
|
261
261
|
});
|
|
262
|
+
|
|
263
|
+
describe("demo mode instances.demo.yml", () => {
|
|
264
|
+
const repoRoot = path.resolve(import.meta.dir, "..", "..");
|
|
265
|
+
|
|
266
|
+
test("instances.demo.yml exists in repo root", () => {
|
|
267
|
+
const demoFile = path.join(repoRoot, "instances.demo.yml");
|
|
268
|
+
expect(fs.existsSync(demoFile)).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("instances.demo.yml contains demo target connection", () => {
|
|
272
|
+
const demoFile = path.join(repoRoot, "instances.demo.yml");
|
|
273
|
+
const content = fs.readFileSync(demoFile, "utf8");
|
|
274
|
+
expect(content).toContain("name: target_database");
|
|
275
|
+
expect(content).toContain("conn_str: postgresql://monitor:monitor_pass@target-db:5432/target_database");
|
|
276
|
+
expect(content).toContain("is_enabled: true");
|
|
277
|
+
expect(content).toContain("preset_metrics: full");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("instances.demo.yml has required YAML structure", () => {
|
|
281
|
+
const demoFile = path.join(repoRoot, "instances.demo.yml");
|
|
282
|
+
const content = fs.readFileSync(demoFile, "utf8");
|
|
283
|
+
// Verify it's a YAML list (starts with "- name:")
|
|
284
|
+
expect(content).toMatch(/^- name: target_database/m);
|
|
285
|
+
// Verify required fields are present with correct indentation
|
|
286
|
+
expect(content).toMatch(/^\s+conn_str:/m);
|
|
287
|
+
expect(content).toMatch(/^\s+preset_metrics: full/m);
|
|
288
|
+
expect(content).toMatch(/^\s+is_enabled: true/m);
|
|
289
|
+
// ~sink_type~ is a sed token substituted by generate-pgwatch-sources.sh; values: postgres, prometheus
|
|
290
|
+
expect(content).toMatch(/^\s+sink_type: ~sink_type~/m);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("instances.yml is gitignored (not tracked)", () => {
|
|
294
|
+
const gitignore = fs.readFileSync(path.join(repoRoot, ".gitignore"), "utf8");
|
|
295
|
+
expect(gitignore).toMatch(/^instances\.yml$/m);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("demo config can be copied to instances.yml in temp dir", () => {
|
|
299
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "demo-install-test-"));
|
|
300
|
+
try {
|
|
301
|
+
const demoSrc = path.join(repoRoot, "instances.demo.yml");
|
|
302
|
+
const instancesDest = path.join(tempDir, "instances.yml");
|
|
303
|
+
|
|
304
|
+
fs.copyFileSync(demoSrc, instancesDest);
|
|
305
|
+
|
|
306
|
+
expect(fs.existsSync(instancesDest)).toBe(true);
|
|
307
|
+
const content = fs.readFileSync(instancesDest, "utf8");
|
|
308
|
+
expect(content).toContain("name: target_database");
|
|
309
|
+
expect(content).toContain("conn_str: postgresql://monitor:monitor_pass@target-db:5432/target_database");
|
|
310
|
+
} finally {
|
|
311
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("demo config copy overwrites directory at instances.yml path", () => {
|
|
316
|
+
// Docker bind-mounts create missing paths as directories; the copy must handle this
|
|
317
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "demo-eisdir-test-"));
|
|
318
|
+
try {
|
|
319
|
+
const demoSrc = path.join(repoRoot, "instances.demo.yml");
|
|
320
|
+
const instancesDest = path.join(tempDir, "instances.yml");
|
|
321
|
+
|
|
322
|
+
// Simulate Docker creating a directory at instances.yml path
|
|
323
|
+
fs.mkdirSync(instancesDest);
|
|
324
|
+
expect(fs.statSync(instancesDest).isDirectory()).toBe(true);
|
|
325
|
+
|
|
326
|
+
// The fix: remove directory then copy
|
|
327
|
+
if (fs.statSync(instancesDest).isDirectory()) {
|
|
328
|
+
fs.rmSync(instancesDest, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
fs.copyFileSync(demoSrc, instancesDest);
|
|
331
|
+
|
|
332
|
+
expect(fs.statSync(instancesDest).isFile()).toBe(true);
|
|
333
|
+
const content = fs.readFileSync(instancesDest, "utf8");
|
|
334
|
+
expect(content).toContain("name: target_database");
|
|
335
|
+
} finally {
|
|
336
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|