@@ -10,6 +10,227 @@ const CACHE_DIR = path.join(homedir(), '.cache', 'launchpad')
1010const BINARY_CACHE_DIR = path . join ( CACHE_DIR , 'binaries' , 'packages' )
1111const CACHE_METADATA_FILE = path . join ( CACHE_DIR , 'cache-metadata.json' )
1212
13+ // Shell environment cache configuration
14+ const SHELL_CACHE_DIR = path . join ( CACHE_DIR , 'shell_cache' )
15+ const ENV_CACHE_FILE = path . join ( SHELL_CACHE_DIR , 'env_cache' )
16+
17+ // In-memory cache for fast lookups (singleton pattern)
18+ interface EnvCacheEntry {
19+ projectDir : string
20+ depFile : string
21+ depMtime : number
22+ envDir : string
23+ }
24+
25+ class EnvCacheManager {
26+ private cache : Map < string , EnvCacheEntry > = new Map ( )
27+ private loaded : boolean = false
28+
29+ /**
30+ * Load the entire cache file into memory for O(1) lookups
31+ */
32+ load ( ) : void {
33+ if ( this . loaded )
34+ return
35+
36+ try {
37+ if ( fs . existsSync ( ENV_CACHE_FILE ) ) {
38+ const content = fs . readFileSync ( ENV_CACHE_FILE , 'utf-8' )
39+ const lines = content . trim ( ) . split ( '\n' )
40+
41+ for ( const line of lines ) {
42+ if ( ! line )
43+ continue
44+
45+ const [ projectDir , depFile , depMtime , envDir ] = line . split ( '|' )
46+ if ( projectDir && envDir ) {
47+ this . cache . set ( projectDir , {
48+ projectDir,
49+ depFile : depFile || '' ,
50+ depMtime : Number . parseInt ( depMtime || '0' , 10 ) ,
51+ envDir,
52+ } )
53+ }
54+ }
55+ }
56+
57+ this . loaded = true
58+ }
59+ catch ( error ) {
60+ if ( config . verbose ) {
61+ console . warn ( 'Failed to load environment cache:' , error )
62+ }
63+ }
64+ }
65+
66+ /**
67+ * Get cached environment for a project directory
68+ */
69+ get ( projectDir : string ) : EnvCacheEntry | null {
70+ if ( ! this . loaded )
71+ this . load ( )
72+
73+ return this . cache . get ( projectDir ) || null
74+ }
75+
76+ /**
77+ * Set cached environment for a project directory
78+ */
79+ set ( projectDir : string , depFile : string , envDir : string ) : void {
80+ if ( ! this . loaded )
81+ this . load ( )
82+
83+ // Get mtime of dependency file if it exists
84+ let depMtime = 0
85+ if ( depFile && fs . existsSync ( depFile ) ) {
86+ const stats = fs . statSync ( depFile )
87+ depMtime = Math . floor ( stats . mtimeMs / 1000 )
88+ }
89+
90+ const entry : EnvCacheEntry = {
91+ projectDir,
92+ depFile,
93+ depMtime,
94+ envDir,
95+ }
96+
97+ // Update in-memory cache immediately (instant for next lookups)
98+ this . cache . set ( projectDir , entry )
99+
100+ // Schedule async disk write (don't block)
101+ this . schedulePersist ( )
102+ }
103+
104+ private persistTimer : NodeJS . Timeout | null = null
105+ private persistPending : boolean = false
106+
107+ /**
108+ * Schedule a persist operation (debounced to batch multiple writes)
109+ */
110+ private schedulePersist ( ) : void {
111+ if ( this . persistTimer ) {
112+ // Already scheduled, just mark as pending
113+ this . persistPending = true
114+ return
115+ }
116+
117+ // Schedule persist after a short delay to batch multiple writes
118+ this . persistTimer = setTimeout ( ( ) => {
119+ this . persistTimer = null
120+ this . persist ( )
121+ } , 10 ) // 10ms debounce
122+ }
123+
124+ /**
125+ * Persist cache to disk
126+ */
127+ private persist ( ) : void {
128+ try {
129+ fs . mkdirSync ( SHELL_CACHE_DIR , { recursive : true } )
130+
131+ const lines : string [ ] = [ ]
132+ for ( const entry of this . cache . values ( ) ) {
133+ lines . push ( `${ entry . projectDir } |${ entry . depFile } |${ entry . depMtime } |${ entry . envDir } ` )
134+ }
135+
136+ // Write atomically using temp file
137+ const tempFile = `${ ENV_CACHE_FILE } .tmp.${ process . pid } `
138+ fs . writeFileSync ( tempFile , lines . join ( '\n' ) + '\n' )
139+ fs . renameSync ( tempFile , ENV_CACHE_FILE )
140+ }
141+ catch ( error ) {
142+ if ( config . verbose ) {
143+ console . warn ( 'Failed to persist environment cache:' , error )
144+ }
145+ }
146+ }
147+
148+ /**
149+ * Clear the cache
150+ */
151+ clear ( ) : void {
152+ this . cache . clear ( )
153+ this . loaded = false
154+
155+ try {
156+ if ( fs . existsSync ( ENV_CACHE_FILE ) ) {
157+ fs . unlinkSync ( ENV_CACHE_FILE )
158+ }
159+ }
160+ catch ( error ) {
161+ if ( config . verbose ) {
162+ console . warn ( 'Failed to clear environment cache:' , error )
163+ }
164+ }
165+ }
166+
167+ /**
168+ * Get cache statistics
169+ */
170+ getStats ( ) : { entries : number , size : number } {
171+ if ( ! this . loaded )
172+ this . load ( )
173+
174+ let size = 0
175+ try {
176+ if ( fs . existsSync ( ENV_CACHE_FILE ) ) {
177+ const stats = fs . statSync ( ENV_CACHE_FILE )
178+ size = stats . size
179+ }
180+ }
181+ catch {
182+ // Ignore errors
183+ }
184+
185+ return {
186+ entries : this . cache . size ,
187+ size,
188+ }
189+ }
190+
191+ /**
192+ * Validate cache entries and remove stale ones
193+ */
194+ validate ( ) : number {
195+ if ( ! this . loaded )
196+ this . load ( )
197+
198+ let removed = 0
199+ const toRemove : string [ ] = [ ]
200+
201+ for ( const [ projectDir , entry ] of this . cache . entries ( ) ) {
202+ // Check if environment directory still exists
203+ if ( ! fs . existsSync ( entry . envDir ) ) {
204+ toRemove . push ( projectDir )
205+ continue
206+ }
207+
208+ // Check if dependency file mtime has changed
209+ if ( entry . depFile && fs . existsSync ( entry . depFile ) ) {
210+ const stats = fs . statSync ( entry . depFile )
211+ const currentMtime = Math . floor ( stats . mtimeMs / 1000 )
212+ if ( currentMtime !== entry . depMtime ) {
213+ toRemove . push ( projectDir )
214+ }
215+ }
216+ }
217+
218+ for ( const projectDir of toRemove ) {
219+ this . cache . delete ( projectDir )
220+ removed ++
221+ }
222+
223+ if ( removed > 0 ) {
224+ this . persist ( )
225+ }
226+
227+ return removed
228+ }
229+ }
230+
231+ // Singleton instance
232+ export const envCache = new EnvCacheManager ( )
233+
13234/**
14235 * Load cache metadata
15236 */
0 commit comments