@@ -118,57 +118,193 @@ if (fs.existsSync(servicesDir)) {
118118 const svcPath = path . join ( cwd , svc . path ) ;
119119
120120 if ( ! fs . existsSync ( svcPath ) ) continue ;
121- // Only auto-run node & frontend services (others require language runtime dev tasks)
122- if ( ! [ 'node' , 'frontend' ] . includes ( svc . type ) ) continue ;
123121
124- const pkgPath = path . join ( svcPath , 'package.json' ) ;
125- if ( ! fs . existsSync ( pkgPath ) ) {
126- console . log ( `Skipping ${ svc . name } (no package.json)` ) ;
127- continue ;
128- }
122+ const color = colorFor ( svc . name ) ;
123+ let child = null ;
129124
130- let pkg ;
131- try {
132- pkg = JSON . parse ( fs . readFileSync ( pkgPath , 'utf-8' ) ) ;
133- } catch {
134- console . log ( chalk . yellow ( `Skipping ${ svc . name } (invalid package.json)` ) ) ;
135- continue ;
136- }
125+ // Handle different service types
126+ switch ( svc . type ) {
127+ case 'node' :
128+ case 'frontend' : {
129+ const pkgPath = path . join ( svcPath , 'package.json' ) ;
130+ if ( ! fs . existsSync ( pkgPath ) ) {
131+ console . log ( chalk . yellow ( `Skipping ${ svc . name } (no package.json)` ) ) ;
132+ continue ;
133+ }
137134
138- // Determine which script to run
139- const useScript = pkg . scripts ?. dev ? 'dev' : pkg . scripts ?. start ? 'start' : null ;
140- if ( ! useScript ) {
141- console . log ( chalk . yellow ( `Skipping ${ svc . name } (no "dev" or "start" script)` ) ) ;
142- continue ;
143- }
144- if ( useScript === 'start' ) {
145- console . log ( `running start instead of dev for ${ svc . name } ` ) ;
146- }
135+ let pkg ;
136+ try {
137+ pkg = JSON . parse ( fs . readFileSync ( pkgPath , 'utf-8' ) ) ;
138+ } catch {
139+ console . log ( chalk . yellow ( `Skipping ${ svc . name } (invalid package.json)` ) ) ;
140+ continue ;
141+ }
147142
148- const color = colorFor ( svc . name ) ;
149- const pm = detectPM ( svcPath ) ;
150- const cmd = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : pm === 'bun' ? 'bun' : 'npm' ;
151- const args = [ 'run' , useScript ] ;
152-
153- const child = spawn ( cmd , args , { cwd : svcPath , env : { ...process . env , PORT : String ( svc . port ) } , shell : true } ) ;
154- procs . push ( child ) ;
155- child . stdout . on ( 'data' , d => process . stdout . write ( color ( `[${ svc . name } ] ` ) + d . toString ( ) ) ) ;
156- child . stderr . on ( 'data' , d => process . stderr . write ( color ( `[${ svc . name } ] ` ) + d . toString ( ) ) ) ;
157- child . on ( 'exit' , code => {
158- process . stdout . write ( color ( `[${ svc . name } ] exited with code ${ code } \n` ) ) ;
159- } ) ;
160- // health check
161- const healthUrl = `http://localhost:${ svc . port } /health` ;
162- const hp = waitForHealth ( healthUrl , 30000 ) . then ( ok => {
163- const msg = ok ? chalk . green ( `✔ health OK ${ svc . name } ${ healthUrl } ` ) : chalk . yellow ( `⚠ health timeout ${ svc . name } ${ healthUrl } ` ) ;
164- console . log ( msg ) ;
165- } ) ;
166- healthPromises . push ( hp ) ;
143+ // Determine which script to run
144+ const useScript = pkg . scripts ?. dev ? 'dev' : pkg . scripts ?. start ? 'start' : null ;
145+ if ( ! useScript ) {
146+ console . log ( chalk . yellow ( `Skipping ${ svc . name } (no "dev" or "start" script)` ) ) ;
147+ continue ;
148+ }
149+ if ( useScript === 'start' ) {
150+ console . log ( color ( `[${ svc . name } ] running start instead of dev` ) ) ;
151+ }
152+
153+ const pm = detectPM ( svcPath ) ;
154+ const cmd = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : pm === 'bun' ? 'bun' : 'npm' ;
155+ const args = [ 'run' , useScript ] ;
156+
157+ child = spawn ( cmd , args , { cwd : svcPath , env : { ...process . env , PORT : String ( svc . port ) } , shell : true } ) ;
158+ break ;
159+ }
160+
161+ case 'python' : {
162+ const venvPath = path . join ( svcPath , 'venv' ) ;
163+ const venvBin = path . join ( venvPath , 'bin' ) ;
164+ const venvPython = path . join ( venvBin , 'python' ) ;
165+ const venvPip = path . join ( venvBin , 'pip' ) ;
166+ const venvUvicorn = path . join ( venvBin , 'uvicorn' ) ;
167+
168+ // Create virtual environment if it doesn't exist
169+ if ( ! fs . existsSync ( venvPath ) ) {
170+ console . log ( color ( `[${ svc . name } ] Creating Python virtual environment...` ) ) ;
171+ const venvCreate = spawn ( 'python3' , [ '-m' , 'venv' , 'venv' ] , { cwd : svcPath , stdio : 'pipe' } ) ;
172+ venvCreate . stdout . on ( 'data' , d => process . stdout . write ( color ( `[${ svc . name } :venv] ` ) + d . toString ( ) ) ) ;
173+ venvCreate . stderr . on ( 'data' , d => process . stderr . write ( color ( `[${ svc . name } :venv] ` ) + d . toString ( ) ) ) ;
174+ await new Promise ( resolve => venvCreate . on ( 'close' , resolve ) ) ;
175+ }
176+
177+ // Check for requirements.txt and install dependencies in venv
178+ const reqPath = path . join ( svcPath , 'requirements.txt' ) ;
179+ if ( fs . existsSync ( reqPath ) ) {
180+ console . log ( color ( `[${ svc . name } ] Installing Python dependencies in virtual environment...` ) ) ;
181+ const pipInstall = spawn ( venvPip , [ 'install' , '-r' , 'requirements.txt' ] , { cwd : svcPath , stdio : 'pipe' } ) ;
182+ pipInstall . stdout . on ( 'data' , d => process . stdout . write ( color ( `[${ svc . name } :setup] ` ) + d . toString ( ) ) ) ;
183+ pipInstall . stderr . on ( 'data' , d => process . stderr . write ( color ( `[${ svc . name } :setup] ` ) + d . toString ( ) ) ) ;
184+ await new Promise ( resolve => pipInstall . on ( 'close' , resolve ) ) ;
185+ }
186+
187+ // Look for main.py in app/ or root
188+ const mainPath = fs . existsSync ( path . join ( svcPath , 'app' , 'main.py' ) )
189+ ? path . join ( 'app' , 'main.py' )
190+ : 'main.py' ;
191+
192+ if ( ! fs . existsSync ( path . join ( svcPath , mainPath ) ) ) {
193+ console . log ( chalk . yellow ( `Skipping ${ svc . name } (no ${ mainPath } found)` ) ) ;
194+ continue ;
195+ }
196+
197+ console . log ( color ( `[${ svc . name } ] Starting Python service with uvicorn...` ) ) ;
198+ const module = mainPath . replace ( / \/ / g, '.' ) . replace ( '.py' , '' ) ;
199+
200+ // Use venv's uvicorn if it exists, otherwise try to install it
201+ let uvicornCmd = venvUvicorn ;
202+ if ( ! fs . existsSync ( venvUvicorn ) ) {
203+ console . log ( color ( `[${ svc . name } ] Installing uvicorn in virtual environment...` ) ) ;
204+ const uvicornInstall = spawn ( venvPip , [ 'install' , 'uvicorn[standard]' , 'fastapi' ] , { cwd : svcPath , stdio : 'pipe' } ) ;
205+ uvicornInstall . stdout . on ( 'data' , d => process . stdout . write ( color ( `[${ svc . name } :setup] ` ) + d . toString ( ) ) ) ;
206+ uvicornInstall . stderr . on ( 'data' , d => process . stderr . write ( color ( `[${ svc . name } :setup] ` ) + d . toString ( ) ) ) ;
207+ await new Promise ( resolve => uvicornInstall . on ( 'close' , resolve ) ) ;
208+ }
209+
210+ child = spawn ( uvicornCmd , [ module + ':app' , '--reload' , '--host' , '0.0.0.0' , '--port' , String ( svc . port ) ] , {
211+ cwd : svcPath ,
212+ env : { ...process . env , PORT : String ( svc . port ) } ,
213+ shell : false
214+ } ) ;
215+ break ;
216+ }
217+
218+ case 'go' : {
219+ // Check for go.mod
220+ const goModPath = path . join ( svcPath , 'go.mod' ) ;
221+ if ( fs . existsSync ( goModPath ) ) {
222+ console . log ( color ( `[${ svc . name } ] Installing Go dependencies...` ) ) ;
223+ const goGet = spawn ( 'go' , [ 'mod' , 'download' ] , { cwd : svcPath , stdio : 'pipe' } ) ;
224+ goGet . stdout . on ( 'data' , d => process . stdout . write ( color ( `[${ svc . name } :setup] ` ) + d . toString ( ) ) ) ;
225+ goGet . stderr . on ( 'data' , d => process . stderr . write ( color ( `[${ svc . name } :setup] ` ) + d . toString ( ) ) ) ;
226+ await new Promise ( resolve => goGet . on ( 'close' , resolve ) ) ;
227+ }
228+
229+ // Check for main.go
230+ if ( ! fs . existsSync ( path . join ( svcPath , 'main.go' ) ) ) {
231+ console . log ( chalk . yellow ( `Skipping ${ svc . name } (no main.go found)` ) ) ;
232+ continue ;
233+ }
234+
235+ console . log ( color ( `[${ svc . name } ] Starting Go service...` ) ) ;
236+ child = spawn ( 'go' , [ 'run' , 'main.go' ] , {
237+ cwd : svcPath ,
238+ env : { ...process . env , PORT : String ( svc . port ) } ,
239+ shell : true
240+ } ) ;
241+ break ;
242+ }
243+
244+ case 'java' : {
245+ // Check for pom.xml (Maven) or build.gradle (Gradle)
246+ const pomPath = path . join ( svcPath , 'pom.xml' ) ;
247+ const gradlePath = path . join ( svcPath , 'build.gradle' ) ;
248+
249+ if ( fs . existsSync ( pomPath ) ) {
250+ console . log ( color ( `[${ svc . name } ] Building Java service with Maven...` ) ) ;
251+ const mvnPackage = spawn ( 'mvn' , [ 'clean' , 'package' , '-DskipTests' ] , { cwd : svcPath , stdio : 'pipe' } ) ;
252+ mvnPackage . stdout . on ( 'data' , d => process . stdout . write ( color ( `[${ svc . name } :build] ` ) + d . toString ( ) ) ) ;
253+ mvnPackage . stderr . on ( 'data' , d => process . stderr . write ( color ( `[${ svc . name } :build] ` ) + d . toString ( ) ) ) ;
254+ await new Promise ( resolve => mvnPackage . on ( 'close' , resolve ) ) ;
255+
256+ console . log ( color ( `[${ svc . name } ] Starting Java service with Spring Boot...` ) ) ;
257+ child = spawn ( 'mvn' , [ 'spring-boot:run' ] , {
258+ cwd : svcPath ,
259+ env : { ...process . env , SERVER_PORT : String ( svc . port ) } ,
260+ shell : true
261+ } ) ;
262+ } else if ( fs . existsSync ( gradlePath ) ) {
263+ console . log ( color ( `[${ svc . name } ] Building Java service with Gradle...` ) ) ;
264+ const gradleBuild = spawn ( './gradlew' , [ 'build' , '-x' , 'test' ] , { cwd : svcPath , stdio : 'pipe' } ) ;
265+ gradleBuild . stdout . on ( 'data' , d => process . stdout . write ( color ( `[${ svc . name } :build] ` ) + d . toString ( ) ) ) ;
266+ gradleBuild . stderr . on ( 'data' , d => process . stderr . write ( color ( `[${ svc . name } :build] ` ) + d . toString ( ) ) ) ;
267+ await new Promise ( resolve => gradleBuild . on ( 'close' , resolve ) ) ;
268+
269+ console . log ( color ( `[${ svc . name } ] Starting Java service with Gradle...` ) ) ;
270+ child = spawn ( './gradlew' , [ 'bootRun' ] , {
271+ cwd : svcPath ,
272+ env : { ...process . env , SERVER_PORT : String ( svc . port ) } ,
273+ shell : true
274+ } ) ;
275+ } else {
276+ console . log ( chalk . yellow ( `Skipping ${ svc . name } (no pom.xml or build.gradle found)` ) ) ;
277+ continue ;
278+ }
279+ break ;
280+ }
281+
282+ default :
283+ console . log ( chalk . yellow ( `Skipping ${ svc . name } (unsupported service type: ${ svc . type } )` ) ) ;
284+ continue ;
285+ }
286+
287+ if ( child ) {
288+ procs . push ( child ) ;
289+ child . stdout . on ( 'data' , d => process . stdout . write ( color ( `[${ svc . name } ] ` ) + d . toString ( ) ) ) ;
290+ child . stderr . on ( 'data' , d => process . stderr . write ( color ( `[${ svc . name } ] ` ) + d . toString ( ) ) ) ;
291+ child . on ( 'exit' , code => {
292+ process . stdout . write ( color ( `[${ svc . name } ] exited with code ${ code } \n` ) ) ;
293+ } ) ;
294+
295+ // Health check
296+ const healthUrl = `http://localhost:${ svc . port } /health` ;
297+ const hp = waitForHealth ( healthUrl , 30000 ) . then ( ok => {
298+ const msg = ok ? chalk . green ( `✔ health OK ${ svc . name } ${ healthUrl } ` ) : chalk . yellow ( `⚠ health timeout ${ svc . name } ${ healthUrl } ` ) ;
299+ console . log ( msg ) ;
300+ } ) ;
301+ healthPromises . push ( hp ) ;
302+ }
167303 }
168304}
169305
170306 if ( ! procs . length ) {
171- console . log ( chalk . yellow ( 'No auto-runnable Node/Frontend services found. Use --docker to start all via compose.' ) ) ;
307+ console . log ( chalk . yellow ( 'No services found to start . Use --docker to start all via compose.' ) ) ;
172308 // ✅ FIXED: Exit cleanly when running in CI/test mode
173309 if ( process . env . CI === 'true' ) {
174310 process . exit ( 0 ) ;
0 commit comments