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.
@@ -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
- // 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
- }
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
- const { instancesFile: instancesPath, projectDir } = await resolveOrInitPaths();
2546
- const demoSrc = path.resolve(projectDir, "instances.demo.yml");
2547
- if (fs.existsSync(demoSrc)) {
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("⚠ instances.demo.yml not found — demo target not configured\n");
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 content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
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;
@@ -13151,7 +13151,7 @@ var {
13151
13151
  // package.json
13152
13152
  var package_default = {
13153
13153
  name: "postgresai",
13154
- version: "0.15.0-dev.5",
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.5";
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
- 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
- }
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: instancesPath, projectDir: projectDir2 } = await resolveOrInitPaths();
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(instancesPath, emptyInstancesContent, "utf8");
31437
- console.log(`Instances file: ${instancesPath}`);
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(instancesPath, body, "utf8");
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(instancesPath, body, "utf8");
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 { instancesFile: instancesPath, projectDir: projectDir2 } = await resolveOrInitPaths();
31553
- const demoSrc = path6.resolve(projectDir2, "instances.demo.yml");
31554
- if (fs6.existsSync(demoSrc)) {
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 content2 = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : "";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.15.0-dev.5",
3
+ "version": "0.15.0-dev.6",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
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([
@@ -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
+ });