@@ -242,25 +242,32 @@ class ProjectManager {
242242
243243 const id = uuidv4 ( ) ;
244244
245- // Validate that required PHP version is installed before creating project
246- const phpVersion = config . phpVersion || '8.3' ;
245+ // Detect project type early (needed for conditional PHP check)
246+ const projectType = config . type || ( await this . detectProjectType ( config . path ) ) ;
247+
247248 const { app } = require ( 'electron' ) ;
248249 const resourcePath = this . configStore . get ( 'resourcePath' ) || path . join ( app . getPath ( 'userData' ) , 'resources' ) ;
249250 const platform = process . platform === 'win32' ? 'win' : process . platform === 'darwin' ? 'mac' : 'linux' ;
250- const phpDir = path . join ( resourcePath , 'php' , phpVersion , platform ) ;
251- const phpExe = platform === 'win' ? 'php.exe' : 'php' ;
252- const phpCgiExe = platform === 'win' ? 'php-cgi.exe' : 'php-cgi' ;
253251
254- const isPlaywright = process . env . PLAYWRIGHT_TEST === 'true' ;
252+ // Validate that required PHP version is installed before creating project.
253+ // Node.js projects do not require PHP.
254+ if ( projectType !== 'nodejs' ) {
255+ const phpVersion = config . phpVersion || '8.3' ;
256+ const phpDir = path . join ( resourcePath , 'php' , phpVersion , platform ) ;
257+ const phpExe = platform === 'win' ? 'php.exe' : 'php' ;
258+ const phpCgiExe = platform === 'win' ? 'php-cgi.exe' : 'php-cgi' ;
255259
256- if ( ! isPlaywright && ( ! await fs . pathExists ( path . join ( phpDir , phpExe ) ) || ! await fs . pathExists ( path . join ( phpDir , phpCgiExe ) ) ) ) {
257- throw new Error ( `PHP ${ phpVersion } is not installed. Please download it from the Binary Manager before creating a project.` ) ;
260+ const isPlaywright = process . env . PLAYWRIGHT_TEST === 'true' ;
261+
262+ if ( ! isPlaywright && ( ! await fs . pathExists ( path . join ( phpDir , phpExe ) ) || ! await fs . pathExists ( path . join ( phpDir , phpCgiExe ) ) ) ) {
263+ throw new Error ( `PHP ${ phpVersion } is not installed. Please download it from the Binary Manager before creating a project.` ) ;
264+ }
258265 }
259266
260267 // Re-fetch projects list (it may have changed after removing failed project)
261268 const currentProjects = this . configStore . get ( 'projects' , [ ] ) ;
262269
263- // Find available port
270+ // Find available port (for the web-server proxy)
264271 const usedPorts = currentProjects . map ( ( p ) => p . port ) ;
265272 let port = settings . portRangeStart || 8000 ;
266273 while ( usedPorts . includes ( port ) ) {
@@ -274,8 +281,15 @@ class ProjectManager {
274281 sslPort ++ ;
275282 }
276283
277- // Detect project type if not specified
278- const projectType = config . type || ( await this . detectProjectType ( config . path ) ) ;
284+ // Node.js app internal port (used as upstream for the reverse proxy).
285+ // Reserve a separate block starting at 3000 so it doesn't clash with web-server ports.
286+ let nodePort = config . nodePort || 3000 ;
287+ if ( projectType === 'nodejs' ) {
288+ const usedNodePorts = currentProjects . map ( ( p ) => p . nodePort ) . filter ( Boolean ) ;
289+ while ( usedNodePorts . includes ( nodePort ) ) {
290+ nodePort ++ ;
291+ }
292+ }
279293
280294 // Determine default web server version from installed versions
281295 let defaultWebServerVersion = '1.28' ;
@@ -316,15 +330,19 @@ class ProjectManager {
316330 redis : config . services ?. redis || false ,
317331 redisVersion : config . services ?. redisVersion || '7.4' ,
318332 queue : config . services ?. queue || false ,
319- // Node.js for projects that need it
320- nodejs : config . services ?. nodejs || false ,
333+ // Node.js is always enabled for nodejs-type projects
334+ nodejs : projectType === 'nodejs' ? true : ( config . services ?. nodejs || false ) ,
321335 nodejsVersion : config . services ?. nodejsVersion || '20' ,
322336 } ,
323337 environment : this . getDefaultEnvironment ( projectType , config . name , port ) ,
324338 supervisor : {
325339 workers : config . supervisor ?. workers || 1 ,
326340 processes : [ ] ,
327341 } ,
342+ // Node.js reverse-proxy port (only meaningful for nodejs-type projects)
343+ nodePort : projectType === 'nodejs' ? nodePort : undefined ,
344+ // Node.js start command for supervisor (only for nodejs-type projects)
345+ nodeStartCommand : projectType === 'nodejs' ? ( config . nodeStartCommand || 'npm start' ) : undefined ,
328346 createdAt : new Date ( ) . toISOString ( ) ,
329347 lastStarted : null ,
330348 // Compatibility warnings acknowledged by user
@@ -409,18 +427,33 @@ class ProjectManager {
409427 } ) ;
410428 }
411429
412- // Save project first (before installation which might take time)
413- // Re-fetch to ensure we have latest list
414- const projectsToSave = this . configStore . get ( 'projects' , [ ] ) ;
415- projectsToSave . push ( project ) ;
416- this . configStore . set ( 'projects' , projectsToSave ) ;
417-
418- // Auto-install CLI if not already installed
419- await this . ensureCliInstalled ( ) ;
430+ // Set up Node.js start process for nodejs-type projects
431+ if ( project . type === 'nodejs' ) {
432+ const nodejsVersion = project . services ?. nodejsVersion || '20' ;
433+ const nodeResourcePath = this . configStore . get ( 'resourcePath' ) || path . join ( require ( 'electron' ) . app . getPath ( 'userData' ) , 'resources' ) ;
434+ const nodePlatform = process . platform === 'win32' ? 'win' : process . platform === 'darwin' ? 'mac' : 'linux' ;
435+ const nodeDir = path . join ( nodeResourcePath , 'nodejs' , nodejsVersion , nodePlatform ) ;
436+ const nodeExe = process . platform === 'win32'
437+ ? path . join ( nodeDir , 'node.exe' )
438+ : path . join ( nodeDir , 'bin' , 'node' ) ;
439+ project . supervisor . processes . push ( {
440+ name : 'nodejs-app' ,
441+ command : project . nodeStartCommand || 'npm start' ,
442+ autostart : true ,
443+ autorestart : true ,
444+ numprocs : 1 ,
445+ environment : {
446+ PORT : String ( project . nodePort || 3000 ) ,
447+ NODE_PATH : nodeDir ,
448+ } ,
449+ } ) ;
450+ }
420451
421452 // Install fresh framework OR clone from repository - run async without blocking
422453 if ( config . installFresh || config . projectSource === 'clone' ) {
423- // Mark project as installing
454+ // Mark project as installing BEFORE saving to store.
455+ // This way, if the app crashes during installation, the project is saved
456+ // with installing:true and can be retried on restart or re-creation.
424457 project . installing = true ;
425458
426459 // Store clone config for runInstallation
@@ -429,8 +462,19 @@ class ProjectManager {
429462 authType : config . authType || 'public' ,
430463 accessToken : config . accessToken ,
431464 } : null ;
465+ }
432466
433- // Run installation in background (don't await)
467+ // Save project first (before installation which might take time)
468+ // Re-fetch to ensure we have latest list
469+ const projectsToSave = this . configStore . get ( 'projects' , [ ] ) ;
470+ projectsToSave . push ( project ) ;
471+ this . configStore . set ( 'projects' , projectsToSave ) ;
472+
473+ // Auto-install CLI if not already installed
474+ await this . ensureCliInstalled ( ) ;
475+
476+ // Start the installation in background (don't await)
477+ if ( config . installFresh || config . projectSource === 'clone' ) {
434478 this . runInstallation ( project , mainWindow ) . catch ( error => {
435479 this . managers . log ?. systemError ( 'Background installation failed' , { project : project . name , error : error . message } ) ;
436480 } ) ;
@@ -499,13 +543,29 @@ class ProjectManager {
499543 if ( await fs . pathExists ( project . path ) ) {
500544 const files = await fs . readdir ( project . path ) ;
501545 if ( files . length > 0 ) {
502- sendOutput ( `Warning: Directory ${ project . path } is not empty. Skipping Laravel installation.` , 'warning' ) ;
503- sendOutput ( 'If you want a fresh installation, please choose an empty directory.' , 'info' ) ;
504- project . installError = 'Directory not empty' ;
505- project . installing = false ;
506- this . updateProjectInStore ( project ) ;
507- sendOutput ( '' , 'complete' ) ; // Signal completion
508- return ;
546+ // Allow retrying if it looks like a partial/failed Laravel install:
547+ // A real Laravel project has artisan, composer.json, app/, etc.
548+ // A partial install might only have a public/ folder or similar stubs.
549+ const laravelIndicators = [ 'artisan' , 'composer.json' , 'app' , 'bootstrap' , 'config' ] ;
550+ const hasLaravelFiles = files . some ( f => laravelIndicators . includes ( f . toLowerCase ( ) ) ) ;
551+
552+ if ( hasLaravelFiles ) {
553+ sendOutput ( `Warning: Directory ${ project . path } already contains a Laravel project. Skipping installation.` , 'warning' ) ;
554+ sendOutput ( 'If you want a fresh installation, please choose an empty directory.' , 'info' ) ;
555+ project . installError = 'Directory not empty' ;
556+ project . installing = false ;
557+ this . updateProjectInStore ( project ) ;
558+ sendOutput ( '' , 'complete' ) ; // Signal completion
559+ return ;
560+ } else {
561+ // Partial/failed installation (e.g. only public/ was created). Clean it up.
562+ sendOutput ( `Cleaning up partial installation at ${ project . path } ...` , 'info' ) ;
563+ try {
564+ await fs . remove ( project . path ) ;
565+ } catch ( cleanErr ) {
566+ sendOutput ( `Warning: Could not clean up partial files: ${ cleanErr . message } ` , 'warning' ) ;
567+ }
568+ }
509569 }
510570 }
511571
0 commit comments