@@ -34,6 +34,44 @@ import {
3434 parseRecipeSpec
3535} from './cli-utils' ;
3636
37+ const isTTY = process . stdout . isTTY ?? false ;
38+ const SPINNER_FRAMES = [ '⠋' , '⠙' , '⠹' , '⠸' , '⠼' , '⠴' , '⠦' , '⠧' , '⠇' , '⠏' ] ;
39+
40+ class Spinner {
41+ private frameIndex = 0 ;
42+ private interval : NodeJS . Timeout | null = null ;
43+ private message = '' ;
44+
45+ start ( message : string ) : void {
46+ if ( ! isTTY ) return ;
47+ this . message = message ;
48+ this . render ( ) ;
49+ this . interval = setInterval ( ( ) => this . render ( ) , 80 ) ;
50+ }
51+
52+ update ( message : string ) : void {
53+ this . message = message ;
54+ if ( ! isTTY ) return ;
55+ this . render ( ) ;
56+ }
57+
58+ private render ( ) : void {
59+ const frame = SPINNER_FRAMES [ this . frameIndex ] ;
60+ this . frameIndex = ( this . frameIndex + 1 ) % SPINNER_FRAMES . length ;
61+ process . stdout . write ( `\r\x1b[K${ frame } ${ this . message } ` ) ;
62+ }
63+
64+ stop ( ) : void {
65+ if ( this . interval ) {
66+ clearInterval ( this . interval ) ;
67+ this . interval = null ;
68+ }
69+ if ( isTTY ) {
70+ process . stdout . write ( '\r\x1b[K' ) ;
71+ }
72+ }
73+ }
74+
3775// Import language modules to register printers
3876import '../text' ;
3977import '../json' ;
@@ -127,9 +165,13 @@ async function main() {
127165 console . log ( `Running recipe: ${ recipe . name } ` ) ;
128166 }
129167
130- // Discover source files
168+ const spinner = new Spinner ( ) ;
131169 const projectRoot = process . cwd ( ) ;
170+
171+ // Discover source files
172+ spinner . start ( 'Discovering source files...' ) ;
132173 const sourceFiles = await discoverFiles ( projectRoot , opts . verbose ) ;
174+ spinner . stop ( ) ;
133175
134176 if ( sourceFiles . length === 0 ) {
135177 console . log ( 'No source files found to process.' ) ;
@@ -141,7 +183,11 @@ async function main() {
141183 }
142184
143185 // Parse all source files
144- const parsedFiles = await parseFiles ( sourceFiles , projectRoot , opts . verbose ) ;
186+ spinner . start ( `Parsing ${ sourceFiles . length } files...` ) ;
187+ const parsedFiles = await parseFiles ( sourceFiles , projectRoot , opts . verbose , ( current , total , filePath ) => {
188+ spinner . update ( `Parsing [${ current } /${ total } ] ${ filePath } ` ) ;
189+ } ) ;
190+ spinner . stop ( ) ;
145191
146192 if ( parsedFiles . length === 0 ) {
147193 console . log ( 'No files could be parsed.' ) ;
@@ -151,8 +197,18 @@ async function main() {
151197 // Run the recipe with streaming output
152198 const ctx = new ExecutionContext ( ) ;
153199 let changeCount = 0 ;
200+ let processedCount = 0 ;
201+ const totalFiles = parsedFiles . length ;
202+
203+ spinner . start ( `Running recipe on ${ totalFiles } files...` ) ;
154204
155205 for await ( const result of scheduleRunStreaming ( recipe , parsedFiles , ctx ) ) {
206+ processedCount ++ ;
207+ const currentPath = result . after ?. sourcePath ?? result . before ?. sourcePath ?? '' ;
208+ spinner . update ( `Processing [${ processedCount } /${ totalFiles } ] ${ currentPath } ` ) ;
209+
210+ const statusMsg = `Processing [${ processedCount } /${ totalFiles } ] ${ currentPath } ` ;
211+
156212 if ( opts . dryRun ) {
157213 // Print colorized diff immediately (skip empty diffs)
158214 const diff = await result . diff ( ) ;
@@ -162,7 +218,9 @@ async function main() {
162218 ) ;
163219 if ( hasChanges ) {
164220 changeCount ++ ;
221+ spinner . stop ( ) ;
165222 console . log ( colorizeDiff ( diff ) ) ;
223+ spinner . start ( statusMsg ) ;
166224 }
167225 } else {
168226 // Apply changes immediately
@@ -175,20 +233,26 @@ async function main() {
175233 await fsp . writeFile ( filePath , content ) ;
176234
177235 changeCount ++ ;
236+ spinner . stop ( ) ;
178237 if ( result . before ) {
179238 console . log ( ` Modified: ${ result . after . sourcePath } ` ) ;
180239 } else {
181240 console . log ( ` Created: ${ result . after . sourcePath } ` ) ;
182241 }
242+ spinner . start ( statusMsg ) ;
183243 } else if ( result . before ) {
184244 const filePath = path . join ( projectRoot , result . before . sourcePath ) ;
185245 await fsp . unlink ( filePath ) ;
186246 changeCount ++ ;
247+ spinner . stop ( ) ;
187248 console . log ( ` Deleted: ${ result . before . sourcePath } ` ) ;
249+ spinner . start ( statusMsg ) ;
188250 }
189251 }
190252 }
191253
254+ spinner . stop ( ) ;
255+
192256 if ( changeCount === 0 ) {
193257 console . log ( 'No changes to make.' ) ;
194258 } else if ( opts . dryRun ) {
0 commit comments