@@ -2472,11 +2472,13 @@ cli
2472
2472
const installPrefix = install_prefix ( ) . string
2473
2473
2474
2474
// Helper function to get global dependencies from deps.yaml files
2475
- const getGlobalDependencies = async ( ) : Promise < Set < string > > => {
2475
+ const getGlobalDependencies = async ( ) : Promise < { globalDeps : Set < string > , explicitTrue : Set < string > , hadTopLevelGlobal : boolean } > => {
2476
2476
const globalDeps = new Set < string > ( )
2477
+ const explicitTrue = new Set < string > ( )
2478
+ let hadTopLevelGlobal = false
2477
2479
2478
2480
if ( ! options ?. keepGlobal ) {
2479
- return globalDeps
2481
+ return { globalDeps, explicitTrue , hadTopLevelGlobal }
2480
2482
}
2481
2483
2482
2484
// Common locations for global deps.yaml files
@@ -2501,89 +2503,114 @@ cli
2501
2503
const lines = content . split ( '\n' )
2502
2504
let topLevelGlobal = false
2503
2505
let inDependencies = false
2504
- let currentIndent = 0
2506
+ let depsIndent = - 1 // indent level of entries under dependencies
2507
+ let currentIndent = 0 // indent of the 'dependencies:' line
2508
+ const explicitFalse : Set < string > = new Set ( )
2505
2509
2506
- for ( const line of lines ) {
2510
+ for ( let idx = 0 ; idx < lines . length ; idx ++ ) {
2511
+ const line = lines [ idx ]
2507
2512
const trimmed = line . trim ( )
2513
+ const lineIndent = line . length - line . trimStart ( ) . length
2508
2514
2509
2515
// Skip empty lines and comments
2510
- if ( ! trimmed || trimmed . startsWith ( '#' ) ) {
2516
+ if ( ! trimmed || trimmed . startsWith ( '#' ) )
2511
2517
continue
2512
- }
2513
2518
2514
2519
// Check for top-level global flag
2515
- if ( trimmed . startsWith ( 'global:' ) ) {
2520
+ if ( lineIndent === 0 && trimmed . startsWith ( 'global:' ) ) {
2516
2521
const value = trimmed . split ( ':' ) [ 1 ] ?. trim ( )
2517
2522
topLevelGlobal = value === 'true' || value === 'yes'
2523
+ if ( topLevelGlobal )
2524
+ hadTopLevelGlobal = true
2518
2525
continue
2519
2526
}
2520
2527
2521
2528
// Check for dependencies section
2522
2529
if ( trimmed . startsWith ( 'dependencies:' ) ) {
2523
2530
inDependencies = true
2524
- currentIndent = line . length - line . trimStart ( ) . length
2531
+ currentIndent = lineIndent
2532
+ depsIndent = - 1
2525
2533
continue
2526
2534
}
2527
2535
2528
- // If we're in dependencies section
2529
- if ( inDependencies ) {
2530
- const lineIndent = line . length - line . trimStart ( ) . length
2536
+ if ( ! inDependencies )
2537
+ continue
2531
2538
2532
- // If we're back to the same or less indentation, we're out of dependencies
2533
- if ( lineIndent <= currentIndent && trimmed . length > 0 ) {
2534
- inDependencies = false
2535
- continue
2536
- }
2539
+ // If we're back to the same or less indentation, we're out of dependencies
2540
+ if ( lineIndent <= currentIndent && trimmed . length > 0 ) {
2541
+ inDependencies = false
2542
+ depsIndent = - 1
2543
+ continue
2544
+ }
2537
2545
2538
- // Parse dependency entry
2539
- if ( lineIndent > currentIndent && trimmed . includes ( ':' ) ) {
2540
- const depName = trimmed . split ( ':' ) [ 0 ] . trim ( )
2546
+ if ( ! trimmed . includes ( ':' ) )
2547
+ continue
2541
2548
2542
- if ( depName && ! depName . startsWith ( '#' ) ) {
2543
- // Check if this is a simple string value or object
2544
- const colonIndex = trimmed . indexOf ( ':' )
2545
- const afterColon = trimmed . substring ( colonIndex + 1 ) . trim ( )
2549
+ // Establish dependency entry indent on first child line
2550
+ if ( depsIndent === - 1 && lineIndent > currentIndent )
2551
+ depsIndent = lineIndent
2546
2552
2547
- if ( afterColon && ! afterColon . startsWith ( '{' ) && afterColon !== '' ) {
2548
- // Simple string format - use top-level global flag
2549
- if ( topLevelGlobal ) {
2550
- globalDeps . add ( depName )
2551
- }
2552
- }
2553
- else {
2554
- // Object format - need to check for individual global flag
2555
- // Look for the global flag in subsequent lines
2556
- let checkingForGlobal = true
2557
- let foundGlobal = false
2558
-
2559
- for ( let i = lines . indexOf ( line ) + 1 ; i < lines . length && checkingForGlobal ; i ++ ) {
2560
- const nextLine = lines [ i ]
2561
- const nextTrimmed = nextLine . trim ( )
2562
- const nextIndent = nextLine . length - nextLine . trimStart ( ) . length
2563
-
2564
- // If we're back to same or less indentation, stop looking
2565
- if ( nextIndent <= lineIndent && nextTrimmed . length > 0 ) {
2566
- checkingForGlobal = false
2567
- break
2568
- }
2569
-
2570
- // Check for global flag
2571
- if ( nextTrimmed . startsWith ( 'global:' ) ) {
2572
- const globalValue = nextTrimmed . split ( ':' ) [ 1 ] ?. trim ( )
2573
- foundGlobal = globalValue === 'true' || globalValue === 'yes'
2574
- checkingForGlobal = false
2575
- }
2576
- }
2553
+ // Only treat lines at the dependency-entry indent as dependency keys
2554
+ if ( lineIndent !== depsIndent )
2555
+ continue
2577
2556
2578
- // If we found an explicit global flag, use it; otherwise use top-level
2579
- if ( foundGlobal || ( topLevelGlobal && ! foundGlobal ) ) {
2580
- globalDeps . add ( depName )
2581
- }
2582
- }
2557
+ const depName = trimmed . split ( ':' ) [ 0 ] . trim ( )
2558
+ // Only consider plausible package domains (contain '.' or '/') and skip common keys
2559
+ if ( ! depName || depName === 'version' || depName === 'global' || ( ! depName . includes ( '.' ) && ! depName . includes ( '/' ) ) )
2560
+ continue
2561
+
2562
+ const colonIndex = trimmed . indexOf ( ':' )
2563
+ const afterColon = trimmed . substring ( colonIndex + 1 ) . trim ( )
2564
+
2565
+ if ( afterColon && ! afterColon . startsWith ( '{' ) && afterColon !== '' ) {
2566
+ // Simple string format - use top-level global flag only
2567
+ if ( topLevelGlobal )
2568
+ globalDeps . add ( depName )
2569
+ }
2570
+ else {
2571
+ // Object format - look ahead for an explicit global flag within this entry
2572
+ let foundGlobal = false
2573
+ for ( let j = idx + 1 ; j < lines . length ; j ++ ) {
2574
+ const nextLine = lines [ j ]
2575
+ const nextTrimmed = nextLine . trim ( )
2576
+ const nextIndent = nextLine . length - nextLine . trimStart ( ) . length
2577
+ // Stop if out of this entry block
2578
+ if ( nextIndent <= depsIndent && nextTrimmed . length > 0 )
2579
+ break
2580
+ if ( nextTrimmed . startsWith ( 'global:' ) ) {
2581
+ const globalValue = nextTrimmed . split ( ':' ) [ 1 ] ?. trim ( )
2582
+ foundGlobal = globalValue === 'true' || globalValue === 'yes'
2583
+ if ( globalValue === 'false' || globalValue === 'no' )
2584
+ explicitFalse . add ( depName )
2585
+ break
2583
2586
}
2584
2587
}
2588
+ if ( foundGlobal ) {
2589
+ explicitTrue . add ( depName )
2590
+ globalDeps . add ( depName )
2591
+ }
2592
+ else if ( topLevelGlobal ) {
2593
+ globalDeps . add ( depName )
2594
+ }
2585
2595
}
2586
2596
}
2597
+
2598
+ // Remove any explicitly false packages from both sets
2599
+ for ( const pkg of explicitFalse ) {
2600
+ globalDeps . delete ( pkg )
2601
+ explicitTrue . delete ( pkg )
2602
+ }
2603
+
2604
+ // Final safety: remove non-domain keys accidentally parsed
2605
+ const isDomainLike = ( s : string ) => s . includes ( '.' ) || s . includes ( '/' )
2606
+ for ( const pkg of Array . from ( globalDeps ) ) {
2607
+ if ( ! isDomainLike ( pkg ) )
2608
+ globalDeps . delete ( pkg )
2609
+ }
2610
+ for ( const pkg of Array . from ( explicitTrue ) ) {
2611
+ if ( ! isDomainLike ( pkg ) )
2612
+ explicitTrue . delete ( pkg )
2613
+ }
2587
2614
}
2588
2615
2589
2616
parseSimpleYaml ( content )
@@ -2596,11 +2623,60 @@ cli
2596
2623
}
2597
2624
}
2598
2625
2599
- return globalDeps
2626
+ return { globalDeps, explicitTrue , hadTopLevelGlobal }
2600
2627
}
2601
2628
2602
2629
// Get global dependencies
2603
- const globalDeps = await getGlobalDependencies ( )
2630
+ const { globalDeps, explicitTrue, hadTopLevelGlobal } = await getGlobalDependencies ( )
2631
+
2632
+ // Determine Launchpad-managed services to stop (respecting --keep-global)
2633
+ const {
2634
+ getAllServiceDefinitions,
2635
+ getServiceStatus,
2636
+ stopService,
2637
+ disableService,
2638
+ removeServiceFile,
2639
+ getServiceFilePath,
2640
+ } = await import ( '../src/services' )
2641
+
2642
+ const serviceDefs = getAllServiceDefinitions ( )
2643
+ const candidateServices = serviceDefs . filter ( ( def ) => {
2644
+ if ( options ?. keepGlobal && def . packageDomain ) {
2645
+ return ! globalDeps . has ( def . packageDomain )
2646
+ }
2647
+ return true
2648
+ } )
2649
+
2650
+ const runningServices : string [ ] = [ ]
2651
+ for ( const def of candidateServices ) {
2652
+ if ( ! def . name )
2653
+ continue
2654
+ let shouldInclude = false
2655
+ try {
2656
+ const status = await getServiceStatus ( def . name )
2657
+ if ( status !== 'stopped' )
2658
+ shouldInclude = true
2659
+ }
2660
+ catch { }
2661
+
2662
+ // Include if a service file exists
2663
+ try {
2664
+ const serviceFile = getServiceFilePath ( def . name )
2665
+ if ( serviceFile && fs . existsSync ( serviceFile ) )
2666
+ shouldInclude = true
2667
+ }
2668
+ catch { }
2669
+
2670
+ // Include if a data directory exists
2671
+ try {
2672
+ if ( def . dataDirectory && fs . existsSync ( def . dataDirectory ) )
2673
+ shouldInclude = true
2674
+ }
2675
+ catch { }
2676
+
2677
+ if ( shouldInclude )
2678
+ runningServices . push ( def . name )
2679
+ }
2604
2680
2605
2681
// Helper function to get all Launchpad-managed binaries from package metadata
2606
2682
const getLaunchpadBinaries = ( ) : Array < { binary : string , package : string , fullPath : string } > => {
@@ -2863,11 +2939,21 @@ cli
2863
2939
} )
2864
2940
}
2865
2941
2942
+ // Show services that would be stopped
2943
+ if ( runningServices . length > 0 ) {
2944
+ console . log ( '' )
2945
+ console . log ( '🛑 Services that would be stopped:' )
2946
+ runningServices . forEach ( ( name ) => {
2947
+ console . log ( ` • ${ name } ` )
2948
+ } )
2949
+ }
2950
+
2866
2951
// Show preserved global dependencies
2867
- if ( options ?. keepGlobal && globalDeps . size > 0 ) {
2952
+ if ( options ?. keepGlobal && ( explicitTrue . size > 0 || hadTopLevelGlobal ) ) {
2868
2953
console . log ( '' )
2869
2954
console . log ( '✅ Global dependencies that would be preserved:' )
2870
- Array . from ( globalDeps ) . sort ( ) . forEach ( ( dep ) => {
2955
+ const toPrint = explicitTrue . size > 0 ? explicitTrue : globalDeps
2956
+ Array . from ( toPrint ) . sort ( ) . forEach ( ( dep ) => {
2871
2957
console . log ( ` • ${ dep } ` )
2872
2958
} )
2873
2959
}
@@ -2879,7 +2965,26 @@ cli
2879
2965
}
2880
2966
2881
2967
// Actually perform cleanup
2882
- if ( existingDirs . length > 0 || launchpadBinaries . length > 0 ) {
2968
+ if ( existingDirs . length > 0 || launchpadBinaries . length > 0 || runningServices . length > 0 ) {
2969
+ // Stop Launchpad-managed services first so files can be removed cleanly
2970
+ if ( runningServices . length > 0 ) {
2971
+ console . log ( `🛑 Stopping ${ runningServices . length } Launchpad service(s)...` )
2972
+ for ( const name of runningServices ) {
2973
+ try {
2974
+ await stopService ( name )
2975
+ }
2976
+ catch { }
2977
+ try {
2978
+ await disableService ( name )
2979
+ }
2980
+ catch { }
2981
+ try {
2982
+ await removeServiceFile ( name )
2983
+ }
2984
+ catch { }
2985
+ }
2986
+ }
2987
+
2883
2988
console . log ( `📊 Cleaning ${ formatSize ( totalSize ) } of data (${ totalFiles } files)...` )
2884
2989
console . log ( '' )
2885
2990
@@ -2933,10 +3038,11 @@ cli
2933
3038
console . log ( '💡 Cache was preserved. Use `launchpad cache:clear` to remove cached downloads.' )
2934
3039
}
2935
3040
2936
- if ( options ?. keepGlobal && globalDeps . size > 0 ) {
3041
+ if ( options ?. keepGlobal && ( explicitTrue . size > 0 || hadTopLevelGlobal ) ) {
2937
3042
console . log ( '' )
2938
3043
console . log ( '✅ Global dependencies were preserved:' )
2939
- Array . from ( globalDeps ) . sort ( ) . forEach ( ( dep ) => {
3044
+ const toPrint = explicitTrue . size > 0 ? explicitTrue : globalDeps
3045
+ Array . from ( toPrint ) . sort ( ) . forEach ( ( dep ) => {
2940
3046
console . log ( ` • ${ dep } ` )
2941
3047
} )
2942
3048
}
0 commit comments