react-doctor 0.5.6-dev.8908f98 → 0.5.6-dev.93b796d
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 +370 -208
- package/dist/index.d.ts +4 -3
- package/dist/index.js +60 -28
- package/dist/lsp.js +66 -39
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a06f0514-b1fa-5452-9c19-0140438862f8")}catch(e){}}();
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as NodeChildProcess from "node:child_process";
|
|
5
5
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -14,7 +14,7 @@ import * as OS from "node:os";
|
|
|
14
14
|
import os, { tmpdir } from "node:os";
|
|
15
15
|
import { parseJSON5 } from "confbox";
|
|
16
16
|
import * as NodeUrl from "node:url";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
18
|
import { createJiti } from "jiti";
|
|
19
19
|
import * as Crypto from "node:crypto";
|
|
20
20
|
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
@@ -36793,6 +36793,11 @@ const ES_TARGET_YEAR_BY_NAME = {
|
|
|
36793
36793
|
esnext: 9999
|
|
36794
36794
|
};
|
|
36795
36795
|
/**
|
|
36796
|
+
* tsconfig filenames probed when resolving a project's TypeScript
|
|
36797
|
+
* compiler options — the root config first, then a monorepo base config.
|
|
36798
|
+
*/
|
|
36799
|
+
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
36800
|
+
/**
|
|
36796
36801
|
* Project-config files that `StagedFiles.materialize` copies into
|
|
36797
36802
|
* the temp directory alongside staged sources so oxlint resolves
|
|
36798
36803
|
* `tsconfig` / `package.json` / lint configs the same way it would
|
|
@@ -39941,8 +39946,8 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
39941
39946
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
39942
39947
|
return patterns;
|
|
39943
39948
|
};
|
|
39949
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39944
39950
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
39945
|
-
const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39946
39951
|
const readJsonFileSafe = (filePath) => {
|
|
39947
39952
|
let rawContents;
|
|
39948
39953
|
try {
|
|
@@ -39958,10 +39963,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
39958
39963
|
};
|
|
39959
39964
|
const readKnipConfig = (rootDirectory) => {
|
|
39960
39965
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
39961
|
-
if (isRecord$
|
|
39966
|
+
if (isRecord$2(knipJson)) return knipJson;
|
|
39962
39967
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
39963
|
-
const packageKnipConfig = isRecord$
|
|
39964
|
-
return isRecord$
|
|
39968
|
+
const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
|
|
39969
|
+
return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
|
|
39965
39970
|
};
|
|
39966
39971
|
const normalizePatternList = (value) => {
|
|
39967
39972
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -39973,10 +39978,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
39973
39978
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
39974
39979
|
};
|
|
39975
39980
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
39976
|
-
if (!isRecord$
|
|
39981
|
+
if (!isRecord$2(workspaces)) return [];
|
|
39977
39982
|
const patterns = [];
|
|
39978
39983
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
39979
|
-
if (!isRecord$
|
|
39984
|
+
if (!isRecord$2(workspaceConfig)) continue;
|
|
39980
39985
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
39981
39986
|
}
|
|
39982
39987
|
return patterns;
|
|
@@ -39986,12 +39991,11 @@ const collectKnipPatterns = (rootDirectory, settingName) => {
|
|
|
39986
39991
|
if (!config) return [];
|
|
39987
39992
|
return [...normalizePatternList(config[settingName]), ...collectKnipWorkspacePatterns(config.workspaces, settingName)];
|
|
39988
39993
|
};
|
|
39989
|
-
const collectDeadCodeIgnorePatterns = (rootDirectory
|
|
39994
|
+
const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
39990
39995
|
const seen = /* @__PURE__ */ new Set();
|
|
39991
39996
|
const sources = [
|
|
39992
39997
|
readIgnoreFile(path.join(rootDirectory, ".gitignore")),
|
|
39993
39998
|
collectIgnorePatterns(rootDirectory),
|
|
39994
|
-
userConfig?.ignore?.files ?? [],
|
|
39995
39999
|
collectKnipPatterns(rootDirectory, "ignore")
|
|
39996
40000
|
];
|
|
39997
40001
|
for (const source of sources) for (const pattern of source) seen.add(pattern);
|
|
@@ -40022,8 +40026,6 @@ const toCanonicalPath = (filePath) => {
|
|
|
40022
40026
|
};
|
|
40023
40027
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40024
40028
|
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
40025
|
-
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
40026
|
-
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40027
40029
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40028
40030
|
const inputChunks = [];
|
|
40029
40031
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40081,7 +40083,7 @@ process.stdin.on("end", () => {
|
|
|
40081
40083
|
});
|
|
40082
40084
|
`;
|
|
40083
40085
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
40084
|
-
for (const filename of TSCONFIG_FILENAMES
|
|
40086
|
+
for (const filename of TSCONFIG_FILENAMES) {
|
|
40085
40087
|
const candidate = Path.join(rootDirectory, filename);
|
|
40086
40088
|
if (NFS.existsSync(candidate)) return candidate;
|
|
40087
40089
|
}
|
|
@@ -40269,11 +40271,10 @@ const runDeadCodeWorkerWithTimeout = (handle, timeoutMs) => new Promise((resolve
|
|
|
40269
40271
|
});
|
|
40270
40272
|
});
|
|
40271
40273
|
const checkDeadCode = async (options) => {
|
|
40272
|
-
const { userConfig } = options;
|
|
40273
40274
|
const rootDirectory = toCanonicalPath(options.rootDirectory);
|
|
40274
40275
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40275
40276
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40276
|
-
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory
|
|
40277
|
+
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
40277
40278
|
const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40278
40279
|
rootDirectory,
|
|
40279
40280
|
entryPatterns,
|
|
@@ -41322,8 +41323,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41322
41323
|
}
|
|
41323
41324
|
return enabled;
|
|
41324
41325
|
};
|
|
41325
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
41326
|
-
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41326
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
|
|
41327
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41327
41328
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41328
41329
|
const jsPlugins = [];
|
|
41329
41330
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -41383,7 +41384,6 @@ const resolveOxlintBinary = () => {
|
|
|
41383
41384
|
return Path.join(oxlintPackageDirectory, "bin", "oxlint");
|
|
41384
41385
|
};
|
|
41385
41386
|
const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
|
|
41386
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
41387
41387
|
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
41388
41388
|
for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
|
|
41389
41389
|
return null;
|
|
@@ -41755,7 +41755,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
|
|
|
41755
41755
|
const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
|
|
41756
41756
|
let currentNode = identifier.parent;
|
|
41757
41757
|
while (currentNode) {
|
|
41758
|
-
if (
|
|
41758
|
+
if (isScopeBoundary(currentNode)) {
|
|
41759
41759
|
if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
|
|
41760
41760
|
}
|
|
41761
41761
|
if (currentNode === sourceFile) return false;
|
|
@@ -41846,11 +41846,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
|
|
|
41846
41846
|
});
|
|
41847
41847
|
return resolution;
|
|
41848
41848
|
};
|
|
41849
|
-
const isScopeNode = isScopeBoundary;
|
|
41850
41849
|
const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
|
|
41851
41850
|
let currentNode = identifier.parent;
|
|
41852
41851
|
while (currentNode) {
|
|
41853
|
-
if (
|
|
41852
|
+
if (isScopeBoundary(currentNode)) {
|
|
41854
41853
|
const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
|
|
41855
41854
|
if (resolution) return resolution;
|
|
41856
41855
|
}
|
|
@@ -42020,9 +42019,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42020
42019
|
try {
|
|
42021
42020
|
parsed = JSON.parse(sanitizedStdout);
|
|
42022
42021
|
} catch {
|
|
42023
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42022
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42024
42023
|
}
|
|
42025
|
-
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42024
|
+
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42026
42025
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
42027
42026
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
42028
42027
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -42313,6 +42312,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
42313
42312
|
NFS.closeSync(fileHandle);
|
|
42314
42313
|
}
|
|
42315
42314
|
};
|
|
42315
|
+
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
42316
|
+
/**
|
|
42317
|
+
* Detects an oxlint config-load crash caused by the optional
|
|
42318
|
+
* `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
|
|
42319
|
+
* builds the partial-failure note for it; returns `null` when the failure
|
|
42320
|
+
* was anything else.
|
|
42321
|
+
*
|
|
42322
|
+
* oxlint prints a framed error to stdout (not stderr) and exits non-zero
|
|
42323
|
+
* when a `jsPlugins` entry can't be imported; that non-JSON stdout
|
|
42324
|
+
* surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
|
|
42325
|
+
* config load on it, leaving the plugin in would drop every curated
|
|
42326
|
+
* react-doctor diagnostic too — so the caller retries with the plugin
|
|
42327
|
+
* stripped (issue #833). Both markers sit at the start of oxlint's
|
|
42328
|
+
* message, so they survive the `preview` slice even for deep pnpm paths.
|
|
42329
|
+
*/
|
|
42330
|
+
const reactHooksJsPluginDropNote = (error) => {
|
|
42331
|
+
if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
|
|
42332
|
+
const { preview } = error.reason;
|
|
42333
|
+
if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
|
|
42334
|
+
const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
|
|
42335
|
+
return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
|
|
42336
|
+
};
|
|
42316
42337
|
/**
|
|
42317
42338
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
42318
42339
|
*
|
|
@@ -42340,15 +42361,16 @@ const runOxlint = async (options) => {
|
|
|
42340
42361
|
const pluginPath = resolvePluginPath();
|
|
42341
42362
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
42342
42363
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
42343
|
-
const buildConfig = (
|
|
42364
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
42344
42365
|
pluginPath,
|
|
42345
42366
|
project,
|
|
42346
42367
|
customRulesOnly,
|
|
42347
|
-
extendsPaths:
|
|
42368
|
+
extendsPaths: overrides.extendsPaths,
|
|
42348
42369
|
ignoredTags,
|
|
42349
42370
|
serverAuthFunctionNames,
|
|
42350
42371
|
severityControls,
|
|
42351
|
-
userPlugins
|
|
42372
|
+
userPlugins,
|
|
42373
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
42352
42374
|
});
|
|
42353
42375
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42354
42376
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -42384,12 +42406,22 @@ const runOxlint = async (options) => {
|
|
|
42384
42406
|
outputMaxBytes,
|
|
42385
42407
|
concurrency: options.concurrency
|
|
42386
42408
|
});
|
|
42387
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
42409
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42388
42410
|
try {
|
|
42389
42411
|
return await runBatches();
|
|
42390
42412
|
} catch (error) {
|
|
42413
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
42414
|
+
if (reactHooksJsDropNote !== null) {
|
|
42415
|
+
writeOxlintConfig(configPath, buildConfig({
|
|
42416
|
+
extendsPaths,
|
|
42417
|
+
disableReactHooksJsPlugin: true
|
|
42418
|
+
}));
|
|
42419
|
+
const diagnostics = await runBatches();
|
|
42420
|
+
onPartialFailure?.(reactHooksJsDropNote);
|
|
42421
|
+
return diagnostics;
|
|
42422
|
+
}
|
|
42391
42423
|
if (extendsPaths.length === 0) throw error;
|
|
42392
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
42424
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
42393
42425
|
return await runBatches();
|
|
42394
42426
|
}
|
|
42395
42427
|
} finally {
|
|
@@ -43841,7 +43873,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
|
|
|
43841
43873
|
"false"
|
|
43842
43874
|
]);
|
|
43843
43875
|
const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
|
|
43844
|
-
const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(
|
|
43876
|
+
const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
|
|
43845
43877
|
const detectCiProvider = () => {
|
|
43846
43878
|
for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
|
|
43847
43879
|
return isCiFlagSet(process.env.CI) ? "unknown" : null;
|
|
@@ -43866,6 +43898,42 @@ const detectCodingAgent = () => {
|
|
|
43866
43898
|
const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
|
|
43867
43899
|
const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
|
|
43868
43900
|
//#endregion
|
|
43901
|
+
//#region src/cli/utils/detect-terminal-kind.ts
|
|
43902
|
+
const TERMINAL_BY_TERM_PROGRAM = [
|
|
43903
|
+
["vscode", "vscode"],
|
|
43904
|
+
["iTerm.app", "iterm"],
|
|
43905
|
+
["Apple_Terminal", "apple-terminal"],
|
|
43906
|
+
["WezTerm", "wezterm"],
|
|
43907
|
+
["ghostty", "ghostty"],
|
|
43908
|
+
["Hyper", "hyper"],
|
|
43909
|
+
["Tabby", "tabby"],
|
|
43910
|
+
["rio", "rio"]
|
|
43911
|
+
];
|
|
43912
|
+
/**
|
|
43913
|
+
* Best-effort label for the terminal emulator / editor hosting the CLI,
|
|
43914
|
+
* derived from terminal-identity env vars. Recorded as the `terminalKind` run
|
|
43915
|
+
* tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
|
|
43916
|
+
* …) — the split Sentry can't otherwise see. Low-cardinality and free of any
|
|
43917
|
+
* username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
|
|
43918
|
+
* win over the outer emulator because that's the surface a user is reading in;
|
|
43919
|
+
* "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
|
|
43920
|
+
*/
|
|
43921
|
+
const detectTerminalKind = (env = process.env) => {
|
|
43922
|
+
if (env.NVIM) return "neovim";
|
|
43923
|
+
if (env.VIM_TERMINAL) return "vim";
|
|
43924
|
+
const termProgram = env.TERM_PROGRAM;
|
|
43925
|
+
if (termProgram) {
|
|
43926
|
+
for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
|
|
43927
|
+
}
|
|
43928
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
|
|
43929
|
+
if (env.WT_SESSION) return "windows-terminal";
|
|
43930
|
+
if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
|
|
43931
|
+
if (env.VTE_VERSION) return "vte";
|
|
43932
|
+
if (env.TMUX) return "tmux";
|
|
43933
|
+
if (isCiEnvironment(env)) return "ci";
|
|
43934
|
+
return "unknown";
|
|
43935
|
+
};
|
|
43936
|
+
//#endregion
|
|
43869
43937
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43870
43938
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43871
43939
|
//#endregion
|
|
@@ -43888,6 +43956,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
|
|
|
43888
43956
|
const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
|
|
43889
43957
|
//#endregion
|
|
43890
43958
|
//#region src/cli/utils/constants.ts
|
|
43959
|
+
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
43891
43960
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
43892
43961
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
43893
43962
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -43972,7 +44041,7 @@ const makeNoopConsole = () => ({
|
|
|
43972
44041
|
});
|
|
43973
44042
|
//#endregion
|
|
43974
44043
|
//#region src/cli/utils/version.ts
|
|
43975
|
-
const VERSION = "0.5.6-dev.
|
|
44044
|
+
const VERSION = "0.5.6-dev.93b796d";
|
|
43976
44045
|
//#endregion
|
|
43977
44046
|
//#region src/cli/utils/json-mode.ts
|
|
43978
44047
|
let context = null;
|
|
@@ -44122,6 +44191,7 @@ const buildRunContext = () => {
|
|
|
44122
44191
|
viaAction: isOfficialGithubAction(),
|
|
44123
44192
|
codingAgent: detectCodingAgent(),
|
|
44124
44193
|
interactive: !isNonInteractiveEnvironment(),
|
|
44194
|
+
terminalKind: detectTerminalKind(),
|
|
44125
44195
|
jsonMode: isJsonModeActive(),
|
|
44126
44196
|
invokedVia: detectInvokedVia()
|
|
44127
44197
|
};
|
|
@@ -44192,6 +44262,7 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44192
44262
|
viaAction: runContext.viaAction,
|
|
44193
44263
|
codingAgent: runContext.codingAgent,
|
|
44194
44264
|
interactive: runContext.interactive,
|
|
44265
|
+
terminalKind: runContext.terminalKind,
|
|
44195
44266
|
jsonMode: runContext.jsonMode,
|
|
44196
44267
|
invokedVia: runContext.invokedVia,
|
|
44197
44268
|
nodeMajor: runContext.nodeMajor
|
|
@@ -44330,13 +44401,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44330
44401
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44331
44402
|
* standard `SENTRY_RELEASE` override.
|
|
44332
44403
|
*/
|
|
44333
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.
|
|
44404
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.93b796d`;
|
|
44334
44405
|
/**
|
|
44335
44406
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44336
44407
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44337
44408
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44338
44409
|
*/
|
|
44339
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.
|
|
44410
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.93b796d") ? "development" : "production");
|
|
44340
44411
|
/**
|
|
44341
44412
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44342
44413
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -48118,7 +48189,7 @@ const AGENT_GUIDANCE_LINES = [
|
|
|
48118
48189
|
"Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
|
|
48119
48190
|
"Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
|
|
48120
48191
|
"Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
|
|
48121
|
-
"Run `npx react-doctor@latest --verbose --
|
|
48192
|
+
"Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
|
|
48122
48193
|
"When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
|
|
48123
48194
|
"Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
|
|
48124
48195
|
"For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
|
|
@@ -48193,6 +48264,15 @@ const boxText = (content, innerWidth) => {
|
|
|
48193
48264
|
].join("\n");
|
|
48194
48265
|
};
|
|
48195
48266
|
//#endregion
|
|
48267
|
+
//#region src/cli/utils/resolve-absolute-path.ts
|
|
48268
|
+
/**
|
|
48269
|
+
* Resolves a diagnostic's `filePath` (relative to its project root, or
|
|
48270
|
+
* already absolute) to an absolute path. Shared by the code-frame reader and
|
|
48271
|
+
* the terminal hyperlink builder so both turn a relative path into the same
|
|
48272
|
+
* on-disk location.
|
|
48273
|
+
*/
|
|
48274
|
+
const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
|
|
48275
|
+
//#endregion
|
|
48196
48276
|
//#region src/cli/utils/build-code-frame.ts
|
|
48197
48277
|
/**
|
|
48198
48278
|
* Renders a syntax-highlighted source excerpt around a diagnostic site
|
|
@@ -48203,7 +48283,7 @@ const boxText = (content, innerWidth) => {
|
|
|
48203
48283
|
*/
|
|
48204
48284
|
const buildCodeFrame = (input) => {
|
|
48205
48285
|
if (input.line <= 0) return null;
|
|
48206
|
-
const absolutePath =
|
|
48286
|
+
const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
|
|
48207
48287
|
let source;
|
|
48208
48288
|
try {
|
|
48209
48289
|
source = NFS.readFileSync(absolutePath, "utf8");
|
|
@@ -48243,6 +48323,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
|
|
|
48243
48323
|
const DIVIDER_INDENT = " ";
|
|
48244
48324
|
const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
|
|
48245
48325
|
//#endregion
|
|
48326
|
+
//#region src/cli/utils/format-hyperlink.ts
|
|
48327
|
+
const OSC = "\x1B]";
|
|
48328
|
+
const ST = "\x1B\\";
|
|
48329
|
+
/**
|
|
48330
|
+
* Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
|
|
48331
|
+
* are exactly `text`; the link is carried in escape sequences a capable
|
|
48332
|
+
* terminal turns into a click target.
|
|
48333
|
+
*/
|
|
48334
|
+
const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
|
|
48335
|
+
//#endregion
|
|
48246
48336
|
//#region src/cli/utils/indent-multiline-text.ts
|
|
48247
48337
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
48248
48338
|
//#endregion
|
|
@@ -48396,17 +48486,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
|
|
|
48396
48486
|
}
|
|
48397
48487
|
return clusters;
|
|
48398
48488
|
};
|
|
48399
|
-
const
|
|
48489
|
+
const formatClusterLocationText = (cluster) => {
|
|
48490
|
+
const { filePath } = cluster.diagnostics[0];
|
|
48491
|
+
if (cluster.startLine <= 0) return filePath;
|
|
48492
|
+
if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
|
|
48493
|
+
return `${filePath}:${cluster.startLine}`;
|
|
48494
|
+
};
|
|
48495
|
+
const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
|
|
48400
48496
|
const lead = cluster.diagnostics[0];
|
|
48401
48497
|
const contextTag = formatFileContextTag(lead);
|
|
48402
|
-
|
|
48403
|
-
if (
|
|
48404
|
-
return `${lead.filePath
|
|
48498
|
+
const location = formatClusterLocationText(cluster);
|
|
48499
|
+
if (!hyperlinks) return `${location}${contextTag}`;
|
|
48500
|
+
return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
|
|
48405
48501
|
};
|
|
48406
|
-
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
|
|
48502
|
+
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
|
|
48407
48503
|
const lead = cluster.diagnostics[0];
|
|
48408
48504
|
const isMultiSite = cluster.diagnostics.length > 1;
|
|
48409
|
-
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
|
|
48505
|
+
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
|
|
48410
48506
|
const codeFrame = renderCodeFrame ? buildCodeFrame({
|
|
48411
48507
|
filePath: lead.filePath,
|
|
48412
48508
|
line: cluster.startLine,
|
|
@@ -48425,7 +48521,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
|
|
|
48425
48521
|
}
|
|
48426
48522
|
return lines;
|
|
48427
48523
|
};
|
|
48428
|
-
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
|
|
48524
|
+
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
|
|
48429
48525
|
const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
|
|
48430
48526
|
const { severity } = representative;
|
|
48431
48527
|
const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
|
|
@@ -48445,7 +48541,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48445
48541
|
}
|
|
48446
48542
|
const renderCodeFrame = severity === "error";
|
|
48447
48543
|
const sites = renderEverySite ? ruleDiagnostics : [representative];
|
|
48448
|
-
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
|
|
48544
|
+
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
|
|
48449
48545
|
return lines;
|
|
48450
48546
|
};
|
|
48451
48547
|
const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
|
|
@@ -48458,7 +48554,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48458
48554
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48459
48555
|
};
|
|
48460
48556
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48461
|
-
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
|
|
48557
|
+
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48462
48558
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
48463
48559
|
if (topRuleGroups.length === 0) return {
|
|
48464
48560
|
lines: [],
|
|
@@ -48468,7 +48564,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
|
|
|
48468
48564
|
const blockOffsets = [];
|
|
48469
48565
|
for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
|
|
48470
48566
|
blockOffsets.push(lines.length);
|
|
48471
|
-
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
|
|
48567
|
+
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
|
|
48472
48568
|
lines.push("");
|
|
48473
48569
|
}
|
|
48474
48570
|
return {
|
|
@@ -48506,18 +48602,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
|
|
|
48506
48602
|
* single Effect.forEach over Console.log so failures or fiber
|
|
48507
48603
|
* interruption produce predictable partial output.
|
|
48508
48604
|
*/
|
|
48509
|
-
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
|
|
48605
|
+
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
|
|
48510
48606
|
const sectionPause = onboarding.sectionPause ?? void_;
|
|
48511
48607
|
const animateCountUp = onboarding.animateCountUp ?? false;
|
|
48512
48608
|
const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
|
|
48513
48609
|
let detailLines;
|
|
48514
48610
|
let topErrorBlockOffsets = [];
|
|
48515
48611
|
if (!isVerbose) {
|
|
48516
|
-
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
|
|
48612
|
+
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
|
|
48517
48613
|
detailLines = topErrors.lines;
|
|
48518
48614
|
topErrorBlockOffsets = topErrors.blockOffsets;
|
|
48519
48615
|
} else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
|
|
48520
|
-
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
|
|
48616
|
+
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
|
|
48521
48617
|
});
|
|
48522
48618
|
const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
|
|
48523
48619
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
@@ -48578,6 +48674,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
|
|
|
48578
48674
|
//#endregion
|
|
48579
48675
|
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
48580
48676
|
const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
|
|
48677
|
+
//#endregion
|
|
48678
|
+
//#region src/cli/utils/supports-hyperlinks.ts
|
|
48679
|
+
const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
|
|
48680
|
+
"iTerm.app",
|
|
48681
|
+
"WezTerm",
|
|
48682
|
+
"vscode",
|
|
48683
|
+
"Hyper",
|
|
48684
|
+
"ghostty",
|
|
48685
|
+
"Tabby",
|
|
48686
|
+
"rio"
|
|
48687
|
+
]);
|
|
48688
|
+
const parseVteVersion = (raw) => {
|
|
48689
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
48690
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
48691
|
+
};
|
|
48692
|
+
/**
|
|
48693
|
+
* Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
|
|
48694
|
+
* from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
|
|
48695
|
+
* overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
|
|
48696
|
+
* forces on), mirroring how the ecosystem's terminal libraries gate the same
|
|
48697
|
+
* feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
|
|
48698
|
+
* raw escape rather than a link). Unknown terminals default to off.
|
|
48699
|
+
*/
|
|
48700
|
+
const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
|
|
48701
|
+
const forced = env.FORCE_HYPERLINK;
|
|
48702
|
+
if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
|
|
48703
|
+
if (stream.isTTY !== true) return false;
|
|
48704
|
+
if (env.TERM === "dumb") return false;
|
|
48705
|
+
if (isCiEnvironment(env)) return false;
|
|
48706
|
+
if (env.WT_SESSION) return true;
|
|
48707
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
|
|
48708
|
+
if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
|
|
48709
|
+
return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
|
|
48710
|
+
};
|
|
48711
|
+
//#endregion
|
|
48712
|
+
//#region src/cli/utils/should-render-hyperlinks.ts
|
|
48713
|
+
/**
|
|
48714
|
+
* Whether to emit OSC 8 clickable `file:line` locations for this run: a
|
|
48715
|
+
* hyperlink-capable terminal AND not a coding agent (whose output parsers
|
|
48716
|
+
* would choke on the escape sequences).
|
|
48717
|
+
*/
|
|
48718
|
+
const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
|
|
48581
48719
|
const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
|
|
48582
48720
|
const FALSY_FLAG_VALUES = new Set([
|
|
48583
48721
|
"",
|
|
@@ -48597,10 +48735,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48597
48735
|
};
|
|
48598
48736
|
//#endregion
|
|
48599
48737
|
//#region src/cli/utils/onboarding-state.ts
|
|
48600
|
-
const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
|
|
48601
48738
|
const ONBOARDED_AT_KEY = "onboardedAt";
|
|
48602
48739
|
const getOnboardingStore = (options = {}) => new Conf({
|
|
48603
|
-
projectName:
|
|
48740
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48604
48741
|
cwd: options.cwd
|
|
48605
48742
|
});
|
|
48606
48743
|
const hasCompletedOnboarding = (options = {}) => {
|
|
@@ -49056,6 +49193,78 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
49056
49193
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
49057
49194
|
};
|
|
49058
49195
|
//#endregion
|
|
49196
|
+
//#region src/cli/utils/git-hook-shared.ts
|
|
49197
|
+
const HOOK_FILE_NAME = "pre-commit";
|
|
49198
|
+
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49199
|
+
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49200
|
+
const HUSKY_HOOKS_PATH = ".husky";
|
|
49201
|
+
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49202
|
+
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49203
|
+
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49204
|
+
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49205
|
+
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49206
|
+
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49207
|
+
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49208
|
+
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49209
|
+
"rm -f \"$react_doctor_output\";",
|
|
49210
|
+
"else",
|
|
49211
|
+
"rm -f \"$react_doctor_output\";",
|
|
49212
|
+
`printf "%s\\n" "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2;`,
|
|
49213
|
+
"fi"
|
|
49214
|
+
].join(" ");
|
|
49215
|
+
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49216
|
+
const runGit = (projectRoot, args) => {
|
|
49217
|
+
try {
|
|
49218
|
+
return execFileSync("git", [...args], {
|
|
49219
|
+
cwd: projectRoot,
|
|
49220
|
+
encoding: "utf8",
|
|
49221
|
+
stdio: [
|
|
49222
|
+
"ignore",
|
|
49223
|
+
"pipe",
|
|
49224
|
+
"ignore"
|
|
49225
|
+
]
|
|
49226
|
+
}).trim();
|
|
49227
|
+
} catch {
|
|
49228
|
+
return null;
|
|
49229
|
+
}
|
|
49230
|
+
};
|
|
49231
|
+
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
49232
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49233
|
+
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
49234
|
+
const readPackageJson = (projectRoot) => {
|
|
49235
|
+
try {
|
|
49236
|
+
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
49237
|
+
} catch {
|
|
49238
|
+
return null;
|
|
49239
|
+
}
|
|
49240
|
+
};
|
|
49241
|
+
const writeJsonFile$1 = (filePath, value) => {
|
|
49242
|
+
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
49243
|
+
};
|
|
49244
|
+
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
49245
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49246
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49247
|
+
return [
|
|
49248
|
+
"dependencies",
|
|
49249
|
+
"devDependencies",
|
|
49250
|
+
"optionalDependencies"
|
|
49251
|
+
].some((fieldName) => {
|
|
49252
|
+
const dependencies = packageJson[fieldName];
|
|
49253
|
+
return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
49254
|
+
});
|
|
49255
|
+
};
|
|
49256
|
+
const packageHasRecordKey = (projectRoot, key) => {
|
|
49257
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49258
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
|
|
49259
|
+
};
|
|
49260
|
+
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
49261
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49262
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49263
|
+
const value = packageJson[key];
|
|
49264
|
+
return isRecord$1(value) && isRecord$1(value[nestedKey]);
|
|
49265
|
+
};
|
|
49266
|
+
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
49267
|
+
//#endregion
|
|
49059
49268
|
//#region src/cli/utils/scan-result-cache.ts
|
|
49060
49269
|
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
49061
49270
|
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
@@ -49066,7 +49275,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
|
49066
49275
|
"eslint-plugin-react-hooks/package.json"
|
|
49067
49276
|
];
|
|
49068
49277
|
const bundledRequire = createRequire(import.meta.url);
|
|
49069
|
-
const isRecord
|
|
49278
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49070
49279
|
const normalizeForStableJson = (value) => {
|
|
49071
49280
|
if (value === null) return null;
|
|
49072
49281
|
if (value === void 0) return void 0;
|
|
@@ -49095,24 +49304,9 @@ const stringifyStableJson = (value) => {
|
|
|
49095
49304
|
}
|
|
49096
49305
|
};
|
|
49097
49306
|
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
49098
|
-
const
|
|
49099
|
-
try {
|
|
49100
|
-
return execFileSync("git", [...args], {
|
|
49101
|
-
cwd: directory,
|
|
49102
|
-
encoding: "utf8",
|
|
49103
|
-
stdio: [
|
|
49104
|
-
"ignore",
|
|
49105
|
-
"pipe",
|
|
49106
|
-
"ignore"
|
|
49107
|
-
]
|
|
49108
|
-
}).trim();
|
|
49109
|
-
} catch {
|
|
49110
|
-
return null;
|
|
49111
|
-
}
|
|
49112
|
-
};
|
|
49113
|
-
const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49307
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49114
49308
|
const isWorktreeClean = (projectDirectory) => {
|
|
49115
|
-
const status = runGit
|
|
49309
|
+
const status = runGit(projectDirectory, [
|
|
49116
49310
|
"status",
|
|
49117
49311
|
"--porcelain=v1",
|
|
49118
49312
|
"--untracked-files=normal"
|
|
@@ -49120,7 +49314,7 @@ const isWorktreeClean = (projectDirectory) => {
|
|
|
49120
49314
|
return status !== null && status.length === 0;
|
|
49121
49315
|
};
|
|
49122
49316
|
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
49123
|
-
const output = runGit
|
|
49317
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
49124
49318
|
if (output === null) return true;
|
|
49125
49319
|
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
49126
49320
|
};
|
|
@@ -49133,7 +49327,7 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49133
49327
|
const readPersistedCache = (cacheFilePath) => {
|
|
49134
49328
|
try {
|
|
49135
49329
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49136
|
-
if (!isRecord
|
|
49330
|
+
if (!isRecord(parsed) || parsed.version !== 1) return {
|
|
49137
49331
|
version: 1,
|
|
49138
49332
|
entries: []
|
|
49139
49333
|
};
|
|
@@ -49143,8 +49337,8 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49143
49337
|
};
|
|
49144
49338
|
const entries = [];
|
|
49145
49339
|
for (const entry of parsed.entries) {
|
|
49146
|
-
if (!isRecord
|
|
49147
|
-
if (!isRecord
|
|
49340
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
49341
|
+
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
49148
49342
|
entries.push(entry);
|
|
49149
49343
|
}
|
|
49150
49344
|
return {
|
|
@@ -49676,6 +49870,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49676
49870
|
}
|
|
49677
49871
|
const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
|
|
49678
49872
|
const pause = onboardingSectionPause(animateRender);
|
|
49873
|
+
const useHyperlinks = shouldRenderHyperlinks(process.stdout);
|
|
49679
49874
|
const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
|
|
49680
49875
|
const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
|
|
49681
49876
|
if (printedDiagnostics.length === 0) {
|
|
@@ -49701,7 +49896,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49701
49896
|
yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
|
|
49702
49897
|
sectionPause: pause,
|
|
49703
49898
|
animateCountUp: animateRender
|
|
49704
|
-
});
|
|
49899
|
+
}, useHyperlinks);
|
|
49705
49900
|
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
49706
49901
|
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
49707
49902
|
yield* log(highlighter.gray(` ${demotedDiagnosticCount} demoted from the ${options.outputSurface} surface (e.g. design cleanup) — run \`npx react-doctor@latest .\` locally for the full list.`));
|
|
@@ -49894,16 +50089,7 @@ const buildHandoffPayload = (input) => {
|
|
|
49894
50089
|
try {
|
|
49895
50090
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
49896
50091
|
} catch {}
|
|
49897
|
-
const lines = [
|
|
49898
|
-
"Before fixing anything, first ask me whether I'd like to add React Doctor to CI so every pull request is scanned automatically. Explain that:",
|
|
49899
|
-
`- It's used by teams at ${CI_TRUST_COMPANIES}.`,
|
|
49900
|
-
"- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
|
|
49901
|
-
`- Setup guide: ${CI_URL}`,
|
|
49902
|
-
"If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
|
|
49903
|
-
"",
|
|
49904
|
-
`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`,
|
|
49905
|
-
""
|
|
49906
|
-
];
|
|
50092
|
+
const lines = [`Fix the top ${topGroups.length} React Doctor ${topGroups.length === 1 ? "issue" : "issues"} in ${input.projectName} on this pass — leave the rest for a follow-up.`, ""];
|
|
49907
50093
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
49908
50094
|
const representative = ruleDiagnostics[0];
|
|
49909
50095
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
@@ -49964,78 +50150,6 @@ const detectAvailableAgents = async () => {
|
|
|
49964
50150
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
49965
50151
|
};
|
|
49966
50152
|
//#endregion
|
|
49967
|
-
//#region src/cli/utils/git-hook-shared.ts
|
|
49968
|
-
const HOOK_FILE_NAME = "pre-commit";
|
|
49969
|
-
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49970
|
-
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49971
|
-
const HUSKY_HOOKS_PATH = ".husky";
|
|
49972
|
-
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49973
|
-
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49974
|
-
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49975
|
-
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49976
|
-
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49977
|
-
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49978
|
-
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49979
|
-
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49980
|
-
"rm -f \"$react_doctor_output\";",
|
|
49981
|
-
"else",
|
|
49982
|
-
"rm -f \"$react_doctor_output\";",
|
|
49983
|
-
`printf "%s\\n" "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2;`,
|
|
49984
|
-
"fi"
|
|
49985
|
-
].join(" ");
|
|
49986
|
-
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49987
|
-
const runGit = (projectRoot, args) => {
|
|
49988
|
-
try {
|
|
49989
|
-
return execFileSync("git", [...args], {
|
|
49990
|
-
cwd: projectRoot,
|
|
49991
|
-
encoding: "utf8",
|
|
49992
|
-
stdio: [
|
|
49993
|
-
"ignore",
|
|
49994
|
-
"pipe",
|
|
49995
|
-
"ignore"
|
|
49996
|
-
]
|
|
49997
|
-
}).trim();
|
|
49998
|
-
} catch {
|
|
49999
|
-
return null;
|
|
50000
|
-
}
|
|
50001
|
-
};
|
|
50002
|
-
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
50003
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50004
|
-
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
50005
|
-
const readPackageJson = (projectRoot) => {
|
|
50006
|
-
try {
|
|
50007
|
-
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
50008
|
-
} catch {
|
|
50009
|
-
return null;
|
|
50010
|
-
}
|
|
50011
|
-
};
|
|
50012
|
-
const writeJsonFile$1 = (filePath, value) => {
|
|
50013
|
-
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50014
|
-
};
|
|
50015
|
-
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
50016
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50017
|
-
if (!isRecord(packageJson)) return false;
|
|
50018
|
-
return [
|
|
50019
|
-
"dependencies",
|
|
50020
|
-
"devDependencies",
|
|
50021
|
-
"optionalDependencies"
|
|
50022
|
-
].some((fieldName) => {
|
|
50023
|
-
const dependencies = packageJson[fieldName];
|
|
50024
|
-
return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
50025
|
-
});
|
|
50026
|
-
};
|
|
50027
|
-
const packageHasRecordKey = (projectRoot, key) => {
|
|
50028
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50029
|
-
return isRecord(packageJson) && isRecord(packageJson[key]);
|
|
50030
|
-
};
|
|
50031
|
-
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
50032
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50033
|
-
if (!isRecord(packageJson)) return false;
|
|
50034
|
-
const value = packageJson[key];
|
|
50035
|
-
return isRecord(value) && isRecord(value[nestedKey]);
|
|
50036
|
-
};
|
|
50037
|
-
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
50038
|
-
//#endregion
|
|
50039
50153
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50040
50154
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
50041
50155
|
const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
|
|
@@ -50061,31 +50175,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
|
|
|
50061
50175
|
};
|
|
50062
50176
|
const hasDoctorScript = (projectRoot) => {
|
|
50063
50177
|
const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
|
|
50064
|
-
if (!isRecord(packageJson)) return false;
|
|
50178
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50065
50179
|
const scripts = packageJson.scripts;
|
|
50066
|
-
if (!isRecord(scripts)) return false;
|
|
50180
|
+
if (!isRecord$1(scripts)) return false;
|
|
50067
50181
|
return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
|
|
50068
50182
|
};
|
|
50069
50183
|
const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
|
|
50070
50184
|
const dependencies = packageJson[fieldName];
|
|
50071
|
-
return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50185
|
+
return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50072
50186
|
});
|
|
50073
50187
|
const installDoctorScript = (options) => {
|
|
50074
50188
|
const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
|
|
50075
50189
|
const packageJsonPath = getPackageJsonPath(packageDirectory);
|
|
50076
50190
|
const packageJson = readPackageJson(packageDirectory);
|
|
50077
|
-
if (!isRecord(packageJson)) return {
|
|
50191
|
+
if (!isRecord$1(packageJson)) return {
|
|
50078
50192
|
packageJsonPath,
|
|
50079
50193
|
scriptStatus: "skipped",
|
|
50080
50194
|
scriptReason: "missing-or-invalid-package-json"
|
|
50081
50195
|
};
|
|
50082
50196
|
const scripts = packageJson.scripts;
|
|
50083
50197
|
const scriptTarget = (() => {
|
|
50084
|
-
if (scripts !== void 0 && !isRecord(scripts)) return {
|
|
50198
|
+
if (scripts !== void 0 && !isRecord$1(scripts)) return {
|
|
50085
50199
|
status: "skipped",
|
|
50086
50200
|
reason: "invalid-scripts"
|
|
50087
50201
|
};
|
|
50088
|
-
const scriptRecord = isRecord(scripts) ? scripts : {};
|
|
50202
|
+
const scriptRecord = isRecord$1(scripts) ? scripts : {};
|
|
50089
50203
|
if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
|
|
50090
50204
|
scriptName: DOCTOR_SCRIPT_NAME,
|
|
50091
50205
|
status: "existing"
|
|
@@ -50119,7 +50233,7 @@ const installDoctorScript = (options) => {
|
|
|
50119
50233
|
if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
|
|
50120
50234
|
...packageJson,
|
|
50121
50235
|
scripts: {
|
|
50122
|
-
...isRecord(scripts) ? scripts : {},
|
|
50236
|
+
...isRecord$1(scripts) ? scripts : {},
|
|
50123
50237
|
[scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
|
|
50124
50238
|
}
|
|
50125
50239
|
});
|
|
@@ -50273,38 +50387,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50273
50387
|
//#region src/cli/utils/hash-project-root.ts
|
|
50274
50388
|
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50275
50389
|
//#endregion
|
|
50276
|
-
//#region src/cli/utils/
|
|
50277
|
-
const
|
|
50278
|
-
const
|
|
50279
|
-
|
|
50280
|
-
|
|
50281
|
-
});
|
|
50282
|
-
|
|
50283
|
-
|
|
50284
|
-
|
|
50285
|
-
|
|
50286
|
-
|
|
50287
|
-
|
|
50288
|
-
|
|
50289
|
-
};
|
|
50290
|
-
const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
|
|
50291
|
-
try {
|
|
50292
|
-
const store = getActionUpgradeStore(storeOptions);
|
|
50293
|
-
const upgrades = store.get("actionUpgrades", {});
|
|
50294
|
-
store.set("actionUpgrades", {
|
|
50295
|
-
...upgrades,
|
|
50296
|
-
[hashProjectRoot(projectRoot)]: {
|
|
50297
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
50298
|
-
outcome,
|
|
50299
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50390
|
+
//#region src/cli/utils/project-decision-store.ts
|
|
50391
|
+
const createProjectDecisionStore = (storeKey) => {
|
|
50392
|
+
const getStore = (options = {}) => new Conf({
|
|
50393
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
50394
|
+
cwd: options.cwd
|
|
50395
|
+
});
|
|
50396
|
+
return {
|
|
50397
|
+
getConfigPath: (options = {}) => getStore(options).path,
|
|
50398
|
+
hasHandled: (projectRoot, options = {}) => {
|
|
50399
|
+
try {
|
|
50400
|
+
return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
|
|
50401
|
+
} catch {
|
|
50402
|
+
return true;
|
|
50300
50403
|
}
|
|
50301
|
-
}
|
|
50302
|
-
|
|
50303
|
-
|
|
50304
|
-
|
|
50305
|
-
|
|
50404
|
+
},
|
|
50405
|
+
record: (projectRoot, outcome, options = {}) => {
|
|
50406
|
+
try {
|
|
50407
|
+
const store = getStore(options);
|
|
50408
|
+
store.set(storeKey, {
|
|
50409
|
+
...store.get(storeKey, {}),
|
|
50410
|
+
[hashProjectRoot(projectRoot)]: {
|
|
50411
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
50412
|
+
outcome,
|
|
50413
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50414
|
+
}
|
|
50415
|
+
});
|
|
50416
|
+
return true;
|
|
50417
|
+
} catch {
|
|
50418
|
+
return false;
|
|
50419
|
+
}
|
|
50420
|
+
}
|
|
50421
|
+
};
|
|
50306
50422
|
};
|
|
50307
50423
|
//#endregion
|
|
50424
|
+
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50425
|
+
const store$1 = createProjectDecisionStore("actionUpgrades");
|
|
50426
|
+
store$1.getConfigPath;
|
|
50427
|
+
const hasHandledActionUpgrade = store$1.hasHandled;
|
|
50428
|
+
const recordActionUpgradeDecision = store$1.record;
|
|
50429
|
+
//#endregion
|
|
50430
|
+
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50431
|
+
const store = createProjectDecisionStore("ciPrompts");
|
|
50432
|
+
store.getConfigPath;
|
|
50433
|
+
const hasHandledCiPrompt = store.hasHandled;
|
|
50434
|
+
const recordCiPromptDecision = store.record;
|
|
50435
|
+
//#endregion
|
|
50308
50436
|
//#region src/cli/utils/open-url.ts
|
|
50309
50437
|
const resolveOpenCommand = (url) => {
|
|
50310
50438
|
if (process$1.platform === "darwin") return {
|
|
@@ -50760,22 +50888,22 @@ const buildAgentHookScript = () => [
|
|
|
50760
50888
|
"",
|
|
50761
50889
|
"run_react_doctor() {",
|
|
50762
50890
|
" if [ -x ./node_modules/.bin/react-doctor ]; then",
|
|
50763
|
-
" ./node_modules/.bin/react-doctor --verbose --
|
|
50891
|
+
" ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50764
50892
|
" return",
|
|
50765
50893
|
" fi",
|
|
50766
50894
|
"",
|
|
50767
50895
|
" if command -v react-doctor >/dev/null 2>&1; then",
|
|
50768
|
-
" react-doctor --verbose --
|
|
50896
|
+
" react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50769
50897
|
" return",
|
|
50770
50898
|
" fi",
|
|
50771
50899
|
"",
|
|
50772
50900
|
" if command -v pnpm >/dev/null 2>&1; then",
|
|
50773
|
-
" pnpm dlx react-doctor@latest --verbose --
|
|
50901
|
+
" pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50774
50902
|
" return",
|
|
50775
50903
|
" fi",
|
|
50776
50904
|
"",
|
|
50777
50905
|
" if command -v npx >/dev/null 2>&1; then",
|
|
50778
|
-
" npx --yes react-doctor@latest --verbose --
|
|
50906
|
+
" npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50779
50907
|
" return",
|
|
50780
50908
|
" fi",
|
|
50781
50909
|
"",
|
|
@@ -50933,13 +51061,13 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
50933
51061
|
const packageJsonPath = getPackageJsonPath(options.projectRoot);
|
|
50934
51062
|
const didHookExist = NFS.existsSync(packageJsonPath);
|
|
50935
51063
|
const packageJson = readPackageJson(options.projectRoot);
|
|
50936
|
-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
|
|
51064
|
+
const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
|
|
50937
51065
|
const parentKeys = strategy.path.slice(0, -1);
|
|
50938
51066
|
const leafKey = strategy.path[strategy.path.length - 1];
|
|
50939
51067
|
let parent = nextPackageJson;
|
|
50940
51068
|
for (const key of parentKeys) {
|
|
50941
51069
|
const existing = parent[key];
|
|
50942
|
-
const cloned = isRecord(existing) ? { ...existing } : {};
|
|
51070
|
+
const cloned = isRecord$1(existing) ? { ...existing } : {};
|
|
50943
51071
|
parent[key] = cloned;
|
|
50944
51072
|
parent = cloned;
|
|
50945
51073
|
}
|
|
@@ -51110,7 +51238,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
|
|
|
51110
51238
|
const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
|
|
51111
51239
|
const isSimpleGitHooksProject = (projectRoot) => {
|
|
51112
51240
|
const packageJson = readPackageJson(projectRoot);
|
|
51113
|
-
return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51241
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51114
51242
|
};
|
|
51115
51243
|
const getLefthookConfigPath = (projectRoot) => {
|
|
51116
51244
|
for (const fileName of LEFTHOOK_CONFIG_FILES) {
|
|
@@ -51276,7 +51404,7 @@ const detectPackageManager = (projectRoot) => {
|
|
|
51276
51404
|
let currentDirectory = Path.resolve(projectRoot);
|
|
51277
51405
|
while (true) {
|
|
51278
51406
|
const packageJson = readPackageJson(currentDirectory);
|
|
51279
|
-
if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51407
|
+
if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51280
51408
|
const packageManagerName = packageJson.packageManager.split("@")[0];
|
|
51281
51409
|
if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
|
|
51282
51410
|
}
|
|
@@ -51352,12 +51480,12 @@ const isSupplyChainTrustError = (error) => {
|
|
|
51352
51480
|
const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
|
|
51353
51481
|
const installReactDoctorDependency = async (options) => {
|
|
51354
51482
|
const packageJson = readPackageJson(options.projectRoot);
|
|
51355
|
-
if (!isRecord(packageJson)) return {
|
|
51483
|
+
if (!isRecord$1(packageJson)) return {
|
|
51356
51484
|
dependencyStatus: "skipped",
|
|
51357
51485
|
dependencyReason: "missing-or-invalid-package-json"
|
|
51358
51486
|
};
|
|
51359
51487
|
if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
|
|
51360
|
-
if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
|
|
51488
|
+
if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
|
|
51361
51489
|
dependencyStatus: "skipped",
|
|
51362
51490
|
dependencyReason: "invalid-dev-dependencies"
|
|
51363
51491
|
};
|
|
@@ -51521,10 +51649,12 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51521
51649
|
const existingWorkflow = readReactDoctorWorkflow(projectRoot);
|
|
51522
51650
|
const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
|
|
51523
51651
|
const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
|
|
51524
|
-
const
|
|
51652
|
+
const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
|
|
51653
|
+
const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
|
|
51525
51654
|
const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
|
|
51526
51655
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51527
51656
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51657
|
+
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
51528
51658
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51529
51659
|
type: "multiselect",
|
|
51530
51660
|
name: "agents",
|
|
@@ -51771,18 +51901,24 @@ const handoffToAgent = async (input) => {
|
|
|
51771
51901
|
if (!input.interactive || input.diagnostics.length === 0) return;
|
|
51772
51902
|
cliLogger.break();
|
|
51773
51903
|
const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
|
|
51774
|
-
|
|
51904
|
+
const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
|
|
51905
|
+
if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
|
|
51775
51906
|
const ciOutcome = await askAddToGitHubActions();
|
|
51776
51907
|
recordCount(METRIC.agentHandoff, 1, {
|
|
51777
51908
|
outcome: `ci-${ciOutcome}`,
|
|
51778
51909
|
diagnosticsCount: input.diagnostics.length
|
|
51779
51910
|
});
|
|
51780
51911
|
if (ciOutcome === "cancel") return;
|
|
51912
|
+
recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
|
|
51781
51913
|
if (ciOutcome === "yes") {
|
|
51782
51914
|
await setUpGitHubActions({ rootDirectory: input.rootDirectory });
|
|
51783
51915
|
cliLogger.break();
|
|
51784
51916
|
}
|
|
51785
|
-
} else await maybeOfferActionUpgrade(projectRootForCi);
|
|
51917
|
+
} else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
|
|
51918
|
+
else recordCount(METRIC.agentHandoff, 1, {
|
|
51919
|
+
outcome: "ci-suppressed",
|
|
51920
|
+
diagnosticsCount: input.diagnostics.length
|
|
51921
|
+
});
|
|
51786
51922
|
const { handoffTarget } = await prompts({
|
|
51787
51923
|
type: "select",
|
|
51788
51924
|
name: "handoffTarget",
|
|
@@ -52088,7 +52224,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52088
52224
|
yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
|
|
52089
52225
|
if (displayDiagnostics.length > 0) {
|
|
52090
52226
|
yield* log("");
|
|
52091
|
-
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
|
|
52227
|
+
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
|
|
52092
52228
|
}
|
|
52093
52229
|
const lowestScoredScan = findLowestScoredScan(completedScans);
|
|
52094
52230
|
const aggregateScore = lowestScoredScan?.result.score ?? null;
|
|
@@ -52126,9 +52262,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52126
52262
|
});
|
|
52127
52263
|
//#endregion
|
|
52128
52264
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52129
|
-
const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
|
|
52130
52265
|
const getSetupPromptStore = (options = {}) => new Conf({
|
|
52131
|
-
projectName:
|
|
52266
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
52132
52267
|
cwd: options.cwd
|
|
52133
52268
|
});
|
|
52134
52269
|
const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
|
|
@@ -52139,6 +52274,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
52139
52274
|
return false;
|
|
52140
52275
|
}
|
|
52141
52276
|
};
|
|
52277
|
+
const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
52278
|
+
try {
|
|
52279
|
+
const store = getSetupPromptStore(storeOptions);
|
|
52280
|
+
const projects = store.get("projects", {});
|
|
52281
|
+
const projectKey = getSetupPromptProjectKey(projectRoot);
|
|
52282
|
+
store.set("projects", {
|
|
52283
|
+
...projects,
|
|
52284
|
+
[projectKey]: {
|
|
52285
|
+
...projects[projectKey] ?? {},
|
|
52286
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
52287
|
+
setupPrompt: false
|
|
52288
|
+
}
|
|
52289
|
+
});
|
|
52290
|
+
return true;
|
|
52291
|
+
} catch {
|
|
52292
|
+
return false;
|
|
52293
|
+
}
|
|
52294
|
+
};
|
|
52142
52295
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52143
52296
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52144
52297
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52545,6 +52698,14 @@ const runExplain = async (fileLineArgument, context) => {
|
|
|
52545
52698
|
const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
|
|
52546
52699
|
const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
|
|
52547
52700
|
cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
|
|
52701
|
+
const codeFrame = buildCodeFrame({
|
|
52702
|
+
filePath: diagnostic.filePath,
|
|
52703
|
+
line: diagnostic.line,
|
|
52704
|
+
column: diagnostic.column,
|
|
52705
|
+
endLine: diagnostic.endLine,
|
|
52706
|
+
rootDirectory: targetDirectory
|
|
52707
|
+
});
|
|
52708
|
+
if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
|
|
52548
52709
|
if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
|
|
52549
52710
|
if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
|
|
52550
52711
|
cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
|
|
@@ -52941,6 +53102,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
52941
53102
|
})) {
|
|
52942
53103
|
printAgentInstallHint();
|
|
52943
53104
|
recordCount(METRIC.agentInstallHintShown, 1);
|
|
53105
|
+
disableSetupPrompt(setupProjectRoot);
|
|
52944
53106
|
}
|
|
52945
53107
|
}
|
|
52946
53108
|
} catch (error) {
|
|
@@ -53872,7 +54034,7 @@ ${highlighter.dim("Examples:")}
|
|
|
53872
54034
|
${formatExampleLines([
|
|
53873
54035
|
["react-doctor", "scan the current project"],
|
|
53874
54036
|
["react-doctor ./apps/web", "scan a specific directory"],
|
|
53875
|
-
["react-doctor --
|
|
54037
|
+
["react-doctor --scope changed --base main", "scan only new issues vs. main"],
|
|
53876
54038
|
["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
|
|
53877
54039
|
["react-doctor --staged", "scan staged files (pre-commit hook)"],
|
|
53878
54040
|
["react-doctor --category Security", "show only one diagnostic category"],
|
|
@@ -53948,4 +54110,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
53948
54110
|
export {};
|
|
53949
54111
|
|
|
53950
54112
|
//# sourceMappingURL=cli.js.map
|
|
53951
|
-
//# debugId=
|
|
54113
|
+
//# debugId=a06f0514-b1fa-5452-9c19-0140438862f8
|