@@ -7,6 +7,7 @@ import * as yaml from "js-yaml";
77import * as fs from "fs" ;
88import * as path from "path" ;
99import * as os from "os" ;
10+ import { fileURLToPath } from "url" ;
1011import * as crypto from "node:crypto" ;
1112import { Client } from "pg" ;
1213import { startMcpServer } from "../lib/mcp-server" ;
@@ -503,7 +504,11 @@ async function ensureDefaultMonitoringProject(): Promise<PathResolution> {
503504 }
504505 }
505506
506- // Ensure instances.yml exists as a FILE (avoid Docker creating a directory)
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 } ) ;
511+ }
507512 if ( ! fs . existsSync ( instancesFile ) ) {
508513 const header =
509514 "# PostgreSQL instances to monitor\n" +
@@ -2302,7 +2307,7 @@ mon
23022307 console . log ( "This will install, configure, and start the monitoring system\n" ) ;
23032308
23042309 // Ensure we have a project directory with docker-compose.yml even if running from elsewhere
2305- const { projectDir } = await resolveOrInitPaths ( ) ;
2310+ const { projectDir, instancesFile : instancesPath } = await resolveOrInitPaths ( ) ;
23062311 console . log ( `Project directory: ${ projectDir } \n` ) ;
23072312
23082313 // Save project name to .pgwatch-config if provided (used by reporter container)
@@ -2525,7 +2530,38 @@ mon
25252530 }
25262531 }
25272532 } else {
2528- console . log ( "Step 2: Demo mode enabled - using included demo PostgreSQL database\n" ) ;
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
2547+ console . log ( "Step 2: Demo mode enabled - using included demo PostgreSQL database" ) ;
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+ }
2559+ fs . copyFileSync ( demoSrc , instancesPath ) ;
2560+ console . log ( "✓ Demo monitoring target configured\n" ) ;
2561+ } else {
2562+ console . error ( `Error: instances.demo.yml not found — cannot configure demo target.\nSearched: ${ demoCandidates . join ( ", " ) } \n` ) ;
2563+ process . exit ( 1 ) ;
2564+ }
25292565 }
25302566
25312567 // Step 3: Update configuration
@@ -2880,7 +2916,7 @@ mon
28802916 console . log ( `Project Directory: ${ projectDir } ` ) ;
28812917 console . log ( `Docker Compose File: ${ composeFile } ` ) ;
28822918 console . log ( `Instances File: ${ instancesFile } ` ) ;
2883- if ( fs . existsSync ( instancesFile ) ) {
2919+ if ( fs . existsSync ( instancesFile ) && ! fs . lstatSync ( instancesFile ) . isDirectory ( ) ) {
28842920 console . log ( "\nInstances configuration:\n" ) ;
28852921 const text = fs . readFileSync ( instancesFile , "utf8" ) ;
28862922 process . stdout . write ( text ) ;
@@ -3096,7 +3132,7 @@ targets
30963132 . description ( "list monitoring target databases" )
30973133 . action ( async ( ) => {
30983134 const { instancesFile : instancesPath , projectDir } = await resolveOrInitPaths ( ) ;
3099- if ( ! fs . existsSync ( instancesPath ) ) {
3135+ if ( ! fs . existsSync ( instancesPath ) || fs . lstatSync ( instancesPath ) . isDirectory ( ) ) {
31003136 console . error ( `instances.yml not found in ${ projectDir } ` ) ;
31013137 process . exitCode = 1 ;
31023138 return ;
@@ -3162,7 +3198,7 @@ targets
31623198
31633199 // Check if instance already exists
31643200 try {
3165- if ( fs . existsSync ( file ) ) {
3201+ if ( fs . existsSync ( file ) && ! fs . lstatSync ( file ) . isDirectory ( ) ) {
31663202 const content = fs . readFileSync ( file , "utf8" ) ;
31673203 const instances = yaml . load ( content ) as Instance [ ] | null || [ ] ;
31683204 if ( Array . isArray ( instances ) ) {
@@ -3176,15 +3212,19 @@ targets
31763212 }
31773213 } catch ( err ) {
31783214 // If YAML parsing fails, fall back to simple check
3179- const content = fs . existsSync ( file ) ? fs . readFileSync ( file , "utf8" ) : "" ;
3215+ const isFile = fs . existsSync ( file ) && ! fs . lstatSync ( file ) . isDirectory ( ) ;
3216+ const content = isFile ? fs . readFileSync ( file , "utf8" ) : "" ;
31803217 if ( new RegExp ( `^- name: ${ instanceName } $` , "m" ) . test ( content ) ) {
31813218 console . error ( `Monitoring target '${ instanceName } ' already exists` ) ;
31823219 process . exitCode = 1 ;
31833220 return ;
31843221 }
31853222 }
31863223
3187- // 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+ }
31883228 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` ;
31893229 const content = fs . existsSync ( file ) ? fs . readFileSync ( file , "utf8" ) : "" ;
31903230 fs . appendFileSync ( file , ( content && ! / \n $ / . test ( content ) ? "\n" : "" ) + body , "utf8" ) ;
@@ -3195,7 +3235,7 @@ targets
31953235 . description ( "remove monitoring target database" )
31963236 . action ( async ( name : string ) => {
31973237 const { instancesFile : file } = await resolveOrInitPaths ( ) ;
3198- if ( ! fs . existsSync ( file ) ) {
3238+ if ( ! fs . existsSync ( file ) || fs . lstatSync ( file ) . isDirectory ( ) ) {
31993239 console . error ( "instances.yml not found" ) ;
32003240 process . exitCode = 1 ;
32013241 return ;
@@ -3232,7 +3272,7 @@ targets
32323272 . description ( "test monitoring target database connectivity" )
32333273 . action ( async ( name : string ) => {
32343274 const { instancesFile : instancesPath } = await resolveOrInitPaths ( ) ;
3235- if ( ! fs . existsSync ( instancesPath ) ) {
3275+ if ( ! fs . existsSync ( instancesPath ) || fs . lstatSync ( instancesPath ) . isDirectory ( ) ) {
32363276 console . error ( "instances.yml not found" ) ;
32373277 process . exitCode = 1 ;
32383278 return ;
0 commit comments