postgresai 0.15.0-dev.6 → 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 +26 -14
- package/dist/bin/postgres-ai.js +14 -12
- package/package.json +1 -1
- package/test/init.test.ts +59 -4
- package/test/monitoring.test.ts +2 -2
package/bin/postgres-ai.ts
CHANGED
|
@@ -506,7 +506,7 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
|
|
|
506
506
|
|
|
507
507
|
// Ensure instances.yml exists as a FILE (avoid Docker creating a directory).
|
|
508
508
|
// Docker bind-mounts create missing paths as directories; replace if so.
|
|
509
|
-
if (fs.existsSync(instancesFile) && fs.
|
|
509
|
+
if (fs.existsSync(instancesFile) && fs.lstatSync(instancesFile).isDirectory()) {
|
|
510
510
|
fs.rmSync(instancesFile, { recursive: true, force: true });
|
|
511
511
|
}
|
|
512
512
|
if (!fs.existsSync(instancesFile)) {
|
|
@@ -2530,10 +2530,21 @@ mon
|
|
|
2530
2530
|
}
|
|
2531
2531
|
}
|
|
2532
2532
|
} else {
|
|
2533
|
-
// 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
|
|
2534
2547
|
console.log("Step 2: Demo mode enabled - using included demo PostgreSQL database");
|
|
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
2548
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
2538
2549
|
const demoCandidates = [
|
|
2539
2550
|
path.resolve(currentDir, "..", "..", "instances.demo.yml"), // npm: dist/bin -> package root
|
|
@@ -2541,14 +2552,15 @@ mon
|
|
|
2541
2552
|
];
|
|
2542
2553
|
const demoSrc = demoCandidates.find(p => fs.existsSync(p));
|
|
2543
2554
|
if (demoSrc) {
|
|
2544
|
-
// Remove directory artifact left by Docker bind-mounts
|
|
2545
|
-
if (fs.existsSync(instancesPath) && fs.
|
|
2555
|
+
// Remove directory artifact left by Docker bind-mounts before copying
|
|
2556
|
+
if (fs.existsSync(instancesPath) && fs.lstatSync(instancesPath).isDirectory()) {
|
|
2546
2557
|
fs.rmSync(instancesPath, { recursive: true, force: true });
|
|
2547
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) && !fs.
|
|
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) || fs.
|
|
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) && !fs.
|
|
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,7 @@ targets
|
|
|
3200
3212
|
}
|
|
3201
3213
|
} catch (err) {
|
|
3202
3214
|
// If YAML parsing fails, fall back to simple check
|
|
3203
|
-
const isFile = fs.existsSync(file) && !fs.
|
|
3215
|
+
const isFile = fs.existsSync(file) && !fs.lstatSync(file).isDirectory();
|
|
3204
3216
|
const content = isFile ? fs.readFileSync(file, "utf8") : "";
|
|
3205
3217
|
if (new RegExp(`^- name: ${instanceName}$`, "m").test(content)) {
|
|
3206
3218
|
console.error(`Monitoring target '${instanceName}' already exists`);
|
|
@@ -3210,7 +3222,7 @@ targets
|
|
|
3210
3222
|
}
|
|
3211
3223
|
|
|
3212
3224
|
// Add new instance — if instances.yml is a directory (Docker artifact), replace it with a file
|
|
3213
|
-
if (fs.existsSync(file) && fs.
|
|
3225
|
+
if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
|
|
3214
3226
|
fs.rmSync(file, { recursive: true, force: true });
|
|
3215
3227
|
}
|
|
3216
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`;
|
|
@@ -3223,7 +3235,7 @@ targets
|
|
|
3223
3235
|
.description("remove monitoring target database")
|
|
3224
3236
|
.action(async (name: string) => {
|
|
3225
3237
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
3226
|
-
if (!fs.existsSync(file) || fs.
|
|
3238
|
+
if (!fs.existsSync(file) || fs.lstatSync(file).isDirectory()) {
|
|
3227
3239
|
console.error("instances.yml not found");
|
|
3228
3240
|
process.exitCode = 1;
|
|
3229
3241
|
return;
|
|
@@ -3260,7 +3272,7 @@ targets
|
|
|
3260
3272
|
.description("test monitoring target database connectivity")
|
|
3261
3273
|
.action(async (name: string) => {
|
|
3262
3274
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
3263
|
-
if (!fs.existsSync(instancesPath) || fs.
|
|
3275
|
+
if (!fs.existsSync(instancesPath) || fs.lstatSync(instancesPath).isDirectory()) {
|
|
3264
3276
|
console.error("instances.yml not found");
|
|
3265
3277
|
process.exitCode = 1;
|
|
3266
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,
|
|
@@ -15982,7 +15982,7 @@ var Result = import_lib.default.Result;
|
|
|
15982
15982
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15983
15983
|
var defaults = import_lib.default.defaults;
|
|
15984
15984
|
// package.json
|
|
15985
|
-
var version = "0.15.0-dev.
|
|
15985
|
+
var version = "0.15.0-dev.7";
|
|
15986
15986
|
var package_default2 = {
|
|
15987
15987
|
name: "postgresai",
|
|
15988
15988
|
version,
|
|
@@ -29891,7 +29891,7 @@ async function ensureDefaultMonitoringProject() {
|
|
|
29891
29891
|
throw new Error(`Failed to bootstrap docker-compose.yml: ${msg}`);
|
|
29892
29892
|
}
|
|
29893
29893
|
}
|
|
29894
|
-
if (fs6.existsSync(instancesFile) && fs6.
|
|
29894
|
+
if (fs6.existsSync(instancesFile) && fs6.lstatSync(instancesFile).isDirectory()) {
|
|
29895
29895
|
fs6.rmSync(instancesFile, { recursive: true, force: true });
|
|
29896
29896
|
}
|
|
29897
29897
|
if (!fs6.existsSync(instancesFile)) {
|
|
@@ -31549,15 +31549,17 @@ You can provide either:`);
|
|
|
31549
31549
|
];
|
|
31550
31550
|
const demoSrc = demoCandidates.find((p) => fs6.existsSync(p));
|
|
31551
31551
|
if (demoSrc) {
|
|
31552
|
-
if (fs6.existsSync(instancesPath) && fs6.
|
|
31552
|
+
if (fs6.existsSync(instancesPath) && fs6.lstatSync(instancesPath).isDirectory()) {
|
|
31553
31553
|
fs6.rmSync(instancesPath, { recursive: true, force: true });
|
|
31554
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) && !fs6.
|
|
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) || fs6.
|
|
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) && !fs6.
|
|
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,7 +32090,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
32088
32090
|
}
|
|
32089
32091
|
}
|
|
32090
32092
|
} catch (err) {
|
|
32091
|
-
const isFile = fs6.existsSync(file) && !fs6.
|
|
32093
|
+
const isFile = fs6.existsSync(file) && !fs6.lstatSync(file).isDirectory();
|
|
32092
32094
|
const content2 = isFile ? fs6.readFileSync(file, "utf8") : "";
|
|
32093
32095
|
if (new RegExp(`^- name: ${instanceName}$`, "m").test(content2)) {
|
|
32094
32096
|
console.error(`Monitoring target '${instanceName}' already exists`);
|
|
@@ -32096,7 +32098,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
32096
32098
|
return;
|
|
32097
32099
|
}
|
|
32098
32100
|
}
|
|
32099
|
-
if (fs6.existsSync(file) && fs6.
|
|
32101
|
+
if (fs6.existsSync(file) && fs6.lstatSync(file).isDirectory()) {
|
|
32100
32102
|
fs6.rmSync(file, { recursive: true, force: true });
|
|
32101
32103
|
}
|
|
32102
32104
|
const body = `- name: ${instanceName}
|
|
@@ -32118,7 +32120,7 @@ targets.command("add [connStr] [name]").description("add monitoring target datab
|
|
|
32118
32120
|
});
|
|
32119
32121
|
targets.command("remove <name>").description("remove monitoring target database").action(async (name) => {
|
|
32120
32122
|
const { instancesFile: file } = await resolveOrInitPaths();
|
|
32121
|
-
if (!fs6.existsSync(file) || fs6.
|
|
32123
|
+
if (!fs6.existsSync(file) || fs6.lstatSync(file).isDirectory()) {
|
|
32122
32124
|
console.error("instances.yml not found");
|
|
32123
32125
|
process.exitCode = 1;
|
|
32124
32126
|
return;
|
|
@@ -32147,7 +32149,7 @@ targets.command("remove <name>").description("remove monitoring target database"
|
|
|
32147
32149
|
});
|
|
32148
32150
|
targets.command("test <name>").description("test monitoring target database connectivity").action(async (name) => {
|
|
32149
32151
|
const { instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
32150
|
-
if (!fs6.existsSync(instancesPath) || fs6.
|
|
32152
|
+
if (!fs6.existsSync(instancesPath) || fs6.lstatSync(instancesPath).isDirectory()) {
|
|
32151
32153
|
console.error("instances.yml not found");
|
|
32152
32154
|
process.exitCode = 1;
|
|
32153
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
|
|
|
@@ -1073,9 +1073,64 @@ describe("CLI commands", () => {
|
|
|
1073
1073
|
test("cli: mon local-install --demo configures demo monitoring target", () => {
|
|
1074
1074
|
// --demo should copy instances.demo.yml to instances.yml and print confirmation.
|
|
1075
1075
|
// The command will fail later (no Docker), but we verify the demo target step succeeded.
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
+
}
|
|
1079
1134
|
});
|
|
1080
1135
|
|
|
1081
1136
|
test("cli: mon local-install --demo with global --api-key shows error", () => {
|
package/test/monitoring.test.ts
CHANGED
|
@@ -286,13 +286,13 @@ describe("demo mode instances.demo.yml", () => {
|
|
|
286
286
|
expect(content).toMatch(/^\s+conn_str:/m);
|
|
287
287
|
expect(content).toMatch(/^\s+preset_metrics: full/m);
|
|
288
288
|
expect(content).toMatch(/^\s+is_enabled: true/m);
|
|
289
|
-
// ~sink_type~ is a
|
|
289
|
+
// ~sink_type~ is a sed token substituted by generate-pgwatch-sources.sh; values: postgres, prometheus
|
|
290
290
|
expect(content).toMatch(/^\s+sink_type: ~sink_type~/m);
|
|
291
291
|
});
|
|
292
292
|
|
|
293
293
|
test("instances.yml is gitignored (not tracked)", () => {
|
|
294
294
|
const gitignore = fs.readFileSync(path.join(repoRoot, ".gitignore"), "utf8");
|
|
295
|
-
expect(gitignore).
|
|
295
|
+
expect(gitignore).toMatch(/^instances\.yml$/m);
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
test("demo config can be copied to instances.yml in temp dir", () => {
|