@@ -9,24 +9,29 @@ function getLaunchpadBinary(): string {
9
9
return 'launchpad'
10
10
}
11
11
12
- export function shellcode ( testMode : boolean = false ) : string {
12
+ export function shellcode ( _testMode : boolean = false ) : string {
13
13
// Use the same launchpad binary that's currently running
14
14
const launchpadBinary = getLaunchpadBinary ( )
15
- const testModeCheck = testMode ? '' : ' || "$NODE_ENV" == "test"'
16
15
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'
21
22
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
23
28
24
29
return `
25
30
# MINIMAL LAUNCHPAD SHELL INTEGRATION - DEBUGGING VERSION
26
31
# This is a minimal version to isolate the hanging issue
27
32
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
30
35
return 0 2>/dev/null || exit 0
31
36
fi
32
37
@@ -36,9 +41,64 @@ if [[ "$LAUNCHPAD_SKIP_INITIAL_INTEGRATION" == "1" ]]; then
36
41
return 0 2>/dev/null || exit 0
37
42
fi
38
43
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
+
39
91
# Set up directory change hooks for zsh and bash (do this first, before any processing guards)
40
92
if [[ -n "$ZSH_VERSION" ]]; then
41
93
# 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
+
42
102
__launchpad_chpwd() {
43
103
# Prevent infinite recursion during hook execution
44
104
if [[ "$__LAUNCHPAD_IN_HOOK" == "1" ]]; then
60
120
chpwd_functions+=(__launchpad_chpwd)
61
121
fi
62
122
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
70
131
71
- # Reuse the same environment switching/refresh logic
72
- __launchpad_switch_environment
132
+ # Reuse the same environment switching/refresh logic
133
+ __launchpad_switch_environment
73
134
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
+ }
77
138
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
81
143
fi
82
144
elif [[ -n "$BASH_VERSION" ]]; then
83
145
# bash hook using PROMPT_COMMAND
105
167
106
168
# Environment switching function (called by hooks)
107
169
__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 )
110
172
111
173
# Check if verbose mode is enabled
112
174
local verbose_mode="${ verboseDefault } "
113
175
if [[ -n "$LAUNCHPAD_VERBOSE" ]]; then
114
176
verbose_mode="$LAUNCHPAD_VERBOSE"
115
177
fi
116
178
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
118
188
printf "⏱️ [0ms] Shell integration started for PWD=%s\\n" "$PWD" >&2
119
189
fi
120
190
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)
122
192
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
125
204
fi
126
205
127
206
# Step 2: Always ensure global paths are available (even in projects)
@@ -153,9 +232,21 @@ __launchpad_switch_environment() {
153
232
rehash 2>/dev/null || true
154
233
fi
155
234
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
+
156
247
# Show refresh message if verbose
157
248
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
159
250
fi
160
251
fi
161
252
@@ -166,13 +257,19 @@ __launchpad_switch_environment() {
166
257
# Remove project-specific paths from PATH
167
258
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")
168
259
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
171
262
printf "${ deactivationMessage } \\n" >&2
172
263
fi
173
264
265
+ # Verbose: deactivated environment
266
+ if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
267
+ printf "⚪ Deactivated environment\n" >&2
268
+ fi
269
+
174
270
unset LAUNCHPAD_CURRENT_PROJECT
175
271
unset LAUNCHPAD_ENV_BIN_PATH
272
+ unset __LAUNCHPAD_LAST_ACTIVATION_KEY
176
273
fi
177
274
return 0
178
275
fi
@@ -181,7 +278,7 @@ __launchpad_switch_environment() {
181
278
if [[ -n "$project_dir" ]]; then
182
279
local project_basename=$(basename "$project_dir")
183
280
# 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")
185
282
local project_hash="\${project_basename}_$(echo "$md5hash" | cut -c1-8)"
186
283
187
284
# Check for dependency file to add dependency hash
@@ -195,7 +292,7 @@ __launchpad_switch_environment() {
195
292
196
293
local env_dir="$HOME/.local/share/launchpad/envs/$project_hash"
197
294
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 "")
199
296
if [[ -n "$dep_short" ]]; then
200
297
env_dir="\${env_dir}-d\${dep_short}"
201
298
fi
@@ -220,16 +317,27 @@ __launchpad_switch_environment() {
220
317
export LAUNCHPAD_ENV_BIN_PATH="$env_dir/bin"
221
318
export PATH="$env_dir/bin:$PATH"
222
319
223
- # Show activation message if enabled
320
+ # Show activation message if enabled (only when env changes)
224
321
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
226
331
fi
227
332
else
228
333
# Install dependencies synchronously but with timeout to avoid hanging
229
334
# 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
231
339
# If install succeeded, try to activate the environment
232
- if [[ -d "$env_dir/bin" ]]; then
340
+ if [[ -d "$env_dir/bin" ]]; then
233
341
export LAUNCHPAD_CURRENT_PROJECT="$project_dir"
234
342
export LAUNCHPAD_ENV_BIN_PATH="$env_dir/bin"
235
343
export PATH="$env_dir/bin:$PATH"
@@ -238,17 +346,25 @@ __launchpad_switch_environment() {
238
346
if [[ "${ showMessages } " == "true" ]]; then
239
347
printf "${ activationMessage } \\n" >&2
240
348
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
241
354
fi
242
355
fi
243
356
fi
244
357
fi
245
358
246
359
# 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
252
368
fi
253
369
fi
254
370
}
0 commit comments