react-doctor 0.0.7 → 0.0.9
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/dist/cli.js +366 -91
- package/dist/cli.js.map +1 -1
- package/dist/react-doctor-plugin.d.ts.map +1 -1
- package/dist/react-doctor-plugin.js +218 -75
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,23 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
import ora from "ora";
|
|
15
15
|
import basePrompts from "prompts";
|
|
16
16
|
|
|
17
|
+
//#region src/constants.ts
|
|
18
|
+
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
19
|
+
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
20
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
21
|
+
const ERROR_PREVIEW_LENGTH_CHARS = 200;
|
|
22
|
+
const PERFECT_SCORE = 100;
|
|
23
|
+
const SCORE_GOOD_THRESHOLD = 75;
|
|
24
|
+
const SCORE_OK_THRESHOLD = 50;
|
|
25
|
+
const SCORE_BAR_WIDTH_CHARS = 50;
|
|
26
|
+
const SEPARATOR_LENGTH_CHARS = 40;
|
|
27
|
+
const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;
|
|
28
|
+
const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;
|
|
29
|
+
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
30
|
+
const SHARE_BASE_URL = "https://www.react.doctor/share";
|
|
31
|
+
const OFFLINE_MESSAGE = "You are offline, could not calculate score. Reconnect to calculate.";
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
17
34
|
//#region src/utils/highlighter.ts
|
|
18
35
|
const highlighter = {
|
|
19
36
|
error: pc.red,
|
|
@@ -23,58 +40,74 @@ const highlighter = {
|
|
|
23
40
|
dim: pc.dim
|
|
24
41
|
};
|
|
25
42
|
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/utils/strip-ansi.ts
|
|
45
|
+
const ANSI_ESCAPE_SEQUENCE = String.raw`\u001B\[[0-9;]*m`;
|
|
46
|
+
const ANSI_ESCAPE_PATTERN = new RegExp(ANSI_ESCAPE_SEQUENCE, "g");
|
|
47
|
+
const stripAnsi = (text) => text.replace(ANSI_ESCAPE_PATTERN, "");
|
|
48
|
+
|
|
26
49
|
//#endregion
|
|
27
50
|
//#region src/utils/logger.ts
|
|
51
|
+
const loggerCaptureState = {
|
|
52
|
+
isEnabled: false,
|
|
53
|
+
lines: []
|
|
54
|
+
};
|
|
55
|
+
const captureLogLine = (text) => {
|
|
56
|
+
if (!loggerCaptureState.isEnabled) return;
|
|
57
|
+
loggerCaptureState.lines.push(stripAnsi(text));
|
|
58
|
+
};
|
|
59
|
+
const writeLogLine = (text) => {
|
|
60
|
+
console.log(text);
|
|
61
|
+
captureLogLine(text);
|
|
62
|
+
};
|
|
63
|
+
const startLoggerCapture = () => {
|
|
64
|
+
loggerCaptureState.isEnabled = true;
|
|
65
|
+
loggerCaptureState.lines = [];
|
|
66
|
+
};
|
|
67
|
+
const stopLoggerCapture = () => {
|
|
68
|
+
const capturedOutput = loggerCaptureState.lines.join("\n");
|
|
69
|
+
loggerCaptureState.isEnabled = false;
|
|
70
|
+
loggerCaptureState.lines = [];
|
|
71
|
+
return capturedOutput;
|
|
72
|
+
};
|
|
28
73
|
const logger = {
|
|
29
74
|
error(...args) {
|
|
30
|
-
|
|
75
|
+
writeLogLine(highlighter.error(args.join(" ")));
|
|
31
76
|
},
|
|
32
77
|
warn(...args) {
|
|
33
|
-
|
|
78
|
+
writeLogLine(highlighter.warn(args.join(" ")));
|
|
34
79
|
},
|
|
35
80
|
info(...args) {
|
|
36
|
-
|
|
81
|
+
writeLogLine(highlighter.info(args.join(" ")));
|
|
37
82
|
},
|
|
38
83
|
success(...args) {
|
|
39
|
-
|
|
84
|
+
writeLogLine(highlighter.success(args.join(" ")));
|
|
40
85
|
},
|
|
41
86
|
dim(...args) {
|
|
42
|
-
|
|
87
|
+
writeLogLine(highlighter.dim(args.join(" ")));
|
|
43
88
|
},
|
|
44
89
|
log(...args) {
|
|
45
|
-
|
|
90
|
+
writeLogLine(args.join(" "));
|
|
46
91
|
},
|
|
47
92
|
break() {
|
|
48
|
-
|
|
93
|
+
writeLogLine("");
|
|
49
94
|
}
|
|
50
95
|
};
|
|
51
96
|
|
|
52
97
|
//#endregion
|
|
53
98
|
//#region src/utils/handle-error.ts
|
|
54
|
-
const
|
|
99
|
+
const DEFAULT_HANDLE_ERROR_OPTIONS = { shouldExit: true };
|
|
100
|
+
const handleError = (error, options = DEFAULT_HANDLE_ERROR_OPTIONS) => {
|
|
55
101
|
logger.break();
|
|
56
102
|
logger.error("Something went wrong. Please check the error below for more details.");
|
|
57
103
|
logger.error("If the problem persists, please open an issue on GitHub.");
|
|
58
104
|
logger.error("");
|
|
59
105
|
if (error instanceof Error) logger.error(error.message);
|
|
60
106
|
logger.break();
|
|
61
|
-
process.exit(1);
|
|
107
|
+
if (options.shouldExit) process.exit(1);
|
|
108
|
+
process.exitCode = 1;
|
|
62
109
|
};
|
|
63
110
|
|
|
64
|
-
//#endregion
|
|
65
|
-
//#region src/constants.ts
|
|
66
|
-
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
67
|
-
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
68
|
-
const MILLISECONDS_PER_SECOND = 1e3;
|
|
69
|
-
const SEPARATOR_LENGTH_CHARS = 62;
|
|
70
|
-
const ERROR_PREVIEW_LENGTH_CHARS = 200;
|
|
71
|
-
const PERFECT_SCORE = 100;
|
|
72
|
-
const SCORE_GOOD_THRESHOLD = 75;
|
|
73
|
-
const SCORE_OK_THRESHOLD = 50;
|
|
74
|
-
const SCORE_BAR_WIDTH_CHARS = 50;
|
|
75
|
-
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
76
|
-
const OFFLINE_MESSAGE = "You are offline, could not calculate score. Reconnect to calculate.";
|
|
77
|
-
|
|
78
111
|
//#endregion
|
|
79
112
|
//#region src/utils/calculate-score.ts
|
|
80
113
|
const calculateScore = async (diagnostics) => {
|
|
@@ -325,11 +358,13 @@ const discoverProject = (directory) => {
|
|
|
325
358
|
if (!reactVersion) reactVersion = ancestorInfo.reactVersion;
|
|
326
359
|
if (framework === "unknown") framework = ancestorInfo.framework;
|
|
327
360
|
}
|
|
361
|
+
const projectName = packageJson.name ?? path.basename(directory);
|
|
328
362
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json"));
|
|
329
363
|
const sourceFileCount = countSourceFiles(directory);
|
|
330
364
|
const hasReactCompiler = detectReactCompiler(directory, packageJson);
|
|
331
365
|
return {
|
|
332
366
|
rootDirectory: directory,
|
|
367
|
+
projectName,
|
|
333
368
|
reactVersion,
|
|
334
369
|
framework,
|
|
335
370
|
hasTypeScript,
|
|
@@ -448,12 +483,45 @@ const silenced = async (fn) => {
|
|
|
448
483
|
console.warn = originalWarn;
|
|
449
484
|
}
|
|
450
485
|
};
|
|
451
|
-
const
|
|
486
|
+
const findMonorepoRoot = (directory) => {
|
|
487
|
+
let currentDirectory = path.dirname(directory);
|
|
488
|
+
while (currentDirectory !== path.dirname(currentDirectory)) {
|
|
489
|
+
if (fs.existsSync(path.join(currentDirectory, "pnpm-workspace.yaml")) || (() => {
|
|
490
|
+
const packageJsonPath = path.join(currentDirectory, "package.json");
|
|
491
|
+
if (!fs.existsSync(packageJsonPath)) return false;
|
|
492
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
493
|
+
return Array.isArray(packageJson.workspaces) || packageJson.workspaces?.packages;
|
|
494
|
+
})()) return currentDirectory;
|
|
495
|
+
currentDirectory = path.dirname(currentDirectory);
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
};
|
|
499
|
+
const runKnipWithOptions = async (knipCwd, workspaceName) => {
|
|
452
500
|
const options = await silenced(() => createOptions({
|
|
453
|
-
cwd:
|
|
454
|
-
isShowProgress: false
|
|
501
|
+
cwd: knipCwd,
|
|
502
|
+
isShowProgress: false,
|
|
503
|
+
...workspaceName ? { workspace: workspaceName } : {}
|
|
455
504
|
}));
|
|
456
|
-
|
|
505
|
+
return await silenced(() => main(options));
|
|
506
|
+
};
|
|
507
|
+
const hasNodeModules = (directory) => {
|
|
508
|
+
const nodeModulesPath = path.join(directory, "node_modules");
|
|
509
|
+
return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();
|
|
510
|
+
};
|
|
511
|
+
const runKnip = async (rootDirectory) => {
|
|
512
|
+
const monorepoRoot = findMonorepoRoot(rootDirectory);
|
|
513
|
+
if (!(hasNodeModules(rootDirectory) || monorepoRoot !== null && hasNodeModules(monorepoRoot))) return [];
|
|
514
|
+
let knipResult;
|
|
515
|
+
if (monorepoRoot) {
|
|
516
|
+
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
517
|
+
const workspaceName = (fs.existsSync(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) : {}).name ?? path.basename(rootDirectory);
|
|
518
|
+
try {
|
|
519
|
+
knipResult = await runKnipWithOptions(monorepoRoot, workspaceName);
|
|
520
|
+
} catch {
|
|
521
|
+
knipResult = await runKnipWithOptions(rootDirectory);
|
|
522
|
+
}
|
|
523
|
+
} else knipResult = await runKnipWithOptions(rootDirectory);
|
|
524
|
+
const { issues } = knipResult;
|
|
457
525
|
const diagnostics = [];
|
|
458
526
|
for (const unusedFile of issues.files) diagnostics.push({
|
|
459
527
|
filePath: path.relative(rootDirectory, unusedFile),
|
|
@@ -482,7 +550,7 @@ const NEXTJS_RULES = {
|
|
|
482
550
|
"react-doctor/nextjs-no-img-element": "warn",
|
|
483
551
|
"react-doctor/nextjs-async-client-component": "error",
|
|
484
552
|
"react-doctor/nextjs-no-a-element": "warn",
|
|
485
|
-
"react-doctor/nextjs-no-use-search-params-without-suspense": "
|
|
553
|
+
"react-doctor/nextjs-no-use-search-params-without-suspense": "warn",
|
|
486
554
|
"react-doctor/nextjs-no-client-fetch-for-server-data": "warn",
|
|
487
555
|
"react-doctor/nextjs-missing-metadata": "warn",
|
|
488
556
|
"react-doctor/nextjs-no-client-side-redirect": "warn",
|
|
@@ -514,12 +582,6 @@ const REACT_COMPILER_RULES = {
|
|
|
514
582
|
"react-hooks-js/incompatible-library": "error",
|
|
515
583
|
"react-hooks-js/todo": "error"
|
|
516
584
|
};
|
|
517
|
-
const REACT_PERF_RULES = {
|
|
518
|
-
"react-perf/jsx-no-new-object-as-prop": "warn",
|
|
519
|
-
"react-perf/jsx-no-new-array-as-prop": "warn",
|
|
520
|
-
"react-perf/jsx-no-new-function-as-prop": "warn",
|
|
521
|
-
"react-perf/jsx-no-jsx-as-prop": "warn"
|
|
522
|
-
};
|
|
523
585
|
const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
|
|
524
586
|
categories: {
|
|
525
587
|
correctness: "off",
|
|
@@ -549,7 +611,6 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
|
|
|
549
611
|
"react/jsx-no-script-url": "error",
|
|
550
612
|
"react/no-render-return-value": "warn",
|
|
551
613
|
"react/no-string-refs": "warn",
|
|
552
|
-
"react/no-unescaped-entities": "warn",
|
|
553
614
|
"react/no-is-mounted": "warn",
|
|
554
615
|
"react/require-render-return": "error",
|
|
555
616
|
"react/no-unknown-property": "warn",
|
|
@@ -568,7 +629,7 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
|
|
|
568
629
|
"jsx-a11y/label-has-associated-control": "warn",
|
|
569
630
|
"jsx-a11y/no-distracting-elements": "error",
|
|
570
631
|
"jsx-a11y/iframe-has-title": "warn",
|
|
571
|
-
...hasReactCompiler ? REACT_COMPILER_RULES :
|
|
632
|
+
...hasReactCompiler ? REACT_COMPILER_RULES : {},
|
|
572
633
|
"react-doctor/no-derived-state-effect": "error",
|
|
573
634
|
"react-doctor/no-fetch-in-effect": "error",
|
|
574
635
|
"react-doctor/no-cascading-set-state": "warn",
|
|
@@ -578,7 +639,6 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
|
|
|
578
639
|
"react-doctor/rerender-lazy-state-init": "warn",
|
|
579
640
|
"react-doctor/rerender-functional-setstate": "warn",
|
|
580
641
|
"react-doctor/rerender-dependencies": "error",
|
|
581
|
-
"react-doctor/no-generic-handler-names": "warn",
|
|
582
642
|
"react-doctor/no-giant-component": "warn",
|
|
583
643
|
"react-doctor/no-render-in-render": "warn",
|
|
584
644
|
"react-doctor/no-nested-component-definition": "error",
|
|
@@ -586,7 +646,7 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
|
|
|
586
646
|
"react-doctor/no-layout-property-animation": "error",
|
|
587
647
|
"react-doctor/rerender-memo-with-default-value": "warn",
|
|
588
648
|
"react-doctor/rendering-animate-svg-wrapper": "warn",
|
|
589
|
-
"react-doctor/
|
|
649
|
+
"react-doctor/no-inline-prop-on-memo-component": "warn",
|
|
590
650
|
"react-doctor/rendering-hydration-no-flicker": "warn",
|
|
591
651
|
"react-doctor/no-transition-all": "warn",
|
|
592
652
|
"react-doctor/no-global-css-variable-animation": "error",
|
|
@@ -868,12 +928,17 @@ const spinner = (text) => ({ start() {
|
|
|
868
928
|
};
|
|
869
929
|
} });
|
|
870
930
|
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/utils/indent-multiline-text.ts
|
|
933
|
+
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
934
|
+
|
|
871
935
|
//#endregion
|
|
872
936
|
//#region src/scan.ts
|
|
873
937
|
const SEVERITY_ORDER = {
|
|
874
938
|
error: 0,
|
|
875
939
|
warning: 1
|
|
876
940
|
};
|
|
941
|
+
const colorizeBySeverity = (text, severity) => severity === "error" ? highlighter.error(text) : highlighter.warn(text);
|
|
877
942
|
const sortBySeverity = (diagnosticGroups) => diagnosticGroups.toSorted(([, diagnosticsA], [, diagnosticsB]) => {
|
|
878
943
|
return SEVERITY_ORDER[diagnosticsA[0].severity] - SEVERITY_ORDER[diagnosticsB[0].severity];
|
|
879
944
|
});
|
|
@@ -891,11 +956,11 @@ const printDiagnostics = (diagnostics, isVerbose) => {
|
|
|
891
956
|
const sortedRuleGroups = sortBySeverity([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
|
|
892
957
|
for (const [, ruleDiagnostics] of sortedRuleGroups) {
|
|
893
958
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
894
|
-
const icon = firstDiagnostic.severity === "error" ?
|
|
959
|
+
const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
|
|
895
960
|
const count = ruleDiagnostics.length;
|
|
896
|
-
const countLabel = count > 1 ? ` (${count})
|
|
961
|
+
const countLabel = count > 1 ? colorizeBySeverity(` (${count})`, firstDiagnostic.severity) : "";
|
|
897
962
|
logger.log(` ${icon} ${firstDiagnostic.message}${countLabel}`);
|
|
898
|
-
if (firstDiagnostic.help) logger.dim(
|
|
963
|
+
if (firstDiagnostic.help) logger.dim(indentMultilineText(firstDiagnostic.help, " "));
|
|
899
964
|
if (isVerbose) {
|
|
900
965
|
const fileLines = buildFileLineMap(ruleDiagnostics);
|
|
901
966
|
for (const [filePath, lines] of fileLines) {
|
|
@@ -942,12 +1007,39 @@ const colorizeByScore = (text, score) => {
|
|
|
942
1007
|
if (score >= SCORE_OK_THRESHOLD) return highlighter.warn(text);
|
|
943
1008
|
return highlighter.error(text);
|
|
944
1009
|
};
|
|
945
|
-
const
|
|
1010
|
+
const createFramedLine = (plainText, renderedText = plainText) => ({
|
|
1011
|
+
plainText,
|
|
1012
|
+
renderedText
|
|
1013
|
+
});
|
|
1014
|
+
const buildScoreBarSegments = (score) => {
|
|
946
1015
|
const filledCount = Math.round(score / PERFECT_SCORE * SCORE_BAR_WIDTH_CHARS);
|
|
947
1016
|
const emptyCount = SCORE_BAR_WIDTH_CHARS - filledCount;
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1017
|
+
return {
|
|
1018
|
+
filledSegment: "█".repeat(filledCount),
|
|
1019
|
+
emptySegment: "░".repeat(emptyCount)
|
|
1020
|
+
};
|
|
1021
|
+
};
|
|
1022
|
+
const buildPlainScoreBar = (score) => {
|
|
1023
|
+
const { filledSegment, emptySegment } = buildScoreBarSegments(score);
|
|
1024
|
+
return `${filledSegment}${emptySegment}`;
|
|
1025
|
+
};
|
|
1026
|
+
const buildScoreBar = (score) => {
|
|
1027
|
+
const { filledSegment, emptySegment } = buildScoreBarSegments(score);
|
|
1028
|
+
return colorizeByScore(filledSegment, score) + highlighter.dim(emptySegment);
|
|
1029
|
+
};
|
|
1030
|
+
const printFramedBox = (framedLines) => {
|
|
1031
|
+
if (framedLines.length === 0) return;
|
|
1032
|
+
const borderColorizer = highlighter.dim;
|
|
1033
|
+
const outerIndent = " ".repeat(SUMMARY_BOX_OUTER_INDENT_CHARS);
|
|
1034
|
+
const horizontalPadding = " ".repeat(SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);
|
|
1035
|
+
const maximumLineLength = Math.max(...framedLines.map((framedLine) => framedLine.plainText.length));
|
|
1036
|
+
const borderLine = "─".repeat(maximumLineLength + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2);
|
|
1037
|
+
logger.log(`${outerIndent}${borderColorizer(`┌${borderLine}┐`)}`);
|
|
1038
|
+
for (const framedLine of framedLines) {
|
|
1039
|
+
const trailingSpaces = " ".repeat(maximumLineLength - framedLine.plainText.length);
|
|
1040
|
+
logger.log(`${outerIndent}${borderColorizer("│")}${horizontalPadding}${framedLine.renderedText}${trailingSpaces}${horizontalPadding}${borderColorizer("│")}`);
|
|
1041
|
+
}
|
|
1042
|
+
logger.log(`${outerIndent}${borderColorizer(`└${borderLine}┘`)}`);
|
|
951
1043
|
};
|
|
952
1044
|
const printScoreGauge = (score, label) => {
|
|
953
1045
|
const scoreDisplay = colorizeByScore(`${score}`, score);
|
|
@@ -974,25 +1066,65 @@ const printBranding = (score) => {
|
|
|
974
1066
|
logger.log(` React Doctor ${highlighter.dim("(www.react.doctor)")}`);
|
|
975
1067
|
logger.break();
|
|
976
1068
|
};
|
|
977
|
-
const
|
|
1069
|
+
const buildShareUrl = (diagnostics, scoreResult, projectName) => {
|
|
1070
|
+
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
1071
|
+
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
1072
|
+
const affectedFileCount = collectAffectedFiles(diagnostics).size;
|
|
1073
|
+
const params = new URLSearchParams();
|
|
1074
|
+
params.set("p", projectName);
|
|
1075
|
+
if (scoreResult) params.set("s", String(scoreResult.score));
|
|
1076
|
+
if (errorCount > 0) params.set("e", String(errorCount));
|
|
1077
|
+
if (warningCount > 0) params.set("w", String(warningCount));
|
|
1078
|
+
if (affectedFileCount > 0) params.set("f", String(affectedFileCount));
|
|
1079
|
+
return `${SHARE_BASE_URL}?${params.toString()}`;
|
|
1080
|
+
};
|
|
1081
|
+
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName) => {
|
|
978
1082
|
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
979
1083
|
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
980
1084
|
const affectedFileCount = collectAffectedFiles(diagnostics).size;
|
|
981
1085
|
const elapsed = formatElapsedTime(elapsedMilliseconds);
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
logger.break();
|
|
1086
|
+
const summaryLineParts = [];
|
|
1087
|
+
const summaryLinePartsPlain = [];
|
|
1088
|
+
if (errorCount > 0) {
|
|
1089
|
+
const errorText = `✗ ${errorCount} error${errorCount === 1 ? "" : "s"}`;
|
|
1090
|
+
summaryLinePartsPlain.push(errorText);
|
|
1091
|
+
summaryLineParts.push(highlighter.error(errorText));
|
|
989
1092
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1093
|
+
if (warningCount > 0) {
|
|
1094
|
+
const warningText = `⚠ ${warningCount} warning${warningCount === 1 ? "" : "s"}`;
|
|
1095
|
+
summaryLinePartsPlain.push(warningText);
|
|
1096
|
+
summaryLineParts.push(highlighter.warn(warningText));
|
|
1097
|
+
}
|
|
1098
|
+
const fileCountText = `across ${affectedFileCount} file${affectedFileCount === 1 ? "" : "s"}`;
|
|
1099
|
+
const elapsedTimeText = `in ${elapsed}`;
|
|
1100
|
+
summaryLinePartsPlain.push(fileCountText);
|
|
1101
|
+
summaryLinePartsPlain.push(elapsedTimeText);
|
|
1102
|
+
summaryLineParts.push(highlighter.dim(fileCountText));
|
|
1103
|
+
summaryLineParts.push(highlighter.dim(elapsedTimeText));
|
|
1104
|
+
const summaryFramedLines = [];
|
|
1105
|
+
if (scoreResult) {
|
|
1106
|
+
const [eyes, mouth] = getDoctorFace(scoreResult.score);
|
|
1107
|
+
const scoreColorizer = (text) => colorizeByScore(text, scoreResult.score);
|
|
1108
|
+
summaryFramedLines.push(createFramedLine("┌─────┐", scoreColorizer("┌─────┐")));
|
|
1109
|
+
summaryFramedLines.push(createFramedLine(`│ ${eyes} │`, scoreColorizer(`│ ${eyes} │`)));
|
|
1110
|
+
summaryFramedLines.push(createFramedLine(`│ ${mouth} │`, scoreColorizer(`│ ${mouth} │`)));
|
|
1111
|
+
summaryFramedLines.push(createFramedLine("└─────┘", scoreColorizer("└─────┘")));
|
|
1112
|
+
summaryFramedLines.push(createFramedLine("React Doctor (www.react.doctor)", `React Doctor ${highlighter.dim("(www.react.doctor)")}`));
|
|
1113
|
+
summaryFramedLines.push(createFramedLine(""));
|
|
1114
|
+
const scoreLinePlainText = `${scoreResult.score} / ${PERFECT_SCORE} ${scoreResult.label}`;
|
|
1115
|
+
const scoreLineRenderedText = `${colorizeByScore(String(scoreResult.score), scoreResult.score)} / ${PERFECT_SCORE} ${colorizeByScore(scoreResult.label, scoreResult.score)}`;
|
|
1116
|
+
summaryFramedLines.push(createFramedLine(scoreLinePlainText, scoreLineRenderedText));
|
|
1117
|
+
summaryFramedLines.push(createFramedLine(""));
|
|
1118
|
+
summaryFramedLines.push(createFramedLine(buildPlainScoreBar(scoreResult.score), buildScoreBar(scoreResult.score)));
|
|
1119
|
+
summaryFramedLines.push(createFramedLine(""));
|
|
1120
|
+
} else {
|
|
1121
|
+
summaryFramedLines.push(createFramedLine("React Doctor (www.react.doctor)", `React Doctor ${highlighter.dim("(www.react.doctor)")}`));
|
|
1122
|
+
summaryFramedLines.push(createFramedLine(""));
|
|
1123
|
+
summaryFramedLines.push(createFramedLine(OFFLINE_MESSAGE, highlighter.dim(OFFLINE_MESSAGE)));
|
|
1124
|
+
summaryFramedLines.push(createFramedLine(""));
|
|
1125
|
+
}
|
|
1126
|
+
summaryFramedLines.push(createFramedLine(summaryLinePartsPlain.join(" "), summaryLineParts.join(" ")));
|
|
1127
|
+
printFramedBox(summaryFramedLines);
|
|
996
1128
|
try {
|
|
997
1129
|
const diagnosticsDirectory = writeDiagnosticsDirectory(diagnostics);
|
|
998
1130
|
logger.break();
|
|
@@ -1000,6 +1132,9 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult) => {
|
|
|
1000
1132
|
} catch {
|
|
1001
1133
|
logger.break();
|
|
1002
1134
|
}
|
|
1135
|
+
const shareUrl = buildShareUrl(diagnostics, scoreResult, projectName);
|
|
1136
|
+
logger.break();
|
|
1137
|
+
logger.dim(` Share your results: ${highlighter.info(shareUrl)}`);
|
|
1003
1138
|
};
|
|
1004
1139
|
const scan = async (directory, options) => {
|
|
1005
1140
|
const startTime = performance.now();
|
|
@@ -1063,18 +1198,46 @@ const scan = async (directory, options) => {
|
|
|
1063
1198
|
return;
|
|
1064
1199
|
}
|
|
1065
1200
|
printDiagnostics(diagnostics, options.verbose);
|
|
1066
|
-
printSummary(diagnostics, elapsedMilliseconds, scoreResult);
|
|
1201
|
+
printSummary(diagnostics, elapsedMilliseconds, scoreResult, projectInfo.projectName);
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
//#endregion
|
|
1205
|
+
//#region src/utils/should-select-all-choices.ts
|
|
1206
|
+
const shouldSelectAllChoices = (choiceStates) => {
|
|
1207
|
+
return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
|
|
1067
1208
|
};
|
|
1068
1209
|
|
|
1069
1210
|
//#endregion
|
|
1070
1211
|
//#region src/utils/prompts.ts
|
|
1212
|
+
const require = createRequire(import.meta.url);
|
|
1213
|
+
const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
|
|
1214
|
+
let didPatchMultiselectToggleAll = false;
|
|
1071
1215
|
const onCancel = () => {
|
|
1072
1216
|
logger.break();
|
|
1073
1217
|
logger.log("Cancelled.");
|
|
1074
1218
|
logger.break();
|
|
1075
1219
|
process.exit(0);
|
|
1076
1220
|
};
|
|
1221
|
+
const patchMultiselectToggleAll = () => {
|
|
1222
|
+
if (didPatchMultiselectToggleAll) return;
|
|
1223
|
+
didPatchMultiselectToggleAll = true;
|
|
1224
|
+
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
1225
|
+
multiselectPromptConstructor.prototype.toggleAll = function() {
|
|
1226
|
+
const isCurrentChoiceDisabled = Boolean(this.value[this.cursor]?.disabled);
|
|
1227
|
+
if (this.maxChoices !== void 0 || isCurrentChoiceDisabled) {
|
|
1228
|
+
this.bell();
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const shouldSelectAllEnabledChoices = shouldSelectAllChoices(this.value);
|
|
1232
|
+
for (const choiceState of this.value) {
|
|
1233
|
+
if (choiceState.disabled) continue;
|
|
1234
|
+
choiceState.selected = shouldSelectAllEnabledChoices;
|
|
1235
|
+
}
|
|
1236
|
+
this.render();
|
|
1237
|
+
};
|
|
1238
|
+
};
|
|
1077
1239
|
const prompts = (questions) => {
|
|
1240
|
+
patchMultiselectToggleAll();
|
|
1078
1241
|
return basePrompts(questions, { onCancel });
|
|
1079
1242
|
};
|
|
1080
1243
|
|
|
@@ -1084,10 +1247,14 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
|
|
|
1084
1247
|
let packages = listWorkspacePackages(rootDirectory);
|
|
1085
1248
|
if (packages.length === 0) packages = discoverReactSubprojects(rootDirectory);
|
|
1086
1249
|
if (packages.length === 0) return [rootDirectory];
|
|
1250
|
+
if (packages.length === 1) {
|
|
1251
|
+
logger.log(`${highlighter.success("✔")} Select projects to scan ${highlighter.dim("›")} ${packages[0].name}`);
|
|
1252
|
+
return [packages[0].directory];
|
|
1253
|
+
}
|
|
1087
1254
|
if (projectFlag) return resolveProjectFlag(projectFlag, packages);
|
|
1088
1255
|
if (skipPrompts) {
|
|
1089
1256
|
printDiscoveredProjects(packages);
|
|
1090
|
-
|
|
1257
|
+
return packages.map((workspacePackage) => workspacePackage.directory);
|
|
1091
1258
|
}
|
|
1092
1259
|
return promptProjectSelection(packages, rootDirectory);
|
|
1093
1260
|
};
|
|
@@ -1150,9 +1317,10 @@ const writeConfig = (config) => {
|
|
|
1150
1317
|
const installSkill = () => {
|
|
1151
1318
|
try {
|
|
1152
1319
|
execSync(`npx -y skills add ${SKILL_REPO}`, { stdio: "inherit" });
|
|
1153
|
-
return true;
|
|
1154
1320
|
} catch {
|
|
1155
|
-
|
|
1321
|
+
logger.break();
|
|
1322
|
+
logger.dim("Skill install failed. You can install manually:");
|
|
1323
|
+
logger.dim(` npx skills add ${SKILL_REPO}`);
|
|
1156
1324
|
}
|
|
1157
1325
|
};
|
|
1158
1326
|
const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
@@ -1160,8 +1328,9 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
|
1160
1328
|
if (config.skillPromptDismissed) return;
|
|
1161
1329
|
if (shouldSkipPrompts) return;
|
|
1162
1330
|
logger.break();
|
|
1163
|
-
logger.log(`${highlighter.info("💡")}
|
|
1164
|
-
logger.dim(
|
|
1331
|
+
logger.log(`${highlighter.info("💡")} Have your coding agent fix these issues automatically?`);
|
|
1332
|
+
logger.dim(` Install the ${highlighter.info("react-doctor")} skill to teach Cursor, Claude Code, Copilot,`);
|
|
1333
|
+
logger.dim(" Ami, and other AI agents how to diagnose and fix these React issues.");
|
|
1165
1334
|
logger.break();
|
|
1166
1335
|
const { shouldInstall } = await prompts({
|
|
1167
1336
|
type: "confirm",
|
|
@@ -1171,14 +1340,7 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
|
1171
1340
|
});
|
|
1172
1341
|
if (shouldInstall) {
|
|
1173
1342
|
logger.break();
|
|
1174
|
-
|
|
1175
|
-
logger.break();
|
|
1176
|
-
logger.success("Skill installed!");
|
|
1177
|
-
} else {
|
|
1178
|
-
logger.break();
|
|
1179
|
-
logger.dim("Skill install failed. You can install manually:");
|
|
1180
|
-
logger.dim(` npx skills add ${SKILL_REPO}`);
|
|
1181
|
-
}
|
|
1343
|
+
installSkill();
|
|
1182
1344
|
}
|
|
1183
1345
|
writeConfig({
|
|
1184
1346
|
...config,
|
|
@@ -1186,15 +1348,85 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
|
1186
1348
|
});
|
|
1187
1349
|
};
|
|
1188
1350
|
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/utils/global-install.ts
|
|
1353
|
+
const isGloballyInstalled = () => {
|
|
1354
|
+
try {
|
|
1355
|
+
return !execSync("which react-doctor", {
|
|
1356
|
+
stdio: "pipe",
|
|
1357
|
+
encoding: "utf-8"
|
|
1358
|
+
}).trim().includes("/_npx/");
|
|
1359
|
+
} catch {
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
const maybeInstallGlobally = () => {
|
|
1364
|
+
try {
|
|
1365
|
+
if (isGloballyInstalled()) return;
|
|
1366
|
+
const child = spawn("npm", [
|
|
1367
|
+
"install",
|
|
1368
|
+
"-g",
|
|
1369
|
+
"react-doctor@latest"
|
|
1370
|
+
], {
|
|
1371
|
+
detached: true,
|
|
1372
|
+
stdio: "ignore"
|
|
1373
|
+
});
|
|
1374
|
+
child.on("error", () => {});
|
|
1375
|
+
child.unref();
|
|
1376
|
+
} catch {}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
//#endregion
|
|
1380
|
+
//#region src/utils/copy-to-clipboard.ts
|
|
1381
|
+
const getClipboardCommands = () => {
|
|
1382
|
+
if (process.platform === "darwin") return [{
|
|
1383
|
+
command: "pbcopy",
|
|
1384
|
+
args: []
|
|
1385
|
+
}];
|
|
1386
|
+
if (process.platform === "win32") return [{
|
|
1387
|
+
command: "clip",
|
|
1388
|
+
args: []
|
|
1389
|
+
}];
|
|
1390
|
+
return [
|
|
1391
|
+
{
|
|
1392
|
+
command: "wl-copy",
|
|
1393
|
+
args: []
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
command: "xclip",
|
|
1397
|
+
args: ["-selection", "clipboard"]
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
command: "xsel",
|
|
1401
|
+
args: ["--clipboard", "--input"]
|
|
1402
|
+
}
|
|
1403
|
+
];
|
|
1404
|
+
};
|
|
1405
|
+
const copyToClipboard = (text) => {
|
|
1406
|
+
const clipboardCommands = getClipboardCommands();
|
|
1407
|
+
for (const clipboardCommand of clipboardCommands) if (spawnSync(clipboardCommand.command, clipboardCommand.args, {
|
|
1408
|
+
input: text,
|
|
1409
|
+
stdio: [
|
|
1410
|
+
"pipe",
|
|
1411
|
+
"ignore",
|
|
1412
|
+
"ignore"
|
|
1413
|
+
],
|
|
1414
|
+
encoding: "utf8"
|
|
1415
|
+
}).status === 0) return true;
|
|
1416
|
+
return false;
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1189
1419
|
//#endregion
|
|
1190
1420
|
//#region src/cli.ts
|
|
1191
|
-
const VERSION = "0.0.
|
|
1421
|
+
const VERSION = "0.0.9";
|
|
1192
1422
|
process.on("SIGINT", () => process.exit(0));
|
|
1193
1423
|
process.on("SIGTERM", () => process.exit(0));
|
|
1194
|
-
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("--fix", "open Ami to auto-fix all issues").action(async (directory, flags) => {
|
|
1424
|
+
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("--fix", "open Ami to auto-fix all issues").option("--prompt", "copy latest scan output to clipboard").action(async (directory, flags) => {
|
|
1425
|
+
const isScoreOnly = flags.score && !flags.prompt;
|
|
1426
|
+
const shouldCopyPromptOutput = flags.prompt;
|
|
1427
|
+
if (shouldCopyPromptOutput) startLoggerCapture();
|
|
1195
1428
|
try {
|
|
1196
1429
|
const resolvedDirectory = path.resolve(directory);
|
|
1197
|
-
const isScoreOnly = flags.score;
|
|
1198
1430
|
if (!isScoreOnly) {
|
|
1199
1431
|
logger.log(`react-doctor v${VERSION}`);
|
|
1200
1432
|
logger.break();
|
|
@@ -1202,10 +1434,20 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1202
1434
|
const scanOptions = {
|
|
1203
1435
|
lint: flags.lint,
|
|
1204
1436
|
deadCode: flags.deadCode,
|
|
1205
|
-
verbose: Boolean(flags.verbose),
|
|
1437
|
+
verbose: flags.prompt || Boolean(flags.verbose),
|
|
1206
1438
|
scoreOnly: isScoreOnly
|
|
1207
1439
|
};
|
|
1208
|
-
const
|
|
1440
|
+
const isAutomatedEnvironment = [
|
|
1441
|
+
process.env.CI,
|
|
1442
|
+
process.env.CLAUDECODE,
|
|
1443
|
+
process.env.CURSOR_TRACE_ID,
|
|
1444
|
+
process.env.CURSOR_AGENT,
|
|
1445
|
+
process.env.CODEX_CI,
|
|
1446
|
+
process.env.OPENCODE,
|
|
1447
|
+
process.env.AMP_HOME,
|
|
1448
|
+
process.env.AMI
|
|
1449
|
+
].some(Boolean);
|
|
1450
|
+
const shouldSkipPrompts = flags.yes || isAutomatedEnvironment || !process.stdin.isTTY;
|
|
1209
1451
|
const projectDirectories = await selectProjects(resolvedDirectory, flags.project, shouldSkipPrompts);
|
|
1210
1452
|
for (const projectDirectory of projectDirectories) {
|
|
1211
1453
|
if (!isScoreOnly) {
|
|
@@ -1216,17 +1458,23 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
1216
1458
|
if (!isScoreOnly) logger.break();
|
|
1217
1459
|
}
|
|
1218
1460
|
if (flags.fix) openAmiToFix(resolvedDirectory);
|
|
1219
|
-
if (!isScoreOnly
|
|
1461
|
+
if (!isScoreOnly && !flags.prompt) {
|
|
1462
|
+
await maybePromptSkillInstall(shouldSkipPrompts);
|
|
1463
|
+
if (!shouldSkipPrompts && !flags.fix) await maybePromptAmiFix(resolvedDirectory);
|
|
1464
|
+
}
|
|
1220
1465
|
} catch (error) {
|
|
1221
|
-
handleError(error);
|
|
1466
|
+
handleError(error, { shouldExit: !shouldCopyPromptOutput });
|
|
1467
|
+
} finally {
|
|
1468
|
+
if (shouldCopyPromptOutput) copyPromptToClipboard(stopLoggerCapture(), !isScoreOnly);
|
|
1222
1469
|
}
|
|
1223
1470
|
}).addHelpText("after", `
|
|
1224
1471
|
${highlighter.dim("Learn more:")}
|
|
1225
1472
|
${highlighter.info("https://github.com/aidenybai/react-doctor")}
|
|
1226
1473
|
`);
|
|
1227
1474
|
const AMI_INSTALL_URL = "https://ami.dev/install.sh";
|
|
1228
|
-
const
|
|
1229
|
-
const
|
|
1475
|
+
const FIX_PROMPT = "Fix all issues reported in the react-doctor diagnostics below, one by one. After applying fixes, run `react-dcotor` again to verify the results improved.";
|
|
1476
|
+
const REACT_DOCTOR_OUTPUT_LABEL = "react-doctor output";
|
|
1477
|
+
const SCAN_SUMMARY_SEPARATOR = "─".repeat(SEPARATOR_LENGTH_CHARS);
|
|
1230
1478
|
const isAmiInstalled = () => {
|
|
1231
1479
|
try {
|
|
1232
1480
|
execSync("ls /Applications/Ami.app", { stdio: "ignore" });
|
|
@@ -1250,19 +1498,45 @@ const openAmiToFix = (directory) => {
|
|
|
1250
1498
|
const resolvedDirectory = path.resolve(directory);
|
|
1251
1499
|
if (!isAmiInstalled()) installAmi();
|
|
1252
1500
|
logger.log("Opening Ami to fix react-doctor issues...");
|
|
1253
|
-
const
|
|
1254
|
-
const encodedPrompt = encodeURIComponent(AMI_FIX_PROMPT);
|
|
1255
|
-
const openProjectDeeplink = `ami://open-project?cwd=${encodedDirectory}`;
|
|
1256
|
-
const newChatDeeplink = `ami://new-chat?prompt=${encodedPrompt}&mode=agent&send=true`;
|
|
1501
|
+
const deeplink = `ami://open-project?cwd=${encodeURIComponent(resolvedDirectory)}&prompt=${encodeURIComponent(FIX_PROMPT)}&mode=agent`;
|
|
1257
1502
|
try {
|
|
1258
|
-
execSync(`open "${
|
|
1503
|
+
execSync(`open "${deeplink}"`, { stdio: "ignore" });
|
|
1259
1504
|
logger.success("Opened Ami with react-doctor fix prompt.");
|
|
1260
1505
|
} catch {
|
|
1261
1506
|
logger.break();
|
|
1262
|
-
logger.dim("Could not open Ami automatically. Open
|
|
1263
|
-
logger.info(
|
|
1264
|
-
|
|
1507
|
+
logger.dim("Could not open Ami automatically. Open this URL manually:");
|
|
1508
|
+
logger.info(deeplink);
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
const buildPromptWithOutput = (reactDoctorOutput) => {
|
|
1512
|
+
const summaryStartIndex = reactDoctorOutput.indexOf(SCAN_SUMMARY_SEPARATOR);
|
|
1513
|
+
const normalizedReactDoctorOutput = (summaryStartIndex === -1 ? reactDoctorOutput : reactDoctorOutput.slice(0, summaryStartIndex).trimEnd()).trim();
|
|
1514
|
+
return `${FIX_PROMPT}\n\n${REACT_DOCTOR_OUTPUT_LABEL}:\n\`\`\`\n${normalizedReactDoctorOutput.length > 0 ? normalizedReactDoctorOutput : "No output captured."}\n\`\`\``;
|
|
1515
|
+
};
|
|
1516
|
+
const copyPromptToClipboard = (reactDoctorOutput, shouldLogResult) => {
|
|
1517
|
+
const promptWithOutput = buildPromptWithOutput(reactDoctorOutput);
|
|
1518
|
+
const didCopyPromptToClipboard = copyToClipboard(promptWithOutput);
|
|
1519
|
+
if (!shouldLogResult) return;
|
|
1520
|
+
if (didCopyPromptToClipboard) {
|
|
1521
|
+
logger.success("Copied latest scan output to clipboard");
|
|
1522
|
+
return;
|
|
1265
1523
|
}
|
|
1524
|
+
logger.warn("Could not copy prompt to clipboard automatically. Use this prompt:");
|
|
1525
|
+
logger.info(promptWithOutput);
|
|
1526
|
+
};
|
|
1527
|
+
const maybePromptAmiFix = async (directory) => {
|
|
1528
|
+
logger.break();
|
|
1529
|
+
logger.log(`Fix these issues with ${highlighter.info("Ami")}?`);
|
|
1530
|
+
logger.dim(" Ami is a coding agent built to understand your codebase and fix issues");
|
|
1531
|
+
logger.dim(` automatically. Learn more at ${highlighter.info("https://ami.dev")}`);
|
|
1532
|
+
logger.break();
|
|
1533
|
+
const { shouldFix } = await prompts({
|
|
1534
|
+
type: "confirm",
|
|
1535
|
+
name: "shouldFix",
|
|
1536
|
+
message: "Open Ami to fix?",
|
|
1537
|
+
initial: true
|
|
1538
|
+
});
|
|
1539
|
+
if (shouldFix) openAmiToFix(directory);
|
|
1266
1540
|
};
|
|
1267
1541
|
const fixAction = (directory) => {
|
|
1268
1542
|
try {
|
|
@@ -1272,10 +1546,11 @@ const fixAction = (directory) => {
|
|
|
1272
1546
|
}
|
|
1273
1547
|
};
|
|
1274
1548
|
const fixCommand = new Command("fix").description("Open Ami to auto-fix react-doctor issues").argument("[directory]", "project directory", ".").action(fixAction);
|
|
1275
|
-
const installAmiCommand = new Command("install-ami").description("
|
|
1549
|
+
const installAmiCommand = new Command("install-ami").description("Install Ami and open it to auto-fix issues").argument("[directory]", "project directory", ".").action(fixAction);
|
|
1276
1550
|
program.addCommand(fixCommand);
|
|
1277
1551
|
program.addCommand(installAmiCommand);
|
|
1278
1552
|
const main$1 = async () => {
|
|
1553
|
+
maybeInstallGlobally();
|
|
1279
1554
|
await program.parseAsync();
|
|
1280
1555
|
};
|
|
1281
1556
|
main$1();
|