react-doctor 0.0.23 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli.js +62 -237
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,15 +38,15 @@ Use `--verbose` to see affected files and line numbers:
|
|
|
38
38
|
npx -y react-doctor@latest . --verbose
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
## Install
|
|
41
|
+
## Install for your coding agent
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Teach your coding agent all 47+ React best practice rules:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
curl -fsSL https://react.doctor/install-skill.sh | bash
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Supports Cursor, Claude Code, Amp Code, Codex, Gemini CLI, OpenCode, Windsurf, and Antigravity.
|
|
50
50
|
|
|
51
51
|
## Options
|
|
52
52
|
|
|
@@ -62,8 +62,8 @@ Options:
|
|
|
62
62
|
-y, --yes skip prompts, scan all workspace projects
|
|
63
63
|
--project <name> select workspace project (comma-separated for multiple)
|
|
64
64
|
--diff [base] scan only files changed vs base branch
|
|
65
|
+
--no-ami skip Ami-related prompts
|
|
65
66
|
--fix open Ami to auto-fix all issues
|
|
66
|
-
--prompt copy latest scan output to clipboard
|
|
67
67
|
-h, --help display help for command
|
|
68
68
|
```
|
|
69
69
|
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
4
|
-
import fs, { existsSync, mkdirSync,
|
|
5
|
-
import os, {
|
|
4
|
+
import fs, { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import os, { tmpdir } from "node:os";
|
|
6
6
|
import path, { join } from "node:path";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
@@ -23,7 +23,6 @@ const PERFECT_SCORE = 100;
|
|
|
23
23
|
const SCORE_GOOD_THRESHOLD = 75;
|
|
24
24
|
const SCORE_OK_THRESHOLD = 50;
|
|
25
25
|
const SCORE_BAR_WIDTH_CHARS = 50;
|
|
26
|
-
const SEPARATOR_LENGTH_CHARS = 40;
|
|
27
26
|
const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;
|
|
28
27
|
const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;
|
|
29
28
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
@@ -41,11 +40,6 @@ const ERROR_RULE_PENALTY = 1.5;
|
|
|
41
40
|
const WARNING_RULE_PENALTY = .75;
|
|
42
41
|
const ERROR_ESTIMATED_FIX_RATE = .85;
|
|
43
42
|
const WARNING_ESTIMATED_FIX_RATE = .8;
|
|
44
|
-
const buildDiagnosticPayload = (diagnostics) => diagnostics.map((diagnostic) => ({
|
|
45
|
-
plugin: diagnostic.plugin,
|
|
46
|
-
rule: diagnostic.rule,
|
|
47
|
-
severity: diagnostic.severity
|
|
48
|
-
}));
|
|
49
43
|
const getScoreLabel = (score) => {
|
|
50
44
|
if (score >= SCORE_GOOD_THRESHOLD) return "Great";
|
|
51
45
|
if (score >= SCORE_OK_THRESHOLD) return "Needs work";
|
|
@@ -84,7 +78,7 @@ const calculateScore = async (diagnostics) => {
|
|
|
84
78
|
const response = await fetch(SCORE_API_URL, {
|
|
85
79
|
method: "POST",
|
|
86
80
|
headers: { "Content-Type": "application/json" },
|
|
87
|
-
body: JSON.stringify({ diagnostics
|
|
81
|
+
body: JSON.stringify({ diagnostics })
|
|
88
82
|
});
|
|
89
83
|
if (!response.ok) return null;
|
|
90
84
|
return await response.json();
|
|
@@ -97,7 +91,7 @@ const fetchEstimatedScore = async (diagnostics) => {
|
|
|
97
91
|
const response = await fetch(ESTIMATE_SCORE_API_URL, {
|
|
98
92
|
method: "POST",
|
|
99
93
|
headers: { "Content-Type": "application/json" },
|
|
100
|
-
body: JSON.stringify({ diagnostics
|
|
94
|
+
body: JSON.stringify({ diagnostics })
|
|
101
95
|
});
|
|
102
96
|
if (!response.ok) return estimateScoreLocally(diagnostics);
|
|
103
97
|
return await response.json();
|
|
@@ -183,6 +177,11 @@ const VITE_CONFIG_FILENAMES = [
|
|
|
183
177
|
"vite.config.mjs",
|
|
184
178
|
"vite.config.cjs"
|
|
185
179
|
];
|
|
180
|
+
const EXPO_APP_CONFIG_FILENAMES = [
|
|
181
|
+
"app.json",
|
|
182
|
+
"app.config.js",
|
|
183
|
+
"app.config.ts"
|
|
184
|
+
];
|
|
186
185
|
const REACT_COMPILER_CONFIG_PATTERN = /react-compiler|reactCompiler/;
|
|
187
186
|
const FRAMEWORK_PACKAGES = {
|
|
188
187
|
next: "nextjs",
|
|
@@ -369,6 +368,7 @@ const detectReactCompiler = (directory, packageJson) => {
|
|
|
369
368
|
if (hasCompilerInConfigFiles(directory, NEXT_CONFIG_FILENAMES)) return true;
|
|
370
369
|
if (hasCompilerInConfigFiles(directory, BABEL_CONFIG_FILENAMES)) return true;
|
|
371
370
|
if (hasCompilerInConfigFiles(directory, VITE_CONFIG_FILENAMES)) return true;
|
|
371
|
+
if (hasCompilerInConfigFiles(directory, EXPO_APP_CONFIG_FILENAMES)) return true;
|
|
372
372
|
let ancestorDirectory = path.dirname(directory);
|
|
373
373
|
while (ancestorDirectory !== path.dirname(ancestorDirectory)) {
|
|
374
374
|
const ancestorPackagePath = path.join(ancestorDirectory, "package.json");
|
|
@@ -462,57 +462,29 @@ const highlighter = {
|
|
|
462
462
|
dim: pc.dim
|
|
463
463
|
};
|
|
464
464
|
|
|
465
|
-
//#endregion
|
|
466
|
-
//#region src/utils/strip-ansi.ts
|
|
467
|
-
const ANSI_ESCAPE_SEQUENCE = String.raw`\u001B\[[0-9;]*m`;
|
|
468
|
-
const ANSI_ESCAPE_PATTERN = new RegExp(ANSI_ESCAPE_SEQUENCE, "g");
|
|
469
|
-
const stripAnsi = (text) => text.replace(ANSI_ESCAPE_PATTERN, "");
|
|
470
|
-
|
|
471
465
|
//#endregion
|
|
472
466
|
//#region src/utils/logger.ts
|
|
473
|
-
const loggerCaptureState = {
|
|
474
|
-
isEnabled: false,
|
|
475
|
-
lines: []
|
|
476
|
-
};
|
|
477
|
-
const captureLogLine = (text) => {
|
|
478
|
-
if (!loggerCaptureState.isEnabled) return;
|
|
479
|
-
loggerCaptureState.lines.push(stripAnsi(text));
|
|
480
|
-
};
|
|
481
|
-
const writeLogLine = (text) => {
|
|
482
|
-
console.log(text);
|
|
483
|
-
captureLogLine(text);
|
|
484
|
-
};
|
|
485
|
-
const startLoggerCapture = () => {
|
|
486
|
-
loggerCaptureState.isEnabled = true;
|
|
487
|
-
loggerCaptureState.lines = [];
|
|
488
|
-
};
|
|
489
|
-
const stopLoggerCapture = () => {
|
|
490
|
-
const capturedOutput = loggerCaptureState.lines.join("\n");
|
|
491
|
-
loggerCaptureState.isEnabled = false;
|
|
492
|
-
loggerCaptureState.lines = [];
|
|
493
|
-
return capturedOutput;
|
|
494
|
-
};
|
|
495
467
|
const logger = {
|
|
496
468
|
error(...args) {
|
|
497
|
-
|
|
469
|
+
console.log(highlighter.error(args.join(" ")));
|
|
498
470
|
},
|
|
499
471
|
warn(...args) {
|
|
500
|
-
|
|
472
|
+
console.log(highlighter.warn(args.join(" ")));
|
|
501
473
|
},
|
|
502
474
|
info(...args) {
|
|
503
|
-
|
|
475
|
+
console.log(highlighter.info(args.join(" ")));
|
|
504
476
|
},
|
|
505
477
|
success(...args) {
|
|
506
|
-
|
|
478
|
+
console.log(highlighter.success(args.join(" ")));
|
|
507
479
|
},
|
|
508
480
|
dim(...args) {
|
|
509
|
-
|
|
481
|
+
console.log(highlighter.dim(args.join(" ")));
|
|
510
482
|
},
|
|
511
483
|
log(...args) {
|
|
512
|
-
|
|
484
|
+
console.log(args.join(" "));
|
|
513
485
|
},
|
|
514
486
|
break() {
|
|
515
|
-
|
|
487
|
+
console.log("");
|
|
516
488
|
}
|
|
517
489
|
};
|
|
518
490
|
|
|
@@ -1429,46 +1401,6 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1429
1401
|
};
|
|
1430
1402
|
};
|
|
1431
1403
|
|
|
1432
|
-
//#endregion
|
|
1433
|
-
//#region src/utils/copy-to-clipboard.ts
|
|
1434
|
-
const getClipboardCommands = () => {
|
|
1435
|
-
if (process.platform === "darwin") return [{
|
|
1436
|
-
command: "pbcopy",
|
|
1437
|
-
args: []
|
|
1438
|
-
}];
|
|
1439
|
-
if (process.platform === "win32") return [{
|
|
1440
|
-
command: "clip",
|
|
1441
|
-
args: []
|
|
1442
|
-
}];
|
|
1443
|
-
return [
|
|
1444
|
-
{
|
|
1445
|
-
command: "wl-copy",
|
|
1446
|
-
args: []
|
|
1447
|
-
},
|
|
1448
|
-
{
|
|
1449
|
-
command: "xclip",
|
|
1450
|
-
args: ["-selection", "clipboard"]
|
|
1451
|
-
},
|
|
1452
|
-
{
|
|
1453
|
-
command: "xsel",
|
|
1454
|
-
args: ["--clipboard", "--input"]
|
|
1455
|
-
}
|
|
1456
|
-
];
|
|
1457
|
-
};
|
|
1458
|
-
const copyToClipboard = (text) => {
|
|
1459
|
-
const clipboardCommands = getClipboardCommands();
|
|
1460
|
-
for (const clipboardCommand of clipboardCommands) if (spawnSync(clipboardCommand.command, clipboardCommand.args, {
|
|
1461
|
-
input: text,
|
|
1462
|
-
stdio: [
|
|
1463
|
-
"pipe",
|
|
1464
|
-
"ignore",
|
|
1465
|
-
"ignore"
|
|
1466
|
-
],
|
|
1467
|
-
encoding: "utf8"
|
|
1468
|
-
}).status === 0) return true;
|
|
1469
|
-
return false;
|
|
1470
|
-
};
|
|
1471
|
-
|
|
1472
1404
|
//#endregion
|
|
1473
1405
|
//#region src/utils/get-diff-files.ts
|
|
1474
1406
|
const getCurrentBranch = (directory) => {
|
|
@@ -1514,12 +1446,33 @@ const getChangedFilesSinceBranch = (directory, baseBranch) => {
|
|
|
1514
1446
|
return [];
|
|
1515
1447
|
}
|
|
1516
1448
|
};
|
|
1449
|
+
const getUncommittedChangedFiles = (directory) => {
|
|
1450
|
+
try {
|
|
1451
|
+
const output = execSync("git diff --name-only --diff-filter=ACMR --relative HEAD", {
|
|
1452
|
+
cwd: directory,
|
|
1453
|
+
stdio: "pipe"
|
|
1454
|
+
}).toString().trim();
|
|
1455
|
+
if (!output) return [];
|
|
1456
|
+
return output.split("\n").filter(Boolean);
|
|
1457
|
+
} catch {
|
|
1458
|
+
return [];
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1517
1461
|
const getDiffInfo = (directory, explicitBaseBranch) => {
|
|
1518
1462
|
const currentBranch = getCurrentBranch(directory);
|
|
1519
1463
|
if (!currentBranch) return null;
|
|
1520
1464
|
const baseBranch = explicitBaseBranch ?? detectDefaultBranch(directory);
|
|
1521
1465
|
if (!baseBranch) return null;
|
|
1522
|
-
if (currentBranch === baseBranch)
|
|
1466
|
+
if (currentBranch === baseBranch) {
|
|
1467
|
+
const uncommittedFiles = getUncommittedChangedFiles(directory);
|
|
1468
|
+
if (uncommittedFiles.length === 0) return null;
|
|
1469
|
+
return {
|
|
1470
|
+
currentBranch,
|
|
1471
|
+
baseBranch,
|
|
1472
|
+
changedFiles: uncommittedFiles,
|
|
1473
|
+
isCurrentChanges: true
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1523
1476
|
return {
|
|
1524
1477
|
currentBranch,
|
|
1525
1478
|
baseBranch,
|
|
@@ -1528,34 +1481,6 @@ const getDiffInfo = (directory, explicitBaseBranch) => {
|
|
|
1528
1481
|
};
|
|
1529
1482
|
const filterSourceFiles = (filePaths) => filePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));
|
|
1530
1483
|
|
|
1531
|
-
//#endregion
|
|
1532
|
-
//#region src/utils/global-install.ts
|
|
1533
|
-
const isGloballyInstalled = () => {
|
|
1534
|
-
try {
|
|
1535
|
-
return !execSync("which react-doctor", {
|
|
1536
|
-
stdio: "pipe",
|
|
1537
|
-
encoding: "utf-8"
|
|
1538
|
-
}).trim().includes("/_npx/");
|
|
1539
|
-
} catch {
|
|
1540
|
-
return false;
|
|
1541
|
-
}
|
|
1542
|
-
};
|
|
1543
|
-
const maybeInstallGlobally = () => {
|
|
1544
|
-
try {
|
|
1545
|
-
if (isGloballyInstalled()) return;
|
|
1546
|
-
const child = spawn("npm", [
|
|
1547
|
-
"install",
|
|
1548
|
-
"-g",
|
|
1549
|
-
"react-doctor@latest"
|
|
1550
|
-
], {
|
|
1551
|
-
detached: true,
|
|
1552
|
-
stdio: "ignore"
|
|
1553
|
-
});
|
|
1554
|
-
child.on("error", () => {});
|
|
1555
|
-
child.unref();
|
|
1556
|
-
} catch {}
|
|
1557
|
-
};
|
|
1558
|
-
|
|
1559
1484
|
//#endregion
|
|
1560
1485
|
//#region src/utils/handle-error.ts
|
|
1561
1486
|
const DEFAULT_HANDLE_ERROR_OPTIONS = { shouldExit: true };
|
|
@@ -1704,62 +1629,9 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
1704
1629
|
return selectedDirectories;
|
|
1705
1630
|
};
|
|
1706
1631
|
|
|
1707
|
-
//#endregion
|
|
1708
|
-
//#region src/utils/skill-prompt.ts
|
|
1709
|
-
const CONFIG_DIRECTORY = join(homedir(), ".react-doctor");
|
|
1710
|
-
const CONFIG_FILE = join(CONFIG_DIRECTORY, "config.json");
|
|
1711
|
-
const SKILL_REPO = "millionco/react-doctor";
|
|
1712
|
-
const readConfig = () => {
|
|
1713
|
-
try {
|
|
1714
|
-
if (!existsSync(CONFIG_FILE)) return {};
|
|
1715
|
-
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
1716
|
-
} catch {
|
|
1717
|
-
return {};
|
|
1718
|
-
}
|
|
1719
|
-
};
|
|
1720
|
-
const writeConfig = (config) => {
|
|
1721
|
-
try {
|
|
1722
|
-
if (!existsSync(CONFIG_DIRECTORY)) mkdirSync(CONFIG_DIRECTORY, { recursive: true });
|
|
1723
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
1724
|
-
} catch {}
|
|
1725
|
-
};
|
|
1726
|
-
const installSkill = () => {
|
|
1727
|
-
try {
|
|
1728
|
-
execSync(`npx -y skills add ${SKILL_REPO}`, { stdio: "inherit" });
|
|
1729
|
-
} catch {
|
|
1730
|
-
logger.break();
|
|
1731
|
-
logger.dim("Skill install failed. You can install manually:");
|
|
1732
|
-
logger.dim(` npx skills add ${SKILL_REPO}`);
|
|
1733
|
-
}
|
|
1734
|
-
};
|
|
1735
|
-
const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
1736
|
-
const config = readConfig();
|
|
1737
|
-
if (config.skillPromptDismissed) return;
|
|
1738
|
-
if (shouldSkipPrompts) return;
|
|
1739
|
-
logger.break();
|
|
1740
|
-
logger.log(`${highlighter.info("💡")} Have your coding agent fix these issues automatically?`);
|
|
1741
|
-
logger.dim(` Install the ${highlighter.info("react-doctor")} skill to teach Cursor, Claude Code, Copilot,`);
|
|
1742
|
-
logger.dim(" Ami, and other AI agents how to diagnose and fix these React issues.");
|
|
1743
|
-
logger.break();
|
|
1744
|
-
const { shouldInstall } = await prompts({
|
|
1745
|
-
type: "confirm",
|
|
1746
|
-
name: "shouldInstall",
|
|
1747
|
-
message: "Install skill? (recommended)",
|
|
1748
|
-
initial: true
|
|
1749
|
-
});
|
|
1750
|
-
if (shouldInstall) {
|
|
1751
|
-
logger.break();
|
|
1752
|
-
installSkill();
|
|
1753
|
-
writeConfig({
|
|
1754
|
-
...config,
|
|
1755
|
-
skillPromptDismissed: true
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
};
|
|
1759
|
-
|
|
1760
1632
|
//#endregion
|
|
1761
1633
|
//#region src/cli.ts
|
|
1762
|
-
const VERSION = "0.0.
|
|
1634
|
+
const VERSION = "0.0.25";
|
|
1763
1635
|
const exitWithFixHint = () => {
|
|
1764
1636
|
logger.break();
|
|
1765
1637
|
logger.log("Cancelled.");
|
|
@@ -1773,7 +1645,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
|
|
|
1773
1645
|
if (effectiveDiff !== void 0 && effectiveDiff !== false) {
|
|
1774
1646
|
if (diffInfo) return true;
|
|
1775
1647
|
if (!isScoreOnly) {
|
|
1776
|
-
logger.warn("
|
|
1648
|
+
logger.warn("No feature branch or uncommitted changes detected. Running full scan.");
|
|
1777
1649
|
logger.break();
|
|
1778
1650
|
}
|
|
1779
1651
|
return false;
|
|
@@ -1783,18 +1655,16 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
|
|
|
1783
1655
|
if (changedSourceFiles.length === 0) return false;
|
|
1784
1656
|
if (shouldSkipPrompts) return true;
|
|
1785
1657
|
if (isScoreOnly) return false;
|
|
1786
|
-
const {
|
|
1658
|
+
const { shouldScanChangedOnly } = await prompts({
|
|
1787
1659
|
type: "confirm",
|
|
1788
|
-
name: "
|
|
1789
|
-
message: `On branch ${diffInfo.currentBranch} (${changedSourceFiles.length} changed files vs ${diffInfo.baseBranch}). Only scan this branch?`,
|
|
1660
|
+
name: "shouldScanChangedOnly",
|
|
1661
|
+
message: diffInfo.isCurrentChanges ? `Found ${changedSourceFiles.length} uncommitted changed files. Only scan current changes?` : `On branch ${diffInfo.currentBranch} (${changedSourceFiles.length} changed files vs ${diffInfo.baseBranch}). Only scan this branch?`,
|
|
1790
1662
|
initial: true
|
|
1791
1663
|
});
|
|
1792
|
-
return Boolean(
|
|
1664
|
+
return Boolean(shouldScanChangedOnly);
|
|
1793
1665
|
};
|
|
1794
|
-
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--no-ami", "skip Ami-related prompts").option("--fix", "open Ami to auto-fix all issues").
|
|
1795
|
-
const isScoreOnly = flags.score
|
|
1796
|
-
const shouldCopyPromptOutput = flags.prompt;
|
|
1797
|
-
startLoggerCapture();
|
|
1666
|
+
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--no-ami", "skip Ami-related prompts").option("--fix", "open Ami to auto-fix all issues").action(async (directory, flags) => {
|
|
1667
|
+
const isScoreOnly = flags.score;
|
|
1798
1668
|
try {
|
|
1799
1669
|
const resolvedDirectory = path.resolve(directory);
|
|
1800
1670
|
const userConfig = loadConfig(resolvedDirectory);
|
|
@@ -1806,7 +1676,7 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1806
1676
|
const scanOptions = {
|
|
1807
1677
|
lint: isCliOverride("lint") ? flags.lint : userConfig?.lint ?? flags.lint,
|
|
1808
1678
|
deadCode: isCliOverride("deadCode") ? flags.deadCode : userConfig?.deadCode ?? flags.deadCode,
|
|
1809
|
-
verbose:
|
|
1679
|
+
verbose: isCliOverride("verbose") ? Boolean(flags.verbose) : userConfig?.verbose ?? false,
|
|
1810
1680
|
scoreOnly: isScoreOnly,
|
|
1811
1681
|
offline: flags.offline
|
|
1812
1682
|
};
|
|
@@ -1827,7 +1697,8 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1827
1697
|
const diffInfo = getDiffInfo(resolvedDirectory, explicitBaseBranch);
|
|
1828
1698
|
const isDiffMode = await resolveDiffMode(diffInfo, effectiveDiff, shouldSkipPrompts, isScoreOnly);
|
|
1829
1699
|
if (isDiffMode && diffInfo && !isScoreOnly) {
|
|
1830
|
-
|
|
1700
|
+
if (diffInfo.isCurrentChanges) logger.log("Scanning uncommitted changes");
|
|
1701
|
+
else logger.log(`Scanning changes: ${highlighter.info(diffInfo.currentBranch)} → ${highlighter.info(diffInfo.baseBranch)}`);
|
|
1831
1702
|
logger.break();
|
|
1832
1703
|
}
|
|
1833
1704
|
const allDiagnostics = [];
|
|
@@ -1858,18 +1729,10 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1858
1729
|
allDiagnostics.push(...scanResult.diagnostics);
|
|
1859
1730
|
if (!isScoreOnly) logger.break();
|
|
1860
1731
|
}
|
|
1861
|
-
const capturedScanOutput = stopLoggerCapture();
|
|
1862
1732
|
if (flags.fix) openAmiToFix(resolvedDirectory);
|
|
1863
|
-
if (
|
|
1864
|
-
else if (!isScoreOnly) {
|
|
1865
|
-
await maybePromptSkillInstall(shouldSkipAmiPrompts);
|
|
1866
|
-
if (!shouldSkipAmiPrompts && !flags.fix) await maybePromptFix(resolvedDirectory, allDiagnostics, flags.offline ? null : await fetchEstimatedScore(allDiagnostics), capturedScanOutput);
|
|
1867
|
-
}
|
|
1733
|
+
if (!isScoreOnly && !shouldSkipAmiPrompts && !flags.fix) await maybePromptFix(resolvedDirectory, allDiagnostics, flags.offline ? null : await fetchEstimatedScore(allDiagnostics));
|
|
1868
1734
|
} catch (error) {
|
|
1869
|
-
handleError(error
|
|
1870
|
-
} finally {
|
|
1871
|
-
const remainingOutput = stopLoggerCapture();
|
|
1872
|
-
if (shouldCopyPromptOutput && remainingOutput) copyPromptToClipboard(remainingOutput, !isScoreOnly);
|
|
1735
|
+
handleError(error);
|
|
1873
1736
|
}
|
|
1874
1737
|
}).addHelpText("after", `
|
|
1875
1738
|
${highlighter.dim("Learn more:")}
|
|
@@ -1884,9 +1747,6 @@ const colorizeByScore = (text, score) => {
|
|
|
1884
1747
|
return highlighter.error(text);
|
|
1885
1748
|
};
|
|
1886
1749
|
const DEEPLINK_FIX_PROMPT = "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.";
|
|
1887
|
-
const CLIPBOARD_FIX_PROMPT = "Fix all issues reported in the react-doctor diagnostics below, one by one. After applying fixes, run `npx -y react-doctor@latest .` again to verify the results improved.";
|
|
1888
|
-
const REACT_DOCTOR_OUTPUT_LABEL = "react-doctor output";
|
|
1889
|
-
const SCAN_SUMMARY_SEPARATOR = "─".repeat(SEPARATOR_LENGTH_CHARS);
|
|
1890
1750
|
const isAmiInstalled = () => {
|
|
1891
1751
|
if (process.platform === "darwin") return existsSync("/Applications/Ami.app") || existsSync(path.join(os.homedir(), "Applications", "Ami.app"));
|
|
1892
1752
|
if (process.platform === "win32") {
|
|
@@ -1955,24 +1815,7 @@ const openAmiToFix = (directory) => {
|
|
|
1955
1815
|
logger.info(webDeeplink);
|
|
1956
1816
|
}
|
|
1957
1817
|
};
|
|
1958
|
-
const buildPromptWithOutput = (reactDoctorOutput) => {
|
|
1959
|
-
const summaryStartIndex = reactDoctorOutput.indexOf(SCAN_SUMMARY_SEPARATOR);
|
|
1960
|
-
const normalizedReactDoctorOutput = (summaryStartIndex === -1 ? reactDoctorOutput : reactDoctorOutput.slice(0, summaryStartIndex).trimEnd()).trim();
|
|
1961
|
-
return `${CLIPBOARD_FIX_PROMPT}\n\n${REACT_DOCTOR_OUTPUT_LABEL}:\n\`\`\`\n${normalizedReactDoctorOutput.length > 0 ? normalizedReactDoctorOutput : "No output captured."}\n\`\`\``;
|
|
1962
|
-
};
|
|
1963
|
-
const copyPromptToClipboard = (reactDoctorOutput, shouldLogResult) => {
|
|
1964
|
-
const promptWithOutput = buildPromptWithOutput(reactDoctorOutput);
|
|
1965
|
-
const didCopyPromptToClipboard = copyToClipboard(promptWithOutput);
|
|
1966
|
-
if (!shouldLogResult) return;
|
|
1967
|
-
if (didCopyPromptToClipboard) {
|
|
1968
|
-
logger.success("Copied latest scan output to clipboard");
|
|
1969
|
-
return;
|
|
1970
|
-
}
|
|
1971
|
-
logger.warn("Could not copy prompt to clipboard automatically. Use this prompt:");
|
|
1972
|
-
logger.info(promptWithOutput);
|
|
1973
|
-
};
|
|
1974
1818
|
const FIX_METHOD_AMI = "ami";
|
|
1975
|
-
const FIX_METHOD_CLIPBOARD = "clipboard";
|
|
1976
1819
|
const FIX_COMMAND_HINT = "npx react-doctor@latest --fix";
|
|
1977
1820
|
const buildAmiBanner = (issueCount, currentScore, estimatedScore) => {
|
|
1978
1821
|
const currentScoreDisplay = colorizeByScore(String(currentScore), currentScore);
|
|
@@ -1989,13 +1832,6 @@ const buildAmiBanner = (issueCount, currentScore, estimatedScore) => {
|
|
|
1989
1832
|
createFramedLine(`Free to use. ${AMI_WEBSITE_URL}`, `Free to use. ${highlighter.info(AMI_WEBSITE_URL)}`)
|
|
1990
1833
|
]);
|
|
1991
1834
|
};
|
|
1992
|
-
const buildClipboardWarningBanner = () => renderFramedBoxString([
|
|
1993
|
-
createFramedLine("⚠ Other agents may not fix these issues well.", `${highlighter.warn("⚠")} Other agents may not fix these issues well.`),
|
|
1994
|
-
createFramedLine(""),
|
|
1995
|
-
createFramedLine("react-doctor diagnostics require React-specific context"),
|
|
1996
|
-
createFramedLine("that general-purpose agents often miss, leading to"),
|
|
1997
|
-
createFramedLine("incomplete or incorrect fixes.")
|
|
1998
|
-
]);
|
|
1999
1835
|
const buildSkipBanner = (issueCount, estimatedScore) => {
|
|
2000
1836
|
const issueLabel = issueCount === 1 ? "issue" : "issues";
|
|
2001
1837
|
const estimatedScoreDisplay = colorizeByScore(`~${estimatedScore}`, estimatedScore);
|
|
@@ -2008,10 +1844,9 @@ const buildSkipBanner = (issueCount, estimatedScore) => {
|
|
|
2008
1844
|
const configureFixBanners = (issueCount, estimatedScoreResult) => {
|
|
2009
1845
|
const { currentScore, estimatedScore } = estimatedScoreResult;
|
|
2010
1846
|
setSelectBanner(buildAmiBanner(issueCount, currentScore, estimatedScore), 0);
|
|
2011
|
-
setSelectBanner(
|
|
2012
|
-
setSelectBanner(buildSkipBanner(issueCount, estimatedScore), 2);
|
|
1847
|
+
setSelectBanner(buildSkipBanner(issueCount, estimatedScore), 1);
|
|
2013
1848
|
};
|
|
2014
|
-
const maybePromptFix = async (directory, diagnostics, estimatedScoreResult
|
|
1849
|
+
const maybePromptFix = async (directory, diagnostics, estimatedScoreResult) => {
|
|
2015
1850
|
if (diagnostics.length === 0) return;
|
|
2016
1851
|
logger.break();
|
|
2017
1852
|
if (estimatedScoreResult) configureFixBanners(diagnostics.length, estimatedScoreResult);
|
|
@@ -2019,26 +1854,17 @@ const maybePromptFix = async (directory, diagnostics, estimatedScoreResult, capt
|
|
|
2019
1854
|
type: "select",
|
|
2020
1855
|
name: "fixMethod",
|
|
2021
1856
|
message: "Fix issues?",
|
|
2022
|
-
choices: [
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
description: "Other agents may lack context for react-doctor fixes",
|
|
2031
|
-
value: FIX_METHOD_CLIPBOARD
|
|
2032
|
-
},
|
|
2033
|
-
{
|
|
2034
|
-
title: "Skip",
|
|
2035
|
-
value: "skip"
|
|
2036
|
-
}
|
|
2037
|
-
]
|
|
1857
|
+
choices: [{
|
|
1858
|
+
title: "Use ami.dev (recommended)",
|
|
1859
|
+
description: "Optimized coding agent for React Doctor",
|
|
1860
|
+
value: FIX_METHOD_AMI
|
|
1861
|
+
}, {
|
|
1862
|
+
title: "Skip",
|
|
1863
|
+
value: "skip"
|
|
1864
|
+
}]
|
|
2038
1865
|
});
|
|
2039
1866
|
clearSelectBanner();
|
|
2040
1867
|
if (fixMethod === FIX_METHOD_AMI) openAmiToFix(directory);
|
|
2041
|
-
else if (fixMethod === FIX_METHOD_CLIPBOARD) copyPromptToClipboard(capturedScanOutput, true);
|
|
2042
1868
|
else {
|
|
2043
1869
|
logger.break();
|
|
2044
1870
|
logger.dim(` Run ${highlighter.info(FIX_COMMAND_HINT)} anytime to fix issues.`);
|
|
@@ -2056,7 +1882,6 @@ const installAmiCommand = new Command("install-ami").description("Install Ami an
|
|
|
2056
1882
|
program.addCommand(fixCommand);
|
|
2057
1883
|
program.addCommand(installAmiCommand);
|
|
2058
1884
|
const main$1 = async () => {
|
|
2059
|
-
maybeInstallGlobally();
|
|
2060
1885
|
await program.parseAsync();
|
|
2061
1886
|
};
|
|
2062
1887
|
main$1();
|