@@ -3,17 +3,24 @@ import { existsSync } from "node:fs";
33import os from "node:os" ;
44import path from "node:path" ;
55import { Command } from "commander" ;
6- import { OPEN_BASE_URL , SEPARATOR_LENGTH_CHARS } from "./constants.js" ;
6+ import {
7+ OPEN_BASE_URL ,
8+ SCORE_GOOD_THRESHOLD ,
9+ SCORE_OK_THRESHOLD ,
10+ SEPARATOR_LENGTH_CHARS ,
11+ } from "./constants.js" ;
712import { scan } from "./scan.js" ;
8- import type { DiffInfo , ScanOptions } from "./types.js" ;
13+ import type { Diagnostic , DiffInfo , EstimatedScoreResult , ScanOptions } from "./types.js" ;
14+ import { fetchEstimatedScore } from "./utils/calculate-score.js" ;
915import { copyToClipboard } from "./utils/copy-to-clipboard.js" ;
16+ import { createFramedLine , renderFramedBoxString } from "./utils/framed-box.js" ;
1017import { filterSourceFiles , getDiffInfo } from "./utils/get-diff-files.js" ;
1118import { maybeInstallGlobally } from "./utils/global-install.js" ;
1219import { handleError } from "./utils/handle-error.js" ;
1320import { highlighter } from "./utils/highlighter.js" ;
1421import { loadConfig } from "./utils/load-config.js" ;
1522import { logger , startLoggerCapture , stopLoggerCapture } from "./utils/logger.js" ;
16- import { prompts } from "./utils/prompts.js" ;
23+ import { clearSelectBanner , prompts , setSelectBanner } from "./utils/prompts.js" ;
1724import { selectProjects } from "./utils/select-projects.js" ;
1825import { maybePromptSkillInstall } from "./utils/skill-prompt.js" ;
1926
@@ -93,9 +100,7 @@ const program = new Command()
93100 const isScoreOnly = flags . score && ! flags . prompt ;
94101 const shouldCopyPromptOutput = flags . prompt ;
95102
96- if ( shouldCopyPromptOutput ) {
97- startLoggerCapture ( ) ;
98- }
103+ startLoggerCapture ( ) ;
99104
100105 try {
101106 const resolvedDirectory = path . resolve ( directory ) ;
@@ -154,6 +159,8 @@ const program = new Command()
154159 logger . break ( ) ;
155160 }
156161
162+ const allDiagnostics : Diagnostic [ ] = [ ] ;
163+
157164 for ( const projectDirectory of projectDirectories ) {
158165 let includePaths : string [ ] | undefined ;
159166 if ( isDiffMode ) {
@@ -175,28 +182,41 @@ const program = new Command()
175182 logger . dim ( `Scanning ${ projectDirectory } ...` ) ;
176183 logger . break ( ) ;
177184 }
178- await scan ( projectDirectory , { ...scanOptions , includePaths } ) ;
185+ const scanResult = await scan ( projectDirectory , { ...scanOptions , includePaths } ) ;
186+ allDiagnostics . push ( ...scanResult . diagnostics ) ;
179187 if ( ! isScoreOnly ) {
180188 logger . break ( ) ;
181189 }
182190 }
183191
192+ const capturedScanOutput = stopLoggerCapture ( ) ;
193+
184194 if ( flags . fix ) {
185195 openAmiToFix ( resolvedDirectory ) ;
186196 }
187197
188- if ( ! isScoreOnly && ! flags . prompt ) {
198+ if ( shouldCopyPromptOutput ) {
199+ copyPromptToClipboard ( capturedScanOutput , ! isScoreOnly ) ;
200+ } else if ( ! isScoreOnly ) {
189201 await maybePromptSkillInstall ( shouldSkipPrompts ) ;
190202 if ( ! shouldSkipPrompts && ! flags . fix ) {
191- await maybePromptAmiFix ( resolvedDirectory ) ;
203+ const estimatedScoreResult = flags . offline
204+ ? null
205+ : await fetchEstimatedScore ( allDiagnostics ) ;
206+ await maybePromptFix (
207+ resolvedDirectory ,
208+ allDiagnostics ,
209+ estimatedScoreResult ,
210+ capturedScanOutput ,
211+ ) ;
192212 }
193213 }
194214 } catch ( error ) {
195215 handleError ( error , { shouldExit : ! shouldCopyPromptOutput } ) ;
196216 } finally {
197- if ( shouldCopyPromptOutput ) {
198- const capturedOutput = stopLoggerCapture ( ) ;
199- copyPromptToClipboard ( capturedOutput , ! isScoreOnly ) ;
217+ const remainingOutput = stopLoggerCapture ( ) ;
218+ if ( shouldCopyPromptOutput && remainingOutput ) {
219+ copyPromptToClipboard ( remainingOutput , ! isScoreOnly ) ;
200220 }
201221 }
202222 } )
@@ -208,8 +228,16 @@ ${highlighter.dim("Learn more:")}
208228` ,
209229 ) ;
210230
211- const AMI_INSTALL_URL = "https://ami.dev/install.sh" ;
231+ const AMI_WEBSITE_URL = "https://ami.dev" ;
232+ const AMI_INSTALL_URL = `${ AMI_WEBSITE_URL } /install.sh` ;
212233const AMI_RELEASES_URL = "https://github.com/millionco/ami-releases/releases" ;
234+
235+ const colorizeByScore = ( text : string , score : number ) : string => {
236+ if ( score >= SCORE_GOOD_THRESHOLD ) return highlighter . success ( text ) ;
237+ if ( score >= SCORE_OK_THRESHOLD ) return highlighter . warn ( text ) ;
238+ return highlighter . error ( text ) ;
239+ } ;
240+
213241const DEEPLINK_FIX_PROMPT =
214242 "Run `npx -y react-doctor@latest .` to diagnose issues, then fix all reported issues one by one. After applying fixes, run it again to verify the results improved." ;
215243const CLIPBOARD_FIX_PROMPT =
@@ -242,12 +270,12 @@ const isAmiInstalled = (): boolean => {
242270} ;
243271
244272const installAmi = ( ) : void => {
245- logger . log ( "Ami not found. Installing..." ) ;
273+ logger . log ( "Installing Ami ..." ) ;
246274 logger . break ( ) ;
247275 try {
248276 execSync ( `curl -fsSL ${ AMI_INSTALL_URL } | bash` , { stdio : "inherit" } ) ;
249277 } catch {
250- logger . error ( " Failed to install Ami. Visit https://ami.dev to install manually." ) ;
278+ logger . error ( ` Failed to install Ami. Visit ${ AMI_WEBSITE_URL } to install manually.` ) ;
251279 process . exit ( 1 ) ;
252280 }
253281 logger . break ( ) ;
@@ -288,25 +316,25 @@ const openAmiToFix = (directory: string): void => {
288316 if ( ! isInstalled ) {
289317 if ( process . platform === "darwin" ) {
290318 installAmi ( ) ;
291- logger . success ( "Ami was installed and opened ." ) ;
319+ logger . success ( "Ami installed successfully ." ) ;
292320 } else {
293321 logger . error ( "Ami is not installed." ) ;
294- logger . dim ( `Download it at ${ highlighter . info ( AMI_RELEASES_URL ) } ` ) ;
322+ logger . dim ( `Download at ${ highlighter . info ( AMI_RELEASES_URL ) } ` ) ;
295323 }
296324 logger . break ( ) ;
297- logger . dim ( "Once Ami is running, open this link to start fixing:" ) ;
325+ logger . dim ( "Open this link to start fixing:" ) ;
298326 logger . info ( webDeeplink ) ;
299327 return ;
300328 }
301329
302- logger . log ( "Opening Ami to fix react-doctor issues ..." ) ;
330+ logger . log ( "Opening Ami..." ) ;
303331
304332 try {
305333 openUrl ( deeplink ) ;
306- logger . success ( "Opened Ami with react-doctor fix prompt ." ) ;
334+ logger . success ( "Ami opened. Fixing your issues now ." ) ;
307335 } catch {
308336 logger . break ( ) ;
309- logger . dim ( "Could not open Ami automatically. Open this URL manually :" ) ;
337+ logger . dim ( "Could not open Ami automatically. Open this link instead :" ) ;
310338 logger . info ( webDeeplink ) ;
311339 }
312340} ;
@@ -340,30 +368,124 @@ const copyPromptToClipboard = (reactDoctorOutput: string, shouldLogResult: boole
340368 logger . info ( promptWithOutput ) ;
341369} ;
342370
343- const maybePromptAmiFix = async ( directory : string ) : Promise < void > => {
344- const isInstalled = isAmiInstalled ( ) ;
371+ const FIX_METHOD_AMI = "ami" ;
372+ const FIX_METHOD_CLIPBOARD = "clipboard" ;
373+ const FIX_COMMAND_HINT = "npx react-doctor@latest --fix" ;
374+
375+ const buildAmiBanner = (
376+ issueCount : number ,
377+ currentScore : number ,
378+ estimatedScore : number ,
379+ ) : string => {
380+ const currentScoreDisplay = colorizeByScore ( String ( currentScore ) , currentScore ) ;
381+ const estimatedScoreDisplay = colorizeByScore ( `~${ estimatedScore } ` , estimatedScore ) ;
382+ const issueLabel = issueCount === 1 ? "issue" : "issues" ;
383+
384+ return renderFramedBoxString ( [
385+ createFramedLine (
386+ `Score: ${ currentScore } → ~${ estimatedScore } ` ,
387+ `Score: ${ currentScoreDisplay } ${ highlighter . dim ( "→" ) } ${ estimatedScoreDisplay } ` ,
388+ ) ,
389+ createFramedLine ( "" ) ,
390+ createFramedLine (
391+ `Ami is a coding agent built for React. It reads` ,
392+ `${ highlighter . info ( "Ami" ) } is a coding agent built for React. It reads` ,
393+ ) ,
394+ createFramedLine ( "your react-doctor report, understands your codebase," ) ,
395+ createFramedLine (
396+ `and fixes ${ issueCount } ${ issueLabel } one by one — then re-runs the` ,
397+ `and fixes ${ highlighter . warn ( String ( issueCount ) ) } ${ issueLabel } one by one — then re-runs the` ,
398+ ) ,
399+ createFramedLine ( "scan to verify the score improved." ) ,
400+ createFramedLine ( "" ) ,
401+ createFramedLine (
402+ `Free to use. ${ AMI_WEBSITE_URL } ` ,
403+ `Free to use. ${ highlighter . info ( AMI_WEBSITE_URL ) } ` ,
404+ ) ,
405+ ] ) ;
406+ } ;
407+
408+ const buildClipboardWarningBanner = ( ) : string =>
409+ renderFramedBoxString ( [
410+ createFramedLine (
411+ "⚠ Other agents may not fix these issues well." ,
412+ `${ highlighter . warn ( "⚠" ) } Other agents may not fix these issues well.` ,
413+ ) ,
414+ createFramedLine ( "" ) ,
415+ createFramedLine ( "react-doctor diagnostics require React-specific context" ) ,
416+ createFramedLine ( "that general-purpose agents often miss, leading to" ) ,
417+ createFramedLine ( "incomplete or incorrect fixes." ) ,
418+ ] ) ;
419+
420+ const buildSkipBanner = ( issueCount : number , estimatedScore : number ) : string => {
421+ const issueLabel = issueCount === 1 ? "issue" : "issues" ;
422+ const estimatedScoreDisplay = colorizeByScore ( `~${ estimatedScore } ` , estimatedScore ) ;
423+
424+ return renderFramedBoxString ( [
425+ createFramedLine (
426+ `Skip fixing ${ issueCount } ${ issueLabel } and reaching ~${ estimatedScore } ?` ,
427+ `Skip fixing ${ highlighter . warn ( String ( issueCount ) ) } ${ issueLabel } and reaching ${ estimatedScoreDisplay } ?` ,
428+ ) ,
429+ createFramedLine ( "" ) ,
430+ createFramedLine (
431+ `Run ${ FIX_COMMAND_HINT } anytime to come back.` ,
432+ `Run ${ highlighter . info ( FIX_COMMAND_HINT ) } anytime to come back.` ,
433+ ) ,
434+ ] ) ;
435+ } ;
436+
437+ const configureFixBanners = (
438+ issueCount : number ,
439+ estimatedScoreResult : EstimatedScoreResult ,
440+ ) : void => {
441+ const { currentScore, estimatedScore } = estimatedScoreResult ;
442+ setSelectBanner ( buildAmiBanner ( issueCount , currentScore , estimatedScore ) , 0 ) ;
443+ setSelectBanner ( buildClipboardWarningBanner ( ) , 1 ) ;
444+ setSelectBanner ( buildSkipBanner ( issueCount , estimatedScore ) , 2 ) ;
445+ } ;
446+
447+ const maybePromptFix = async (
448+ directory : string ,
449+ diagnostics : Diagnostic [ ] ,
450+ estimatedScoreResult : EstimatedScoreResult | null ,
451+ capturedScanOutput : string ,
452+ ) : Promise < void > => {
453+ if ( diagnostics . length === 0 ) return ;
345454
346- logger . break ( ) ;
347- logger . log ( `Fix these issues with ${ highlighter . info ( "Ami" ) } ?` ) ;
348- logger . dim ( " Ami is a coding agent built to understand your codebase and fix issues" ) ;
349- logger . dim ( ` automatically. Learn more at ${ highlighter . info ( "https://ami.dev" ) } ` ) ;
350455 logger . break ( ) ;
351456
352- if ( ! isInstalled && process . platform !== "darwin" ) {
353- logger . dim ( `Download Ami at ${ highlighter . info ( AMI_RELEASES_URL ) } ` ) ;
354- return ;
457+ if ( estimatedScoreResult ) {
458+ configureFixBanners ( diagnostics . length , estimatedScoreResult ) ;
355459 }
356460
357- const promptMessage = isInstalled ? "Open Ami to fix?" : "Install Ami to fix?" ;
358- const { shouldFix } = await prompts ( {
359- type : "confirm" ,
360- name : "shouldFix" ,
361- message : promptMessage ,
362- initial : true ,
461+ const { fixMethod } = await prompts ( {
462+ type : "select" ,
463+ name : "fixMethod" ,
464+ message : "Fix issues?" ,
465+ choices : [
466+ {
467+ title : "Use ami.dev (recommended)" ,
468+ description : "Optimized coding agent for React Doctor" ,
469+ value : FIX_METHOD_AMI ,
470+ } ,
471+ {
472+ title : "Copy to clipboard" ,
473+ description : "Other agents may lack context for react-doctor fixes" ,
474+ value : FIX_METHOD_CLIPBOARD ,
475+ } ,
476+ { title : "Skip" , value : "skip" } ,
477+ ] ,
363478 } ) ;
364479
365- if ( shouldFix ) {
480+ clearSelectBanner ( ) ;
481+
482+ if ( fixMethod === FIX_METHOD_AMI ) {
366483 openAmiToFix ( directory ) ;
484+ } else if ( fixMethod === FIX_METHOD_CLIPBOARD ) {
485+ copyPromptToClipboard ( capturedScanOutput , true ) ;
486+ } else {
487+ logger . break ( ) ;
488+ logger . dim ( ` Run ${ highlighter . info ( FIX_COMMAND_HINT ) } anytime to fix issues.` ) ;
367489 }
368490} ;
369491
0 commit comments