Skip to content

Commit 61406e0

Browse files
committed
fix
1 parent 1b07fa2 commit 61406e0

File tree

9 files changed

+472
-84
lines changed

9 files changed

+472
-84
lines changed

packages/react-doctor/src/cli.ts

Lines changed: 159 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@ import { existsSync } from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
55
import { 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";
712
import { 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";
915
import { copyToClipboard } from "./utils/copy-to-clipboard.js";
16+
import { createFramedLine, renderFramedBoxString } from "./utils/framed-box.js";
1017
import { filterSourceFiles, getDiffInfo } from "./utils/get-diff-files.js";
1118
import { maybeInstallGlobally } from "./utils/global-install.js";
1219
import { handleError } from "./utils/handle-error.js";
1320
import { highlighter } from "./utils/highlighter.js";
1421
import { loadConfig } from "./utils/load-config.js";
1522
import { logger, startLoggerCapture, stopLoggerCapture } from "./utils/logger.js";
16-
import { prompts } from "./utils/prompts.js";
23+
import { clearSelectBanner, prompts, setSelectBanner } from "./utils/prompts.js";
1724
import { selectProjects } from "./utils/select-projects.js";
1825
import { 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`;
212233
const 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+
213241
const 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.";
215243
const CLIPBOARD_FIX_PROMPT =
@@ -242,12 +270,12 @@ const isAmiInstalled = (): boolean => {
242270
};
243271

244272
const 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

packages/react-doctor/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;
2222

2323
export const SCORE_API_URL = "https://www.react.doctor/api/score";
2424

25+
export const ESTIMATE_SCORE_API_URL = "https://www.react.doctor/api/estimate-score";
26+
2527
export const SHARE_BASE_URL = "https://www.react.doctor/share";
2628

2729
export const OPEN_BASE_URL = "https://www.react.doctor/open";

0 commit comments

Comments
 (0)