react-doctor 0.2.9 → 0.2.11-dev.15e5fec
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 +26 -0
- package/dist/{cli-logger-BliQX9s8.js → cli-logger-CSZagq1E.js} +598 -93
- package/dist/cli.js +194 -110
- package/dist/index.d.ts +26 -2
- package/dist/index.js +592 -94
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-uZX_iqCz.js";
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as isMonorepoRoot, C as filterDiagnosticsForSurface, D as getDiffInfo, E as formatReactDoctorError, F as restoreLegacyThrow, I as runInspect, L as toRelativePath, M as layerOtlp, N as listWorkspacePackages, O as groupBy, P as resolveScanTarget, S as discoverReactSubprojects, T as formatErrorChain, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as isReactDoctorError, k as highlighter, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as filterSourceFiles, x as buildRulePromptUrl, y as buildJsonReport } from "./cli-logger-CSZagq1E.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { execFileSync, execSync } from "node:child_process";
|
|
5
5
|
import path, { join } from "node:path";
|
|
6
|
-
import { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import fs, { accessSync, chmodSync, constants, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
7
7
|
import process$1 from "node:process";
|
|
8
8
|
import * as Effect from "effect/Effect";
|
|
9
9
|
import * as Layer from "effect/Layer";
|
|
@@ -6220,6 +6220,8 @@ const padRuleNameToColumn = (ruleName, columnWidth) => {
|
|
|
6220
6220
|
return ruleName + " ".repeat(columnWidth - ruleName.length);
|
|
6221
6221
|
};
|
|
6222
6222
|
const grayLine = (text) => highlighter.gray(text);
|
|
6223
|
+
const FETCH_FIX_RECIPE_LABEL = "Fetch & follow the canonical fix recipe before fixing";
|
|
6224
|
+
const formatFixRecipeLine = (diagnostic) => `${FETCH_FIX_RECIPE_LABEL}: ${buildRulePromptUrl(diagnostic.plugin, diagnostic.rule)}`;
|
|
6223
6225
|
const buildCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
|
|
6224
6226
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
6225
6227
|
const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
|
|
@@ -6255,6 +6257,7 @@ const buildVerboseRuleGroupLines = (ruleKey, ruleDiagnostics, ruleNameColumnWidt
|
|
|
6255
6257
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
6256
6258
|
lines.push(grayLine(indentMultilineText(firstDiagnostic.message, " ")));
|
|
6257
6259
|
if (firstDiagnostic.help) lines.push(grayLine(indentMultilineText(`→ ${firstDiagnostic.help}`, " ")));
|
|
6260
|
+
lines.push(grayLine(` ${formatFixRecipeLine(firstDiagnostic)}`));
|
|
6258
6261
|
const fileSites = buildVerboseSiteMap(ruleDiagnostics);
|
|
6259
6262
|
for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
|
|
6260
6263
|
lines.push(grayLine(` ${filePath}:${site.line}`));
|
|
@@ -6299,6 +6302,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
|
|
|
6299
6302
|
];
|
|
6300
6303
|
if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
|
|
6301
6304
|
if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
|
|
6305
|
+
sections.push("", formatFixRecipeLine(firstDiagnostic));
|
|
6302
6306
|
sections.push("", "Files:");
|
|
6303
6307
|
const fileSites = buildVerboseSiteMap(ruleDiagnostics);
|
|
6304
6308
|
for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
|
|
@@ -6320,11 +6324,11 @@ const colorizeByScore = (text, score) => {
|
|
|
6320
6324
|
return highlighter.error(text);
|
|
6321
6325
|
};
|
|
6322
6326
|
//#endregion
|
|
6327
|
+
//#region src/cli/utils/constants.ts
|
|
6328
|
+
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
6329
|
+
const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
|
|
6330
|
+
//#endregion
|
|
6323
6331
|
//#region src/cli/utils/render-score-header.ts
|
|
6324
|
-
const SCORE_BAR_ANIMATION_FRAME_COUNT = 40;
|
|
6325
|
-
const SCORE_BAR_ANIMATION_FRAME_DELAY_MS = 50;
|
|
6326
|
-
const PERFECT_SCORE_RAINBOW_FRAME_COUNT = 16;
|
|
6327
|
-
const PERFECT_SCORE_RAINBOW_FRAME_DELAY_MS = 50;
|
|
6328
6332
|
const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
|
|
6329
6333
|
const RAINBOW_GRADIENT_WIDTH = 80;
|
|
6330
6334
|
const RAINBOW_OKLCH_LIGHTNESS = .638;
|
|
@@ -6433,8 +6437,8 @@ const buildInitialScoreHeaderLine = ({ isPerfectScore, shouldAnimate, lineIndex,
|
|
|
6433
6437
|
};
|
|
6434
6438
|
const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectName) => Effect.gen(function* () {
|
|
6435
6439
|
const isPerfectScore = score === 100;
|
|
6436
|
-
for (let frame = 0; frame <=
|
|
6437
|
-
const progress = easeOutCubic(frame /
|
|
6440
|
+
for (let frame = 0; frame <= 40; frame += 1) {
|
|
6441
|
+
const progress = easeOutCubic(frame / 40);
|
|
6438
6442
|
const animatedScore = Math.round(score * progress);
|
|
6439
6443
|
if (isPerfectScore) {
|
|
6440
6444
|
yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[4A"}\r${buildRainbowScoreHeaderFrame({
|
|
@@ -6444,16 +6448,16 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
|
|
|
6444
6448
|
frame,
|
|
6445
6449
|
projectName
|
|
6446
6450
|
})}`);
|
|
6447
|
-
if (frame <
|
|
6451
|
+
if (frame < 40) yield* sleep(50);
|
|
6448
6452
|
continue;
|
|
6449
6453
|
}
|
|
6450
6454
|
const animatedScoreLine = buildScoreLine(animatedScore, score, label, projectName);
|
|
6451
6455
|
const animatedBarLine = buildScoreBar(animatedScore, score);
|
|
6452
6456
|
yield* writeScoreHeaderLine(`${frame === 0 ? "" : "\x1B[2A"}\r${buildScoreHeaderLine(scoreFaceLine, animatedScoreLine)}\n\r${buildScoreHeaderLine(barFaceLine, animatedBarLine)}\n`);
|
|
6453
|
-
if (frame <
|
|
6457
|
+
if (frame < 40) yield* sleep(50);
|
|
6454
6458
|
}
|
|
6455
6459
|
if (!isPerfectScore) return;
|
|
6456
|
-
for (let frame = 0; frame <
|
|
6460
|
+
for (let frame = 0; frame < 16; frame += 1) {
|
|
6457
6461
|
yield* writeScoreHeaderLine(`\x1b[4A\r${buildRainbowScoreHeaderFrame({
|
|
6458
6462
|
score,
|
|
6459
6463
|
displayScore: score,
|
|
@@ -6461,9 +6465,9 @@ const printAnimatedScore = (scoreFaceLine, barFaceLine, score, label, projectNam
|
|
|
6461
6465
|
frame,
|
|
6462
6466
|
projectName
|
|
6463
6467
|
})}`);
|
|
6464
|
-
yield* sleep(
|
|
6468
|
+
yield* sleep(50);
|
|
6465
6469
|
}
|
|
6466
|
-
yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label,
|
|
6470
|
+
yield* writeScoreHeaderLine(`\x1b[4A\r${buildFinalPerfectScoreHeaderFrame(score, label, 16, projectName)}\x1b[2A`);
|
|
6467
6471
|
});
|
|
6468
6472
|
const printScoreHeader = (scoreResult, projectName) => Effect.gen(function* () {
|
|
6469
6473
|
const isPerfectScore = scoreResult.score === 100;
|
|
@@ -6539,7 +6543,11 @@ const printCountsSummaryLine = (diagnostics, isVerbose) => Effect.gen(function*
|
|
|
6539
6543
|
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
6540
6544
|
const issueText = (errorCount > 0 ? highlighter.error : warningCount > 0 ? highlighter.warn : highlighter.dim)(`${totalIssueCount} ${totalIssueCount === 1 ? "issue" : "issues"}`);
|
|
6541
6545
|
yield* Console.log(` ${issueText}`);
|
|
6542
|
-
if (!isVerbose && totalIssueCount > 0)
|
|
6546
|
+
if (!isVerbose && totalIssueCount > 0) {
|
|
6547
|
+
const exampleDiagnostic = diagnostics.find((diagnostic) => diagnostic.severity === "error") ?? diagnostics[0];
|
|
6548
|
+
yield* Console.log(highlighter.dim(` Run ${highlighter.info("npx react-doctor@latest --verbose")} to list every issue with its fix-recipe URL`));
|
|
6549
|
+
yield* Console.log(highlighter.dim(` Each rule links a canonical fix recipe to fetch & follow before fixing, e.g. ${highlighter.info(buildRulePromptUrl(exampleDiagnostic.plugin, exampleDiagnostic.rule))}`));
|
|
6550
|
+
}
|
|
6543
6551
|
});
|
|
6544
6552
|
const printSummary = (input) => Effect.gen(function* () {
|
|
6545
6553
|
if (input.scoreResult) yield* printScoreHeader(input.scoreResult, input.projectName);
|
|
@@ -6666,7 +6674,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
|
|
|
6666
6674
|
const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
|
|
6667
6675
|
//#endregion
|
|
6668
6676
|
//#region src/cli/utils/version.ts
|
|
6669
|
-
const VERSION = "0.2.
|
|
6677
|
+
const VERSION = "0.2.11-dev.15e5fec";
|
|
6670
6678
|
//#endregion
|
|
6671
6679
|
//#region src/inspect.ts
|
|
6672
6680
|
const silentConsole = makeNoopConsole();
|
|
@@ -6764,9 +6772,7 @@ const runInspectWithRuntime = async (directory, options, userConfig, hasConfigOv
|
|
|
6764
6772
|
const output = await Effect.runPromise(restoreLegacyThrow(programWithLayers));
|
|
6765
6773
|
const didLintFail = lintBindingMissing || output.didLintFail;
|
|
6766
6774
|
const lintFailureReason = lintBindingMissing ? `oxlint native binding not found for Node ${process.version}; expected one matching ${OXLINT_NODE_REQUIREMENT}` : output.lintFailureReason;
|
|
6767
|
-
|
|
6768
|
-
const isNativeBindingFailure = lintFailureReasonTag === "OxlintUnavailable" || lintFailureReasonTag === "OxlintSpawnFailed";
|
|
6769
|
-
if (!options.scoreOnly && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (isNativeBindingFailure && /native binding/.test(lintFailureReason)) runConsole(Console.log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
|
|
6775
|
+
if (!options.scoreOnly && !lintBindingMissing && output.didLintFail && lintFailureReason !== null) if (output.lintFailureReasonKind === "native-binding-missing") runConsole(Console.log(highlighter.gray(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`)));
|
|
6770
6776
|
else runConsole(Console.error(highlighter.error(lintFailureReason)));
|
|
6771
6777
|
const inspectDiagnostics = output.diagnostics;
|
|
6772
6778
|
const score = didLintFail ? null : output.score;
|
|
@@ -6854,10 +6860,6 @@ const finalizeAndRender = (input) => Effect.gen(function* () {
|
|
|
6854
6860
|
return buildResult();
|
|
6855
6861
|
});
|
|
6856
6862
|
//#endregion
|
|
6857
|
-
//#region src/cli/utils/constants.ts
|
|
6858
|
-
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
6859
|
-
const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
|
|
6860
|
-
//#endregion
|
|
6861
6863
|
//#region src/cli/utils/get-staged-files.ts
|
|
6862
6864
|
const stagedFilesLayer = StagedFiles.layerNode.pipe(Layer.provide(Git.layerNode));
|
|
6863
6865
|
const getStagedSourceFiles = async (directory) => {
|
|
@@ -7072,15 +7074,16 @@ const buildIssuesSummary = (input) => {
|
|
|
7072
7074
|
if (input.score) lines.push(`Score: ${input.score.score}/100`);
|
|
7073
7075
|
lines.push(`${input.diagnostics.length} issues found`);
|
|
7074
7076
|
lines.push("");
|
|
7075
|
-
const sortedRules = [...groupBy([...input.diagnostics], (diagnostic) => diagnostic.rule).entries()].sort(([, diagnosticsA], [, diagnosticsB]) => diagnosticsB.length - diagnosticsA.length);
|
|
7077
|
+
const sortedRules = [...groupBy([...input.diagnostics], (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()].sort(([, diagnosticsA], [, diagnosticsB]) => diagnosticsB.length - diagnosticsA.length);
|
|
7076
7078
|
const visibleRules = sortedRules.slice(0, MAX_RULES_SHOWN);
|
|
7077
|
-
for (const [
|
|
7079
|
+
for (const [ruleKey, ruleDiagnostics] of visibleRules) {
|
|
7078
7080
|
const severity = ruleDiagnostics[0].severity;
|
|
7079
7081
|
const uniqueFiles = [...new Set(ruleDiagnostics.map((diagnostic) => diagnostic.filePath))];
|
|
7080
7082
|
const shownFiles = uniqueFiles.slice(0, MAX_FILES_PER_RULE);
|
|
7081
7083
|
const remainingFileCount = uniqueFiles.length - shownFiles.length;
|
|
7082
|
-
lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${
|
|
7084
|
+
lines.push(`${severity === "error" ? "ERROR" : "WARN"} ${ruleKey} (×${ruleDiagnostics.length})`);
|
|
7083
7085
|
lines.push(` ${ruleDiagnostics[0].message}`);
|
|
7086
|
+
lines.push(` ${formatFixRecipeLine(ruleDiagnostics[0])}`);
|
|
7084
7087
|
for (const filePath of shownFiles) {
|
|
7085
7088
|
const firstSite = ruleDiagnostics.find((diagnostic) => diagnostic.filePath === filePath && diagnostic.line > 0);
|
|
7086
7089
|
lines.push(` - ${filePath}${firstSite ? `:${firstSite.line}` : ""}`);
|
|
@@ -7100,11 +7103,12 @@ const buildIssuesSummary = (input) => {
|
|
|
7100
7103
|
lines.push("");
|
|
7101
7104
|
lines.push("## How to fix");
|
|
7102
7105
|
lines.push("1. Run `npx react-doctor@latest --verbose` to see full details");
|
|
7103
|
-
lines.push("2.
|
|
7104
|
-
lines.push("3.
|
|
7105
|
-
lines.push("4.
|
|
7106
|
-
lines.push("5.
|
|
7107
|
-
lines.push("6.
|
|
7106
|
+
lines.push("2. For each rule above, fetch & follow its canonical fix recipe URL before fixing.");
|
|
7107
|
+
lines.push("3. Fix errors first, then warnings. Start with high-count rules.");
|
|
7108
|
+
lines.push("4. Read the code before acting. Treat findings as hypotheses, not commands.");
|
|
7109
|
+
lines.push("5. Fix root causes, not symptoms. Don't suppress rules without evidence.");
|
|
7110
|
+
lines.push("6. Run `npx react-doctor@latest --verbose --diff` after changes to verify.");
|
|
7111
|
+
lines.push("7. Split unrelated fixes into separate PRs.");
|
|
7108
7112
|
return lines.join("\n");
|
|
7109
7113
|
};
|
|
7110
7114
|
const copyToClipboard = (text) => {
|
|
@@ -7159,6 +7163,29 @@ const promptCopyIssues = async (input) => {
|
|
|
7159
7163
|
else cliLogger.log(issuesSummary);
|
|
7160
7164
|
};
|
|
7161
7165
|
//#endregion
|
|
7166
|
+
//#region src/cli/utils/path-format.ts
|
|
7167
|
+
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
7168
|
+
//#endregion
|
|
7169
|
+
//#region src/cli/utils/read-changed-files-from.ts
|
|
7170
|
+
const isSafeRelativePath = (filePath) => {
|
|
7171
|
+
if (filePath.length === 0) return false;
|
|
7172
|
+
if (filePath.includes("\0")) return false;
|
|
7173
|
+
if (path.isAbsolute(filePath)) return false;
|
|
7174
|
+
const normalized = path.posix.normalize(filePath);
|
|
7175
|
+
if (normalized === "." || normalized.startsWith("../") || normalized === "..") return false;
|
|
7176
|
+
return normalized === filePath;
|
|
7177
|
+
};
|
|
7178
|
+
const readChangedFilesFrom = (filePath) => {
|
|
7179
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
7180
|
+
const uniqueFiles = /* @__PURE__ */ new Set();
|
|
7181
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
7182
|
+
const candidate = toForwardSlashes(line.trim());
|
|
7183
|
+
if (!isSafeRelativePath(candidate)) continue;
|
|
7184
|
+
uniqueFiles.add(candidate);
|
|
7185
|
+
}
|
|
7186
|
+
return [...uniqueFiles];
|
|
7187
|
+
};
|
|
7188
|
+
//#endregion
|
|
7162
7189
|
//#region src/cli/utils/render-multi-project-summary.ts
|
|
7163
7190
|
const SUMMARY_BAR_WIDTH_CHARS = 20;
|
|
7164
7191
|
const buildMiniBar = (score) => {
|
|
@@ -7221,6 +7248,7 @@ const printMultiProjectSummary = (input) => Effect.gen(function* () {
|
|
|
7221
7248
|
};
|
|
7222
7249
|
});
|
|
7223
7250
|
const longestProjectNameLength = Math.max(...entries.map((entry) => entry.projectName.length));
|
|
7251
|
+
yield* Console.log("");
|
|
7224
7252
|
for (const entry of entries) yield* Console.log(buildSummaryLine(entry, longestProjectNameLength));
|
|
7225
7253
|
yield* Console.log("");
|
|
7226
7254
|
});
|
|
@@ -7416,7 +7444,7 @@ const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
7416
7444
|
return true;
|
|
7417
7445
|
};
|
|
7418
7446
|
const shouldPromptInstallSetup = (options) => {
|
|
7419
|
-
if (!options.hasScoredScan) return false;
|
|
7447
|
+
if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
|
|
7420
7448
|
if (options.isJsonMode) return false;
|
|
7421
7449
|
if (options.isScoreOnly) return false;
|
|
7422
7450
|
if (options.isStaged) return false;
|
|
@@ -7426,13 +7454,13 @@ const shouldPromptInstallSetup = (options) => {
|
|
|
7426
7454
|
return !hasDoctorScript(options.projectRoot);
|
|
7427
7455
|
};
|
|
7428
7456
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
7429
|
-
if (options.
|
|
7457
|
+
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
7430
7458
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
7431
|
-
for (const scanDirectory of options.
|
|
7459
|
+
for (const scanDirectory of options.scanDirectories) {
|
|
7432
7460
|
const packageDirectory = findNearestPackageDirectory(scanDirectory, options.scanRoot) ?? findNearestPackageDirectory(scanDirectory) ?? scanDirectory;
|
|
7433
7461
|
packageDirectories.add(packageDirectory);
|
|
7434
7462
|
}
|
|
7435
|
-
if (packageDirectories.size !== 1) return
|
|
7463
|
+
if (packageDirectories.size !== 1) return findNearestPackageDirectory(options.scanRoot, options.scanRoot);
|
|
7436
7464
|
return [...packageDirectories][0] ?? null;
|
|
7437
7465
|
};
|
|
7438
7466
|
const defaultWait = (milliseconds) => new Promise((resolve) => {
|
|
@@ -7467,7 +7495,7 @@ const warnSetupPromptFailure = async (options, error) => {
|
|
|
7467
7495
|
return;
|
|
7468
7496
|
}
|
|
7469
7497
|
try {
|
|
7470
|
-
const { cliLogger } = await import("./cli-logger-
|
|
7498
|
+
const { cliLogger } = await import("./cli-logger-CSZagq1E.js").then((n) => n.n);
|
|
7471
7499
|
cliLogger.warn(message);
|
|
7472
7500
|
} catch {}
|
|
7473
7501
|
};
|
|
@@ -7483,7 +7511,7 @@ const promptInstallSetup = async (options) => {
|
|
|
7483
7511
|
writeLine("You can always run `npx react-doctor@latest install` to set it up later.");
|
|
7484
7512
|
return;
|
|
7485
7513
|
}
|
|
7486
|
-
const install = options.install ?? (await Promise.resolve().then(() =>
|
|
7514
|
+
const install = options.install ?? (await Promise.resolve().then(() => install_react_doctor_exports)).runInstallReactDoctor;
|
|
7487
7515
|
const previousExitCode = process.exitCode;
|
|
7488
7516
|
let setupExitCode;
|
|
7489
7517
|
try {
|
|
@@ -7502,7 +7530,7 @@ const promptInstallSetup = async (options) => {
|
|
|
7502
7530
|
}
|
|
7503
7531
|
};
|
|
7504
7532
|
const shouldShowAgentInstallHint = (options) => {
|
|
7505
|
-
if (!options.hasScoredScan) return false;
|
|
7533
|
+
if (!(options.hasCompletedScan ?? options.hasScoredScan ?? false)) return false;
|
|
7506
7534
|
if (options.isJsonMode) return false;
|
|
7507
7535
|
if (options.isScoreOnly) return false;
|
|
7508
7536
|
if (options.isStaged) return false;
|
|
@@ -7567,15 +7595,17 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isQui
|
|
|
7567
7595
|
const { scanScope } = await prompts({
|
|
7568
7596
|
type: "select",
|
|
7569
7597
|
name: "scanScope",
|
|
7570
|
-
message: "
|
|
7598
|
+
message: "Choose what to scan",
|
|
7571
7599
|
choices: [{
|
|
7572
7600
|
title: "Full codebase",
|
|
7601
|
+
description: "Scan every source file",
|
|
7573
7602
|
value: "full"
|
|
7574
7603
|
}, {
|
|
7575
|
-
title: `Changed files (${changedSourceFiles.length})`,
|
|
7604
|
+
title: diffInfo.isCurrentChanges ? `Uncommitted changes (${changedSourceFiles.length})` : `Changed files on ${diffInfo.currentBranch ?? "this branch"} (${changedSourceFiles.length})`,
|
|
7605
|
+
description: diffInfo.isCurrentChanges ? "Compare working tree changes against HEAD" : `Compare against ${diffInfo.baseBranch} from the branch merge-base`,
|
|
7576
7606
|
value: "branch"
|
|
7577
7607
|
}],
|
|
7578
|
-
initial: 0
|
|
7608
|
+
initial: diffInfo.isCurrentChanges ? 0 : 1
|
|
7579
7609
|
});
|
|
7580
7610
|
return scanScope === "branch";
|
|
7581
7611
|
};
|
|
@@ -7605,17 +7635,16 @@ const VALID_FAIL_ON_LEVELS = new Set([
|
|
|
7605
7635
|
"warning",
|
|
7606
7636
|
"none"
|
|
7607
7637
|
]);
|
|
7608
|
-
const DEFAULT_FAIL_ON_LEVEL = "
|
|
7638
|
+
const DEFAULT_FAIL_ON_LEVEL = "none";
|
|
7609
7639
|
const isValidFailOnLevel = (level) => VALID_FAIL_ON_LEVELS.has(level);
|
|
7610
7640
|
const resolveFailOnLevel = (flags, userConfig) => {
|
|
7611
7641
|
const sourceValue = flags.failOn ?? userConfig?.failOn ?? DEFAULT_FAIL_ON_LEVEL;
|
|
7612
7642
|
if (isValidFailOnLevel(sourceValue)) return sourceValue;
|
|
7613
|
-
cliLogger.warn(`Invalid failOn level "${sourceValue}". Expected one of: error, warning, none. Falling back to "
|
|
7614
|
-
return
|
|
7643
|
+
cliLogger.warn(`Invalid failOn level "${sourceValue}". Expected one of: error, warning, none. Falling back to "${DEFAULT_FAIL_ON_LEVEL}".`);
|
|
7644
|
+
return DEFAULT_FAIL_ON_LEVEL;
|
|
7615
7645
|
};
|
|
7616
7646
|
//#endregion
|
|
7617
7647
|
//#region src/cli/utils/resolve-project-diff-include-paths.ts
|
|
7618
|
-
const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
|
|
7619
7648
|
const resolveProjectDiffIncludePaths = (rootDirectory, projectDirectory, diffInfo) => {
|
|
7620
7649
|
const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);
|
|
7621
7650
|
const relativeProjectDirectory = toForwardSlashes(path.relative(rootDirectory, projectDirectory));
|
|
@@ -7709,7 +7738,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
|
|
|
7709
7738
|
}
|
|
7710
7739
|
if (packages.length === 0) return [rootDirectory];
|
|
7711
7740
|
if (packages.length === 1) {
|
|
7712
|
-
cliLogger.log(`${highlighter.success("✔")} Select projects
|
|
7741
|
+
cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages[0].name}`);
|
|
7713
7742
|
return [packages[0].directory];
|
|
7714
7743
|
}
|
|
7715
7744
|
if (projectFlag) return resolveProjectFlag(projectFlag, packages);
|
|
@@ -7733,13 +7762,13 @@ const resolveProjectFlag = (projectFlag, workspacePackages) => {
|
|
|
7733
7762
|
return resolvedDirectories;
|
|
7734
7763
|
};
|
|
7735
7764
|
const printDiscoveredProjects = (packages) => {
|
|
7736
|
-
cliLogger.log(`${highlighter.success("✔")} Select projects
|
|
7765
|
+
cliLogger.log(`${highlighter.success("✔")} Select projects ${highlighter.dim("›")} ${packages.map((workspacePackage) => workspacePackage.name).join(", ")}`);
|
|
7737
7766
|
};
|
|
7738
7767
|
const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
7739
7768
|
const { selectedDirectories } = await prompts({
|
|
7740
7769
|
type: "multiselect",
|
|
7741
7770
|
name: "selectedDirectories",
|
|
7742
|
-
message: "Select projects
|
|
7771
|
+
message: "Select projects",
|
|
7743
7772
|
choices: workspacePackages.map((workspacePackage) => ({
|
|
7744
7773
|
title: workspacePackage.name,
|
|
7745
7774
|
description: path.relative(rootDirectory, workspacePackage.directory),
|
|
@@ -7814,7 +7843,7 @@ const validateModeFlags = (flags) => {
|
|
|
7814
7843
|
if (flags.yes && flags.full) throw new Error("Cannot combine --yes and --full; pick one.");
|
|
7815
7844
|
if (flags.score && flags.json) throw new Error("Cannot combine --score and --json; pick one output mode.");
|
|
7816
7845
|
if (flags.prComment && (flags.json || flags.score)) throw new Error("--pr-comment cannot be combined with --json or --score.");
|
|
7817
|
-
if (flags.annotations &&
|
|
7846
|
+
if (flags.annotations && flags.score) throw new Error("--annotations cannot be combined with --score.");
|
|
7818
7847
|
if (flags.explain !== void 0 && flags.why !== void 0) throw new Error("Use --explain or --why, not both — they're aliases of the same flag.");
|
|
7819
7848
|
if ((flags.explain ?? flags.why) !== void 0 && (flags.json || flags.score || flags.annotations || flags.staged)) throw new Error("--explain cannot be combined with --json, --score, --annotations, or --staged.");
|
|
7820
7849
|
};
|
|
@@ -7840,6 +7869,12 @@ const finalizeScans = (input) => {
|
|
|
7840
7869
|
const ciFailureDiagnostics = filterDiagnosticsForSurface(input.diagnostics, "ciFailure", input.userConfig);
|
|
7841
7870
|
if (!input.isScoreOnly && shouldFailForDiagnostics(ciFailureDiagnostics, resolveFailOnLevel(input.flags, input.userConfig))) process.exitCode = 1;
|
|
7842
7871
|
};
|
|
7872
|
+
const buildChangedFilesDiffInfo = (changedFiles) => ({
|
|
7873
|
+
currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
|
|
7874
|
+
baseBranch: process.env.GITHUB_BASE_REF?.trim() || "pull request target",
|
|
7875
|
+
changedFiles,
|
|
7876
|
+
isCurrentChanges: false
|
|
7877
|
+
});
|
|
7843
7878
|
const inspectAction = async (directory, flags) => {
|
|
7844
7879
|
const isScoreOnly = Boolean(flags.score);
|
|
7845
7880
|
const isJsonMode = Boolean(flags.json);
|
|
@@ -7896,7 +7931,14 @@ const inspectAction = async (directory, flags) => {
|
|
|
7896
7931
|
cliLogger.log(`Scanning ${highlighter.info(`${stagedFiles.length}`)} staged files...`);
|
|
7897
7932
|
cliLogger.break();
|
|
7898
7933
|
}
|
|
7899
|
-
const
|
|
7934
|
+
const tempDirectory = mkdtempSync(path.join(tmpdir(), STAGED_FILES_TEMP_DIR_PREFIX));
|
|
7935
|
+
const snapshot = await materializeStagedFiles(resolvedDirectory, stagedFiles, tempDirectory).catch((error) => {
|
|
7936
|
+
rmSync(tempDirectory, {
|
|
7937
|
+
recursive: true,
|
|
7938
|
+
force: true
|
|
7939
|
+
});
|
|
7940
|
+
throw error;
|
|
7941
|
+
});
|
|
7900
7942
|
try {
|
|
7901
7943
|
const scanResult = await inspect(snapshot.tempDirectory, {
|
|
7902
7944
|
...scanOptions,
|
|
@@ -7905,7 +7947,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
7905
7947
|
});
|
|
7906
7948
|
const remappedDiagnostics = scanResult.diagnostics.map((diagnostic) => ({
|
|
7907
7949
|
...diagnostic,
|
|
7908
|
-
filePath: path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(snapshot.tempDirectory, resolvedDirectory) : diagnostic.filePath
|
|
7950
|
+
filePath: path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath.replaceAll(snapshot.tempDirectory, () => resolvedDirectory) : diagnostic.filePath
|
|
7909
7951
|
}));
|
|
7910
7952
|
finalizeScans({
|
|
7911
7953
|
diagnostics: remappedDiagnostics,
|
|
@@ -7935,9 +7977,10 @@ const inspectAction = async (directory, flags) => {
|
|
|
7935
7977
|
return;
|
|
7936
7978
|
}
|
|
7937
7979
|
const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts);
|
|
7980
|
+
const changedFilesDiffInfo = flags.changedFilesFrom && !flags.full ? buildChangedFilesDiffInfo(readChangedFilesFrom(path.resolve(flags.changedFilesFrom))) : null;
|
|
7938
7981
|
const effectiveDiff = resolveEffectiveDiff(flags, userConfig);
|
|
7939
|
-
const diffInfo = effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null;
|
|
7940
|
-
const isDiffMode = await resolveDiffMode(diffInfo, effectiveDiff, skipPrompts, isQuiet);
|
|
7982
|
+
const diffInfo = changedFilesDiffInfo ?? (changedFilesDiffInfo === null && (effectiveDiff !== void 0 && effectiveDiff !== false || !skipPrompts && !isQuiet) ? await getDiffInfo(resolvedDirectory, typeof effectiveDiff === "string" ? effectiveDiff : void 0) : null);
|
|
7983
|
+
const isDiffMode = changedFilesDiffInfo !== null || await resolveDiffMode(diffInfo, effectiveDiff, skipPrompts, isQuiet);
|
|
7941
7984
|
setJsonReportMode(isDiffMode ? "diff" : "full");
|
|
7942
7985
|
if (isDiffMode && diffInfo && !isQuiet) {
|
|
7943
7986
|
if (diffInfo.isCurrentChanges) cliLogger.log("Scanning uncommitted changes");
|
|
@@ -7996,13 +8039,13 @@ const inspectAction = async (directory, flags) => {
|
|
|
7996
8039
|
});
|
|
7997
8040
|
const setupProjectRoot = resolveInstallSetupProjectRoot({
|
|
7998
8041
|
scanRoot: resolvedDirectory,
|
|
7999
|
-
|
|
8042
|
+
scanDirectories: projectDirectories
|
|
8000
8043
|
});
|
|
8001
8044
|
if (setupProjectRoot !== null) {
|
|
8002
|
-
const
|
|
8045
|
+
const hasCompletedScan = completedScans.length > 0;
|
|
8003
8046
|
await promptInstallSetup({
|
|
8004
8047
|
projectRoot: setupProjectRoot,
|
|
8005
|
-
|
|
8048
|
+
hasCompletedScan,
|
|
8006
8049
|
issueCount: filterDiagnosticsForSurface(allDiagnostics, scanOptions.outputSurface ?? "cli", userConfig).length,
|
|
8007
8050
|
isJsonMode,
|
|
8008
8051
|
isScoreOnly,
|
|
@@ -8011,7 +8054,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
8011
8054
|
});
|
|
8012
8055
|
if (shouldShowAgentInstallHint({
|
|
8013
8056
|
projectRoot: setupProjectRoot,
|
|
8014
|
-
|
|
8057
|
+
hasCompletedScan,
|
|
8015
8058
|
isJsonMode,
|
|
8016
8059
|
isScoreOnly,
|
|
8017
8060
|
isStaged: Boolean(flags.staged)
|
|
@@ -8643,8 +8686,12 @@ const installReactDoctorGitHook = (options) => {
|
|
|
8643
8686
|
return installDirectGitHook(options);
|
|
8644
8687
|
};
|
|
8645
8688
|
//#endregion
|
|
8646
|
-
//#region src/cli/utils/install-
|
|
8647
|
-
var
|
|
8689
|
+
//#region src/cli/utils/install-react-doctor.ts
|
|
8690
|
+
var install_react_doctor_exports = /* @__PURE__ */ __exportAll({ runInstallReactDoctor: () => runInstallReactDoctor });
|
|
8691
|
+
const SETUP_OPTION_GIT_HOOK = "git-hook";
|
|
8692
|
+
const SETUP_OPTION_AGENT_HOOKS = "agent-hooks";
|
|
8693
|
+
const SETUP_OPTION_WORKFLOW = "workflow";
|
|
8694
|
+
const SETUP_OPTION_SKIP = "skip";
|
|
8648
8695
|
const CONFIG_ONLY_GIT_HOOK_KINDS = new Set([
|
|
8649
8696
|
"ghooks",
|
|
8650
8697
|
"git-hooks-js",
|
|
@@ -8825,7 +8872,32 @@ const getSkillSourceDirectory = () => {
|
|
|
8825
8872
|
const distDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
8826
8873
|
return path.join(distDirectory, "skills", SKILL_NAME);
|
|
8827
8874
|
};
|
|
8828
|
-
const
|
|
8875
|
+
const canInstallNativeAgentHooks = (agents) => agents.some((agent) => agent === "claude-code" || agent === "cursor");
|
|
8876
|
+
const buildWorkflowContent = () => [
|
|
8877
|
+
"name: React Doctor",
|
|
8878
|
+
"",
|
|
8879
|
+
"on:",
|
|
8880
|
+
" pull_request:",
|
|
8881
|
+
" types: [opened, synchronize, reopened, ready_for_review]",
|
|
8882
|
+
"",
|
|
8883
|
+
"permissions:",
|
|
8884
|
+
" contents: read",
|
|
8885
|
+
" pull-requests: write",
|
|
8886
|
+
" issues: write",
|
|
8887
|
+
"",
|
|
8888
|
+
"concurrency:",
|
|
8889
|
+
" group: react-doctor-${{ github.event.pull_request.number || github.ref }}",
|
|
8890
|
+
" cancel-in-progress: true",
|
|
8891
|
+
"",
|
|
8892
|
+
"jobs:",
|
|
8893
|
+
" react-doctor:",
|
|
8894
|
+
" runs-on: ubuntu-latest",
|
|
8895
|
+
" steps:",
|
|
8896
|
+
" - uses: actions/checkout@v5",
|
|
8897
|
+
" - uses: millionco/react-doctor@main",
|
|
8898
|
+
""
|
|
8899
|
+
].join("\n");
|
|
8900
|
+
const runInstallReactDoctor = async (options = {}) => {
|
|
8829
8901
|
const requestedProjectRoot = options.projectRoot ?? process.cwd();
|
|
8830
8902
|
const projectRoot = findNearestPackageDirectory(requestedProjectRoot) ?? requestedProjectRoot;
|
|
8831
8903
|
const sourceDir = options.sourceDir ?? getSkillSourceDirectory();
|
|
@@ -8846,7 +8918,8 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8846
8918
|
const gitHookTarget = options.gitHookPath === void 0 ? detectGitHookTarget(projectRoot) : options.gitHookPath === null ? null : buildManualGitHookTarget(options.gitHookPath, projectRoot);
|
|
8847
8919
|
const gitHookPath = gitHookTarget?.hookPath;
|
|
8848
8920
|
const promptOptions = options.onPromptCancel === void 0 ? {} : { onCancel: options.onPromptCancel };
|
|
8849
|
-
const
|
|
8921
|
+
const prompt = options.prompt ?? prompts;
|
|
8922
|
+
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
8850
8923
|
type: "multiselect",
|
|
8851
8924
|
name: "agents",
|
|
8852
8925
|
message: `Install the ${highlighter.info(`/react-doctor`)} skill for:`,
|
|
@@ -8859,13 +8932,48 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8859
8932
|
min: 1
|
|
8860
8933
|
}, promptOptions)).agents ?? [];
|
|
8861
8934
|
if (selectedAgents.length === 0) return;
|
|
8862
|
-
const
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8935
|
+
const workflowsDirectory = path.join(projectRoot, ".github", "workflows");
|
|
8936
|
+
const workflowTargetPath = path.join(workflowsDirectory, "react-doctor.yml");
|
|
8937
|
+
const hasExistingWorkflows = existsSync(workflowsDirectory);
|
|
8938
|
+
const canInstallWorkflow = !existsSync(workflowTargetPath);
|
|
8939
|
+
const setupActionChoices = [
|
|
8940
|
+
...gitHookPath === null || gitHookPath === void 0 ? [] : [{
|
|
8941
|
+
title: "Pre-commit hook",
|
|
8942
|
+
description: "Check staged changes before each commit",
|
|
8943
|
+
value: SETUP_OPTION_GIT_HOOK,
|
|
8944
|
+
selected: true
|
|
8945
|
+
}],
|
|
8946
|
+
...canInstallNativeAgentHooks(selectedAgents) ? [{
|
|
8947
|
+
title: "Agent hooks",
|
|
8948
|
+
description: "Ask Claude Code or Cursor to scan after code edits",
|
|
8949
|
+
value: SETUP_OPTION_AGENT_HOOKS,
|
|
8950
|
+
selected: Boolean(options.agentHooks)
|
|
8951
|
+
}] : [],
|
|
8952
|
+
...canInstallWorkflow ? [{
|
|
8953
|
+
title: "GitHub Actions workflow",
|
|
8954
|
+
description: "Scan pull requests in CI",
|
|
8955
|
+
value: SETUP_OPTION_WORKFLOW,
|
|
8956
|
+
selected: hasExistingWorkflows
|
|
8957
|
+
}] : []
|
|
8958
|
+
];
|
|
8959
|
+
const setupChoices = setupActionChoices.length === 0 ? [] : [{
|
|
8960
|
+
title: "Skip optional setup",
|
|
8961
|
+
description: "Install only the agent skill and package setup",
|
|
8962
|
+
value: SETUP_OPTION_SKIP,
|
|
8963
|
+
selected: false
|
|
8964
|
+
}, ...setupActionChoices];
|
|
8965
|
+
const selectedSetupOptions = skipPrompts || setupChoices.length === 0 ? [] : (await prompt({
|
|
8966
|
+
type: "multiselect",
|
|
8967
|
+
name: "setupOptions",
|
|
8968
|
+
message: "Select additional React Doctor setup:",
|
|
8969
|
+
choices: setupChoices,
|
|
8970
|
+
instructions: false
|
|
8971
|
+
}, promptOptions)).setupOptions ?? [];
|
|
8972
|
+
const selectedSetupActions = selectedSetupOptions.filter((setupOption) => setupOption !== SETUP_OPTION_SKIP);
|
|
8973
|
+
const didSkipOptionalSetup = selectedSetupActions.length === 0 && selectedSetupOptions.includes(SETUP_OPTION_SKIP);
|
|
8974
|
+
const shouldInstallGitHook = gitHookPath !== null && gitHookPath !== void 0 && (Boolean(options.yes) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_GIT_HOOK));
|
|
8975
|
+
const shouldInstallAgentHooks = Boolean(options.agentHooks) || !didSkipOptionalSetup && selectedSetupActions.includes(SETUP_OPTION_AGENT_HOOKS);
|
|
8976
|
+
const shouldInstallWorkflow = !skipPrompts && !didSkipOptionalSetup && canInstallWorkflow && selectedSetupActions.includes(SETUP_OPTION_WORKFLOW);
|
|
8869
8977
|
if (options.dryRun) {
|
|
8870
8978
|
cliLogger.log(`Dry run — would install ${SKILL_NAME} skill for:`);
|
|
8871
8979
|
for (const agent of selectedAgents) cliLogger.dim(` - ${getSkillAgentConfig(agent).displayName}`);
|
|
@@ -8874,6 +8982,7 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8874
8982
|
cliLogger.dim(" Dev dependency: react-doctor");
|
|
8875
8983
|
if (shouldInstallGitHook) cliLogger.dim(` Git hook: ${gitHookPath}`);
|
|
8876
8984
|
if (shouldInstallAgentHooks) cliLogger.dim(" Agent hooks: Claude Code / Cursor when selected");
|
|
8985
|
+
if (shouldInstallWorkflow) cliLogger.dim(` GitHub Actions workflow: ${path.relative(projectRoot, workflowTargetPath)}`);
|
|
8877
8986
|
return;
|
|
8878
8987
|
}
|
|
8879
8988
|
const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
|
|
@@ -8921,47 +9030,15 @@ const runInstallSkill = async (options = {}) => {
|
|
|
8921
9030
|
throw error;
|
|
8922
9031
|
}
|
|
8923
9032
|
}
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
}, promptOptions);
|
|
8934
|
-
if (shouldInstallWorkflow) {
|
|
8935
|
-
if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
|
|
8936
|
-
const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
|
|
8937
|
-
try {
|
|
8938
|
-
writeFileSync(workflowTargetPath, [
|
|
8939
|
-
"name: React Doctor",
|
|
8940
|
-
"",
|
|
8941
|
-
"on:",
|
|
8942
|
-
" pull_request:",
|
|
8943
|
-
" branches: [main]",
|
|
8944
|
-
"",
|
|
8945
|
-
"permissions:",
|
|
8946
|
-
" contents: read",
|
|
8947
|
-
" pull-requests: write",
|
|
8948
|
-
"",
|
|
8949
|
-
"jobs:",
|
|
8950
|
-
" react-doctor:",
|
|
8951
|
-
" runs-on: ubuntu-latest",
|
|
8952
|
-
" steps:",
|
|
8953
|
-
" - uses: actions/checkout@v4",
|
|
8954
|
-
" - uses: millionco/react-doctor@main",
|
|
8955
|
-
" with:",
|
|
8956
|
-
" github-token: ${{ secrets.GITHUB_TOKEN }}",
|
|
8957
|
-
" diff: main",
|
|
8958
|
-
""
|
|
8959
|
-
].join("\n"));
|
|
8960
|
-
workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
|
|
8961
|
-
} catch (error) {
|
|
8962
|
-
workflowSpinner.fail("Failed to add GitHub Actions workflow.");
|
|
8963
|
-
throw error;
|
|
8964
|
-
}
|
|
9033
|
+
if (shouldInstallWorkflow) {
|
|
9034
|
+
if (!hasExistingWorkflows) mkdirSync(workflowsDirectory, { recursive: true });
|
|
9035
|
+
const workflowSpinner = spinner("Adding GitHub Actions workflow...").start();
|
|
9036
|
+
try {
|
|
9037
|
+
writeFileSync(workflowTargetPath, buildWorkflowContent());
|
|
9038
|
+
workflowSpinner.succeed(`GitHub Actions workflow added at ${path.relative(projectRoot, workflowTargetPath)}.`);
|
|
9039
|
+
} catch (error) {
|
|
9040
|
+
workflowSpinner.fail("Failed to add GitHub Actions workflow.");
|
|
9041
|
+
throw error;
|
|
8965
9042
|
}
|
|
8966
9043
|
}
|
|
8967
9044
|
};
|
|
@@ -8971,7 +9048,7 @@ const installAction = async (options, command) => {
|
|
|
8971
9048
|
Effect.runSync(printBrandedHeader);
|
|
8972
9049
|
try {
|
|
8973
9050
|
const parentOptions = command?.parent?.opts?.();
|
|
8974
|
-
await
|
|
9051
|
+
await runInstallReactDoctor({
|
|
8975
9052
|
yes: options.yes ?? parentOptions?.yes,
|
|
8976
9053
|
dryRun: options.dryRun,
|
|
8977
9054
|
agentHooks: options.agentHooks,
|
|
@@ -9015,6 +9092,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
9015
9092
|
"--yes"
|
|
9016
9093
|
]),
|
|
9017
9094
|
longOptionsWithRequiredValues: new Set([
|
|
9095
|
+
"--changed-files-from",
|
|
9018
9096
|
"--explain",
|
|
9019
9097
|
"--fail-on",
|
|
9020
9098
|
"--project",
|
|
@@ -9108,10 +9186,16 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
9108
9186
|
];
|
|
9109
9187
|
};
|
|
9110
9188
|
//#endregion
|
|
9189
|
+
//#region src/cli/utils/unref-stdin.ts
|
|
9190
|
+
const unrefStdin = () => {
|
|
9191
|
+
process.stdin.unref?.();
|
|
9192
|
+
};
|
|
9193
|
+
//#endregion
|
|
9111
9194
|
//#region src/cli/index.ts
|
|
9112
9195
|
process.on("SIGINT", exitGracefully);
|
|
9113
9196
|
process.on("SIGTERM", exitGracefully);
|
|
9114
|
-
|
|
9197
|
+
unrefStdin();
|
|
9198
|
+
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("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--changed-files-from <file>", "internal: scan source files listed in a newline-delimited changed-files file").option("--no-score", "skip the score API and the share URL").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none (default: none)").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--pr-comment", "tune CLI output for sticky PR comments (drops weak-signal rule families like `design` from the printed list and the fail-on gate; configure via config.surfaces)").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").option("--why <file:line>", "alias for --explain").option("--respect-inline-disables", "respect inline `// eslint-disable*` / `// oxlint-disable*` comments (default)").option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").addHelpText("after", `
|
|
9115
9199
|
${highlighter.dim("Configuration:")}
|
|
9116
9200
|
Place a ${highlighter.info("react-doctor.config.json")} (or ${highlighter.info("\"reactDoctor\"")} key in your package.json) in the project root.
|
|
9117
9201
|
CLI flags always override config values. See the README for the full schema.
|