@@ -25,6 +25,104 @@ const version = packageJson.default?.version || packageJson.version || '0.0.0'
25
25
// Default version for setup command (derived from package.json version)
26
26
const DEFAULT_SETUP_VERSION = `v${ version } `
27
27
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
+
28
126
/**
29
127
* Core setup logic that can be called from both setup and upgrade commands
30
128
* Returns true if verification succeeded, false if it failed
@@ -745,11 +843,20 @@ async function installGlobalDependencies(options: {
745
843
const globalEnvDir = path . join ( homedir ( ) , '.local' , 'share' , 'launchpad' , 'global' )
746
844
747
845
try {
846
+ // Suppress internal summary to avoid duplicate success lines
847
+ process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY = 'true'
748
848
const results = await install ( filteredPackages , globalEnvDir )
849
+ delete process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY
749
850
750
851
// Create symlinks to ~/.local/bin for global accessibility
751
852
await createGlobalBinarySymlinks ( globalEnvDir )
752
853
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
+
753
860
if ( ! options . quiet ) {
754
861
if ( results . length > 0 ) {
755
862
console . log ( `🎉 Successfully installed ${ filteredPackages . length } global dependencies (${ results . length } binaries)` )
@@ -823,13 +930,21 @@ cli
823
930
824
931
const defaultInstallPath = path . join ( homedir ( ) , '.local' , 'share' , 'launchpad' , 'global' )
825
932
const installPath = options . path || defaultInstallPath
933
+ process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY = 'true'
826
934
const results = await installDependenciesOnly ( packageList , installPath )
935
+ delete process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY
827
936
828
937
// Create symlinks to ~/.local/bin for global accessibility when using default path
829
938
if ( ! options . path ) {
830
939
await createGlobalBinarySymlinks ( installPath )
831
940
}
832
941
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
+
833
948
if ( ! options . quiet && options . verbose && results . length > 0 ) {
834
949
// Only show file list in verbose mode since installDependenciesOnly already shows summary
835
950
results . forEach ( ( file ) => {
@@ -850,13 +965,21 @@ cli
850
965
// Use Launchpad global directory by default instead of /usr/local
851
966
const defaultInstallPath = path . join ( homedir ( ) , '.local' , 'share' , 'launchpad' , 'global' )
852
967
const installPath = options . path || defaultInstallPath
968
+ process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY = 'true'
853
969
const results = await install ( packageList , installPath )
970
+ delete process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY
854
971
855
972
// Create symlinks to ~/.local/bin for global accessibility when using default path
856
973
if ( ! options . path ) {
857
974
await createGlobalBinarySymlinks ( installPath )
858
975
}
859
976
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
+
860
983
if ( ! options . quiet ) {
861
984
if ( results . length > 0 ) {
862
985
console . log ( `🎉 Successfully installed ${ packageList . join ( ', ' ) } (${ results . length } ${ results . length === 1 ? 'binary' : 'binaries' } )` )
0 commit comments