postgresai 0.15.0-dev.5 → 0.15.0-dev.6
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 +39 -35
- package/dist/bin/postgres-ai.js +38 -34
- package/package.json +1 -1
- package/test/init.test.ts +8 -0
- 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.statSync(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,25 @@ mon
|
|
|
2540
2530
|
}
|
|
2541
2531
|
}
|
|
2542
2532
|
} else {
|
|
2543
|
-
// Demo mode: copy instances.demo.yml → instances.yml so the demo target is active
|
|
2533
|
+
// Demo mode: copy bundled instances.demo.yml → instances.yml so the demo target is active
|
|
2544
2534
|
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2535
|
+
// Use import.meta.url instead of __dirname — bundlers bake in __dirname at build time.
|
|
2536
|
+
// Check multiple candidate paths (npm package vs repo dev layout).
|
|
2537
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
2538
|
+
const demoCandidates = [
|
|
2539
|
+
path.resolve(currentDir, "..", "..", "instances.demo.yml"), // npm: dist/bin -> package root
|
|
2540
|
+
path.resolve(currentDir, "..", "..", "..", "instances.demo.yml"), // dev: cli/bin -> repo root
|
|
2541
|
+
];
|
|
2542
|
+
const demoSrc = demoCandidates.find(p => fs.existsSync(p));
|
|
2543
|
+
if (demoSrc) {
|
|
2544
|
+
// Remove directory artifact left by Docker bind-mounts
|
|
2545
|
+
if (fs.existsSync(instancesPath) && fs.statSync(instancesPath).isDirectory()) {
|
|
2546
|
+
fs.rmSync(instancesPath, { recursive: true, force: true });
|
|
2547
|
+
}
|
|
2548
2548
|
fs.copyFileSync(demoSrc, instancesPath);
|
|
2549
2549
|
console.log("✓ Demo monitoring target configured\n");
|
|
2550
2550
|
} else {
|
|
2551
|
-
console.error(
|
|
2551
|
+
console.error(`⚠ instances.demo.yml not found — demo target not configured (searched: ${demoCandidates.join(", ")})\n`);
|
|
2552
2552
|
}
|
|
2553
2553
|
}
|
|
2554
2554
|
|
|
@@ -2904,7 +2904,7 @@ mon
|
|
|
2904
2904
|
console.log(`Project Directory: ${projectDir}`);
|
|
2905
2905
|
console.log(`Docker Compose File: ${composeFile}`);
|
|
2906
2906
|
console.log(`Instances File: ${instancesFile}`);
|
|
2907
|
-
if (fs.existsSync(instancesFile)) {
|
|
2907
|
+
if (fs.existsSync(instancesFile) && !fs.statSync(instancesFile).isDirectory()) {
|
|
2908
2908
|
console.log("\nInstances configuration:\n");
|
|
2909
2909
|
const text = fs.readFileSync(instancesFile, "utf8");
|
|
2910
2910
|
process.stdout.write(text);
|
|
@@ -3120,7 +3120,7 @@ targets
|
|
|
3120
3120
|
.description("list monitoring target databases")
|
|
3121
3121
|
.action(async () => {
|
|
3122
3122
|
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
3123
|
-
if (!fs.existsSync(instancesPath)) {
|
|
3123
|
+
if (!fs.existsSync(instancesPath) || fs.statSync(instancesPath).isDirectory()) {
|
|
3124
3124
|
console.error(`instances.yml not found in ${projectDir}`);
|
|
3125
3125
|
process.exitCode = 1;
|
|
3126
3126
|
return;
|
|
@@ -3186,7 +3186,7 @@ targets
|
|
|
3186
3186
|
|
|
3187
3187
|
// Check if instance already exists
|
|
3188
3188
|
try {
|
|
3189
|
-
if (fs.existsSync(file)) {
|
|
3189
|
+
if (fs.existsSync(file) && !fs.statSync(file).isDirectory()) {
|
|
3190
3190
|
const content = fs.readFileSync(file, "utf8");
|
|
3191
3191
|
const instances = yaml.load(content) as Instance[] | null || [];
|
|
3192
3192
|
if (Array.isArray(instances)) {
|
|
@@ -3200,7 +3200,8 @@ targets
|
|
|
3200
3200
|
}
|
|
3201
3201
|
} catch (err) {
|
|
3202
3202
|
// If YAML parsing fails, fall back to simple check
|
|
3203
|
-
const
|
|
3203
|
+
const isFile = fs.existsSync(file) && !fs.statSync(file).isDirectory();
|
|
3204
|
+
const content = isFile ? fs.readFileSync(file, "utf8") : "";
|
|
3204
3205
|
if (new RegExp(`^- name: ${instanceName}$`, "m").test(content)) {
|
|
3205
3206
|
console.error(`Monitoring target '${instanceName}' already exists`);
|
|
3206
3207
|
process.exitCode = 1;
|
|
@@ -3208,7 +3209,10 @@ targets
|
|
|
3208
3209
|
}
|
|
3209
3210
|
}
|
|
3210
3211
|
|
|
3211
|
-
// Add new instance
|
|
3212
|
+
// Add new instance — if instances.yml is a directory (Docker artifact), replace it with a file
|
|
3213
|
+
if (fs.existsSync(file) && fs.statSync(file).isDirectory()) {
|
|
3214
|
+
fs.rmSync(file, { recursive: true, force: true });
|
|
3215
|
+
}
|
|
3212
3216
|
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
3217
|
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
3214
3218
|
fs.appendFileSync(file, (content && !/\n$/.test(content) ? "\n" : "") + body, "utf8");
|
|
@@ -3219,7 +3223,7 @@ targets
|
|
|
3219
3223
|
.description("remove monitoring target database")
|
|
3220
3224
|
.action(async (name: string) => {
|
|
3221
3225
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
3222
|
-
if (!fs.existsSync(file)) {
|
|
3226
|
+
if (!fs.existsSync(file) || fs.statSync(file).isDirectory()) {
|
|
3223
3227
|
console.error("instances.yml not found");
|
|
3224
3228
|
process.exitCode = 1;
|
|
3225
3229
|
return;
|
|
@@ -3256,7 +3260,7 @@ targets
|
|
|
3256
3260
|
.description("test monitoring target database connectivity")
|
|
3257
3261
|
.action(async (name: string) => {
|
|
3258
3262
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
3259
|
-
if (!fs.existsSync(instancesPath)) {
|
|
3263
|
+
if (!fs.existsSync(instancesPath) || fs.statSync(instancesPath).isDirectory()) {
|
|
3260
3264
|
console.error("instances.yml not found");
|
|
3261
3265
|
process.exitCode = 1;
|
|
3262
3266
|
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.6",
|
|
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.6";
|
|
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.statSync(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,14 +31542,21 @@ 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.statSync(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(`\u26A0 instances.demo.yml not found \u2014 demo target not configured
|
|
31559
|
+
console.error(`\u26A0 instances.demo.yml not found \u2014 demo target not configured (searched: ${demoCandidates.join(", ")})
|
|
31560
31560
|
`);
|
|
31561
31561
|
}
|
|
31562
31562
|
}
|
|
@@ -31845,7 +31845,7 @@ mon.command("config").description("show monitoring services configuration").acti
|
|
|
31845
31845
|
console.log(`Project Directory: ${projectDir}`);
|
|
31846
31846
|
console.log(`Docker Compose File: ${composeFile}`);
|
|
31847
31847
|
console.log(`Instances File: ${instancesFile}`);
|
|
31848
|
-
if (fs6.existsSync(instancesFile)) {
|
|
31848
|
+
if (fs6.existsSync(instancesFile) && !fs6.statSync(instancesFile).isDirectory()) {
|
|
31849
31849
|
console.log(`
|
|
31850
31850
|
Instances configuration:
|
|
31851
31851
|
`);
|
|
@@ -32020,7 +32020,7 @@ mon.command("check").description("monitoring services system readiness check").a
|
|
|
32020
32020
|
var targets = mon.command("targets").description("manage databases to monitor");
|
|
32021
32021
|
targets.command("list").description("list monitoring target databases").action(async () => {
|
|
32022
32022
|
const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
|
|
32023
|
-
if (!fs6.existsSync(instancesPath)) {
|
|
32023
|
+
if (!fs6.existsSync(instancesPath) || fs6.statSync(instancesPath).isDirectory()) {
|
|
32024
32024
|
console.error(`instances.yml not found in ${projectDir}`);
|
|
32025
32025
|
process.exitCode = 1;
|
|
32026
32026
|
return;
|
|
@@ -32075,7 +32075,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
32075
32075
|
const db = m[5];
|
|
32076
32076
|
const instanceName = name && name.trim() ? name.trim() : `${host}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
32077
32077
|
try {
|
|
32078
|
-
if (fs6.existsSync(file)) {
|
|
32078
|
+
if (fs6.existsSync(file) && !fs6.statSync(file).isDirectory()) {
|
|
32079
32079
|
const content2 = fs6.readFileSync(file, "utf8");
|
|
32080
32080
|
const instances = load(content2) || [];
|
|
32081
32081
|
if (Array.isArray(instances)) {
|
|
@@ -32088,13 +32088,17 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
32088
32088
|
}
|
|
32089
32089
|
}
|
|
32090
32090
|
} catch (err) {
|
|
32091
|
-
const
|
|
32091
|
+
const isFile = fs6.existsSync(file) && !fs6.statSync(file).isDirectory();
|
|
32092
|
+
const content2 = isFile ? fs6.readFileSync(file, "utf8") : "";
|
|
32092
32093
|
if (new RegExp(`^- name: ${instanceName}$`, "m").test(content2)) {
|
|
32093
32094
|
console.error(`Monitoring target '${instanceName}' already exists`);
|
|
32094
32095
|
process.exitCode = 1;
|
|
32095
32096
|
return;
|
|
32096
32097
|
}
|
|
32097
32098
|
}
|
|
32099
|
+
if (fs6.existsSync(file) && fs6.statSync(file).isDirectory()) {
|
|
32100
|
+
fs6.rmSync(file, { recursive: true, force: true });
|
|
32101
|
+
}
|
|
32098
32102
|
const body = `- name: ${instanceName}
|
|
32099
32103
|
conn_str: ${connStr}
|
|
32100
32104
|
preset_metrics: full
|
|
@@ -32114,7 +32118,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
32114
32118
|
});
|
|
32115
32119
|
targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
|
|
32116
32120
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
32117
|
-
if (!fs6.existsSync(file)) {
|
|
32121
|
+
if (!fs6.existsSync(file) || fs6.statSync(file).isDirectory()) {
|
|
32118
32122
|
console.error("instances.yml not found");
|
|
32119
32123
|
process.exitCode = 1;
|
|
32120
32124
|
return;
|
|
@@ -32143,7 +32147,7 @@ targets.command("remove <name>").description("remove monitoring target database"
|
|
|
32143
32147
|
});
|
|
32144
32148
|
targets.command("test <name>").description("test monitoring target database connectivity").action(async (name) => {
|
|
32145
32149
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
32146
|
-
if (!fs6.existsSync(instancesPath)) {
|
|
32150
|
+
if (!fs6.existsSync(instancesPath) || fs6.statSync(instancesPath).isDirectory()) {
|
|
32147
32151
|
console.error("instances.yml not found");
|
|
32148
32152
|
process.exitCode = 1;
|
|
32149
32153
|
return;
|
package/package.json
CHANGED
package/test/init.test.ts
CHANGED
|
@@ -1070,6 +1070,14 @@ 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
|
+
const r = runCli(["mon", "local-install", "--demo"]);
|
|
1077
|
+
expect(r.stdout).toMatch(/Demo mode enabled/);
|
|
1078
|
+
expect(r.stdout).toMatch(/Demo monitoring target configured/);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1073
1081
|
test("cli: mon local-install --demo with global --api-key shows error", () => {
|
|
1074
1082
|
// When --demo is used with global --api-key, it should still be detected and error
|
|
1075
1083
|
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 placeholder replaced per-sink by generate-pgwatch-sources.sh
|
|
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).toContain("instances.yml");
|
|
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
|
+
});
|