Skip to content

Commit 1782c6d

Browse files
committed
chore: wip
1 parent ee52259 commit 1782c6d

File tree

5 files changed

+155
-10
lines changed

5 files changed

+155
-10
lines changed

packages/launchpad/bin/cli.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,104 @@ const version = packageJson.default?.version || packageJson.version || '0.0.0'
2525
// Default version for setup command (derived from package.json version)
2626
const DEFAULT_SETUP_VERSION = `v${version}`
2727

28+
// Notify shell integration to refresh global paths on next prompt
29+
function triggerShellGlobalRefresh(): void {
30+
try {
31+
const refreshDir = path.join(homedir(), '.cache', 'launchpad', 'shell_cache')
32+
fs.mkdirSync(refreshDir, { recursive: true })
33+
const marker = path.join(refreshDir, 'global_refresh_needed')
34+
fs.writeFileSync(marker, '')
35+
}
36+
catch {
37+
// Non-fatal: shell will refresh on next activation anyway
38+
}
39+
}
40+
41+
// Install pluggable shell hooks for newly available tools (no hardcoding in shellcode)
42+
function ensurePostInstallHooks(): void {
43+
try {
44+
const home = homedir()
45+
const initDir = path.join(home, '.config', 'launchpad', 'hooks', 'init.d')
46+
const refreshDir = path.join(home, '.config', 'launchpad', 'hooks', 'post-refresh.d')
47+
fs.mkdirSync(initDir, { recursive: true })
48+
fs.mkdirSync(refreshDir, { recursive: true })
49+
50+
// Starship prompt hook (installed only if starship is present)
51+
const starshipPathCandidates = [
52+
path.join(home, '.local', 'bin', 'starship'),
53+
'/usr/local/bin/starship',
54+
'/usr/bin/starship',
55+
]
56+
const hasStarship = starshipPathCandidates.some(p => fs.existsSync(p))
57+
if (hasStarship) {
58+
const hookContent = [
59+
'# Launchpad hook: initialize starship prompt if available',
60+
'if command -v starship >/dev/null 2>&1; then',
61+
' if [[ -n "$ZSH_VERSION" ]]; then',
62+
' eval "$(starship init zsh)" >/dev/null 2>&1 || true',
63+
' elif [[ -n "$BASH_VERSION" ]]; then',
64+
' eval "$(starship init bash)" >/dev/null 2>&1 || true',
65+
' fi',
66+
'fi',
67+
'',
68+
].join('\n')
69+
70+
// Ensure starship wins over other prompt initializers by running late
71+
const initHook = path.join(initDir, '99-starship.sh')
72+
const refreshHook = path.join(refreshDir, '99-starship.sh')
73+
74+
// Write or update if content differs
75+
try {
76+
const existing = fs.existsSync(initHook) ? fs.readFileSync(initHook, 'utf8') : ''
77+
if (existing !== hookContent)
78+
fs.writeFileSync(initHook, hookContent, { mode: 0o644 })
79+
}
80+
catch {}
81+
try {
82+
const existing = fs.existsSync(refreshHook) ? fs.readFileSync(refreshHook, 'utf8') : ''
83+
if (existing !== hookContent)
84+
fs.writeFileSync(refreshHook, hookContent, { mode: 0o644 })
85+
}
86+
catch {}
87+
}
88+
}
89+
catch {
90+
// Best-effort hooks; ignore failures
91+
}
92+
}
93+
94+
function hasShellIntegration(): boolean {
95+
try {
96+
const home = homedir()
97+
const zshrc = path.join(process.env.ZDOTDIR || home, '.zshrc')
98+
const bashrc = path.join(home, '.bashrc')
99+
const bashProfile = path.join(home, '.bash_profile')
100+
const needle1 = 'launchpad dev:shellcode'
101+
const needle2 = 'LAUNCHPAD_SHELL_INTEGRATION=1'
102+
const files = [zshrc, bashrc, bashProfile].filter(f => fs.existsSync(f))
103+
for (const file of files) {
104+
const content = fs.readFileSync(file, 'utf8')
105+
if (content.includes(needle1) || content.includes(needle2))
106+
return true
107+
}
108+
}
109+
catch {}
110+
return false
111+
}
112+
113+
async function ensureShellIntegrationInstalled(): Promise<void> {
114+
try {
115+
if (!hasShellIntegration()) {
116+
// Install integration hooks silently
117+
const { default: integrate } = await import('../src/dev/integrate')
118+
await integrate('install', { dryrun: false })
119+
}
120+
}
121+
catch {
122+
// Best-effort; ignore failures
123+
}
124+
}
125+
28126
/**
29127
* Core setup logic that can be called from both setup and upgrade commands
30128
* Returns true if verification succeeded, false if it failed
@@ -745,11 +843,20 @@ async function installGlobalDependencies(options: {
745843
const globalEnvDir = path.join(homedir(), '.local', 'share', 'launchpad', 'global')
746844

747845
try {
846+
// Suppress internal summary to avoid duplicate success lines
847+
process.env.LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY = 'true'
748848
const results = await install(filteredPackages, globalEnvDir)
849+
delete process.env.LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY
749850

750851
// Create symlinks to ~/.local/bin for global accessibility
751852
await createGlobalBinarySymlinks(globalEnvDir)
752853

854+
// Ensure shell integration is installed for current user
855+
await ensureShellIntegrationInstalled()
856+
// Ensure post-install hooks are present and signal shell to refresh
857+
ensurePostInstallHooks()
858+
triggerShellGlobalRefresh()
859+
753860
if (!options.quiet) {
754861
if (results.length > 0) {
755862
console.log(`🎉 Successfully installed ${filteredPackages.length} global dependencies (${results.length} binaries)`)
@@ -823,13 +930,21 @@ cli
823930

824931
const defaultInstallPath = path.join(homedir(), '.local', 'share', 'launchpad', 'global')
825932
const installPath = options.path || defaultInstallPath
933+
process.env.LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY = 'true'
826934
const results = await installDependenciesOnly(packageList, installPath)
935+
delete process.env.LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY
827936

828937
// Create symlinks to ~/.local/bin for global accessibility when using default path
829938
if (!options.path) {
830939
await createGlobalBinarySymlinks(installPath)
831940
}
832941

942+
// Ensure shell integration is installed for current user
943+
await ensureShellIntegrationInstalled()
944+
// Ensure post-install hooks are present and signal shell to refresh
945+
ensurePostInstallHooks()
946+
triggerShellGlobalRefresh()
947+
833948
if (!options.quiet && options.verbose && results.length > 0) {
834949
// Only show file list in verbose mode since installDependenciesOnly already shows summary
835950
results.forEach((file) => {
@@ -850,13 +965,21 @@ cli
850965
// Use Launchpad global directory by default instead of /usr/local
851966
const defaultInstallPath = path.join(homedir(), '.local', 'share', 'launchpad', 'global')
852967
const installPath = options.path || defaultInstallPath
968+
process.env.LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY = 'true'
853969
const results = await install(packageList, installPath)
970+
delete process.env.LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY
854971

855972
// Create symlinks to ~/.local/bin for global accessibility when using default path
856973
if (!options.path) {
857974
await createGlobalBinarySymlinks(installPath)
858975
}
859976

977+
// Ensure shell integration is installed for current user
978+
await ensureShellIntegrationInstalled()
979+
// Ensure post-install hooks are present and signal shell to refresh
980+
ensurePostInstallHooks()
981+
triggerShellGlobalRefresh()
982+
860983
if (!options.quiet) {
861984
if (results.length > 0) {
862985
console.log(`🎉 Successfully installed ${packageList.join(', ')} (${results.length} ${results.length === 1 ? 'binary' : 'binaries'})`)

packages/launchpad/src/dev/shellcode.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,9 +881,23 @@ __launchpad_ensure_system_path() {
881881
# Always ensure system paths are available
882882
__launchpad_ensure_system_path
883883
884+
# Generic hook sourcing (allows prompt/tool activation without hardcoding)
885+
__launchpad_source_hooks_dir() {
886+
local dir="$1"
887+
if [[ -d "$dir" ]]; then
888+
for hook in "$dir"/*.sh; do
889+
if [[ -f "$hook" ]]; then
890+
# shellcheck disable=SC1090
891+
source "$hook" >/dev/null 2>&1 || true
892+
fi
893+
done
894+
fi
895+
}
896+
884897
# One-time setup on shell initialization
885898
__launchpad_setup_global_deps
886899
__launchpad_ensure_global_path
900+
__launchpad_source_hooks_dir "$HOME/.config/launchpad/hooks/init.d"
887901
888902
# Clear command hash table on initial load
889903
hash -r 2>/dev/null || true
@@ -898,6 +912,7 @@ __launchpad_auto_refresh_check() {
898912
# Remove marker and refresh
899913
rm -f "$refresh_marker" 2>/dev/null
900914
__launchpad_refresh_global_paths
915+
__launchpad_source_hooks_dir "$HOME/.config/launchpad/hooks/post-refresh.d"
901916
fi
902917
}
903918

packages/launchpad/src/install-core.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,7 @@ export async function installPackage(packageName: string, packageSpec: string, i
297297
const packageDir = path.join(installPath, domain, `v${version}`)
298298
await createLibrarySymlinks(packageDir, domain)
299299

300-
if (config.verbose) {
301-
console.log(`Successfully installed ${domain} v${version}`)
302-
}
300+
// Per-package success is logged once later with consistent formatting
303301

304302
return installedFiles
305303
}

packages/launchpad/src/install-main.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable no-console */
22
import type { PackageSpec } from './types'
33
import fs from 'node:fs'
4+
import process from 'node:process'
45
import { config } from './config'
56
import { resolveAllDependencies } from './dependency-resolution'
67
import { installPackage } from './install-core'
@@ -149,16 +150,17 @@ export async function install(packages: PackageSpec | PackageSpec[], basePath?:
149150
}
150151
}
151152

