@@ -82,7 +82,8 @@ export async function startService(serviceName: string): Promise<boolean> {
82
82
}
83
83
84
84
// In test mode, still validate service exists but mock the actual operation
85
- if ( process . env . NODE_ENV === 'test' || process . env . LAUNCHPAD_TEST_MODE === 'true' ) {
85
+ // Skip test mode for E2E validation tests
86
+ if ( ( process . env . NODE_ENV === 'test' || process . env . LAUNCHPAD_TEST_MODE === 'true' ) && ! process . env . LAUNCHPAD_E2E_TEST ) {
86
87
try {
87
88
const service = await getOrCreateServiceInstance ( serviceName )
88
89
console . warn ( `🧪 Test mode: Mocking start of service ${ serviceName } ` )
@@ -271,16 +272,18 @@ export async function startService(serviceName: string): Promise<boolean> {
271
272
void checkServiceHealth ( service )
272
273
} , 2000 )
273
274
274
- // Mark operation success
275
+ // Mark operation success and update service status
276
+ service . status = 'running'
277
+ service . lastCheckedAt = new Date ( )
275
278
operation . result = 'success'
276
279
operation . duration = 0
277
280
manager . operations . push ( operation )
278
281
return true
279
282
}
280
283
catch ( error ) {
281
- console . error ( `❌ Failed to start service ${ serviceName } : ${ error instanceof Error ? error . message : String ( error ) } ` )
284
+ console . error ( `❌ Failed to start service ${ serviceName } : ${ error } ` )
282
285
operation . result = 'failure'
283
- operation . error = error instanceof Error ? error . message : String ( error )
286
+ operation . error = error
284
287
operation . duration = 0
285
288
manager . operations . push ( operation )
286
289
return false
@@ -304,7 +307,8 @@ export async function stopService(serviceName: string): Promise<boolean> {
304
307
}
305
308
306
309
// In test mode, still validate and track operations
307
- if ( process . env . NODE_ENV === 'test' || process . env . LAUNCHPAD_TEST_MODE === 'true' ) {
310
+ // Skip test mode for E2E validation tests
311
+ if ( ( process . env . NODE_ENV === 'test' || process . env . LAUNCHPAD_TEST_MODE === 'true' ) && ! process . env . LAUNCHPAD_E2E_TEST ) {
308
312
const service = manager . services . get ( serviceName )
309
313
310
314
if ( ! service ) {
@@ -379,19 +383,19 @@ export async function stopService(serviceName: string): Promise<boolean> {
379
383
manager . operations . push ( operation )
380
384
381
385
console . error ( `❌ Failed to stop ${ serviceName } : ${ operation . error } ` )
382
- return false
386
+ return { success : false , error : 'Service stop failed' }
383
387
}
384
388
}
385
389
386
390
/**
387
391
* Restart a service
388
392
*/
389
- export async function restartService ( serviceName : string ) : Promise < boolean > {
393
+ export async function restartService ( serviceName : string ) : Promise < { success : boolean , error ?: string } > {
390
394
console . warn ( `🔄 Restarting ${ serviceName } ...` )
391
395
392
396
const stopSuccess = await stopService ( serviceName )
393
397
if ( ! stopSuccess ) {
394
- return false
398
+ return { success : false , error : 'Failed to stop service for restart' }
395
399
}
396
400
397
401
// Wait a moment before starting
@@ -436,7 +440,7 @@ export async function enableService(serviceName: string): Promise<boolean> {
436
440
operation . error = error instanceof Error ? error . message : String ( error )
437
441
operation . duration = 0
438
442
manager . operations . push ( operation )
439
- return false
443
+ return { success : false , error : 'Service stop failed' }
440
444
}
441
445
}
442
446
@@ -480,7 +484,7 @@ export async function enableService(serviceName: string): Promise<boolean> {
480
484
manager . operations . push ( operation )
481
485
482
486
console . error ( `❌ Failed to enable ${ serviceName } : ${ operation . error } ` )
483
- return false
487
+ return { success : false , error : 'Service stop failed' }
484
488
}
485
489
}
486
490
@@ -501,7 +505,8 @@ export async function disableService(serviceName: string): Promise<boolean> {
501
505
}
502
506
503
507
// In test mode, still validate and track operations
504
- if ( process . env . NODE_ENV === 'test' || process . env . LAUNCHPAD_TEST_MODE === 'true' ) {
508
+ // Skip test mode for E2E validation tests
509
+ if ( ( process . env . NODE_ENV === 'test' || process . env . LAUNCHPAD_TEST_MODE === 'true' ) && ! process . env . LAUNCHPAD_E2E_TEST ) {
505
510
const service = manager . services . get ( serviceName )
506
511
507
512
if ( ! service ) {
@@ -569,7 +574,7 @@ export async function disableService(serviceName: string): Promise<boolean> {
569
574
manager . operations . push ( operation )
570
575
571
576
console . error ( `❌ Failed to disable ${ serviceName } : ${ operation . error } ` )
572
- return false
577
+ return { success : false , error : 'Service stop failed' }
573
578
}
574
579
}
575
580
@@ -650,7 +655,7 @@ async function isServiceInitialized(service: ServiceInstance): Promise<boolean>
650
655
if ( definition ?. dataDirectory ) {
651
656
const dataDir = service . dataDir || definition . dataDirectory
652
657
if ( ! fs . existsSync ( dataDir ) ) {
653
- return false
658
+ return { success : false , error : 'Service stop failed' }
654
659
}
655
660
656
661
// For databases, check if data directory has initialization files
@@ -867,55 +872,54 @@ async function ensureServicePackageInstalled(service: ServiceInstance): Promise<
867
872
throw new Error ( `Invalid package domain for ${ definition . displayName } : ${ definition . packageDomain } ` )
868
873
}
869
874
870
- // Try multiple import strategies for the install function
875
+ // Import the install function with proper error handling
871
876
let install : any
872
877
try {
873
- // First try the main install module
874
- const installModule = await import ( '../install-main' )
875
- install = installModule . install
878
+ const { install : installFn } = await import ( '../install-main' )
879
+ install = installFn
876
880
if ( typeof install !== 'function' ) {
877
- throw new Error ( 'install function not found in install-main ' )
881
+ throw new Error ( 'install function not found or not a function ' )
878
882
}
879
883
} catch ( importError ) {
880
- try {
881
- // Fallback to the install index
882
- const installModule = await import ( '../install' )
883
- install = installModule . install
884
- if ( typeof install !== 'function' ) {
885
- throw new Error ( 'install function not found in install index' )
886
- }
887
- } catch ( fallbackError ) {
888
- console . error ( `❌ Failed to import install function from both modules:` )
889
- console . error ( ` - install-main: ${ importError instanceof Error ? importError . message : String ( importError ) } ` )
890
- console . error ( ` - install: ${ fallbackError instanceof Error ? fallbackError . message : String ( fallbackError ) } ` )
891
- return false
892
- }
884
+ console . error ( `❌ Failed to import install function: ${ importError instanceof Error ? importError . message : String ( importError ) } ` )
885
+ return { success : false , error : 'Service stop failed' }
893
886
}
894
887
895
888
// Install the main service package - this will automatically install all dependencies
896
- // thanks to our fixed dependency resolution
897
889
const installPath = `${ process . env . HOME } /.local`
898
890
899
- // Call install with proper error handling
891
+ // Call install with proper error handling and shorter timeout
900
892
try {
901
- await install ( [ definition . packageDomain ] , installPath )
893
+ if ( config . verbose ) {
894
+ console . warn ( `📦 Installing ${ definition . displayName } package (${ definition . packageDomain } )...` )
895
+ }
896
+
897
+ // Add shorter timeout to prevent hanging - 5 minutes should be enough
898
+ const installPromise = install ( [ definition . packageDomain ] , installPath )
899
+ const timeoutPromise = new Promise < boolean > ( ( _ , reject ) => {
900
+ setTimeout ( ( ) => reject ( new Error ( `Package installation timeout after 5 minutes` ) ) , 5 * 60 * 1000 )
901
+ } )
902
+
903
+ await Promise . race ( [ installPromise , timeoutPromise ] )
904
+
905
+ if ( config . verbose ) {
906
+ console . log ( `✅ ${ definition . displayName } package installed successfully` )
907
+ }
902
908
} catch ( installError ) {
903
- // If the install fails, provide detailed error information
904
- console . error ( `❌ Package installation failed for ${ definition . displayName } :` )
905
- console . error ( ` - Package domain: ${ definition . packageDomain } ` )
906
- console . error ( ` - Install path: ${ installPath } ` )
907
- console . error ( ` - Error: ${ installError instanceof Error ? installError . message : String ( installError ) } ` )
908
-
909
- if ( installError instanceof Error && installError . stack ) {
910
- console . error ( ` - Stack trace: ${ installError . stack } ` )
909
+ // If installation fails or times out, try to continue without the package
910
+ console . warn ( `⚠️ Package installation failed for ${ definition . displayName } , continuing without it` )
911
+ console . warn ( ` - Error: ${ installError instanceof Error ? installError . message : String ( installError ) } ` )
912
+
913
+ // Check if binary is already available in system PATH as fallback
914
+ const { findBinaryInPath } = await import ( '../utils' )
915
+ if ( findBinaryInPath ( definition . executable ) ) {
916
+ console . warn ( `✅ Found ${ definition . executable } in system PATH, using system version` )
917
+ return true
911
918
}
912
-
913
- return false
919
+
920
+ return { success : false , error : 'Service stop failed' }
914
921
}
915
922
916
- if ( config . verbose )
917
- console . log ( `✅ ${ definition . displayName } package installed successfully` )
918
-
919
923
// Verify installation worked by checking in the Launchpad environment
920
924
const binaryPath = findBinaryInEnvironment ( definition . executable , installPath )
921
925
if ( ! binaryPath ) {
@@ -926,7 +930,7 @@ async function ensureServicePackageInstalled(service: ServiceInstance): Promise<
926
930
}
927
931
catch ( error ) {
928
932
console . error ( `❌ Failed to install ${ definition . displayName } : ${ error instanceof Error ? error . message : String ( error ) } ` )
929
- return false
933
+ return { success : false , error : 'Service stop failed' }
930
934
}
931
935
}
932
936
@@ -969,7 +973,7 @@ async function ensurePHPDatabaseExtensions(_service: ServiceInstance): Promise<b
969
973
} )
970
974
971
975
if ( ! checkResult ) {
972
- return false
976
+ return { success : false , error : 'Service stop failed' }
973
977
}
974
978
975
979
const loadedExtensions = output . toLowerCase ( ) . split ( '\n' ) . map ( line => line . trim ( ) )
@@ -987,11 +991,11 @@ async function ensurePHPDatabaseExtensions(_service: ServiceInstance): Promise<b
987
991
if ( config . verbose )
988
992
console . warn ( `💡 Launchpad ships precompiled PHP binaries with common DB extensions. We'll select the correct binary for your project automatically.` )
989
993
// Do not attempt PECL here. Let binary-downloader pick the right PHP and shims load the project php.ini
990
- return false
994
+ return { success : false , error : 'Service stop failed' }
991
995
}
992
996
catch ( error ) {
993
997
console . error ( `❌ Failed to check PHP extensions: ${ error instanceof Error ? error . message : String ( error ) } ` )
994
- return false
998
+ return { success : false , error : 'Service stop failed' }
995
999
}
996
1000
}
997
1001
@@ -1091,7 +1095,7 @@ export async function setupSQLiteForProject(): Promise<boolean> {
1091
1095
}
1092
1096
catch ( error ) {
1093
1097
console . warn ( `⚠️ Could not set up SQLite automatically: ${ error instanceof Error ? error . message : String ( error ) } ` )
1094
- return false
1098
+ return { success : false , error : 'Service stop failed' }
1095
1099
}
1096
1100
}
1097
1101
@@ -1211,7 +1215,7 @@ async function autoInitializeDatabase(service: ServiceInstance): Promise<boolean
1211
1215
}
1212
1216
catch ( error ) {
1213
1217
console . error ( `❌ Failed to initialize PostgreSQL: ${ error instanceof Error ? error . message : String ( error ) } ` )
1214
- return false
1218
+ return { success : false , error : 'Service stop failed' }
1215
1219
}
1216
1220
}
1217
1221
@@ -1243,7 +1247,7 @@ async function autoInitializeDatabase(service: ServiceInstance): Promise<boolean
1243
1247
}
1244
1248
catch ( error ) {
1245
1249
console . error ( `❌ Failed to initialize MySQL: ${ error instanceof Error ? error . message : String ( error ) } ` )
1246
- return false
1250
+ return { success : false , error : 'Service stop failed' }
1247
1251
}
1248
1252
}
1249
1253
0 commit comments