Skip to content

Commit 5298bb7

Browse files
committed
chore: wip
1 parent 6ef193f commit 5298bb7

File tree

4 files changed

+316
-78
lines changed

4 files changed

+316
-78
lines changed

packages/launchpad/bin/cli.ts

Lines changed: 171 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2472,11 +2472,13 @@ cli
24722472
const installPrefix = install_prefix().string
24732473

24742474
// 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 }> => {
24762476
const globalDeps = new Set<string>()
2477+
const explicitTrue = new Set<string>()
2478+
let hadTopLevelGlobal = false
24772479

24782480
if (!options?.keepGlobal) {
2479-
return globalDeps
2481+
return { globalDeps, explicitTrue, hadTopLevelGlobal }
24802482
}
24812483

24822484
// Common locations for global deps.yaml files
@@ -2501,89 +2503,114 @@ cli
25012503
const lines = content.split('\n')
25022504
let topLevelGlobal = false
25032505
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()
25052509

2506-
for (const line of lines) {
2510+
for (let idx = 0; idx < lines.length; idx++) {
2511+
const line = lines[idx]
25072512
const trimmed = line.trim()
2513+
const lineIndent = line.length - line.trimStart().length
25082514

25092515
// Skip empty lines and comments
2510-
if (!trimmed || trimmed.startsWith('#')) {
2516+
if (!trimmed || trimmed.startsWith('#'))
25112517
continue
2512-
}
25132518

25142519
// Check for top-level global flag
2515-
if (trimmed.startsWith('global:')) {
2520+
if (lineIndent === 0 && trimmed.startsWith('global:')) {
25162521
const value = trimmed.split(':')[1]?.trim()
25172522
topLevelGlobal = value === 'true' || value === 'yes'
2523+
if (topLevelGlobal)
2524+
hadTopLevelGlobal = true
25182525
continue
25192526
}
25202527

25212528
// Check for dependencies section
25222529
if (trimmed.startsWith('dependencies:')) {
25232530
inDependencies = true
2524-
currentIndent = line.length - line.trimStart().length
2531+
currentIndent = lineIndent
2532+
depsIndent = -1
25252533
continue
25262534
}
25272535

2528-
// If we're in dependencies section
2529-
if (inDependencies) {
2530-
const lineIndent = line.length - line.trimStart().length
2536+
if (!inDependencies)
2537+
continue
25312538

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+
}
25372545

2538-
// Parse dependency entry
2539-
if (lineIndent > currentIndent && trimmed.includes(':')) {
2540-
const depName = trimmed.split(':')[0].trim()
2546+
if (!trimmed.includes(':'))
2547+
continue
25412548

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
25462552

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
25772556

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
25832586
}
25842587
}
2588+
if (foundGlobal) {
2589+
explicitTrue.add(depName)
2590+
globalDeps.add(depName)
2591+
}
2592+
else if (topLevelGlobal) {
2593+
globalDeps.add(depName)
2594+
}
25852595
}
25862596
}
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+
}
25872614
}
25882615

25892616
parseSimpleYaml(content)
@@ -2596,11 +2623,60 @@ cli
25962623
}
25972624
}
25982625

2599-
return globalDeps
2626+
return { globalDeps, explicitTrue, hadTopLevelGlobal }
26002627
}
26012628

26022629
// 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+
}
26042680

26052681
// Helper function to get all Launchpad-managed binaries from package metadata
26062682
const getLaunchpadBinaries = (): Array<{ binary: string, package: string, fullPath: string }> => {
@@ -2863,11 +2939,21 @@ cli
28632939
})
28642940
}
28652941

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+
28662951
// Show preserved global dependencies
2867-
if (options?.keepGlobal && globalDeps.size > 0) {
2952+
if (options?.keepGlobal && (explicitTrue.size > 0 || hadTopLevelGlobal)) {
28682953
console.log('')
28692954
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) => {
28712957
console.log(` • ${dep}`)
28722958
})
28732959
}
@@ -2879,7 +2965,26 @@ cli
28792965
}
28802966

28812967
// 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+
28832988
console.log(`📊 Cleaning ${formatSize(totalSize)} of data (${totalFiles} files)...`)
28842989
console.log('')
28852990

