react-doctor 0.5.6-dev.eafac9d → 0.5.6-dev.ed0258c
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 +363 -204
- package/dist/index.js +59 -30
- package/dist/lsp.js +65 -41
- package/package.json +3 -3
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
|
|
@@ -39692,15 +39697,10 @@ const buildCapabilities = (project) => {
|
|
|
39692
39697
|
}
|
|
39693
39698
|
if (project.tailwindVersion !== null) {
|
|
39694
39699
|
capabilities.add("tailwind");
|
|
39695
|
-
|
|
39696
|
-
if (isTailwindAtLeast(tailwind, {
|
|
39700
|
+
if (isTailwindAtLeast(parseTailwindMajorMinor(project.tailwindVersion), {
|
|
39697
39701
|
major: 3,
|
|
39698
39702
|
minor: 4
|
|
39699
39703
|
})) capabilities.add("tailwind:3.4");
|
|
39700
|
-
if (tailwind !== null && isTailwindAtLeast(tailwind, {
|
|
39701
|
-
major: 4,
|
|
39702
|
-
minor: 0
|
|
39703
|
-
})) capabilities.add("tailwind:4");
|
|
39704
39704
|
}
|
|
39705
39705
|
if (project.zodVersion !== null) {
|
|
39706
39706
|
capabilities.add("zod");
|
|
@@ -39946,8 +39946,8 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
39946
39946
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
39947
39947
|
return patterns;
|
|
39948
39948
|
};
|
|
39949
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39949
39950
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
39950
|
-
const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39951
39951
|
const readJsonFileSafe = (filePath) => {
|
|
39952
39952
|
let rawContents;
|
|
39953
39953
|
try {
|
|
@@ -39963,10 +39963,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
39963
39963
|
};
|
|
39964
39964
|
const readKnipConfig = (rootDirectory) => {
|
|
39965
39965
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
39966
|
-
if (isRecord$
|
|
39966
|
+
if (isRecord$2(knipJson)) return knipJson;
|
|
39967
39967
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
39968
|
-
const packageKnipConfig = isRecord$
|
|
39969
|
-
return isRecord$
|
|
39968
|
+
const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
|
|
39969
|
+
return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
|
|
39970
39970
|
};
|
|
39971
39971
|
const normalizePatternList = (value) => {
|
|
39972
39972
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -39978,10 +39978,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
39978
39978
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
39979
39979
|
};
|
|
39980
39980
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
39981
|
-
if (!isRecord$
|
|
39981
|
+
if (!isRecord$2(workspaces)) return [];
|
|
39982
39982
|
const patterns = [];
|
|
39983
39983
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
39984
|
-
if (!isRecord$
|
|
39984
|
+
if (!isRecord$2(workspaceConfig)) continue;
|
|
39985
39985
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
39986
39986
|
}
|
|
39987
39987
|
return patterns;
|
|
@@ -40026,8 +40026,6 @@ const toCanonicalPath = (filePath) => {
|
|
|
40026
40026
|
};
|
|
40027
40027
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40028
40028
|
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
40029
|
-
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
40030
|
-
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40031
40029
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40032
40030
|
const inputChunks = [];
|
|
40033
40031
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40085,7 +40083,7 @@ process.stdin.on("end", () => {
|
|
|
40085
40083
|
});
|
|
40086
40084
|
`;
|
|
40087
40085
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
40088
|
-
for (const filename of TSCONFIG_FILENAMES
|
|
40086
|
+
for (const filename of TSCONFIG_FILENAMES) {
|
|
40089
40087
|
const candidate = Path.join(rootDirectory, filename);
|
|
40090
40088
|
if (NFS.existsSync(candidate)) return candidate;
|
|
40091
40089
|
}
|
|
@@ -41325,8 +41323,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41325
41323
|
}
|
|
41326
41324
|
return enabled;
|
|
41327
41325
|
};
|
|
41328
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
41329
|
-
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);
|
|
41330
41328
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41331
41329
|
const jsPlugins = [];
|
|
41332
41330
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -41386,7 +41384,6 @@ const resolveOxlintBinary = () => {
|
|
|
41386
41384
|
return Path.join(oxlintPackageDirectory, "bin", "oxlint");
|
|
41387
41385
|
};
|
|
41388
41386
|
const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
|
|
41389
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
41390
41387
|
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
41391
41388
|
for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
|
|
41392
41389
|
return null;
|
|
@@ -41758,7 +41755,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
|
|
|
41758
41755
|
const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
|
|
41759
41756
|
let currentNode = identifier.parent;
|
|
41760
41757
|
while (currentNode) {
|
|
41761
|
-
if (
|
|
41758
|
+
if (isScopeBoundary(currentNode)) {
|
|
41762
41759
|
if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
|
|
41763
41760
|
}
|
|
41764
41761
|
if (currentNode === sourceFile) return false;
|
|
@@ -41849,11 +41846,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
|
|
|
41849
41846
|
});
|
|
41850
41847
|
return resolution;
|
|
41851
41848
|
};
|
|
41852
|
-
const isScopeNode = isScopeBoundary;
|
|
41853
41849
|
const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
|
|
41854
41850
|
let currentNode = identifier.parent;
|
|
41855
41851
|
while (currentNode) {
|
|
41856
|
-
if (
|
|
41852
|
+
if (isScopeBoundary(currentNode)) {
|
|
41857
41853
|
const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
|
|
41858
41854
|
if (resolution) return resolution;
|
|
41859
41855
|
}
|
|
@@ -42023,9 +42019,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42023
42019
|
try {
|
|
42024
42020
|
parsed = JSON.parse(sanitizedStdout);
|
|
42025
42021
|
} catch {
|
|
42026
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42022
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42027
42023
|
}
|
|
42028
|
-
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) }) });
|
|
42029
42025
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
42030
42026
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
42031
42027
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -42316,6 +42312,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
42316
42312
|
NFS.closeSync(fileHandle);
|
|
42317
42313
|
}
|
|
42318
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
|
+
};
|
|
42319
42337
|
/**
|
|
42320
42338
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
42321
42339
|
*
|
|
@@ -42343,15 +42361,16 @@ const runOxlint = async (options) => {
|
|
|
42343
42361
|
const pluginPath = resolvePluginPath();
|
|
42344
42362
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
42345
42363
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
42346
|
-
const buildConfig = (
|
|
42364
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
42347
42365
|
pluginPath,
|
|
42348
42366
|
project,
|
|
42349
42367
|
customRulesOnly,
|
|
42350
|
-
extendsPaths:
|
|
42368
|
+
extendsPaths: overrides.extendsPaths,
|
|
42351
42369
|
ignoredTags,
|
|
42352
42370
|
serverAuthFunctionNames,
|
|
42353
42371
|
severityControls,
|
|
42354
|
-
userPlugins
|
|
42372
|
+
userPlugins,
|
|
42373
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
42355
42374
|
});
|
|
42356
42375
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42357
42376
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -42387,12 +42406,22 @@ const runOxlint = async (options) => {
|
|
|
42387
42406
|
outputMaxBytes,
|
|
42388
42407
|
concurrency: options.concurrency
|
|
42389
42408
|
});
|
|
42390
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
42409
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42391
42410
|
try {
|
|
42392
42411
|
return await runBatches();
|
|
42393
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
|
+
}
|
|
42394
42423
|
if (extendsPaths.length === 0) throw error;
|
|
42395
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
42424
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
42396
42425
|
return await runBatches();
|
|
42397
42426
|
}
|
|
42398
42427
|
} finally {
|
|
@@ -43844,7 +43873,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
|
|
|
43844
43873
|
"false"
|
|
43845
43874
|
]);
|
|
43846
43875
|
const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
|
|
43847
|
-
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);
|
|
43848
43877
|
const detectCiProvider = () => {
|
|
43849
43878
|
for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
|
|
43850
43879
|
return isCiFlagSet(process.env.CI) ? "unknown" : null;
|
|
@@ -43869,6 +43898,42 @@ const detectCodingAgent = () => {
|
|
|
43869
43898
|
const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
|
|
43870
43899
|
const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
|
|
43871
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
|
|
43872
43937
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43873
43938
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43874
43939
|
//#endregion
|
|
@@ -43891,6 +43956,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
|
|
|
43891
43956
|
const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
|
|
43892
43957
|
//#endregion
|
|
43893
43958
|
//#region src/cli/utils/constants.ts
|
|
43959
|
+
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
43894
43960
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
43895
43961
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
43896
43962
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -43975,7 +44041,7 @@ const makeNoopConsole = () => ({
|
|
|
43975
44041
|
});
|
|
43976
44042
|
//#endregion
|
|
43977
44043
|
//#region src/cli/utils/version.ts
|
|
43978
|
-
const VERSION = "0.5.6-dev.
|
|
44044
|
+
const VERSION = "0.5.6-dev.ed0258c";
|
|
43979
44045
|
//#endregion
|
|
43980
44046
|
//#region src/cli/utils/json-mode.ts
|
|
43981
44047
|
let context = null;
|
|
@@ -44125,6 +44191,7 @@ const buildRunContext = () => {
|
|
|
44125
44191
|
viaAction: isOfficialGithubAction(),
|
|
44126
44192
|
codingAgent: detectCodingAgent(),
|
|
44127
44193
|
interactive: !isNonInteractiveEnvironment(),
|
|
44194
|
+
terminalKind: detectTerminalKind(),
|
|
44128
44195
|
jsonMode: isJsonModeActive(),
|
|
44129
44196
|
invokedVia: detectInvokedVia()
|
|
44130
44197
|
};
|
|
@@ -44195,6 +44262,7 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44195
44262
|
viaAction: runContext.viaAction,
|
|
44196
44263
|
codingAgent: runContext.codingAgent,
|
|
44197
44264
|
interactive: runContext.interactive,
|
|
44265
|
+
terminalKind: runContext.terminalKind,
|
|
44198
44266
|
jsonMode: runContext.jsonMode,
|
|
44199
44267
|
invokedVia: runContext.invokedVia,
|
|
44200
44268
|
nodeMajor: runContext.nodeMajor
|
|
@@ -44333,13 +44401,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44333
44401
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44334
44402
|
* standard `SENTRY_RELEASE` override.
|
|
44335
44403
|
*/
|
|
44336
|
-
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.ed0258c`;
|
|
44337
44405
|
/**
|
|
44338
44406
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44339
44407
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44340
44408
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44341
44409
|
*/
|
|
44342
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.
|
|
44410
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.ed0258c") ? "development" : "production");
|
|
44343
44411
|
/**
|
|
44344
44412
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44345
44413
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -48196,6 +48264,15 @@ const boxText = (content, innerWidth) => {
|
|
|
48196
48264
|
].join("\n");
|
|
48197
48265
|
};
|
|
48198
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
|
|
48199
48276
|
//#region src/cli/utils/build-code-frame.ts
|
|
48200
48277
|
/**
|
|
48201
48278
|
* Renders a syntax-highlighted source excerpt around a diagnostic site
|
|
@@ -48206,7 +48283,7 @@ const boxText = (content, innerWidth) => {
|
|
|
48206
48283
|
*/
|
|
48207
48284
|
const buildCodeFrame = (input) => {
|
|
48208
48285
|
if (input.line <= 0) return null;
|
|
48209
|
-
const absolutePath =
|
|
48286
|
+
const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
|
|
48210
48287
|
let source;
|
|
48211
48288
|
try {
|
|
48212
48289
|
source = NFS.readFileSync(absolutePath, "utf8");
|
|
@@ -48246,6 +48323,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
|
|
|
48246
48323
|
const DIVIDER_INDENT = " ";
|
|
48247
48324
|
const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
|
|
48248
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
|
|
48249
48336
|
//#region src/cli/utils/indent-multiline-text.ts
|
|
48250
48337
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
48251
48338
|
//#endregion
|
|
@@ -48399,17 +48486,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
|
|
|
48399
48486
|
}
|
|
48400
48487
|
return clusters;
|
|
48401
48488
|
};
|
|
48402
|
-
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) => {
|
|
48403
48496
|
const lead = cluster.diagnostics[0];
|
|
48404
48497
|
const contextTag = formatFileContextTag(lead);
|
|
48405
|
-
|
|
48406
|
-
if (
|
|
48407
|
-
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}`;
|
|
48408
48501
|
};
|
|
48409
|
-
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
|
|
48502
|
+
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
|
|
48410
48503
|
const lead = cluster.diagnostics[0];
|
|
48411
48504
|
const isMultiSite = cluster.diagnostics.length > 1;
|
|
48412
|
-
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
|
|
48505
|
+
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
|
|
48413
48506
|
const codeFrame = renderCodeFrame ? buildCodeFrame({
|
|
48414
48507
|
filePath: lead.filePath,
|
|
48415
48508
|
line: cluster.startLine,
|
|
@@ -48428,7 +48521,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
|
|
|
48428
48521
|
}
|
|
48429
48522
|
return lines;
|
|
48430
48523
|
};
|
|
48431
|
-
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
|
|
48524
|
+
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
|
|
48432
48525
|
const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
|
|
48433
48526
|
const { severity } = representative;
|
|
48434
48527
|
const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
|
|
@@ -48448,7 +48541,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48448
48541
|
}
|
|
48449
48542
|
const renderCodeFrame = severity === "error";
|
|
48450
48543
|
const sites = renderEverySite ? ruleDiagnostics : [representative];
|
|
48451
|
-
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));
|
|
48452
48545
|
return lines;
|
|
48453
48546
|
};
|
|
48454
48547
|
const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
|
|
@@ -48461,7 +48554,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48461
48554
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48462
48555
|
};
|
|
48463
48556
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48464
|
-
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
|
|
48557
|
+
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48465
48558
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
48466
48559
|
if (topRuleGroups.length === 0) return {
|
|
48467
48560
|
lines: [],
|
|
@@ -48471,7 +48564,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
|
|
|
48471
48564
|
const blockOffsets = [];
|
|
48472
48565
|
for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
|
|
48473
48566
|
blockOffsets.push(lines.length);
|
|
48474
|
-
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
|
|
48567
|
+
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
|
|
48475
48568
|
lines.push("");
|
|
48476
48569
|
}
|
|
48477
48570
|
return {
|
|
@@ -48509,18 +48602,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
|
|
|
48509
48602
|
* single Effect.forEach over Console.log so failures or fiber
|
|
48510
48603
|
* interruption produce predictable partial output.
|
|
48511
48604
|
*/
|
|
48512
|
-
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* () {
|
|
48513
48606
|
const sectionPause = onboarding.sectionPause ?? void_;
|
|
48514
48607
|
const animateCountUp = onboarding.animateCountUp ?? false;
|
|
48515
48608
|
const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
|
|
48516
48609
|
let detailLines;
|
|
48517
48610
|
let topErrorBlockOffsets = [];
|
|
48518
48611
|
if (!isVerbose) {
|
|
48519
|
-
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
|
|
48612
|
+
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
|
|
48520
48613
|
detailLines = topErrors.lines;
|
|
48521
48614
|
topErrorBlockOffsets = topErrors.blockOffsets;
|
|
48522
48615
|
} else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
|
|
48523
|
-
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
|
|
48616
|
+
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
|
|
48524
48617
|
});
|
|
48525
48618
|
const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
|
|
48526
48619
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
@@ -48581,6 +48674,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
|
|
|
48581
48674
|
//#endregion
|
|
48582
48675
|
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
48583
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();
|
|
48584
48719
|
const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
|
|
48585
48720
|
const FALSY_FLAG_VALUES = new Set([
|
|
48586
48721
|
"",
|
|
@@ -48600,10 +48735,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48600
48735
|
};
|
|
48601
48736
|
//#endregion
|
|
48602
48737
|
//#region src/cli/utils/onboarding-state.ts
|
|
48603
|
-
const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
|
|
48604
48738
|
const ONBOARDED_AT_KEY = "onboardedAt";
|
|
48605
48739
|
const getOnboardingStore = (options = {}) => new Conf({
|
|
48606
|
-
projectName:
|
|
48740
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48607
48741
|
cwd: options.cwd
|
|
48608
48742
|
});
|
|
48609
48743
|
const hasCompletedOnboarding = (options = {}) => {
|
|
@@ -49059,6 +49193,78 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
49059
49193
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
49060
49194
|
};
|
|
49061
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
|
|
49062
49268
|
//#region src/cli/utils/scan-result-cache.ts
|
|
49063
49269
|
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
49064
49270
|
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
@@ -49069,7 +49275,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
|
49069
49275
|
"eslint-plugin-react-hooks/package.json"
|
|
49070
49276
|
];
|
|
49071
49277
|
const bundledRequire = createRequire(import.meta.url);
|
|
49072
|
-
const isRecord
|
|
49278
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49073
49279
|
const normalizeForStableJson = (value) => {
|
|
49074
49280
|
if (value === null) return null;
|
|
49075
49281
|
if (value === void 0) return void 0;
|
|
@@ -49098,24 +49304,9 @@ const stringifyStableJson = (value) => {
|
|
|
49098
49304
|
}
|
|
49099
49305
|
};
|
|
49100
49306
|
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
49101
|
-
const
|
|
49102
|
-
try {
|
|
49103
|
-
return execFileSync("git", [...args], {
|
|
49104
|
-
cwd: directory,
|
|
49105
|
-
encoding: "utf8",
|
|
49106
|
-
stdio: [
|
|
49107
|
-
"ignore",
|
|
49108
|
-
"pipe",
|
|
49109
|
-
"ignore"
|
|
49110
|
-
]
|
|
49111
|
-
}).trim();
|
|
49112
|
-
} catch {
|
|
49113
|
-
return null;
|
|
49114
|
-
}
|
|
49115
|
-
};
|
|
49116
|
-
const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49307
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49117
49308
|
const isWorktreeClean = (projectDirectory) => {
|
|
49118
|
-
const status = runGit
|
|
49309
|
+
const status = runGit(projectDirectory, [
|
|
49119
49310
|
"status",
|
|
49120
49311
|
"--porcelain=v1",
|
|
49121
49312
|
"--untracked-files=normal"
|
|
@@ -49123,7 +49314,7 @@ const isWorktreeClean = (projectDirectory) => {
|
|
|
49123
49314
|
return status !== null && status.length === 0;
|
|
49124
49315
|
};
|
|
49125
49316
|
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
49126
|
-
const output = runGit
|
|
49317
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
49127
49318
|
if (output === null) return true;
|
|
49128
49319
|
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
49129
49320
|
};
|
|
@@ -49136,7 +49327,7 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49136
49327
|
const readPersistedCache = (cacheFilePath) => {
|
|
49137
49328
|
try {
|
|
49138
49329
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49139
|
-
if (!isRecord
|
|
49330
|
+
if (!isRecord(parsed) || parsed.version !== 1) return {
|
|
49140
49331
|
version: 1,
|
|
49141
49332
|
entries: []
|
|
49142
49333
|
};
|
|
@@ -49146,8 +49337,8 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49146
49337
|
};
|
|
49147
49338
|
const entries = [];
|
|
49148
49339
|
for (const entry of parsed.entries) {
|
|
49149
|
-
if (!isRecord
|
|
49150
|
-
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;
|
|
49151
49342
|
entries.push(entry);
|
|
49152
49343
|
}
|
|
49153
49344
|
return {
|
|
@@ -49679,6 +49870,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49679
49870
|
}
|
|
49680
49871
|
const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
|
|
49681
49872
|
const pause = onboardingSectionPause(animateRender);
|
|
49873
|
+
const useHyperlinks = shouldRenderHyperlinks(process.stdout);
|
|
49682
49874
|
const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
|
|
49683
49875
|
const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
|
|
49684
49876
|
if (printedDiagnostics.length === 0) {
|
|
@@ -49704,7 +49896,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49704
49896
|
yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
|
|
49705
49897
|
sectionPause: pause,
|
|
49706
49898
|
animateCountUp: animateRender
|
|
49707
|
-
});
|
|
49899
|
+
}, useHyperlinks);
|
|
49708
49900
|
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
49709
49901
|
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
49710
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.`));
|
|
@@ -49897,16 +50089,7 @@ const buildHandoffPayload = (input) => {
|
|
|
49897
50089
|
try {
|
|
49898
50090
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
49899
50091
|
} catch {}
|
|
49900
|
-
const lines = [
|
|
49901
|
-
"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:",
|
|
49902
|
-
`- It's used by teams at ${CI_TRUST_COMPANIES}.`,
|
|
49903
|
-
"- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
|
|
49904
|
-
`- Setup guide: ${CI_URL}`,
|
|
49905
|
-
"If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
|
|
49906
|
-
"",
|
|
49907
|
-
`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.`,
|
|
49908
|
-
""
|
|
49909
|
-
];
|
|
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.`, ""];
|
|
49910
50093
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
49911
50094
|
const representative = ruleDiagnostics[0];
|
|
49912
50095
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
@@ -49967,78 +50150,6 @@ const detectAvailableAgents = async () => {
|
|
|
49967
50150
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
49968
50151
|
};
|
|
49969
50152
|
//#endregion
|
|
49970
|
-
//#region src/cli/utils/git-hook-shared.ts
|
|
49971
|
-
const HOOK_FILE_NAME = "pre-commit";
|
|
49972
|
-
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49973
|
-
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49974
|
-
const HUSKY_HOOKS_PATH = ".husky";
|
|
49975
|
-
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49976
|
-
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49977
|
-
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49978
|
-
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49979
|
-
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49980
|
-
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49981
|
-
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49982
|
-
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49983
|
-
"rm -f \"$react_doctor_output\";",
|
|
49984
|
-
"else",
|
|
49985
|
-
"rm -f \"$react_doctor_output\";",
|
|
49986
|
-
`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;`,
|
|
49987
|
-
"fi"
|
|
49988
|
-
].join(" ");
|
|
49989
|
-
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49990
|
-
const runGit = (projectRoot, args) => {
|
|
49991
|
-
try {
|
|
49992
|
-
return execFileSync("git", [...args], {
|
|
49993
|
-
cwd: projectRoot,
|
|
49994
|
-
encoding: "utf8",
|
|
49995
|
-
stdio: [
|
|
49996
|
-
"ignore",
|
|
49997
|
-
"pipe",
|
|
49998
|
-
"ignore"
|
|
49999
|
-
]
|
|
50000
|
-
}).trim();
|
|
50001
|
-
} catch {
|
|
50002
|
-
return null;
|
|
50003
|
-
}
|
|
50004
|
-
};
|
|
50005
|
-
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
50006
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50007
|
-
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
50008
|
-
const readPackageJson = (projectRoot) => {
|
|
50009
|
-
try {
|
|
50010
|
-
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
50011
|
-
} catch {
|
|
50012
|
-
return null;
|
|
50013
|
-
}
|
|
50014
|
-
};
|
|
50015
|
-
const writeJsonFile$1 = (filePath, value) => {
|
|
50016
|
-
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50017
|
-
};
|
|
50018
|
-
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
50019
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50020
|
-
if (!isRecord(packageJson)) return false;
|
|
50021
|
-
return [
|
|
50022
|
-
"dependencies",
|
|
50023
|
-
"devDependencies",
|
|
50024
|
-
"optionalDependencies"
|
|
50025
|
-
].some((fieldName) => {
|
|
50026
|
-
const dependencies = packageJson[fieldName];
|
|
50027
|
-
return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
50028
|
-
});
|
|
50029
|
-
};
|
|
50030
|
-
const packageHasRecordKey = (projectRoot, key) => {
|
|
50031
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50032
|
-
return isRecord(packageJson) && isRecord(packageJson[key]);
|
|
50033
|
-
};
|
|
50034
|
-
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
50035
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50036
|
-
if (!isRecord(packageJson)) return false;
|
|
50037
|
-
const value = packageJson[key];
|
|
50038
|
-
return isRecord(value) && isRecord(value[nestedKey]);
|
|
50039
|
-
};
|
|
50040
|
-
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
50041
|
-
//#endregion
|
|
50042
50153
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50043
50154
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
50044
50155
|
const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
|
|
@@ -50064,31 +50175,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
|
|
|
50064
50175
|
};
|
|
50065
50176
|
const hasDoctorScript = (projectRoot) => {
|
|
50066
50177
|
const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
|
|
50067
|
-
if (!isRecord(packageJson)) return false;
|
|
50178
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50068
50179
|
const scripts = packageJson.scripts;
|
|
50069
|
-
if (!isRecord(scripts)) return false;
|
|
50180
|
+
if (!isRecord$1(scripts)) return false;
|
|
50070
50181
|
return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
|
|
50071
50182
|
};
|
|
50072
50183
|
const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
|
|
50073
50184
|
const dependencies = packageJson[fieldName];
|
|
50074
|
-
return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50185
|
+
return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50075
50186
|
});
|
|
50076
50187
|
const installDoctorScript = (options) => {
|
|
50077
50188
|
const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
|
|
50078
50189
|
const packageJsonPath = getPackageJsonPath(packageDirectory);
|
|
50079
50190
|
const packageJson = readPackageJson(packageDirectory);
|
|
50080
|
-
if (!isRecord(packageJson)) return {
|
|
50191
|
+
if (!isRecord$1(packageJson)) return {
|
|
50081
50192
|
packageJsonPath,
|
|
50082
50193
|
scriptStatus: "skipped",
|
|
50083
50194
|
scriptReason: "missing-or-invalid-package-json"
|
|
50084
50195
|
};
|
|
50085
50196
|
const scripts = packageJson.scripts;
|
|
50086
50197
|
const scriptTarget = (() => {
|
|
50087
|
-
if (scripts !== void 0 && !isRecord(scripts)) return {
|
|
50198
|
+
if (scripts !== void 0 && !isRecord$1(scripts)) return {
|
|
50088
50199
|
status: "skipped",
|
|
50089
50200
|
reason: "invalid-scripts"
|
|
50090
50201
|
};
|
|
50091
|
-
const scriptRecord = isRecord(scripts) ? scripts : {};
|
|
50202
|
+
const scriptRecord = isRecord$1(scripts) ? scripts : {};
|
|
50092
50203
|
if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
|
|
50093
50204
|
scriptName: DOCTOR_SCRIPT_NAME,
|
|
50094
50205
|
status: "existing"
|
|
@@ -50122,7 +50233,7 @@ const installDoctorScript = (options) => {
|
|
|
50122
50233
|
if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
|
|
50123
50234
|
...packageJson,
|
|
50124
50235
|
scripts: {
|
|
50125
|
-
...isRecord(scripts) ? scripts : {},
|
|
50236
|
+
...isRecord$1(scripts) ? scripts : {},
|
|
50126
50237
|
[scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
|
|
50127
50238
|
}
|
|
50128
50239
|
});
|
|
@@ -50276,38 +50387,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50276
50387
|
//#region src/cli/utils/hash-project-root.ts
|
|
50277
50388
|
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50278
50389
|
//#endregion
|
|
50279
|
-
//#region src/cli/utils/
|
|
50280
|
-
const
|
|
50281
|
-
const
|
|
50282
|
-
|
|
50283
|
-
|
|
50284
|
-
});
|
|
50285
|
-
|
|
50286
|
-
|
|
50287
|
-
|
|
50288
|
-
|
|
50289
|
-
|
|
50290
|
-
|
|
50291
|
-
|
|
50292
|
-
};
|
|
50293
|
-
const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
|
|
50294
|
-
try {
|
|
50295
|
-
const store = getActionUpgradeStore(storeOptions);
|
|
50296
|
-
const upgrades = store.get("actionUpgrades", {});
|
|
50297
|
-
store.set("actionUpgrades", {
|
|
50298
|
-
...upgrades,
|
|
50299
|
-
[hashProjectRoot(projectRoot)]: {
|
|
50300
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
50301
|
-
outcome,
|
|
50302
|
-
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;
|
|
50303
50403
|
}
|
|
50304
|
-
}
|
|
50305
|
-
|
|
50306
|
-
|
|
50307
|
-
|
|
50308
|
-
|
|
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
|
+
};
|
|
50309
50422
|
};
|
|
50310
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
|
|
50311
50436
|
//#region src/cli/utils/open-url.ts
|
|
50312
50437
|
const resolveOpenCommand = (url) => {
|
|
50313
50438
|
if (process$1.platform === "darwin") return {
|
|
@@ -50936,13 +51061,13 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
50936
51061
|
const packageJsonPath = getPackageJsonPath(options.projectRoot);
|
|
50937
51062
|
const didHookExist = NFS.existsSync(packageJsonPath);
|
|
50938
51063
|
const packageJson = readPackageJson(options.projectRoot);
|
|
50939
|
-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
|
|
51064
|
+
const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
|
|
50940
51065
|
const parentKeys = strategy.path.slice(0, -1);
|
|
50941
51066
|
const leafKey = strategy.path[strategy.path.length - 1];
|
|
50942
51067
|
let parent = nextPackageJson;
|
|
50943
51068
|
for (const key of parentKeys) {
|
|
50944
51069
|
const existing = parent[key];
|
|
50945
|
-
const cloned = isRecord(existing) ? { ...existing } : {};
|
|
51070
|
+
const cloned = isRecord$1(existing) ? { ...existing } : {};
|
|
50946
51071
|
parent[key] = cloned;
|
|
50947
51072
|
parent = cloned;
|
|
50948
51073
|
}
|
|
@@ -51113,7 +51238,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
|
|
|
51113
51238
|
const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
|
|
51114
51239
|
const isSimpleGitHooksProject = (projectRoot) => {
|
|
51115
51240
|
const packageJson = readPackageJson(projectRoot);
|
|
51116
|
-
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"));
|
|
51117
51242
|
};
|
|
51118
51243
|
const getLefthookConfigPath = (projectRoot) => {
|
|
51119
51244
|
for (const fileName of LEFTHOOK_CONFIG_FILES) {
|
|
@@ -51279,7 +51404,7 @@ const detectPackageManager = (projectRoot) => {
|
|
|
51279
51404
|
let currentDirectory = Path.resolve(projectRoot);
|
|
51280
51405
|
while (true) {
|
|
51281
51406
|
const packageJson = readPackageJson(currentDirectory);
|
|
51282
|
-
if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51407
|
+
if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51283
51408
|
const packageManagerName = packageJson.packageManager.split("@")[0];
|
|
51284
51409
|
if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
|
|
51285
51410
|
}
|
|
@@ -51355,12 +51480,12 @@ const isSupplyChainTrustError = (error) => {
|
|
|
51355
51480
|
const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
|
|
51356
51481
|
const installReactDoctorDependency = async (options) => {
|
|
51357
51482
|
const packageJson = readPackageJson(options.projectRoot);
|
|
51358
|
-
if (!isRecord(packageJson)) return {
|
|
51483
|
+
if (!isRecord$1(packageJson)) return {
|
|
51359
51484
|
dependencyStatus: "skipped",
|
|
51360
51485
|
dependencyReason: "missing-or-invalid-package-json"
|
|
51361
51486
|
};
|
|
51362
51487
|
if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
|
|
51363
|
-
if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
|
|
51488
|
+
if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
|
|
51364
51489
|
dependencyStatus: "skipped",
|
|
51365
51490
|
dependencyReason: "invalid-dev-dependencies"
|
|
51366
51491
|
};
|
|
@@ -51524,10 +51649,12 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51524
51649
|
const existingWorkflow = readReactDoctorWorkflow(projectRoot);
|
|
51525
51650
|
const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
|
|
51526
51651
|
const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
|
|
51527
|
-
const
|
|
51652
|
+
const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
|
|
51653
|
+
const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
|
|
51528
51654
|
const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
|
|
51529
51655
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51530
51656
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51657
|
+
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
51531
51658
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51532
51659
|
type: "multiselect",
|
|
51533
51660
|
name: "agents",
|
|
@@ -51774,18 +51901,24 @@ const handoffToAgent = async (input) => {
|
|
|
51774
51901
|
if (!input.interactive || input.diagnostics.length === 0) return;
|
|
51775
51902
|
cliLogger.break();
|
|
51776
51903
|
const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
|
|
51777
|
-
|
|
51904
|
+
const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
|
|
51905
|
+
if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
|
|
51778
51906
|
const ciOutcome = await askAddToGitHubActions();
|
|
51779
51907
|
recordCount(METRIC.agentHandoff, 1, {
|
|
51780
51908
|
outcome: `ci-${ciOutcome}`,
|
|
51781
51909
|
diagnosticsCount: input.diagnostics.length
|
|
51782
51910
|
});
|
|
51783
51911
|
if (ciOutcome === "cancel") return;
|
|
51912
|
+
recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
|
|
51784
51913
|
if (ciOutcome === "yes") {
|
|
51785
51914
|
await setUpGitHubActions({ rootDirectory: input.rootDirectory });
|
|
51786
51915
|
cliLogger.break();
|
|
51787
51916
|
}
|
|
51788
|
-
} 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
|
+
});
|
|
51789
51922
|
const { handoffTarget } = await prompts({
|
|
51790
51923
|
type: "select",
|
|
51791
51924
|
name: "handoffTarget",
|
|
@@ -52091,7 +52224,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52091
52224
|
yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
|
|
52092
52225
|
if (displayDiagnostics.length > 0) {
|
|
52093
52226
|
yield* log("");
|
|
52094
|
-
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));
|
|
52095
52228
|
}
|
|
52096
52229
|
const lowestScoredScan = findLowestScoredScan(completedScans);
|
|
52097
52230
|
const aggregateScore = lowestScoredScan?.result.score ?? null;
|
|
@@ -52129,9 +52262,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52129
52262
|
});
|
|
52130
52263
|
//#endregion
|
|
52131
52264
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52132
|
-
const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
|
|
52133
52265
|
const getSetupPromptStore = (options = {}) => new Conf({
|
|
52134
|
-
projectName:
|
|
52266
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
52135
52267
|
cwd: options.cwd
|
|
52136
52268
|
});
|
|
52137
52269
|
const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
|
|
@@ -52142,6 +52274,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
52142
52274
|
return false;
|
|
52143
52275
|
}
|
|
52144
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
|
+
};
|
|
52145
52295
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52146
52296
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52147
52297
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52548,6 +52698,14 @@ const runExplain = async (fileLineArgument, context) => {
|
|
|
52548
52698
|
const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
|
|
52549
52699
|
const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
|
|
52550
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, " "));
|
|
52551
52709
|
if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
|
|
52552
52710
|
if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
|
|
52553
52711
|
cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
|
|
@@ -52944,6 +53102,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
52944
53102
|
})) {
|
|
52945
53103
|
printAgentInstallHint();
|
|
52946
53104
|
recordCount(METRIC.agentInstallHintShown, 1);
|
|
53105
|
+
disableSetupPrompt(setupProjectRoot);
|
|
52947
53106
|
}
|
|
52948
53107
|
}
|
|
52949
53108
|
} catch (error) {
|
|
@@ -53951,4 +54110,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
53951
54110
|
export {};
|
|
53952
54111
|
|
|
53953
54112
|
//# sourceMappingURL=cli.js.map
|
|
53954
|
-
//# debugId=
|
|
54113
|
+
//# debugId=a06f0514-b1fa-5452-9c19-0140438862f8
|