@@ -105,6 +105,48 @@ async function shouldUpdatePackage(project: string, currentVersion: string, cons
105
105
}
106
106
}
107
107
108
+ /**
109
+ * Check if packages are satisfied specifically within a single environment directory
110
+ * (doesn't check system binaries or other environments)
111
+ */
112
+ async function checkEnvironmentSpecificSatisfaction (
113
+ envDir : string ,
114
+ packages : Array < { project : string , constraint : string } > ,
115
+ ) : Promise < boolean > {
116
+ if ( ! fs . existsSync ( envDir ) || packages . length === 0 ) {
117
+ return packages . length === 0 // True if no packages required, false if env doesn't exist
118
+ }
119
+
120
+ try {
121
+ const { list } = await import ( '../list' )
122
+ const installedPackages = await list ( envDir )
123
+
124
+ for ( const requiredPkg of packages ) {
125
+ const { project, constraint } = requiredPkg
126
+
127
+ const installedPkg = installedPackages . find ( pkg =>
128
+ pkg . project === project || pkg . project . includes ( project . split ( '.' ) [ 0 ] ) ,
129
+ )
130
+
131
+ if ( ! installedPkg ) {
132
+ return false // Package not found in this environment
133
+ }
134
+
135
+ const installedVersion = installedPkg . version . toString ( )
136
+ const satisfiesConstraint = await checkVersionSatisfiesConstraint ( installedVersion , constraint )
137
+
138
+ if ( ! satisfiesConstraint ) {
139
+ return false // Package exists but doesn't satisfy constraint
140
+ }
141
+ }
142
+
143
+ return true // All packages found and satisfy constraints
144
+ }
145
+ catch {
146
+ return false
147
+ }
148
+ }
149
+
108
150
/**
109
151
* Enhanced constraint satisfaction check with update detection across multiple environments
110
152
*/
@@ -190,25 +232,62 @@ async function checkConstraintSatisfaction(
190
232
}
191
233
}
192
234
193
- // Check system PATH for special cases like bun and bash
194
- if ( ! satisfied && ( project === 'bun.sh' || project . includes ( 'bash' ) ) ) {
235
+ // Check system PATH for any package by trying common binary names
236
+ if ( ! satisfied ) {
195
237
try {
196
- const command = project === 'bun.sh' ? 'bun' : 'bash'
197
- const result = spawnSync ( command , [ '--version' ] , { encoding : 'utf8' , timeout : 5000 } )
198
- if ( result . status === 0 && result . stdout ) {
199
- const systemVersion = result . stdout . trim ( )
200
- const satisfiesConstraint = await checkVersionSatisfiesConstraint ( systemVersion , constraint )
201
- const shouldUpdate = await shouldUpdatePackage ( project , systemVersion , constraint )
238
+ // Extract potential binary names from the project domain
239
+ const potentialCommands = [ ]
240
+
241
+ // Handle common patterns: domain.com/package -> package, domain.sh -> domain
242
+ if ( project . includes ( '/' ) ) {
243
+ const parts = project . split ( '/' )
244
+ potentialCommands . push ( parts [ parts . length - 1 ] ) // last part (package name)
245
+ potentialCommands . push ( parts [ 0 ] . split ( '.' ) [ 0 ] ) // first part without TLD
246
+ }
247
+ else if ( project . includes ( '.' ) ) {
248
+ potentialCommands . push ( project . split ( '.' ) [ 0 ] ) // remove TLD
249
+ }
250
+ else {
251
+ potentialCommands . push ( project ) // use as-is
252
+ }
202
253
203
- if ( satisfiesConstraint && ! shouldUpdate ) {
204
- satisfied = true
205
- foundVersion = systemVersion
206
- foundSource = 'system'
207
- }
208
- else if ( satisfiesConstraint && shouldUpdate ) {
209
- needsUpdate = true
210
- foundVersion = systemVersion
211
- foundSource = 'system'
254
+ // Common mappings for well-known packages
255
+ const commonMappings : Record < string , string [ ] > = {
256
+ 'bun.sh' : [ 'bun' ] ,
257
+ 'nodejs.org' : [ 'node' ] ,
258
+ 'python.org' : [ 'python' , 'python3' ] ,
259
+ 'go.dev' : [ 'go' ] ,
260
+ 'rust-lang.org' : [ 'rustc' , 'cargo' ] ,
261
+ 'deno.com' : [ 'deno' ] ,
262
+ 'git-scm.com' : [ 'git' ] ,
263
+ 'docker.com' : [ 'docker' ] ,
264
+ 'kubernetes.io' : [ 'kubectl' , 'kubelet' ] ,
265
+ }
266
+
267
+ if ( commonMappings [ project ] ) {
268
+ potentialCommands . unshift ( ...commonMappings [ project ] )
269
+ }
270
+
271
+ // Try each potential command
272
+ for ( const command of potentialCommands ) {
273
+ const result = spawnSync ( command , [ '--version' ] , { encoding : 'utf8' , timeout : 5000 } )
274
+ if ( result . status === 0 && result . stdout ) {
275
+ const systemVersion = result . stdout . trim ( )
276
+ // Extract version number from output (handle various formats)
277
+ const versionMatch = systemVersion . match ( / ( \d + \. \d + (?: \. \d + ) ? (?: - [ \w . - ] + ) ? ) / )
278
+ if ( versionMatch ) {
279
+ const cleanVersion = versionMatch [ 1 ]
280
+ const satisfiesConstraint = await checkVersionSatisfiesConstraint ( cleanVersion , constraint )
281
+
282
+ // For system binaries, if they satisfy the constraint, we consider them satisfied
283
+ // Don't check for updates since we can't control system installations
284
+ if ( satisfiesConstraint ) {
285
+ satisfied = true
286
+ foundVersion = cleanVersion
287
+ foundSource = 'system'
288
+ break
289
+ }
290
+ }
212
291
}
213
292
}
214
293
}
@@ -296,11 +375,15 @@ async function isEnvironmentReady(
296
375
return result
297
376
}
298
377
299
- // Enhanced check: validate constraints
378
+ // Enhanced check: validate that required packages are actually installed in this environment
379
+ // Don't just check if constraints are satisfied by any source (including system binaries)
380
+ const localSatisfactionCheck = await checkEnvironmentSpecificSatisfaction ( envDir , packages )
381
+
382
+ // Environment is ready only if the specific environment directory has the required packages
383
+ const ready = localSatisfactionCheck || ( hasBinaries && packages . length === 0 )
384
+
385
+ // Also get full constraint check for error reporting
300
386
const constraintCheck = await checkConstraintSatisfaction ( envDir , packages , envType )
301
- // Environment is ready if constraints are satisfied AND no packages are outdated
302
- // OR if local binaries exist and no constraints specified
303
- const ready = constraintCheck . satisfied || ( hasBinaries && packages . length === 0 )
304
387
305
388
// Cache the result with shorter TTL for update responsiveness
306
389
envReadinessCache . set ( cacheKey , {
@@ -355,28 +438,13 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
355
438
const hasLocalBinaries = fs . existsSync ( envBinPath ) && fs . readdirSync ( envBinPath ) . length > 0
356
439
const hasGlobalBinaries = fs . existsSync ( globalBinPath ) && fs . readdirSync ( globalBinPath ) . length > 0
357
440
358
- // If environments have binaries, use fast path with minimal parsing
359
- if ( hasLocalBinaries || hasGlobalBinaries ) {
360
- // For fast path, still need to parse the dependency file for environment variables
361
- // but avoid the heavy sniff module for package resolution
362
- const minimalSniffResult = { pkgs : [ ] , env : { } }
363
-
364
- try {
365
- const depContent = fs . readFileSync ( dependencyFile , 'utf-8' )
366
- const parsed = parse ( depContent )
367
-
368
- // Extract environment variables if they exist
369
- if ( parsed && typeof parsed === 'object' && 'env' in parsed ) {
370
- minimalSniffResult . env = parsed . env || { }
371
- }
372
- }
373
- catch {
374
- // If parsing fails, use empty env
375
- }
376
-
377
- outputShellCode ( dir , envBinPath , envSbinPath , fastProjectHash , minimalSniffResult , globalBinPath , globalSbinPath )
378
- return
379
- }
441
+ // Fast path disabled - always do proper constraint checking to ensure correct versions
442
+ // The fast path was causing issues where global binaries would activate environments
443
+ // even when local packages weren't properly installed
444
+ // if (hasLocalBinaries || hasGlobalBinaries) {
445
+ // outputShellCode(dir, envBinPath, envSbinPath, fastProjectHash, minimalSniffResult, globalBinPath, globalSbinPath)
446
+ // return
447
+ // }
380
448
}
381
449
382
450
// Parse dependency file and separate global vs local dependencies
@@ -385,18 +453,19 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
385
453
386
454
try {
387
455
sniffResult = await sniff ( { string : projectDir } )
388
- } catch ( error ) {
456
+ }
457
+ catch ( error ) {
389
458
// Handle malformed dependency files gracefully
390
459
if ( config . verbose ) {
391
460
console . warn ( `Failed to parse dependency file: ${ error instanceof Error ? error . message : String ( error ) } ` )
392
461
}
393
462
sniffResult = { pkgs : [ ] , env : { } }
394
463
}
395
464
396
- // Only check for global dependencies when not in shell mode or when global env doesn't exist, and not skipping global
465
+ // Always check for global dependencies when not skipping global
397
466
const globalSniffResults : Array < { pkgs : any [ ] , env : Record < string , string > } > = [ ]
398
467
399
- if ( ! shellOutput && ! skipGlobal ) {
468
+ if ( ! skipGlobal ) {
400
469
// Also check for global dependencies from well-known locations
401
470
const globalDepLocations = [
402
471
path . join ( homedir ( ) , '.dotfiles' ) ,
@@ -599,21 +668,66 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
599
668
if ( dryrun ) {
600
669
if ( ! quiet && ! shellOutput ) {
601
670
if ( globalPackages . length > 0 ) {
602
- const globalStatus = globalReady ? 'satisfied by existing installations' : 'would install globally'
671
+ // Check if constraints are satisfied (either by installed packages or system binaries)
672
+ const globalConstraintsSatisfied = globalReadyResult . missingPackages ?. length === 0
673
+ const globalStatus = ( globalReady || globalConstraintsSatisfied ) ? 'satisfied by existing installations' : 'would install globally'
603
674
console . log ( `Global packages: ${ globalPackages . join ( ', ' ) } (${ globalStatus } )` )
604
675
}
605
676
if ( localPackages . length > 0 ) {
606
- const localStatus = localReady ? 'satisfied by existing installations' : 'would install locally'
677
+ // Check if constraints are satisfied (either by installed packages or system binaries)
678
+ const localConstraintsSatisfied = localReadyResult . missingPackages ?. length === 0
679
+ const localStatus = ( localReady || localConstraintsSatisfied ) ? 'satisfied by existing installations' : 'would install locally'
607
680
console . log ( `Local packages: ${ localPackages . join ( ', ' ) } (${ localStatus } )` )
608
681
}
609
682
}
610
683
return
611
684
}
612
685
613
- // For shell output mode with ready environments, output immediately
614
- if ( shellOutput && localReady && globalReady ) {
615
- outputShellCode ( dir , envBinPath , envSbinPath , projectHash , sniffResult , globalBinPath , globalSbinPath )
616
- return
686
+ // For shell output mode, handle different scenarios
687
+ if ( shellOutput ) {
688
+ const hasLocalPackagesInstalled = localReady || localPackages . length === 0
689
+ const hasGlobalPackagesInstalled = globalReady || globalPackages . length === 0
690
+
691
+ // Check if we have any constraint satisfaction by system binaries
692
+ const localConstraintsSatisfied = localReadyResult . missingPackages ?. length === 0
693
+ const globalConstraintsSatisfied = globalReadyResult . missingPackages ?. length === 0
694
+
695
+ // Also check if core dependencies (required for basic functionality) are satisfied
696
+ // even if some optional global packages are missing
697
+ const coreLocalSatisfied = localConstraintsSatisfied || localPackages . length === 0
698
+ const hasOptionalGlobalMissing = globalReadyResult . missingPackages && globalReadyResult . missingPackages . length > 0
699
+ const coreGlobalSatisfied = globalConstraintsSatisfied || ( hasOptionalGlobalMissing && globalPackages . length > 0 )
700
+
701
+ if ( hasLocalPackagesInstalled && hasGlobalPackagesInstalled ) {
702
+ // Ideal case: all packages properly installed
703
+ outputShellCode ( dir , envBinPath , envSbinPath , projectHash , sniffResult , globalBinPath , globalSbinPath )
704
+ return
705
+ }
706
+ else if ( coreLocalSatisfied && coreGlobalSatisfied ) {
707
+ // Fallback case: core constraints satisfied by system binaries, but warn user
708
+ if ( ! hasLocalPackagesInstalled && localPackages . length > 0 ) {
709
+ process . stderr . write ( `⚠️ Local packages not installed but constraints satisfied by system binaries\n` )
710
+ process . stderr . write ( `💡 Run 'launchpad dev .' to install proper versions: ${ localPackages . join ( ', ' ) } \n` )
711
+ }
712
+ if ( ! hasGlobalPackagesInstalled && hasOptionalGlobalMissing ) {
713
+ const missingGlobalPkgs = globalReadyResult . missingPackages ?. map ( p => p . project ) || [ ]
714
+ process . stderr . write ( `⚠️ Some global packages not available: ${ missingGlobalPkgs . join ( ', ' ) } \n` )
715
+ process . stderr . write ( `💡 Install missing global packages if needed\n` )
716
+ }
717
+ outputShellCode ( dir , envBinPath , envSbinPath , projectHash , sniffResult , globalBinPath , globalSbinPath )
718
+ return
719
+ }
720
+ else {
721
+ // No fallback available - require installation
722
+ process . stderr . write ( `❌ Environment not ready: local=${ localReady } , global=${ globalReady } \n` )
723
+ if ( ! localReady && localPackages . length > 0 ) {
724
+ process . stderr . write ( `💡 Local packages need installation: ${ localPackages . join ( ', ' ) } \n` )
725
+ }
726
+ if ( ! globalReady && globalPackages . length > 0 ) {
727
+ process . stderr . write ( `💡 Global packages need installation: ${ globalPackages . join ( ', ' ) } \n` )
728
+ }
729
+ return
730
+ }
617
731
}
618
732
619
733
const results : string [ ] = [ ]
@@ -654,8 +768,14 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
654
768
}
655
769
}
656
770
catch ( error ) {
657
- if ( shellOutput ) {
771
+ if ( shellOutput ) {
658
772
process . stderr . write ( `❌ Failed to install global packages: ${ error instanceof Error ? error . message : String ( error ) } \n` )
773
+
774
+ // Don't mislead users about system binary usage
775
+ const constraintsSatisfiedBySystem = globalReadyResult . missingPackages ?. length === 0
776
+ if ( constraintsSatisfiedBySystem ) {
777
+ process . stderr . write ( `⚠️ System binaries may satisfy global constraints but requested packages failed to install\n` )
778
+ }
659
779
}
660
780
else {
661
781
console . error ( `Failed to install global packages: ${ error instanceof Error ? error . message : String ( error ) } ` )
@@ -719,6 +839,13 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
719
839
const errorMessage = error instanceof Error ? error . message : String ( error )
720
840
process . stderr . write ( `❌ Failed to install local packages: ${ errorMessage } \n` )
721
841
842
+ // Don't mislead users by saying we're using system binaries when they want specific versions
843
+ const constraintsSatisfiedBySystem = localReadyResult . missingPackages ?. length === 0
844
+ if ( constraintsSatisfiedBySystem ) {
845
+ process . stderr . write ( `⚠️ System binaries may satisfy version constraints but requested packages failed to install\n` )
846
+ process . stderr . write ( `💡 Consider resolving installation issues for consistent environments\n` )
847
+ }
848
+
722
849
// Provide helpful guidance for common issues
723
850
if ( errorMessage . includes ( 'bun' ) ) {
724
851
process . stderr . write ( `💡 Tip: Install bun manually with: curl -fsSL https://bun.sh/install | bash\n` )
@@ -750,6 +877,9 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
750
877
cacheSniffResult ( projectHash , sniffResult )
751
878
752
879
if ( shellOutput ) {
880
+ // Always output shell code in shell output mode
881
+ // This ensures environment activation even when installations partially fail
882
+ // but system binaries satisfy constraints
753
883
outputShellCode ( dir , envBinPath , envSbinPath , projectHash , sniffResult , globalBinPath , globalSbinPath )
754
884
}
755
885
else if ( ! quiet ) {
@@ -1132,7 +1262,7 @@ function outputShellCode(dir: string, envBinPath: string, envSbinPath: string, p
1132
1262
1133
1263
// Generate the deactivation function that the test expects
1134
1264
process . stdout . write ( `\n# Deactivation function for directory checking\n` )
1135
- process . stdout . write ( `_pkgx_dev_try_bye () {\n` )
1265
+ process . stdout . write ( `_launchpad_dev_try_bye () {\n` )
1136
1266
process . stdout . write ( ` case "$PWD" in\n` )
1137
1267
process . stdout . write ( ` "${ dir } "*)\n` )
1138
1268
process . stdout . write ( ` # Still in project directory, don't deactivate\n` )
0 commit comments