@@ -9,24 +9,29 @@ function getLaunchpadBinary(): string {
99 return 'launchpad'
1010}
1111
12- export function shellcode ( testMode : boolean = false ) : string {
12+ export function shellcode ( _testMode : boolean = false ) : string {
1313 // Use the same launchpad binary that's currently running
1414 const launchpadBinary = getLaunchpadBinary ( )
15- const testModeCheck = testMode ? '' : ' || "$NODE_ENV" == "test"'
1615
17- // Use default shell message configuration
18- const showMessages = ( typeof process !== 'undefined' && process . env ?. LAUNCHPAD_SHOW_ENV_MESSAGES !== 'false' ) ? 'true' : 'false'
19- const activationMessage = ( ( typeof process !== 'undefined' && process . env ?. LAUNCHPAD_SHELL_ACTIVATION_MESSAGE ) || '✅ Environment activated for \\033[3m$(basename "$project_dir")\\033[0m' ) . replace ( '{path}' , '$(basename "$project_dir")' )
20- const deactivationMessage = ( typeof process !== 'undefined' && process . env ?. LAUNCHPAD_SHELL_DEACTIVATION_MESSAGE ) || 'Environment deactivated'
16+ // Use config-backed shell message configuration with {path} substitution
17+ const showMessages = config . showShellMessages ? 'true' : 'false'
18+ // Replace {path} with shell-evaluated basename
19+ const activationMessage = ( config . shellActivationMessage || '✅ Environment activated for {path}' )
20+ . replace ( '{path}' , '$(basename "$project_dir")' )
21+ const deactivationMessage = config . shellDeactivationMessage || 'Environment deactivated'
2122
22- const verboseDefault = ! ! config . verbose
23+ // Verbosity: default to verbose for shell integration unless explicitly disabled
24+ // Priority: LAUNCHPAD_VERBOSE (runtime) > LAUNCHPAD_SHELL_VERBOSE (env) > config.verbose
25+ const verboseDefault = ( typeof process !== 'undefined' && process . env ?. LAUNCHPAD_SHELL_VERBOSE !== 'false' )
26+ ? true
27+ : ! ! config . verbose
2328
2429 return `
2530# MINIMAL LAUNCHPAD SHELL INTEGRATION - DEBUGGING VERSION
2631# This is a minimal version to isolate the hanging issue
2732
28- # Exit early if shell integration is disabled or in test mode
29- if [[ "$LAUNCHPAD_DISABLE_SHELL_INTEGRATION" == "1"${ testModeCheck } ]]; then
33+ # Exit early if shell integration is disabled or explicit test mode
34+ if [[ "$LAUNCHPAD_DISABLE_SHELL_INTEGRATION" == "1" || "$LAUNCHPAD_TEST_MODE" == "1" ]]; then
3035 return 0 2>/dev/null || exit 0
3136fi
3237
@@ -36,9 +41,64 @@ if [[ "$LAUNCHPAD_SKIP_INITIAL_INTEGRATION" == "1" ]]; then
3641 return 0 2>/dev/null || exit 0
3742fi
3843
44+ # PATH helper: prepend a directory if not already present
45+ __lp_prepend_path() {
46+ local dir="$1"
47+ if [[ -n "$dir" && -d "$dir" ]]; then
48+ case ":$PATH:" in
49+ *":$dir:"*) : ;;
50+ *) PATH="$dir:$PATH"; export PATH ;;
51+ esac
52+ fi
53+ }
54+
55+ # Ensure Launchpad global bin is on PATH early (for globally installed tools)
56+ __lp_prepend_path "$HOME/.local/share/launchpad/global/bin"
57+
58+ # Portable timeout helper: uses timeout, gtimeout (macOS), or no-timeout fallback
59+ lp_timeout() {
60+ local duration="$1"; shift
61+ if command -v timeout >/dev/null 2>&1; then
62+ timeout "$duration" "$@"
63+ elif command -v gtimeout >/dev/null 2>&1; then
64+ gtimeout "$duration" "$@"
65+ else
66+ "$@"
67+ fi
68+ }
69+
70+ # Portable current time in milliseconds
71+ lp_now_ms() {
72+ if [[ -n "$ZSH_VERSION" && -n "$EPOCHREALTIME" ]]; then
73+ # EPOCHREALTIME is like: seconds.microseconds
74+ local sec="\${EPOCHREALTIME%.*}"
75+ local usec="\${EPOCHREALTIME#*.}"
76+ # Zero-pad/truncate to 3 digits for milliseconds
77+ local msec=$(( 10#\${usec:0:3} ))
78+ printf '%d%03d\n' "$sec" "$msec"
79+ elif command -v python3 >/dev/null 2>&1; then
80+ python3 - <<'PY'
81+ import time
82+ print(int(time.time() * 1000))
83+ PY
84+ else
85+ # Fallback: seconds * 1000 (approx)
86+ local s=$(date +%s 2>/dev/null || echo 0)
87+ printf '%d\n' $(( s * 1000 ))
88+ fi
89+ }
90+
3991# Set up directory change hooks for zsh and bash (do this first, before any processing guards)
4092 if [[ -n "$ZSH_VERSION" ]]; then
4193 # zsh hook
94+ # Ensure hook arrays exist
95+ if ! typeset -p chpwd_functions >/dev/null 2>&1; then
96+ typeset -ga chpwd_functions
97+ fi
98+ if ! typeset -p precmd_functions >/dev/null 2>&1; then
99+ typeset -ga precmd_functions
100+ fi
101+
42102 __launchpad_chpwd() {
43103 # Prevent infinite recursion during hook execution
44104 if [[ "$__LAUNCHPAD_IN_HOOK" == "1" ]]; then
60120 chpwd_functions+=(__launchpad_chpwd)
61121 fi
62122
63- # zsh precmd to refresh on each prompt
64- __launchpad_precmd() {
65- # Prevent infinite recursion during hook execution
66- if [[ "$__LAUNCHPAD_IN_HOOK" == "1" ]]; then
67- return 0
68- fi
69- export __LAUNCHPAD_IN_HOOK=1
123+ # Optionally enable a precmd-based refresh if explicitly requested
124+ if [[ "$LAUNCHPAD_USE_PRECMD" == "1" ]]; then
125+ __launchpad_precmd() {
126+ # Prevent infinite recursion during hook execution
127+ if [[ "$__LAUNCHPAD_IN_HOOK" == "1" ]]; then
128+ return 0
129+ fi
130+ export __LAUNCHPAD_IN_HOOK=1
70131
71- # Reuse the same environment switching/refresh logic
72- __launchpad_switch_environment
132+ # Reuse the same environment switching/refresh logic
133+ __launchpad_switch_environment
73134
74- # Clean up hook flag explicitly
75- unset __LAUNCHPAD_IN_HOOK 2>/dev/null || true
76- }
135+ # Clean up hook flag explicitly
136+ unset __LAUNCHPAD_IN_HOOK 2>/dev/null || true
137+ }
77138
78- # Add the precmd hook if not already added
79- if [[ ! " \${precmd_functions[*]} " =~ " __launchpad_precmd " ]]; then
80- precmd_functions+=(__launchpad_precmd)
139+ # Add the precmd hook if not already added
140+ if [[ ! " \${precmd_functions[*]} " =~ " __launchpad_precmd " ]]; then
141+ precmd_functions+=(__launchpad_precmd)
142+ fi
81143 fi
82144elif [[ -n "$BASH_VERSION" ]]; then
83145 # bash hook using PROMPT_COMMAND
105167
106168# Environment switching function (called by hooks)
107169__launchpad_switch_environment() {
108- # Start timer for performance tracking
109- local start_time=$(date +%s%3N 2>/dev/null || echo "0" )
170+ # Start timer for performance tracking (portable)
171+ local start_time=$(lp_now_ms )
110172
111173 # Check if verbose mode is enabled
112174 local verbose_mode="${ verboseDefault } "
113175 if [[ -n "$LAUNCHPAD_VERBOSE" ]]; then
114176 verbose_mode="$LAUNCHPAD_VERBOSE"
115177 fi
116178
117- if [[ "$verbose_mode" == "true" ]]; then
179+ # Dedupe key for verbose printing (avoid duplicate start/completion logs per PWD)
180+ local __lp_verbose_key="$PWD"
181+ local __lp_should_verbose_print="1"
182+ if [[ "$__LAUNCHPAD_LAST_VERBOSE_KEY" == "$__lp_verbose_key" ]]; then
183+ __lp_should_verbose_print="0"
184+ fi
185+ export __LAUNCHPAD_LAST_VERBOSE_KEY="$__lp_verbose_key"
186+
187+ if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
118188 printf "⏱️ [0ms] Shell integration started for PWD=%s\\n" "$PWD" >&2
119189 fi
120190
121- # Step 1: Find project directory using our fast binary (with timeout)
191+ # Step 1: Find project directory using our fast binary (with portable timeout)
122192 local project_dir=""
123- if timeout 0.5s ${ launchpadBinary } dev:find-project-root "$PWD" >/dev/null 2>&1; then
124- project_dir=$(LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 timeout 0.5s ${ launchpadBinary } dev:find-project-root "$PWD" 2>/dev/null || echo "")
193+ if lp_timeout 1s ${ launchpadBinary } dev:find-project-root "$PWD" >/dev/null 2>&1; then
194+ project_dir=$(LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 lp_timeout 1s ${ launchpadBinary } dev:find-project-root "$PWD" 2>/dev/null || echo "")
195+ fi
196+
197+ # Verbose: show project detection result
198+ if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
199+ if [[ -n "$project_dir" ]]; then
200+ printf "📁 Project detected: %s\n" "$project_dir" >&2
201+ else
202+ printf "📁 No project detected (global mode)\n" >&2
203+ fi
125204 fi
126205
127206 # Step 2: Always ensure global paths are available (even in projects)
@@ -153,9 +232,21 @@ __launchpad_switch_environment() {
153232 rehash 2>/dev/null || true
154233 fi
155234
235+ # Initialize starship when it just became available (idempotent)
236+ if command -v starship >/dev/null 2>&1 && [[ -z "$STARSHIP_SHELL" ]]; then
237+ if [[ -n "$ZSH_VERSION" ]]; then
238+ eval "$(starship init zsh)" >/dev/null 2>&1 || true
239+ elif [[ -n "$BASH_VERSION" ]]; then
240+ eval "$(starship init bash)" >/dev/null 2>&1 || true
241+ fi
242+ if [[ "$verbose_mode" == "true" ]]; then
243+ printf "🌟 Initialized Starship prompt after install\n" >&2
244+ fi
245+ fi
246+
156247 # Show refresh message if verbose
157248 if [[ "$verbose_mode" == "true" ]]; then
158- printf "🔄 Shell environment refreshed for newly installed tools\\ n" >&2
249+ printf "🔄 Shell environment refreshed for newly installed tools\n" >&2
159250 fi
160251 fi
161252
@@ -166,13 +257,19 @@ __launchpad_switch_environment() {
166257 # Remove project-specific paths from PATH
167258 export PATH=$(echo "$PATH" | sed "s|$LAUNCHPAD_ENV_BIN_PATH:||g" | sed "s|:$LAUNCHPAD_ENV_BIN_PATH||g" | sed "s|^$LAUNCHPAD_ENV_BIN_PATH$||g")
168259
169- # Show deactivation message if enabled
170- if [[ "${ showMessages } " == "true" ]]; then
260+ # Show deactivation message if enabled (only once per deactivation)
261+ if [[ "${ showMessages } " == "true" && -n "$__LAUNCHPAD_LAST_ACTIVATION_KEY" ]]; then
171262 printf "${ deactivationMessage } \\n" >&2
172263 fi
173264
265+ # Verbose: deactivated environment
266+ if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
267+ printf "⚪ Deactivated environment\n" >&2
268+ fi
269+
174270 unset LAUNCHPAD_CURRENT_PROJECT
175271 unset LAUNCHPAD_ENV_BIN_PATH
272+ unset __LAUNCHPAD_LAST_ACTIVATION_KEY
176273 fi
177274 return 0
178275 fi
@@ -181,7 +278,7 @@ __launchpad_switch_environment() {
181278 if [[ -n "$project_dir" ]]; then
182279 local project_basename=$(basename "$project_dir")
183280 # Use proper MD5 hash to match existing environments
184- local md5hash=$(printf "%s" "$project_dir" | LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 timeout 1s ${ launchpadBinary } dev:md5 /dev/stdin 2>/dev/null || echo "00000000")
281+ local md5hash=$(printf "%s" "$project_dir" | LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 lp_timeout 2s ${ launchpadBinary } dev:md5 /dev/stdin 2>/dev/null || echo "00000000")
185282 local project_hash="\${project_basename}_$(echo "$md5hash" | cut -c1-8)"
186283
187284 # Check for dependency file to add dependency hash
@@ -195,7 +292,7 @@ __launchpad_switch_environment() {
195292
196293 local env_dir="$HOME/.local/share/launchpad/envs/$project_hash"
197294 if [[ -n "$dep_file" ]]; then
198- local dep_short=$(LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 timeout 1s ${ launchpadBinary } dev:md5 "$dep_file" 2>/dev/null | cut -c1-8 || echo "")
295+ local dep_short=$(LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 lp_timeout 2s ${ launchpadBinary } dev:md5 "$dep_file" 2>/dev/null | cut -c1-8 || echo "")
199296 if [[ -n "$dep_short" ]]; then
200297 env_dir="\${env_dir}-d\${dep_short}"
201298 fi
@@ -220,16 +317,27 @@ __launchpad_switch_environment() {
220317 export LAUNCHPAD_ENV_BIN_PATH="$env_dir/bin"
221318 export PATH="$env_dir/bin:$PATH"
222319
223- # Show activation message if enabled
320+ # Show activation message if enabled (only when env changes)
224321 if [[ "${ showMessages } " == "true" ]]; then
225- printf "${ activationMessage } \\n" >&2
322+ if [[ "$__LAUNCHPAD_LAST_ACTIVATION_KEY" != "$env_dir" ]]; then
323+ printf "${ activationMessage } \\n" >&2
324+ fi
325+ fi
326+ export __LAUNCHPAD_LAST_ACTIVATION_KEY="$env_dir"
327+
328+ # Verbose: show activated env path
329+ if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
330+ printf "✅ Activated environment: %s\n" "$env_dir" >&2
226331 fi
227332 else
228333 # Install dependencies synchronously but with timeout to avoid hanging
229334 # Use LAUNCHPAD_SHELL_INTEGRATION=1 to enable proper progress display
230- if LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 LAUNCHPAD_SHELL_INTEGRATION=1 timeout 30s ${ launchpadBinary } install "$project_dir"; then
335+ if LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 LAUNCHPAD_SHELL_INTEGRATION=1 lp_timeout 30s ${ launchpadBinary } install "$project_dir"; then
336+ if [[ "$verbose_mode" == "true" ]]; then
337+ printf "📦 Installed project dependencies (on-demand)\n" >&2
338+ fi
231339 # If install succeeded, try to activate the environment
232- if [[ -d "$env_dir/bin" ]]; then
340+ if [[ -d "$env_dir/bin" ]]; then
233341 export LAUNCHPAD_CURRENT_PROJECT="$project_dir"
234342 export LAUNCHPAD_ENV_BIN_PATH="$env_dir/bin"
235343 export PATH="$env_dir/bin:$PATH"
@@ -238,17 +346,25 @@ __launchpad_switch_environment() {
238346 if [[ "${ showMessages } " == "true" ]]; then
239347 printf "${ activationMessage } \\n" >&2
240348 fi
349+
350+ # Verbose: show activated env path after install
351+ if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
352+ printf "✅ Activated environment after install: %s\n" "$env_dir" >&2
353+ fi
241354 fi
242355 fi
243356 fi
244357 fi
245358
246359 # Show completion time if verbose
247- if [[ "$verbose_mode" == "true" ]]; then
248- local end_time=$(date +%s%3N 2>/dev/null || echo "0")
249- local elapsed=$((end_time - start_time))
250- if [[ "$elapsed" -gt 0 ]]; then
251- printf "⏱️ [%sms] Shell integration completed\\n" "$elapsed" >&2
360+ if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
361+ local end_time=$(lp_now_ms)
362+ # Only print if both are integers
363+ if [[ "$start_time" =~ ^[0-9]+$ && "$end_time" =~ ^[0-9]+$ ]]; then
364+ local elapsed=$(( end_time - start_time ))
365+ if [[ "$elapsed" -ge 0 ]]; then
366+ printf "⏱️ [%sms] Shell integration completed\n" "$elapsed" >&2
367+ fi
252368 fi
253369 fi
254370}
0 commit comments