1+ #!/usr/bin/env node
2+ // ---------------------------------------------------------------------------
3+ // Basic Multi-Service Dev Runner
4+ // ---------------------------------------------------------------------------
5+ // Purpose:
6+ // Lightweight script to start each service's "dev" script in parallel when
7+ // you are NOT using an advanced orchestrator (like Turborepo or Nx).
8+ // Intended for the scaffold's "basic" preset.
9+ //
10+ // High-Level Flow:
11+ // 1. Identify the root directory and the conventional services/ folder.
12+ // 2. Detect which JavaScript package manager to use (npm / pnpm / yarn / bun).
13+ // 3. Enumerate each subfolder in services/.
14+ // 4. For every folder containing a package.json with a dev script, spawn it.
15+ // 5. Stream each child process' output with a readable prefix.
16+ //
17+ // Design Constraints:
18+ // - Zero external dependencies (Node built‑ins only) to stay portable.
19+ // - Does NOT attempt file-change restarts (leave that to each service's own
20+ // tooling, e.g. nodemon, ts-node-dev, next dev, etc.).
21+ // - Ignores non-Node services (e.g., Go, Python, Java) since they don't have
22+ // a package.json dev script; those are expected to run via their own tools.
23+ // - CommonJS for maximum compatibility regardless of root "type: module".
24+ //
25+ // Extending Tips:
26+ // - To include non-Node services, add detection logic (e.g., main.go, pom.xml)
27+ // and spawn the appropriate commands.
28+ // - To implement auto-restart, consider integrating chokidar to watch files
29+ // and restart specific child processes.
30+ // - To add coloring per service, map service names to a color palette and
31+ // wrap the prefix() output (keep it optional for plain CI logs).
32+ // ---------------------------------------------------------------------------
33+
134const { spawn } = require ( 'node:child_process' ) ;
235const fs = require ( 'fs' ) ;
336const path = require ( 'path' ) ;
437
38+ // Root of the repo (assumes script is run from project root via npm script)
539const root = process . cwd ( ) ;
40+
41+ // Convention: all services generated by the scaffold live under services/
42+ // (Older versions used apps/; that was replaced to avoid confusion with Next.js)
643const servicesDir = path . join ( root , 'services' ) ;
744
845if ( ! fs . existsSync ( servicesDir ) ) {
946 console . warn ( '⚠️ services/ directory not found. No local services will be started.' ) ;
1047 process . exit ( 0 ) ;
1148}
1249
50+ // Snapshot the list of entries inside services/ (folders assumed to be services)
1351const services = fs . readdirSync ( servicesDir ) ;
1452
1553// Determine the root package manager heuristically
1654function detectPM ( ) {
55+ // Heuristic: look for lock files in order of preference.
56+ // If multiple exist (rare / misconfigured), first match wins.
1757 if ( fs . existsSync ( path . join ( root , 'pnpm-lock.yaml' ) ) ) return 'pnpm' ;
1858 if ( fs . existsSync ( path . join ( root , 'yarn.lock' ) ) ) return 'yarn' ;
1959 if ( fs . existsSync ( path . join ( root , 'bun.lockb' ) ) ) return 'bun' ;
60+ // Fallback to npm when no known lockfile is detected.
2061 return 'npm' ;
2162}
2263const pm = detectPM ( ) ;
2364
2465if ( services . length === 0 ) {
25- console . warn ( '⚠️ No services found inside services/.' ) ;
66+ // Nothing inside services/; we exit early (not an error condition).
67+ console . log ( 'No Node-based services with package.json to run.' ) ;
2668 process . exit ( 0 ) ;
2769}
2870
2971console . log ( `Using package manager: ${ pm } ` ) ;
3072console . log ( `Discovered services: ${ services . join ( ', ' ) } ` ) ;
3173
74+ // Keep references to spawned child processes so we can forward signals.
3275const procs = [ ] ;
3376
3477function prefix ( name , data ) {
78+ // Prepend the service name to each line of output for readability.
79+ // NOTE: data may contain multiple lines; we leave as‑is to avoid splitting.
3580 process . stdout . write ( `[${ name } ] ${ data } ` ) ;
3681}
3782
@@ -40,45 +85,39 @@ services.forEach((svc) => {
4085 const pkgPath = path . join ( svcPath , 'package.json' ) ;
4186
4287 if ( ! fs . existsSync ( pkgPath ) ) {
43- console . warn ( `⚠️ Skipping ${ svc } (no package.json; likely non-Node service)` ) ;
88+ // Non-Node service (e.g., python, go, java). Skip silently with context.
89+ console . log ( `Skipping ${ svc } (no package.json; likely non-Node service)` ) ;
4490 return ;
4591 }
4692
4793 try {
4894 const pkg = JSON . parse ( fs . readFileSync ( pkgPath , 'utf8' ) ) ;
49- const script = pkg . scripts ?. dev || pkg . scripts ?. start ;
50-
51- if ( ! script ) {
52- console . warn ( `⚠️ Skipping ${ svc } (no "dev" or "start" script in package.json)` ) ;
95+ if ( ! pkg . scripts || ! pkg . scripts . dev ) {
96+ // Node project present but without a dev script; user may add later.
97+ console . log ( `Skipping ${ svc } (no dev script)` ) ;
5398 return ;
5499 }
55-
56- const cmd = pm === 'bun' ? 'bun' : pm ;
57- const args =
58- pm === 'yarn'
59- ? [ 'run' , script === pkg . scripts . dev ? 'dev' : 'start' ]
60- : pm === 'bun'
61- ? [ 'run' , script === pkg . scripts . dev ? 'dev' : 'start' ]
62- : [ 'run' , script === pkg . scripts . dev ? 'dev' : 'start' ] ;
63-
64- const child = spawn ( cmd , args , {
65- cwd : svcPath ,
66- shell : true ,
67- env : process . env ,
68- } ) ;
69-
100+ // Determine spawn command + arguments.
101+ // For yarn classic, `yarn dev` would also work, but keeping uniform form.
102+ const cmd = pm === 'bun' ? 'bun' : pm ; // direct binary (npm, pnpm, yarn, bun)
103+ const args = pm === 'yarn' ? [ 'run' , 'dev' ] : pm === 'bun' ? [ 'run' , 'dev' ] : [ 'run' , 'dev' ] ;
104+ // Could special-case: if (pm==='yarn') args=['dev'] but current pattern is fine.
105+ const child = spawn ( cmd , args , { cwd : svcPath , shell : true , env : process . env } ) ;
70106 procs . push ( child ) ;
71- child . stdout . on ( 'data' , ( d ) => prefix ( svc , d ) ) ;
72- child . stderr . on ( 'data' , ( d ) => prefix ( svc , d ) ) ;
73- child . on ( 'exit' , ( code ) => {
107+ child . stdout . on ( 'data' , d => prefix ( svc , d ) ) ;
108+ child . stderr . on ( 'data' , d => prefix ( svc , d ) ) ;
109+ child . on ( 'exit' , code => {
110+ // Log exit so the developer sees early failures.
74111 console . log ( `[${ svc } ] exited with code ${ code } ` ) ;
75112 } ) ;
76113 } catch ( e ) {
77- console . error ( `❌ Failed to start ${ svc } :` , e . message ) ;
114+ // JSON parse errors or spawn failures bubble here.
115+ console . log ( `Failed to start ${ svc } :` , e . message ) ;
78116 }
79117} ) ;
80118
81119process . on ( 'SIGINT' , ( ) => {
82- procs . forEach ( ( p ) => p . kill ( 'SIGINT' ) ) ;
120+ // Graceful shutdown: forward Ctrl+C to each child, then exit.
121+ procs . forEach ( p => p . kill ( 'SIGINT' ) ) ;
83122 process . exit ( 0 ) ;
84123} ) ;
0 commit comments