Skip to content

Commit 89818e1

Browse files
committed
chore: wip
1 parent c812bf5 commit 89818e1

File tree

3 files changed

+183
-26
lines changed

3 files changed

+183
-26
lines changed

packages/launchpad/src/dev/shellcode.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,55 @@ __launchpad_switch_environment() {
176176
printf "⏱️ [0ms] Shell integration started for PWD=%s\\n" "$PWD" >&2
177177
fi
178178
179+
# Known dependency filenames (keep in sync with DEPENDENCY_FILE_NAMES in src/env.ts)
180+
local _dep_names=(
181+
# Launchpad-specific files (highest priority)
182+
"dependencies.yaml" "dependencies.yml" "deps.yaml" "deps.yml" "pkgx.yaml" "pkgx.yml" "launchpad.yaml" "launchpad.yml"
183+
# Node.js/JavaScript
184+
"package.json"
185+
# Python
186+
"pyproject.toml" "requirements.txt" "setup.py" "Pipfile" "Pipfile.lock"
187+
# Rust
188+
"Cargo.toml"
189+
# Go
190+
"go.mod" "go.sum"
191+
# Ruby
192+
"Gemfile"
193+
# Deno
194+
"deno.json" "deno.jsonc"
195+
# GitHub Actions
196+
"action.yml" "action.yaml"
197+
# Kubernetes/Docker
198+
"skaffold.yaml" "skaffold.yml"
199+
# Version control files
200+
".nvmrc" ".node-version" ".ruby-version" ".python-version" ".terraform-version"
201+
# Package manager files
202+
"yarn.lock" "bun.lockb" ".yarnrc"
203+
)
204+
179205
# Step 1: Find project directory using our fast binary (no artificial timeout)
180206
local project_dir=""
181207
if ${launchpadBinary} dev:find-project-root "$PWD" >/dev/null 2>&1; then
182208
project_dir=$(LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 ${launchpadBinary} dev:find-project-root "$PWD" 2>/dev/null || echo "")
183209
fi
184210
211+
# Fallback: If binary didn't detect a project, scan upwards for known dependency files
212+
if [[ -z "$project_dir" ]]; then
213+
local __dir="$PWD"
214+
while [[ "$__dir" != "/" ]]; do
215+
for name in "\${_dep_names[@]}"; do
216+
if [[ -f "$__dir/$name" ]]; then
217+
project_dir="$__dir"
218+
break
219+
fi
220+
done
221+
if [[ -n "$project_dir" ]]; then
222+
break
223+
fi
224+
__dir="$(dirname "$__dir")"
225+
done
226+
fi
227+
185228
# Verbose: show project detection result
186229
if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
187230
if [[ -n "$project_dir" ]]; then
@@ -270,22 +313,28 @@ __launchpad_switch_environment() {
270313
local project_hash="\${project_basename}_$(echo "$md5hash" | cut -c1-8)"
271314
272315
# Check for dependency file to add dependency hash
316+
# IMPORTANT: keep this list in sync with DEPENDENCY_FILE_NAMES in src/env.ts
273317
local dep_file=""
274-
for name in "dependencies.yaml" "deps.yaml" "pkgx.yaml" "package.json"; do
318+
for name in "\${_dep_names[@]}"; do
275319
if [[ -f "$project_dir/$name" ]]; then
276320
dep_file="$project_dir/$name"
277321
break
278322
fi
279323
done
280324
281325
local env_dir="$HOME/.local/share/launchpad/envs/$project_hash"
326+
local dep_short=""
282327
if [[ -n "$dep_file" ]]; then
283-
local dep_short=$(LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 ${launchpadBinary} dev:md5 "$dep_file" 2>/dev/null | cut -c1-8 || echo "")
328+
dep_short=$(LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 ${launchpadBinary} dev:md5 "$dep_file" 2>/dev/null | cut -c1-8 || echo "")
284329
if [[ -n "$dep_short" ]]; then
285330
env_dir="\${env_dir}-d\${dep_short}"
286331
fi
287332
fi
288333
334+
if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
335+
printf "🔎 dep_file=%q dep_short=%s env_dir=%q\n" "$dep_file" "$dep_short" "$env_dir" >&2
336+
fi
337+
289338
# Check if we're switching projects
290339
if [[ -n "$LAUNCHPAD_CURRENT_PROJECT" && "$LAUNCHPAD_CURRENT_PROJECT" != "$project_dir" ]]; then
291340
# Remove old project paths from PATH
@@ -334,8 +383,21 @@ __launchpad_switch_environment() {
334383
fi
335384
336385
if [[ "$should_attempt" -eq 1 ]]; then
337-
# Suppress installer output in hooks to avoid stray blank lines in the prompt
338-
if LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 LAUNCHPAD_SHELL_INTEGRATION=1 ${launchpadBinary} install "$project_dir" >/dev/null 2>&1; then
386+
# Run installer; show output if verbose, otherwise suppress to keep prompt clean
387+
if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
388+
printf "🚀 Starting on-demand install for %q\n" "$project_dir" >&2
389+
fi
390+
391+
if [[ "$verbose_mode" == "true" ]]; then
392+
# In verbose mode, avoid LAUNCHPAD_SHELL_INTEGRATION=1 so installer prints logs
393+
LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 ${launchpadBinary} install "$project_dir"
394+
install_status=$?
395+
else
396+
LAUNCHPAD_DISABLE_SHELL_INTEGRATION=1 LAUNCHPAD_SHELL_INTEGRATION=1 ${launchpadBinary} install "$project_dir" >/dev/null 2>&1
397+
install_status=$?
398+
fi
399+
400+
if [[ $install_status -eq 0 ]]; then
339401
if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
340402
printf "📦 Installed project dependencies (on-demand)\n" >&2
341403
fi
@@ -356,7 +418,7 @@ __launchpad_switch_environment() {
356418
# Touch backoff marker only on failure to avoid repeated attempts
357419
: > "$backoff_marker" 2>/dev/null || true
358420
if [[ "$verbose_mode" == "true" && "$__lp_should_verbose_print" == "1" ]]; then
359-
printf "⏭️ Deferred on-demand install. Run 'launchpad install %q' manually.\n" "$project_dir" >&2
421+
printf "❌ On-demand install failed (exit %d). Run 'launchpad install %q' manually.\n" "$install_status" "$project_dir" >&2
360422
fi
361423
fi
362424
else

packages/launchpad/src/install-helpers.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -335,31 +335,19 @@ exec "${binaryPath}" "$@"
335335
// Don't spam library paths for every binary - they're mostly the same
336336
}
337337

338-
// Special handling for Bun: create bunx symlink as specified in ts-pkgx script
338+
// Special handling for Bun: create bunx shim and set BUN_INSTALL
339339
if (domain === 'bun.sh' && binary === 'bun') {
340-
// 1. Create bunx symlink to bun (like the ts-pkgx script: ln -s bun bunx)
340+
// 1. Create bunx shim that proxies to `bun x` with proper environment
341341
const bunxShimPath = path.join(targetShimDir, 'bunx')
342342
try {
343343
// Remove existing bunx if it exists
344344
if (fs.existsSync(bunxShimPath)) {
345345
fs.unlinkSync(bunxShimPath)
346346
}
347-
348-
// Create symlink from bunx to bun
349-
fs.symlinkSync('bun', bunxShimPath)
350-
351-
installedBinaries.push('bunx')
352-
353-
if (config.verbose) {
354-
console.warn(`Created bunx symlink: bunx -> bun (as specified in ts-pkgx script)`)
355-
}
356-
}
357-
catch (error) {
358-
if (config.verbose) {
359-
console.warn(`Failed to create bunx symlink: ${error instanceof Error ? error.message : String(error)}`)
360-
}
361-
// Don't fail the installation if bunx symlink creation fails
362347
}
348+
catch { /* ignore */ }
349+
350+
// We will write bunx after establishing BUN_INSTALL below so we can embed the path
363351

364352
// 2. Modify the bun shim to set BUN_INSTALL environment variable
365353
// This is critical for Bun to work correctly, as it needs to know where it's installed
@@ -453,7 +441,7 @@ exec "${binaryPath}" "$@"
453441
}
454442

455443
// Create a custom shim for Bun with BUN_INSTALL environment variable
456-
const customShimContent = `#!/bin/sh
444+
const bunShimContent = `#!/bin/sh
457445
# Launchpad shim for ${binary} (${domain} v${version})
458446
459447
# Set up Bun environment variables
@@ -463,16 +451,29 @@ export BUN_INSTALL="${officialBunDir}"
463451
exec "${binaryPath}" "$@"
464452
`
465453
// Write the custom shim directly to the file
466-
fs.writeFileSync(shimPath, customShimContent)
454+
fs.writeFileSync(shimPath, bunShimContent)
467455
fs.chmodSync(shimPath, 0o755)
468456

469-
// Skip the default shim writing by continuing to the next binary
457+
// Create bunx shim that runs `bun x`
458+
const bunxShimContent = `#!/bin/sh
459+
# Launchpad shim for bunx (${domain} v${version})
460+
461+
# Set up Bun environment variables
462+
export BUN_INSTALL="${officialBunDir}"
463+
464+
# Execute bun x with forwarded args
465+
exec "${binaryPath}" x "$@"
466+
`
467+
fs.writeFileSync(bunxShimPath, bunxShimContent)
468+
fs.chmodSync(bunxShimPath, 0o755)
469+
470470
if (config.verbose) {
471-
console.warn(`Added BUN_INSTALL environment variable to bun shim`)
471+
console.warn(`Created bun shim with BUN_INSTALL and bunx shim that proxies to 'bun x'`)
472472
console.warn(`Created Bun directory structure at ${officialBunDir}`)
473473
}
474474

475475
installedBinaries.push(binary)
476+
installedBinaries.push('bunx')
476477
continue
477478
}
478479
catch (error) {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2+
import fs from 'node:fs'
3+
import os from 'node:os'
4+
import path from 'node:path'
5+
import { createShims } from '../src/install-helpers'
6+
7+
/**
8+
* Tests for Bun shim creation: ensure bunx is a proper shim that runs `bun x`.
9+
*/
10+
describe('Bun shim creation', () => {
11+
let tempDir: string
12+
let installPath: string
13+
let packageDir: string
14+
15+
beforeEach(() => {
16+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'launchpad-bun-shim-'))
17+
installPath = tempDir // install-helpers writes shims to installPath/bin
18+
19+
// Create fake bun package layout: <installPath>/bun.sh/v1.2.3/bin/bun
20+
const domainDir = path.join(tempDir, 'bun.sh')
21+
packageDir = path.join(domainDir, 'v1.2.3')
22+
const binDir = path.join(packageDir, 'bin')
23+
fs.mkdirSync(binDir, { recursive: true })
24+
25+
const bunBinaryPath = path.join(binDir, 'bun')
26+
// Create a minimal executable stub to act as bun binary
27+
fs.writeFileSync(
28+
bunBinaryPath,
29+
'#!/bin/sh\necho "fake bun"\n',
30+
{ mode: 0o755 },
31+
)
32+
})
33+
34+
afterEach(() => {
35+
try {
36+
if (fs.existsSync(tempDir)) {
37+
fs.rmSync(tempDir, { recursive: true, force: true })
38+
}
39+
}
40+
catch {
41+
// ignore cleanup errors
42+
}
43+
})
44+
45+
it('creates bun and bunx shims with correct behavior and env', async () => {
46+
const domain = 'bun.sh'
47+
const version = '1.2.3'
48+
49+
const created = await createShims(packageDir, installPath, domain, version)
50+
51+
// Expect bun and bunx reported
52+
expect(created).toContain('bun')
53+
expect(created).toContain('bunx')
54+
55+
const shimDir = path.join(installPath, 'bin')
56+
const bunShim = path.join(shimDir, 'bun')
57+
const bunxShim = path.join(shimDir, 'bunx')
58+
59+
// Files exist and are executable
60+
expect(fs.existsSync(bunShim)).toBe(true)
61+
expect(fs.existsSync(bunxShim)).toBe(true)
62+
63+
const bunMode = fs.statSync(bunShim).mode & 0o111
64+
const bunxMode = fs.statSync(bunxShim).mode & 0o111
65+
expect(bunMode).not.toBe(0)
66+
expect(bunxMode).not.toBe(0)
67+
68+
const bunShimContent = fs.readFileSync(bunShim, 'utf8')
69+
const bunxShimContent = fs.readFileSync(bunxShim, 'utf8')
70+
71+
// bun shim should export BUN_INSTALL and exec the actual bun binary
72+
expect(bunShimContent).toContain('export BUN_INSTALL=')
73+
expect(bunShimContent).toContain('exec "')
74+
expect(bunShimContent).toContain('" "$@"')
75+
76+
// bunx shim should export BUN_INSTALL and call bun with `x` subcommand
77+
expect(bunxShimContent).toContain('export BUN_INSTALL=')
78+
expect(bunxShimContent).toMatch(/exec\s+".*bun"\s+x\s+"\$@"/)
79+
80+
// Ensure .bun structure and symlinks are created
81+
const officialDir = path.join(installPath, '.bun')
82+
const officialBinDir = path.join(officialDir, 'bin')
83+
const officialBun = path.join(officialBinDir, 'bun')
84+
const officialBunx = path.join(officialBinDir, 'bunx')
85+
expect(fs.existsSync(officialDir)).toBe(true)
86+
expect(fs.existsSync(officialBinDir)).toBe(true)
87+
expect(fs.existsSync(officialBun)).toBe(true)
88+
expect(fs.existsSync(officialBunx)).toBe(true)
89+
90+
// Optional: node symlink should exist pointing to bun (created only if absent)
91+
const nodeShim = path.join(shimDir, 'node')
92+
expect(fs.existsSync(nodeShim)).toBe(true)
93+
})
94+
})

0 commit comments

Comments
 (0)