152-
// Show final summary
153-
if (allInstalledFiles.length > 0) {
153+
// Show final summary (allow CLI to suppress this to avoid duplicate success lines)
154+
const suppressSummary = process.env.LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY === 'true'
155+
if (allInstalledFiles.length > 0 && !suppressSummary) {
154156
if (config.verbose) {
155157
console.log(`✅ Successfully installed ${allInstalledFiles.length} files`)
156158
}
157159
else {
158160
logUniqueMessage(`✅ Successfully set up environment with ${allInstalledFiles.length} files`)
159161
}
160162
}
161-
else {
163+
else if (!suppressSummary) {
162164
if (config.verbose) {
163165
console.log(`ℹ️ No new files installed (packages may have been already installed)`)
164166
}

packages/launchpad/test/global-deps-filtering.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2+
import { spawn } from 'node:child_process'
23
import fs from 'node:fs'
34
import os from 'node:os'
45
import path from 'node:path'
56
import process from 'node:process'
6-
import { spawn } from 'node:child_process'
77

88
function getTestEnv(extra?: Record<string, string>): NodeJS.ProcessEnv {
99
return {
@@ -82,11 +82,18 @@ dependencies:
8282
let stdout = ''
8383
let stderr = ''
8484

85-
proc.stdout.on('data', (data) => { stdout += data.toString() })
86-
proc.stderr.on('data', (data) => { stderr += data.toString() })
85+
proc.stdout.on('data', (data) => {
86+
stdout += data.toString()
87+
})
88+
proc.stderr.on('data', (data) => {
89+
stderr += data.toString()
90+
})
8791

8892
const timeout = setTimeout(() => {
89-
try { proc.kill() } catch {}
93+
try {
94+
proc.kill()
95+
}
96+
catch {}
9097
reject(new Error('Timed out waiting for CLI output'))
9198
}, 20000)
9299

0 commit comments

Comments
 (0)