Skip to content

Commit e37525d

Browse files
committed
chore: wip
1 parent ea25ad4 commit e37525d

File tree

9 files changed

+312
-11
lines changed

9 files changed

+312
-11
lines changed

docs/config.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,77 @@ const config: LaunchpadConfig = {
472472
export default config
473473
```
474474

475+
### Lifecycle Hooks (preSetup, preActivation, postActivation)
476+
477+
In addition to `postSetup`, Launchpad provides three more lifecycle hooks for fine-grained control:
478+
479+
- preSetup: runs before any installation/services start
480+
- preActivation: runs after installs/services, just before activation
481+
- postActivation: runs immediately after activation completes
482+
483+
You can configure them in `launchpad.config.ts` or inline in your dependency file (e.g., `deps.yaml`).
484+
485+
Config file example:
486+
487+
```ts
488+
// launchpad.config.ts
489+
import type { LaunchpadConfig } from '@stacksjs/launchpad'
490+
491+
const config: LaunchpadConfig = {
492+
preSetup: {
493+
enabled: true,
494+
commands: [
495+
{ command: "bash -lc 'echo preSetup'" },
496+
],
497+
},
498+
preActivation: {
499+
enabled: true,
500+
commands: [
501+
{ command: "bash -lc 'echo preActivation'" },
502+
],
503+
},
504+
postActivation: {
505+
enabled: true,
506+
commands: [
507+
{ command: "bash -lc 'echo postActivation'" },
508+
],
509+
},
510+
}
511+
512+
export default config
513+
```
514+
515+
Dependency file example (inline hooks):
516+
517+
```yaml
518+
# deps.yaml
519+
preSetup:
520+
enabled: true
521+
commands:
522+
- { command: "bash -lc 'echo preSetup'" }
523+
524+
postSetup:
525+
enabled: true
526+
commands:
527+
- { command: "bash -lc 'echo postSetup'" }
528+
529+
preActivation:
530+
enabled: true
531+
commands:
532+
- { command: "bash -lc 'echo preActivation'" }
533+
534+
postActivation:
535+
enabled: true
536+
commands:
537+
- { command: "bash -lc 'echo postActivation'" }
538+
```
539+
540+
Notes:
541+
- Inline hooks in `deps.yaml` run alongside config hooks of the same phase.
542+
- preSetup runs before dependency installation and service auto-start.
543+
- preActivation runs after installation/services and before printing the activation message.
544+
- postActivation runs after the final activation message.
545+
475546
## Environment Variables
476547

477548
You can also configure Launchpad using environment variables:

docs/usage.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@ cd ../ # → Automatically deactivates
205205
# dev environment deactivated
206206
```
207207

208+
Hook lifecycle during activation:
209+
210+
- preSetup (before installs/services)
211+
- postSetup (after environment is prepared)
212+
- preActivation (after installs/services, before activation message)
213+
- postActivation (immediately after activation message)
214+
215+
Define hooks in `launchpad.config.ts` or inline in `deps.yaml` (see Configuration → Lifecycle Hooks).
216+
208217
::: tip Prompt Compatibility
209218
If you use **Starship prompt** and see timeout warnings like `[WARN] - (starship::utils): Executing command timed out`, add `command_timeout = 5000` to the top of your `~/.config/starship.toml` file. This gives Starship enough time to detect tool versions from Launchpad-managed binaries. See [Troubleshooting](./troubleshooting.md#starship-prompt-timeout-warnings) for details.
210219
:::

packages/launchpad/src/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ export const defaultConfig: LaunchpadConfig = {
5454
enabled: process.env.LAUNCHPAD_POST_SETUP_ENABLED !== 'false',
5555
commands: [],
5656
},
57+
preSetup: {
58+
enabled: process.env.LAUNCHPAD_PRE_SETUP_ENABLED !== 'false',
59+
commands: [],
60+
},
61+
preActivation: {
62+
enabled: process.env.LAUNCHPAD_PRE_ACTIVATION_ENABLED !== 'false',
63+
commands: [],
64+
},
65+
postActivation: {
66+
enabled: process.env.LAUNCHPAD_POST_ACTIVATION_ENABLED !== 'false',
67+
commands: [],
68+
},
5769
services: {
5870
enabled: process.env.LAUNCHPAD_SERVICES_ENABLED !== 'false',
5971
dataDir: process.env.LAUNCHPAD_SERVICES_DATA_DIR || path.join(homedir(), '.local', 'share', 'launchpad', 'services'),

packages/launchpad/src/dev/dump.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,49 @@ export interface DumpOptions {
2929
skipGlobal?: boolean // Skip global package processing for testing
3030
}
3131

32+
function extractHookCommandsFromDepsYaml(filePath: string, hookName: 'preSetup' | 'postSetup' | 'preActivation' | 'postActivation'): PostSetupCommand[] {
33+
try {
34+
const content = fs.readFileSync(filePath, 'utf8')
35+
const lines = content.split(/\r?\n/)
36+
const cmds: PostSetupCommand[] = []
37+
let inHook = false
38+
let inCommands = false
39+
let baseIndent = 0
40+
for (let i = 0; i < lines.length; i++) {
41+
const raw = lines[i]
42+
const indent = raw.length - raw.trimStart().length
43+
const line = raw.trim()
44+
if (!inHook) {
45+
if (line.startsWith(`${hookName}:`)) {
46+
inHook = true
47+
baseIndent = indent
48+
}
49+
continue
50+
}
51+
if (indent <= baseIndent && line.endsWith(':')) {
52+
break
53+
}
54+
if (!inCommands && line.startsWith('commands:')) {
55+
inCommands = true
56+
continue
57+
}
58+
if (inCommands) {
59+
const m1 = line.match(/command:\s*"([^"]+)"/)
60+
const m2 = line.match(/command:\s*'([^']+)'/)
61+
if (m1 || m2) {
62+
const cmd = (m1?.[1] || m2?.[1]) as string
63+
if (cmd && cmd.length > 0)
64+
cmds.push({ command: cmd })
65+
}
66+
}
67+
}
68+
return cmds
69+
}
70+
catch {
71+
return []
72+
}
73+
}
74+
3275
/**
3376
* Check if packages are installed in the given environment directory
3477
*/
@@ -199,6 +242,13 @@ export async function detectLaravelProject(dir: string): Promise<{ isLaravel: bo
199242
// Ignore errors checking migrations
200243
}
201244

245+
// Execute project-level post-setup commands if enabled (skip in shell integration fast path)
246+
const projectPreSetup = config.preSetup
247+
if (projectPreSetup?.enabled && process.env.LAUNCHPAD_SHELL_INTEGRATION !== '1') {
248+
const preSetupResults = await executepostSetup(dir, projectPreSetup.commands || [])
249+
suggestions.push(...preSetupResults)
250+
}
251+
202252
// Execute project-level post-setup commands if enabled (skip in shell integration fast path)
203253
const projectPostSetup = config.postSetup
204254
if (projectPostSetup?.enabled && process.env.LAUNCHPAD_SHELL_INTEGRATION !== '1') {
@@ -589,6 +639,20 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
589639
try {
590640
// Find dependency file using our comprehensive detection
591641
const dependencyFile = findDependencyFile(dir)
642+
// Early pre-setup hook (before any installs/services) when config present in working dir
643+
try {
644+
if (dependencyFile) {
645+
const fileCmds = extractHookCommandsFromDepsYaml(dependencyFile, 'preSetup')
646+
if (fileCmds.length > 0) {
647+
await executepostSetup(path.dirname(dependencyFile), fileCmds)
648+
}
649+
}
650+
const projectPreSetup = config.preSetup
651+
if (projectPreSetup?.enabled && !shellOutput && !quiet && dependencyFile) {
652+
await executepostSetup(path.dirname(dependencyFile), projectPreSetup.commands || [])
653+
}
654+
}
655+
catch {}
592656

593657
if (!dependencyFile) {
594658
if (!quiet && !shellOutput) {
@@ -840,11 +904,30 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
840904
if (localPackages.length > 0 || globalPackages.length > 0) {
841905
await installPackagesOptimized(localPackages, globalPackages, envDir, globalEnvDir, dryrun, quiet)
842906
// Visual separator after dependency install list
843-
try { console.log() }
907+
try {
908+
console.log()
909+
}
844910
catch {}
845911
}
846912

847913
// Auto-start services for any project that has services configuration
914+
// Pre-activation hook (runs after install/services and before shell activation)
915+
const preActivation = config.preActivation
916+
if (dependencyFile) {
917+
const filePostSetup = extractHookCommandsFromDepsYaml(dependencyFile, 'postSetup')
918+
if (filePostSetup.length > 0) {
919+
await executepostSetup(projectDir, filePostSetup)
920+
}
921+
}
922+
if (preActivation?.enabled && !isShellIntegration) {
923+
await executepostSetup(projectDir, preActivation.commands || [])
924+
}
925+
if (dependencyFile) {
926+
const filePreActivation = extractHookCommandsFromDepsYaml(dependencyFile, 'preActivation')
927+
if (filePreActivation.length > 0) {
928+
await executepostSetup(projectDir, filePreActivation)
929+
}
930+
}
848931
// Suppress interstitial processing messages during service startup phase
849932
const prevProcessing = process.env.LAUNCHPAD_PROCESSING
850933
process.env.LAUNCHPAD_PROCESSING = '0'
@@ -858,6 +941,13 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
858941
await ensureProjectPhpIni(projectDir, envDir)
859942
await maybeRunLaravelPostSetup(projectDir, envDir, isShellIntegration)
860943

944+
// Mark environment as ready for fast shell activation on subsequent prompts
945+
try {
946+
await fs.promises.mkdir(path.join(envDir), { recursive: true })
947+
await fs.promises.writeFile(path.join(envDir, '.launchpad_ready'), '1')
948+
}
949+
catch {}
950+
861951
// Check for Laravel project and provide helpful suggestions
862952
const laravelInfo = await detectLaravelProject(projectDir)
863953
if (laravelInfo.isLaravel) {
@@ -885,6 +975,18 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
885975
const activation = (config.shellActivationMessage || '✅ Environment activated for {path}')
886976
.replace('{path}', process.cwd())
887977
console.log(activation)
978+
979+
// Post-activation hook (file-level then config)
980+
if (dependencyFile) {
981+
const filePostActivation = extractHookCommandsFromDepsYaml(dependencyFile, 'postActivation')
982+
if (filePostActivation.length > 0) {
983+
await executepostSetup(projectDir, filePostActivation)
984+
}
985+
}
986+
const postActivation = config.postActivation
987+
if (postActivation?.enabled) {
988+
await executepostSetup(projectDir, postActivation.commands || [])
989+
}
888990
}
889991
}
890992
catch (error) {

packages/launchpad/src/dev/shellcode.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,23 @@ export function shellcode(testMode: boolean = false): string {
9191

9292
return `
9393
# Launchpad shell integration - Performance Optimized
94+
# Ultra-fast init guard: avoid any heavy work during initial shell startup
95+
if [[ -z "$__LAUNCHPAD_INIT_DONE" ]]; then
96+
export __LAUNCHPAD_INIT_DONE=1
97+
# Preload minimal global paths only; defer scans to first cd
98+
if [[ -d "$HOME/.local/share/launchpad/global/bin" ]]; then
99+
case ":$PATH:" in
100+
*":$HOME/.local/share/launchpad/global/bin:"*) ;;
101+
*) export PATH="$HOME/.local/share/launchpad/global/bin:$PATH" ;;
102+
esac
103+
fi
104+
if [[ -d "$HOME/.local/share/launchpad/global/sbin" ]]; then
105+
case ":$PATH:" in
106+
*":$HOME/.local/share/launchpad/global/sbin:"*) ;;
107+
*) export PATH="$HOME/.local/share/launchpad/global/sbin:$PATH" ;;
108+
esac
109+
fi
110+
fi
94111
# Exit early if shell integration is disabled or in test mode
95112
if [[ "$LAUNCHPAD_DISABLE_SHELL_INTEGRATION" == "1"${testModeCheck} ]]; then
96113
return 0 2>/dev/null || exit 0
@@ -726,14 +743,8 @@ __launchpad_chpwd() {
726743
eval "$env_output" 2>/dev/null || true
727744
fi
728745
729-
# After activation, attempt to run Laravel post-setup if configured
730-
if command -v launchpad >/dev/null 2>&1; then
731-
# Best-effort: run migrate if configured and php works
732-
if command -v php >/dev/null 2>&1; then
733-
# Trigger a lightweight Laravel detect to run post-setup via CLI (quiet)
734-
LAUNCHPAD_SHOW_ENV_MESSAGES=false launchpad dev "$project_dir" --quiet >/dev/null 2>&1 || true
735-
fi
736-
fi
746+
# Defer post-setup quietly without background job output; precmd hook will refresh
747+
touch "$HOME/.cache/launchpad/shell_cache/global_refresh_needed" 2>/dev/null || true
737748
738749
# Ensure global dependencies are still in PATH after project setup
739750
__launchpad_ensure_global_path
@@ -907,7 +918,7 @@ __launchpad_source_hooks_dir() {
907918
908919
# One-time setup on shell initialization
909920
__launchpad_setup_global_deps
910-
__launchpad_ensure_global_path
921+
__launchpad_ensure_global_path_fast
911922
__launchpad_source_hooks_dir "$HOME/.config/launchpad/hooks/init.d"
912923
913924
# Clear command hash table on initial load

packages/launchpad/src/logging.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint-disable no-console */
22
import fs from 'node:fs'
33
import process from 'node:process'
4-
import { config } from './config'
54

65
// Global message deduplication for shell mode
76
const shellModeMessageCache = new Set<string>()

packages/launchpad/src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@ export interface LaunchpadConfig {
6969
enabled?: boolean
7070
commands?: PostSetupCommand[]
7171
}
72+
/** Project-level pre-setup commands (run before any installation/services) */
73+
preSetup?: {
74+
enabled?: boolean
75+
commands?: PostSetupCommand[]
76+
}
77+
/** Commands to run just before activation (after install/services) */
78+
preActivation?: {
79+
enabled?: boolean
80+
commands?: PostSetupCommand[]
81+
}
82+
/** Commands to run right after activation completes */
83+
postActivation?: {
84+
enabled?: boolean
85+
commands?: PostSetupCommand[]
86+
}
7287
services?: {
7388
enabled?: boolean
7489
autoStart?: boolean

0 commit comments

Comments
 (0)