Skip to content

Commit 0d8ac79

Browse files
committed
chore: wip
1 parent 5ab5c64 commit 0d8ac79

File tree

7 files changed

+361
-57
lines changed

7 files changed

+361
-57
lines changed

packages/launchpad/src/dev/dump.ts

Lines changed: 184 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,48 @@ async function shouldUpdatePackage(project: string, currentVersion: string, cons
105105
}
106106
}
107107

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+
108150
/**
109151
* Enhanced constraint satisfaction check with update detection across multiple environments
110152
*/
@@ -190,25 +232,62 @@ async function checkConstraintSatisfaction(
190232
}
191233
}
192234

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) {
195237
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+
}
202253

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+
}
212291
}
213292
}
214293
}
@@ -296,11 +375,15 @@ async function isEnvironmentReady(
296375
return result
297376
}
298377

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
300386
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)
304387

305388
// Cache the result with shorter TTL for update responsiveness
306389
envReadinessCache.set(cacheKey, {
@@ -355,28 +438,13 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
355438
const hasLocalBinaries = fs.existsSync(envBinPath) && fs.readdirSync(envBinPath).length > 0
356439
const hasGlobalBinaries = fs.existsSync(globalBinPath) && fs.readdirSync(globalBinPath).length > 0
357440

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+
// }
380448
}
381449

382450
// Parse dependency file and separate global vs local dependencies
@@ -385,18 +453,19 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
385453

386454
try {
387455
sniffResult = await sniff({ string: projectDir })
388-
} catch (error) {
456+
}
457+
catch (error) {
389458
// Handle malformed dependency files gracefully
390459
if (config.verbose) {
391460
console.warn(`Failed to parse dependency file: ${error instanceof Error ? error.message : String(error)}`)
392461
}
393462
sniffResult = { pkgs: [], env: {} }
394463
}
395464

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
397466
const globalSniffResults: Array<{ pkgs: any[], env: Record<string, string> }> = []
398467

399-
if (!shellOutput && !skipGlobal) {
468+
if (!skipGlobal) {
400469
// Also check for global dependencies from well-known locations
401470
const globalDepLocations = [
402471
path.join(homedir(), '.dotfiles'),
@@ -599,21 +668,66 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
599668
if (dryrun) {
600669
if (!quiet && !shellOutput) {
601670
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'
603674
console.log(`Global packages: ${globalPackages.join(', ')} (${globalStatus})`)
604675
}
605676
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'
607680
console.log(`Local packages: ${localPackages.join(', ')} (${localStatus})`)
608681
}
609682
}
610683
return
611684
}
612685

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+
}
617731
}
618732

619733
const results: string[] = []
@@ -654,8 +768,14 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
654768
}
655769
}
656770
catch (error) {
657-
if (shellOutput) {
771+
if (shellOutput) {
658772
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+
}
659779
}
660780
else {
661781
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
719839
const errorMessage = error instanceof Error ? error.message : String(error)
720840
process.stderr.write(`❌ Failed to install local packages: ${errorMessage}\n`)
721841

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+
722849
// Provide helpful guidance for common issues
723850
if (errorMessage.includes('bun')) {
724851
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
750877
cacheSniffResult(projectHash, sniffResult)
751878

752879
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
753883
outputShellCode(dir, envBinPath, envSbinPath, projectHash, sniffResult, globalBinPath, globalSbinPath)
754884
}
755885
else if (!quiet) {
@@ -1132,7 +1262,7 @@ function outputShellCode(dir: string, envBinPath: string, envSbinPath: string, p
11321262

11331263
// Generate the deactivation function that the test expects
11341264
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`)
11361266
process.stdout.write(` case "$PWD" in\n`)
11371267
process.stdout.write(` "${dir}"*)\n`)
11381268
process.stdout.write(` # Still in project directory, don't deactivate\n`)

packages/launchpad/test/dev.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ describe('Dev Commands', () => {
645645

646646
it('should prioritize local environment over global and system', async () => {
647647
// Create a fake local environment
648+
// eslint-disable-next-line ts/no-require-imports
648649
const crypto = require('node:crypto')
649650
const projectHash = `${path.basename(tempDir)}_${crypto.createHash('md5').update(tempDir).digest('hex').slice(0, 8)}`
650651
const localEnvDir = path.join(os.homedir(), '.local', 'share', 'launchpad', projectHash)

0 commit comments

Comments
 (0)