@@ -2933,10 +3038,11 @@ cli
29333038
console.log('💡 Cache was preserved. Use `launchpad cache:clear` to remove cached downloads.')
29343039
}
29353040

2936-
if (options?.keepGlobal && globalDeps.size > 0) {
3041+
if (options?.keepGlobal && (explicitTrue.size > 0 || hadTopLevelGlobal)) {
29373042
console.log('')
29383043
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) => {
29403046
console.log(` • ${dep}`)
29413047
})
29423048
}

packages/launchpad/src/dev/dump.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
821821
await ensureProjectPhpIni(projectDir, envDir)
822822
try {
823823
await setupProjectServices(projectDir, sniffResult, true)
824-
// Also run post-setup once in shell fast path (idempotent marker)
824+
// Run project-configured post-setup once in shell fast path (idempotent)
825825
await maybeRunProjectPostSetup(projectDir, envDir, true)
826826
}
827827
catch {}
@@ -1016,7 +1016,7 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
10161016
// Ensure project php.ini exists only
10171017
await ensureProjectPhpIni(projectDir, envDir)
10181018

1019-
// Run project-level post-setup once (idempotent marker)
1019+
// Run project-configured post-setup once (idempotent marker)
10201020
try {
10211021
await maybeRunProjectPostSetup(projectDir, envDir, true)
10221022
}
@@ -1578,11 +1578,6 @@ async function createPhpShimsAfterInstall(envDir: string): Promise<void> {
15781578
*/
15791579
async function maybeRunProjectPostSetup(projectDir: string, envDir: string, _isShellIntegration: boolean): Promise<void> {
15801580
try {
1581-
const projectPostSetup = config.postSetup
1582-
if (!projectPostSetup?.enabled) {
1583-
return
1584-
}
1585-
15861581
// Use a marker file inside env to avoid re-running on every prompt
15871582
const markerDir = path.join(envDir, 'pkgs')
15881583
const markerFile = path.join(markerDir, '.post_setup_done')
@@ -1597,9 +1592,37 @@ async function maybeRunProjectPostSetup(projectDir: string, envDir: string, _isS
15971592
}
15981593
catch {}
15991594

1595+
// Aggregate post-setup commands from both runtime config and project-local config
1596+
const commands: PostSetupCommand[] = []
1597+
1598+
// 1) Runtime config (global/app config)
1599+
const projectPostSetup = config.postSetup
1600+
if (projectPostSetup?.enabled && Array.isArray(projectPostSetup.commands)) {
1601+
commands.push(...projectPostSetup.commands)
1602+
}
1603+
1604+
// 2) Project-local launchpad.config.ts (if present)
1605+
try {
1606+
const configPathTs = path.join(projectDir, 'launchpad.config.ts')
1607+
if (fs.existsSync(configPathTs)) {
1608+
const mod = await import(configPathTs)
1609+
const local = mod.default || mod
1610+
if (local?.postSetup?.enabled && Array.isArray(local.postSetup.commands)) {
1611+
commands.push(...local.postSetup.commands)
1612+
}
1613+
}
1614+
}
1615+
catch {
1616+
// Ignore import errors; absence or parse errors should not fail setup
1617+
}
1618+
1619+
if (commands.length === 0) {
1620+
return
1621+
}
1622+
16001623
// Execute and mark
1601-
const results = await executepostSetup(projectDir, projectPostSetup.commands || [])
1602-
if ((results && results.length > 0) || true) {
1624+
const results = await executepostSetup(projectDir, commands)
1625+
if (results && results.length > 0) {
16031626
try {
16041627
fs.writeFileSync(markerFile, new Date().toISOString())
16051628
}
@@ -1611,6 +1634,9 @@ async function maybeRunProjectPostSetup(projectDir: string, envDir: string, _isS
16111634
}
16121635
}
16131636

1637+
// Deprecated: kept here for reference only (no longer used)
1638+
// Previously attempted a local fallback migration; removed per product decision.
1639+
16141640
/**
16151641
* Auto-setup services for any project based on deps.yaml services configuration
16161642
*/

0 commit comments

Comments
 (0)