react-doctor 0.5.6-dev.451beeb → 0.5.6-dev.50999f4
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 +468 -260
- package/dist/index.js +94 -70
- package/dist/lsp.js +113 -92
- package/package.json +2 -2
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]="496f49cf-4038-5332-b301-ffedc55210ba")}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";
|
|
@@ -35893,6 +35893,7 @@ const isLargeMinifiedFile = (absolutePath) => {
|
|
|
35893
35893
|
if (sizeBytes < 2e4) return false;
|
|
35894
35894
|
return isMinifiedSource(absolutePath);
|
|
35895
35895
|
};
|
|
35896
|
+
const isErrnoException = (error) => error instanceof Error && "code" in error;
|
|
35896
35897
|
const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
35897
35898
|
"EACCES",
|
|
35898
35899
|
"EPERM",
|
|
@@ -35902,11 +35903,7 @@ const IGNORABLE_READDIR_ERROR_CODES = new Set([
|
|
|
35902
35903
|
"ELOOP",
|
|
35903
35904
|
"ENAMETOOLONG"
|
|
35904
35905
|
]);
|
|
35905
|
-
const isIgnorableReaddirError = (error) =>
|
|
35906
|
-
if (typeof error !== "object" || error === null) return false;
|
|
35907
|
-
const errorCode = error.code;
|
|
35908
|
-
return typeof errorCode === "string" && IGNORABLE_READDIR_ERROR_CODES.has(errorCode);
|
|
35909
|
-
};
|
|
35906
|
+
const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
|
|
35910
35907
|
const readDirectoryEntries = (directoryPath) => {
|
|
35911
35908
|
try {
|
|
35912
35909
|
return NFS.readdirSync(directoryPath, { withFileTypes: true });
|
|
@@ -35953,7 +35950,7 @@ const readPackageJsonUncached = (packageJsonPath) => {
|
|
|
35953
35950
|
return JSON.parse(NFS.readFileSync(packageJsonPath, "utf-8"));
|
|
35954
35951
|
} catch (error) {
|
|
35955
35952
|
if (error instanceof SyntaxError) return {};
|
|
35956
|
-
if (error
|
|
35953
|
+
if (isErrnoException(error)) {
|
|
35957
35954
|
const { code } = error;
|
|
35958
35955
|
if (code === "EISDIR" || code === "EACCES" || code === "EPERM" || code === "ENOENT") return {};
|
|
35959
35956
|
}
|
|
@@ -36678,17 +36675,13 @@ const isPackageJsonReactNativeAware = (packageJson) => {
|
|
|
36678
36675
|
return false;
|
|
36679
36676
|
};
|
|
36680
36677
|
const hasReactNativeWorkspaceAnywhere = (rootDirectory, rootPackageJson) => someWorkspacePackageJson(rootDirectory, rootPackageJson, isPackageJsonReactNativeAware);
|
|
36681
|
-
const
|
|
36682
|
-
const spec = packageJson.dependencies?.
|
|
36678
|
+
const getDependencySpec = (packageJson, packageName) => {
|
|
36679
|
+
const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName] ?? packageJson.peerDependencies?.[packageName] ?? packageJson.optionalDependencies?.[packageName];
|
|
36683
36680
|
return typeof spec === "string" ? spec : null;
|
|
36684
36681
|
};
|
|
36685
|
-
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson,
|
|
36682
|
+
const findExpoVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "expo"));
|
|
36686
36683
|
const SHOPIFY_FLASH_LIST_PACKAGE_NAME = "@shopify/flash-list";
|
|
36687
|
-
const
|
|
36688
|
-
const spec = packageJson.dependencies?.["@shopify/flash-list"] ?? packageJson.devDependencies?.["@shopify/flash-list"] ?? packageJson.peerDependencies?.["@shopify/flash-list"] ?? packageJson.optionalDependencies?.["@shopify/flash-list"];
|
|
36689
|
-
return typeof spec === "string" ? spec : null;
|
|
36690
|
-
};
|
|
36691
|
-
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getShopifyFlashListDependencySpec);
|
|
36684
|
+
const findShopifyFlashListVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, SHOPIFY_FLASH_LIST_PACKAGE_NAME));
|
|
36692
36685
|
const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, version }) => {
|
|
36693
36686
|
if (version === null || !isCatalogReference(version)) return version;
|
|
36694
36687
|
const catalogName = extractCatalogName(version);
|
|
@@ -36700,11 +36693,7 @@ const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson,
|
|
|
36700
36693
|
if (!isFile(monorepoPackageJsonPath)) return version;
|
|
36701
36694
|
return resolveCatalogVersion(readPackageJson$1(monorepoPackageJsonPath), packageName, monorepoRoot, catalogName) ?? version;
|
|
36702
36695
|
};
|
|
36703
|
-
const
|
|
36704
|
-
const spec = packageJson.dependencies?.next ?? packageJson.devDependencies?.next ?? packageJson.peerDependencies?.next ?? packageJson.optionalDependencies?.next;
|
|
36705
|
-
return typeof spec === "string" ? spec : null;
|
|
36706
|
-
};
|
|
36707
|
-
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, getNextjsDependencySpec);
|
|
36696
|
+
const findNextjsVersion = (rootDirectory, rootPackageJson) => findInWorkspacePackageJsons(rootDirectory, rootPackageJson, (packageJson) => getDependencySpec(packageJson, "next"));
|
|
36708
36697
|
const getPreactVersion = (packageJson) => {
|
|
36709
36698
|
return {
|
|
36710
36699
|
...packageJson.peerDependencies,
|
|
@@ -36793,6 +36782,11 @@ const ES_TARGET_YEAR_BY_NAME = {
|
|
|
36793
36782
|
esnext: 9999
|
|
36794
36783
|
};
|
|
36795
36784
|
/**
|
|
36785
|
+
* tsconfig filenames probed when resolving a project's TypeScript
|
|
36786
|
+
* compiler options — the root config first, then a monorepo base config.
|
|
36787
|
+
*/
|
|
36788
|
+
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
36789
|
+
/**
|
|
36796
36790
|
* Project-config files that `StagedFiles.materialize` copies into
|
|
36797
36791
|
* the temp directory alongside staged sources so oxlint resolves
|
|
36798
36792
|
* `tsconfig` / `package.json` / lint configs the same way it would
|
|
@@ -37314,6 +37308,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
37314
37308
|
if (detected.major !== required.major) return detected.major > required.major;
|
|
37315
37309
|
return detected.minor >= required.minor;
|
|
37316
37310
|
};
|
|
37311
|
+
const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
|
|
37317
37312
|
var InvalidGlobPatternError = class extends Error {
|
|
37318
37313
|
pattern;
|
|
37319
37314
|
reason;
|
|
@@ -37342,7 +37337,7 @@ const compileGlobPattern = (rawPattern) => {
|
|
|
37342
37337
|
try {
|
|
37343
37338
|
return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
|
|
37344
37339
|
} catch (caughtError) {
|
|
37345
|
-
throw new InvalidGlobPatternError(rawPattern,
|
|
37340
|
+
throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
|
|
37346
37341
|
}
|
|
37347
37342
|
};
|
|
37348
37343
|
const compileGlobPatternsLenient = (patterns, onInvalid) => {
|
|
@@ -38520,7 +38515,6 @@ const PACKAGE_JSON_FILENAME = "package.json";
|
|
|
38520
38515
|
const PACKAGE_JSON_CONFIG_KEY$1 = "reactDoctor";
|
|
38521
38516
|
const LEGACY_CONFIG_FILENAME = "react-doctor.config.json";
|
|
38522
38517
|
const jiti = createJiti(import.meta.url);
|
|
38523
|
-
const formatError = (error) => error instanceof Error ? error.message : String(error);
|
|
38524
38518
|
const importDefaultExport = async (jitiInstance, filePath) => {
|
|
38525
38519
|
const imported = await jitiInstance.import(filePath);
|
|
38526
38520
|
return imported?.default ?? imported;
|
|
@@ -38552,7 +38546,7 @@ const loadModuleConfig = async (filePath) => {
|
|
|
38552
38546
|
try {
|
|
38553
38547
|
return await importDefaultExport(aliasJiti, filePath);
|
|
38554
38548
|
} catch (retryError) {
|
|
38555
|
-
throw new Error(`${
|
|
38549
|
+
throw new Error(`${messageFromUnknown(error)} (retry with ${SELF_PACKAGE_IMPORT_SPECIFIER} aliased to the running react-doctor package also failed: ${messageFromUnknown(retryError)})`, { cause: retryError });
|
|
38556
38550
|
}
|
|
38557
38551
|
}
|
|
38558
38552
|
};
|
|
@@ -38601,7 +38595,7 @@ const loadLegacyConfig = (directory) => {
|
|
|
38601
38595
|
}
|
|
38602
38596
|
warn(`${LEGACY_CONFIG_FILENAME} must contain an object, ignoring.`);
|
|
38603
38597
|
} catch (error) {
|
|
38604
|
-
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${
|
|
38598
|
+
warn(`Failed to load ${LEGACY_CONFIG_FILENAME}: ${messageFromUnknown(error)}`);
|
|
38605
38599
|
}
|
|
38606
38600
|
return {
|
|
38607
38601
|
status: "invalid",
|
|
@@ -38628,7 +38622,7 @@ const loadConfigFromDirectory = async (directory) => {
|
|
|
38628
38622
|
warn(`${CONFIG_BASENAME}.${extension} must export an object, ignoring.`);
|
|
38629
38623
|
sawBrokenConfigFile = true;
|
|
38630
38624
|
} catch (error) {
|
|
38631
|
-
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${
|
|
38625
|
+
warn(`Failed to load ${CONFIG_BASENAME}.${extension}: ${messageFromUnknown(error)}`);
|
|
38632
38626
|
sawBrokenConfigFile = true;
|
|
38633
38627
|
}
|
|
38634
38628
|
}
|
|
@@ -39903,7 +39897,7 @@ const readIgnoreFile = (filePath) => {
|
|
|
39903
39897
|
try {
|
|
39904
39898
|
content = NFS.readFileSync(filePath, "utf-8");
|
|
39905
39899
|
} catch (error) {
|
|
39906
|
-
const errnoCode = error
|
|
39900
|
+
const errnoCode = isErrnoException(error) ? error.code : void 0;
|
|
39907
39901
|
if (errnoCode && errnoCode !== "ENOENT") runSync(warn$1(`Could not read ignore file ${filePath}: ${errnoCode}`));
|
|
39908
39902
|
return [];
|
|
39909
39903
|
}
|
|
@@ -39941,8 +39935,8 @@ const collectIgnorePatterns = (rootDirectory) => {
|
|
|
39941
39935
|
cachedPatternsByRoot.set(rootDirectory, patterns);
|
|
39942
39936
|
return patterns;
|
|
39943
39937
|
};
|
|
39938
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39944
39939
|
const KNIP_JSON_FILENAME = "knip.json";
|
|
39945
|
-
const isRecord$1$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39946
39940
|
const readJsonFileSafe = (filePath) => {
|
|
39947
39941
|
let rawContents;
|
|
39948
39942
|
try {
|
|
@@ -39958,10 +39952,10 @@ const readJsonFileSafe = (filePath) => {
|
|
|
39958
39952
|
};
|
|
39959
39953
|
const readKnipConfig = (rootDirectory) => {
|
|
39960
39954
|
const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
|
|
39961
|
-
if (isRecord$
|
|
39955
|
+
if (isRecord$2(knipJson)) return knipJson;
|
|
39962
39956
|
const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
|
|
39963
|
-
const packageKnipConfig = isRecord$
|
|
39964
|
-
return isRecord$
|
|
39957
|
+
const packageKnipConfig = isRecord$2(packageJson) ? packageJson.knip : null;
|
|
39958
|
+
return isRecord$2(packageKnipConfig) ? packageKnipConfig : null;
|
|
39965
39959
|
};
|
|
39966
39960
|
const normalizePatternList = (value) => {
|
|
39967
39961
|
if (typeof value === "string" && value.length > 0) return [value];
|
|
@@ -39973,10 +39967,10 @@ const prefixWorkspacePatterns = (workspacePattern, patterns) => {
|
|
|
39973
39967
|
return patterns.map((pattern) => pattern.startsWith("!") ? `!${normalizedWorkspacePattern}/${pattern.slice(1)}` : `${normalizedWorkspacePattern}/${pattern}`);
|
|
39974
39968
|
};
|
|
39975
39969
|
const collectKnipWorkspacePatterns = (workspaces, settingName) => {
|
|
39976
|
-
if (!isRecord$
|
|
39970
|
+
if (!isRecord$2(workspaces)) return [];
|
|
39977
39971
|
const patterns = [];
|
|
39978
39972
|
for (const [workspacePattern, workspaceConfig] of Object.entries(workspaces)) {
|
|
39979
|
-
if (!isRecord$
|
|
39973
|
+
if (!isRecord$2(workspaceConfig)) continue;
|
|
39980
39974
|
patterns.push(...prefixWorkspacePatterns(workspacePattern, normalizePatternList(workspaceConfig[settingName])));
|
|
39981
39975
|
}
|
|
39982
39976
|
return patterns;
|
|
@@ -40021,8 +40015,6 @@ const toCanonicalPath = (filePath) => {
|
|
|
40021
40015
|
};
|
|
40022
40016
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40023
40017
|
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
40024
|
-
const TSCONFIG_FILENAMES$1 = ["tsconfig.json", "tsconfig.base.json"];
|
|
40025
|
-
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40026
40018
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40027
40019
|
const inputChunks = [];
|
|
40028
40020
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40080,7 +40072,7 @@ process.stdin.on("end", () => {
|
|
|
40080
40072
|
});
|
|
40081
40073
|
`;
|
|
40082
40074
|
const resolveTsConfigPath = (rootDirectory) => {
|
|
40083
|
-
for (const filename of TSCONFIG_FILENAMES
|
|
40075
|
+
for (const filename of TSCONFIG_FILENAMES) {
|
|
40084
40076
|
const candidate = Path.join(rootDirectory, filename);
|
|
40085
40077
|
if (NFS.existsSync(candidate)) return candidate;
|
|
40086
40078
|
}
|
|
@@ -40461,15 +40453,13 @@ var DeadCode = class DeadCode extends Service()("react-doctor/DeadCode") {
|
|
|
40461
40453
|
})()) }));
|
|
40462
40454
|
static layerOf = (diagnostics) => succeed$3(DeadCode, DeadCode.of({ run: () => fromIterable$1(diagnostics) }));
|
|
40463
40455
|
};
|
|
40464
|
-
const createNodeReadFileLinesSync = (rootDirectory) => {
|
|
40465
|
-
|
|
40466
|
-
|
|
40467
|
-
|
|
40468
|
-
|
|
40469
|
-
|
|
40470
|
-
|
|
40471
|
-
}
|
|
40472
|
-
};
|
|
40456
|
+
const createNodeReadFileLinesSync = (rootDirectory) => (filePath) => {
|
|
40457
|
+
const absolutePath = Path.isAbsolute(filePath) ? filePath : Path.join(rootDirectory, filePath);
|
|
40458
|
+
try {
|
|
40459
|
+
return NFS.readFileSync(absolutePath, "utf-8").split("\n");
|
|
40460
|
+
} catch {
|
|
40461
|
+
return null;
|
|
40462
|
+
}
|
|
40473
40463
|
};
|
|
40474
40464
|
var Files = class Files extends Service()("react-doctor/Files") {
|
|
40475
40465
|
static layerNode = succeed$3(Files, Files.of({
|
|
@@ -40680,7 +40670,10 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40680
40670
|
directory: input.directory,
|
|
40681
40671
|
cause
|
|
40682
40672
|
}) });
|
|
40683
|
-
})
|
|
40673
|
+
}), withSpan("git.exec", { attributes: {
|
|
40674
|
+
"git.command": input.command,
|
|
40675
|
+
"git.subcommand": input.args[0] ?? ""
|
|
40676
|
+
} }));
|
|
40684
40677
|
const runGit = (directory, args) => runCommand({
|
|
40685
40678
|
command: "git",
|
|
40686
40679
|
args,
|
|
@@ -40708,7 +40701,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40708
40701
|
]);
|
|
40709
40702
|
if (candidates.status !== 0) return null;
|
|
40710
40703
|
return trimOrNull(candidates.stdout.split("\n")[0] ?? "");
|
|
40711
|
-
});
|
|
40704
|
+
}).pipe(withSpan("Git.defaultBranch"));
|
|
40712
40705
|
const branchExists = (directory, branch) => runGit(directory, [
|
|
40713
40706
|
"rev-parse",
|
|
40714
40707
|
"--verify",
|
|
@@ -40755,7 +40748,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40755
40748
|
const result = resultOption.value;
|
|
40756
40749
|
if (result.status !== 0) return null;
|
|
40757
40750
|
return parseGithubViewerPermission(result.stdout);
|
|
40758
|
-
}).pipe(catch_$1(() => succeed$2(null)));
|
|
40751
|
+
}).pipe(catch_$1(() => succeed$2(null)), withSpan("Git.githubViewerPermission"));
|
|
40759
40752
|
/**
|
|
40760
40753
|
* Resolves a `--diff A..B` / `A...B` commit range into a changed-file
|
|
40761
40754
|
* selection. Each endpoint is validated with `isSafeGitRevision`
|
|
@@ -40869,7 +40862,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40869
40862
|
changedFiles: splitNullSeparated(diff.stdout),
|
|
40870
40863
|
isCurrentChanges: false
|
|
40871
40864
|
};
|
|
40872
|
-
}),
|
|
40865
|
+
}).pipe(withSpan("Git.diffSelection")),
|
|
40873
40866
|
stagedFilePaths: (directory) => runGit(directory, [
|
|
40874
40867
|
"diff",
|
|
40875
40868
|
"--cached",
|
|
@@ -40911,7 +40904,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40911
40904
|
status: result.status,
|
|
40912
40905
|
stdout: result.stdout
|
|
40913
40906
|
};
|
|
40914
|
-
}),
|
|
40907
|
+
}).pipe(withSpan("Git.grep")),
|
|
40915
40908
|
changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
|
|
40916
40909
|
if (files.length === 0) return [];
|
|
40917
40910
|
if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
|
|
@@ -40927,7 +40920,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
40927
40920
|
]);
|
|
40928
40921
|
if (result.status !== 0) return null;
|
|
40929
40922
|
return parseChangedLineRanges(result.stdout);
|
|
40930
|
-
})
|
|
40923
|
+
}).pipe(withSpan("Git.changedLineRanges"))
|
|
40931
40924
|
});
|
|
40932
40925
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
40933
40926
|
/**
|
|
@@ -41142,7 +41135,7 @@ const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
|
|
|
41142
41135
|
for (const [absolutePath, originalContent] of originalContents) try {
|
|
41143
41136
|
NFS.writeFileSync(absolutePath, originalContent);
|
|
41144
41137
|
} catch (error) {
|
|
41145
|
-
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${
|
|
41138
|
+
process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
|
|
41146
41139
|
}
|
|
41147
41140
|
};
|
|
41148
41141
|
const onExit = () => restore();
|
|
@@ -41248,7 +41241,7 @@ const resolveUserPlugin = (spec, configSourceDirectory) => {
|
|
|
41248
41241
|
try {
|
|
41249
41242
|
resolvedSpecifier = isRelative ? Path.resolve(configSourceDirectory, spec) : candidateRequire.resolve(spec);
|
|
41250
41243
|
} catch (error) {
|
|
41251
|
-
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${
|
|
41244
|
+
warnConfigIssue(`config.plugins entry "${spec}" could not be resolved from ${configSourceDirectory}: ${messageFromUnknown(error)}`);
|
|
41252
41245
|
return null;
|
|
41253
41246
|
}
|
|
41254
41247
|
const { name, ruleNames } = readPluginShape(resolvedSpecifier, (target) => candidateRequire(target));
|
|
@@ -41320,8 +41313,8 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
|
|
|
41320
41313
|
}
|
|
41321
41314
|
return enabled;
|
|
41322
41315
|
};
|
|
41323
|
-
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [] }) => {
|
|
41324
|
-
const reactHooksJsPlugin = resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41316
|
+
const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false }) => {
|
|
41317
|
+
const reactHooksJsPlugin = disableReactHooksJsPlugin ? null : resolveReactHooksJsPlugin(project.hasReactCompiler, customRulesOnly);
|
|
41325
41318
|
const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
|
|
41326
41319
|
const jsPlugins = [];
|
|
41327
41320
|
if (reactHooksJsPlugin) jsPlugins.push(reactHooksJsPlugin.entry);
|
|
@@ -41381,7 +41374,6 @@ const resolveOxlintBinary = () => {
|
|
|
41381
41374
|
return Path.join(oxlintPackageDirectory, "bin", "oxlint");
|
|
41382
41375
|
};
|
|
41383
41376
|
const resolvePluginPath = () => esmRequire.resolve("oxlint-plugin-react-doctor");
|
|
41384
|
-
const TSCONFIG_FILENAMES = ["tsconfig.json", "tsconfig.base.json"];
|
|
41385
41377
|
const resolveTsConfigRelativePath = (rootDirectory) => {
|
|
41386
41378
|
for (const filename of TSCONFIG_FILENAMES) if (NFS.existsSync(Path.join(rootDirectory, filename))) return `./${filename}`;
|
|
41387
41379
|
return null;
|
|
@@ -41753,7 +41745,7 @@ const scopeContainsNonImportBinding = (node, scopeNode, identifierName) => {
|
|
|
41753
41745
|
const isIdentifierShadowedByLocalBinding = (identifier, sourceFile) => {
|
|
41754
41746
|
let currentNode = identifier.parent;
|
|
41755
41747
|
while (currentNode) {
|
|
41756
|
-
if (
|
|
41748
|
+
if (isScopeBoundary(currentNode)) {
|
|
41757
41749
|
if (scopeContainsNonImportBinding(currentNode, currentNode, identifier.text)) return true;
|
|
41758
41750
|
}
|
|
41759
41751
|
if (currentNode === sourceFile) return false;
|
|
@@ -41844,11 +41836,10 @@ const findResolutionInScope = (scopeNode, identifierName, reactImportBindings, s
|
|
|
41844
41836
|
});
|
|
41845
41837
|
return resolution;
|
|
41846
41838
|
};
|
|
41847
|
-
const isScopeNode = isScopeBoundary;
|
|
41848
41839
|
const resolveIdentifierBinding = (identifier, reactImportBindings, sourceFile, visitedDeclarations = /* @__PURE__ */ new Set()) => {
|
|
41849
41840
|
let currentNode = identifier.parent;
|
|
41850
41841
|
while (currentNode) {
|
|
41851
|
-
if (
|
|
41842
|
+
if (isScopeBoundary(currentNode)) {
|
|
41852
41843
|
const resolution = findResolutionInScope(currentNode, identifier.text, reactImportBindings, sourceFile, visitedDeclarations);
|
|
41853
41844
|
if (resolution) return resolution;
|
|
41854
41845
|
}
|
|
@@ -42018,9 +42009,9 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
|
|
|
42018
42009
|
try {
|
|
42019
42010
|
parsed = JSON.parse(sanitizedStdout);
|
|
42020
42011
|
} catch {
|
|
42021
|
-
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42012
|
+
throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42022
42013
|
}
|
|
42023
|
-
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0,
|
|
42014
|
+
if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
|
|
42024
42015
|
const minifiedFileCache = /* @__PURE__ */ new Map();
|
|
42025
42016
|
const isMinifiedDiagnosticFile = (filename) => {
|
|
42026
42017
|
const absolutePath = Path.isAbsolute(filename) ? filename : Path.resolve(rootDirectory || ".", filename);
|
|
@@ -42096,7 +42087,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
|
|
|
42096
42087
|
child.kill("SIGKILL");
|
|
42097
42088
|
reject(new ReactDoctorError({ reason: new OxlintBatchExceeded({
|
|
42098
42089
|
kind: "timeout",
|
|
42099
|
-
detail: `${spawnTimeoutMs /
|
|
42090
|
+
detail: `${spawnTimeoutMs / MILLISECONDS_PER_SECOND}s budget exceeded`
|
|
42100
42091
|
}) }));
|
|
42101
42092
|
}, spawnTimeoutMs);
|
|
42102
42093
|
timeoutHandle.unref?.();
|
|
@@ -42311,6 +42302,28 @@ const writeOxlintConfig = (configPath, configToWrite) => {
|
|
|
42311
42302
|
NFS.closeSync(fileHandle);
|
|
42312
42303
|
}
|
|
42313
42304
|
};
|
|
42305
|
+
const REACT_HOOKS_JS_DROP_PREFIX = "React Compiler rules (react-hooks-js/*) skipped — eslint-plugin-react-hooks failed to load in this environment";
|
|
42306
|
+
/**
|
|
42307
|
+
* Detects an oxlint config-load crash caused by the optional
|
|
42308
|
+
* `react-hooks-js` (eslint-plugin-react-hooks) React Compiler plugin and
|
|
42309
|
+
* builds the partial-failure note for it; returns `null` when the failure
|
|
42310
|
+
* was anything else.
|
|
42311
|
+
*
|
|
42312
|
+
* oxlint prints a framed error to stdout (not stderr) and exits non-zero
|
|
42313
|
+
* when a `jsPlugins` entry can't be imported; that non-JSON stdout
|
|
42314
|
+
* surfaces as `OxlintOutputUnparseable`. Because oxlint fails the WHOLE
|
|
42315
|
+
* config load on it, leaving the plugin in would drop every curated
|
|
42316
|
+
* react-doctor diagnostic too — so the caller retries with the plugin
|
|
42317
|
+
* stripped (issue #833). Both markers sit at the start of oxlint's
|
|
42318
|
+
* message, so they survive the `preview` slice even for deep pnpm paths.
|
|
42319
|
+
*/
|
|
42320
|
+
const reactHooksJsPluginDropNote = (error) => {
|
|
42321
|
+
if (!(error instanceof ReactDoctorError) || error.reason._tag !== "OxlintOutputUnparseable") return null;
|
|
42322
|
+
const { preview } = error.reason;
|
|
42323
|
+
if (!preview.includes("Failed to load JS plugin") || !preview.includes("eslint-plugin-react-hooks")) return null;
|
|
42324
|
+
const underlyingReason = preview.match(/Error:[^\n]*/)?.[0]?.trim();
|
|
42325
|
+
return `${REACT_HOOKS_JS_DROP_PREFIX}${underlyingReason ? `: ${underlyingReason}` : ""}. Other rules ran normally.`;
|
|
42326
|
+
};
|
|
42314
42327
|
/**
|
|
42315
42328
|
* The oxlint runner. Composed of three pieces in `runners/oxlint/`:
|
|
42316
42329
|
*
|
|
@@ -42338,15 +42351,16 @@ const runOxlint = async (options) => {
|
|
|
42338
42351
|
const pluginPath = resolvePluginPath();
|
|
42339
42352
|
const extendsPaths = (adoptExistingLintConfig && !customRulesOnly ? detectUserLintConfigPaths(rootDirectory) : []).filter(canOxlintExtendConfig);
|
|
42340
42353
|
const userPlugins = resolveUserPlugins(userConfig?.plugins, configSourceDirectory);
|
|
42341
|
-
const buildConfig = (
|
|
42354
|
+
const buildConfig = (overrides) => createOxlintConfig({
|
|
42342
42355
|
pluginPath,
|
|
42343
42356
|
project,
|
|
42344
42357
|
customRulesOnly,
|
|
42345
|
-
extendsPaths:
|
|
42358
|
+
extendsPaths: overrides.extendsPaths,
|
|
42346
42359
|
ignoredTags,
|
|
42347
42360
|
serverAuthFunctionNames,
|
|
42348
42361
|
severityControls,
|
|
42349
|
-
userPlugins
|
|
42362
|
+
userPlugins,
|
|
42363
|
+
disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin
|
|
42350
42364
|
});
|
|
42351
42365
|
const restoreDisableDirectives = respectInlineDisables ? () => {} : await neutralizeDisableDirectives(rootDirectory, includePaths);
|
|
42352
42366
|
const configDirectory = NFS.mkdtempSync(Path.join(os.tmpdir(), "react-doctor-oxlintrc-"));
|
|
@@ -42382,12 +42396,22 @@ const runOxlint = async (options) => {
|
|
|
42382
42396
|
outputMaxBytes,
|
|
42383
42397
|
concurrency: options.concurrency
|
|
42384
42398
|
});
|
|
42385
|
-
writeOxlintConfig(configPath, buildConfig(extendsPaths));
|
|
42399
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths }));
|
|
42386
42400
|
try {
|
|
42387
42401
|
return await runBatches();
|
|
42388
42402
|
} catch (error) {
|
|
42403
|
+
const reactHooksJsDropNote = reactHooksJsPluginDropNote(error);
|
|
42404
|
+
if (reactHooksJsDropNote !== null) {
|
|
42405
|
+
writeOxlintConfig(configPath, buildConfig({
|
|
42406
|
+
extendsPaths,
|
|
42407
|
+
disableReactHooksJsPlugin: true
|
|
42408
|
+
}));
|
|
42409
|
+
const diagnostics = await runBatches();
|
|
42410
|
+
onPartialFailure?.(reactHooksJsDropNote);
|
|
42411
|
+
return diagnostics;
|
|
42412
|
+
}
|
|
42389
42413
|
if (extendsPaths.length === 0) throw error;
|
|
42390
|
-
writeOxlintConfig(configPath, buildConfig([]));
|
|
42414
|
+
writeOxlintConfig(configPath, buildConfig({ extendsPaths: [] }));
|
|
42391
42415
|
return await runBatches();
|
|
42392
42416
|
}
|
|
42393
42417
|
} finally {
|
|
@@ -43185,7 +43209,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43185
43209
|
}))))))));
|
|
43186
43210
|
const deadCodeFailureState = yield* get$2(deadCodeFailure);
|
|
43187
43211
|
const scanElapsedMilliseconds = Date.now() - scanStartTime;
|
|
43188
|
-
const scanElapsedSeconds = (scanElapsedMilliseconds /
|
|
43212
|
+
const scanElapsedSeconds = (scanElapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1);
|
|
43189
43213
|
if (!lintFailureState.didFail) if (deadCodeFailureState.didFail) yield* scanProgress.fail(DEAD_CODE_FAIL_TEXT);
|
|
43190
43214
|
else if (input.suppressScanSummary) yield* scanProgress.stop();
|
|
43191
43215
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
@@ -43422,7 +43446,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43422
43446
|
static layerNode = effect(StagedFiles, gen(function* () {
|
|
43423
43447
|
const git = yield* Git;
|
|
43424
43448
|
return StagedFiles.of({
|
|
43425
|
-
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile))),
|
|
43449
|
+
discoverSourceFiles: (directory) => git.stagedFilePaths(directory).pipe(map$3((entries) => entries.filter(isLintableSourceFile)), withSpan("StagedFiles.discoverSourceFiles")),
|
|
43426
43450
|
materialize: ({ directory, stagedFiles, tempDirectory }) => materializeSourceTree({
|
|
43427
43451
|
directory,
|
|
43428
43452
|
files: stagedFiles,
|
|
@@ -43432,7 +43456,7 @@ var StagedFiles = class StagedFiles extends Service()("react-doctor/StagedFiles"
|
|
|
43432
43456
|
tempDirectory: tree.tempDirectory,
|
|
43433
43457
|
stagedFiles: tree.materializedFiles,
|
|
43434
43458
|
cleanup: tree.cleanup
|
|
43435
|
-
})))
|
|
43459
|
+
})), withSpan("StagedFiles.materialize"))
|
|
43436
43460
|
});
|
|
43437
43461
|
}));
|
|
43438
43462
|
/**
|
|
@@ -43839,7 +43863,7 @@ const FALSY_CI_FLAG_VALUES = new Set([
|
|
|
43839
43863
|
"false"
|
|
43840
43864
|
]);
|
|
43841
43865
|
const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
|
|
43842
|
-
const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(
|
|
43866
|
+
const isCiEnvironment = (env = process.env) => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(env[environmentVariable])) || isCiFlagSet(env.CI);
|
|
43843
43867
|
const detectCiProvider = () => {
|
|
43844
43868
|
for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
|
|
43845
43869
|
return isCiFlagSet(process.env.CI) ? "unknown" : null;
|
|
@@ -43864,6 +43888,53 @@ const detectCodingAgent = () => {
|
|
|
43864
43888
|
const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
|
|
43865
43889
|
const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
|
|
43866
43890
|
//#endregion
|
|
43891
|
+
//#region src/cli/utils/detect-terminal-kind.ts
|
|
43892
|
+
const TERMINAL_BY_TERM_PROGRAM = [
|
|
43893
|
+
["vscode", "vscode"],
|
|
43894
|
+
["iTerm.app", "iterm"],
|
|
43895
|
+
["Apple_Terminal", "apple-terminal"],
|
|
43896
|
+
["WezTerm", "wezterm"],
|
|
43897
|
+
["ghostty", "ghostty"],
|
|
43898
|
+
["Hyper", "hyper"],
|
|
43899
|
+
["Tabby", "tabby"],
|
|
43900
|
+
["rio", "rio"]
|
|
43901
|
+
];
|
|
43902
|
+
/**
|
|
43903
|
+
* Best-effort label for the terminal emulator / editor hosting the CLI,
|
|
43904
|
+
* derived from terminal-identity env vars. Recorded as the `terminalKind` run
|
|
43905
|
+
* tag so we can see where React Doctor is actually run (nvim, VS Code, iTerm,
|
|
43906
|
+
* …) — the split Sentry can't otherwise see. Low-cardinality and free of any
|
|
43907
|
+
* username/path/secret, so it's safe as a tag. Editor terminals (nvim/vim)
|
|
43908
|
+
* win over the outer emulator because that's the surface a user is reading in;
|
|
43909
|
+
* "ci" marks a run with no interactive terminal; "unknown" when nothing matches.
|
|
43910
|
+
*/
|
|
43911
|
+
const detectTerminalKind = (env = process.env) => {
|
|
43912
|
+
if (env.NVIM) return "neovim";
|
|
43913
|
+
if (env.VIM_TERMINAL) return "vim";
|
|
43914
|
+
const termProgram = env.TERM_PROGRAM;
|
|
43915
|
+
if (termProgram) {
|
|
43916
|
+
for (const [marker, label] of TERMINAL_BY_TERM_PROGRAM) if (termProgram === marker) return label;
|
|
43917
|
+
}
|
|
43918
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return "kitty";
|
|
43919
|
+
if (env.WT_SESSION) return "windows-terminal";
|
|
43920
|
+
if (env.ALACRITTY_WINDOW_ID || env.TERM === "alacritty") return "alacritty";
|
|
43921
|
+
if (env.VTE_VERSION) return "vte";
|
|
43922
|
+
if (env.TMUX) return "tmux";
|
|
43923
|
+
if (isCiEnvironment(env)) return "ci";
|
|
43924
|
+
return "unknown";
|
|
43925
|
+
};
|
|
43926
|
+
//#endregion
|
|
43927
|
+
//#region src/cli/utils/is-debug-flag.ts
|
|
43928
|
+
/**
|
|
43929
|
+
* Whether the user passed `--debug` (surface the run's Sentry trace id, and
|
|
43930
|
+
* force performance tracing on so there's a trace to surface). Read straight
|
|
43931
|
+
* from argv rather than Commander's parsed flags because `initializeSentry()`
|
|
43932
|
+
* runs before Commander parses — the same reason `shouldEnableSentry()` reads
|
|
43933
|
+
* `--no-score` from argv. Sharing this one reader keeps the init-time sampling
|
|
43934
|
+
* override and the end-of-run print in agreement.
|
|
43935
|
+
*/
|
|
43936
|
+
const isDebugFlagEnabled = (argv = process.argv) => argv.includes("--debug");
|
|
43937
|
+
//#endregion
|
|
43867
43938
|
//#region src/cli/utils/is-git-hook-environment.ts
|
|
43868
43939
|
const isGitHookEnvironment = () => Boolean(process.env.GIT_DIR);
|
|
43869
43940
|
//#endregion
|
|
@@ -43886,6 +43957,7 @@ const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
|
|
|
43886
43957
|
const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCodingAgentEnvironment();
|
|
43887
43958
|
//#endregion
|
|
43888
43959
|
//#region src/cli/utils/constants.ts
|
|
43960
|
+
const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
|
|
43889
43961
|
const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
|
|
43890
43962
|
const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
|
|
43891
43963
|
const GH_DEFAULT_BRANCH_PROBE_TIMEOUT_MS = 5e3;
|
|
@@ -43970,7 +44042,7 @@ const makeNoopConsole = () => ({
|
|
|
43970
44042
|
});
|
|
43971
44043
|
//#endregion
|
|
43972
44044
|
//#region src/cli/utils/version.ts
|
|
43973
|
-
const VERSION = "0.5.6-dev.
|
|
44045
|
+
const VERSION = "0.5.6-dev.50999f4";
|
|
43974
44046
|
//#endregion
|
|
43975
44047
|
//#region src/cli/utils/json-mode.ts
|
|
43976
44048
|
let context = null;
|
|
@@ -44120,7 +44192,9 @@ const buildRunContext = () => {
|
|
|
44120
44192
|
viaAction: isOfficialGithubAction(),
|
|
44121
44193
|
codingAgent: detectCodingAgent(),
|
|
44122
44194
|
interactive: !isNonInteractiveEnvironment(),
|
|
44195
|
+
terminalKind: detectTerminalKind(),
|
|
44123
44196
|
jsonMode: isJsonModeActive(),
|
|
44197
|
+
debug: isDebugFlagEnabled(),
|
|
44124
44198
|
invokedVia: detectInvokedVia()
|
|
44125
44199
|
};
|
|
44126
44200
|
};
|
|
@@ -44190,7 +44264,9 @@ const buildSentryScope = (runContext = buildRunContext()) => {
|
|
|
44190
44264
|
viaAction: runContext.viaAction,
|
|
44191
44265
|
codingAgent: runContext.codingAgent,
|
|
44192
44266
|
interactive: runContext.interactive,
|
|
44267
|
+
terminalKind: runContext.terminalKind,
|
|
44193
44268
|
jsonMode: runContext.jsonMode,
|
|
44269
|
+
debug: runContext.debug,
|
|
44194
44270
|
invokedVia: runContext.invokedVia,
|
|
44195
44271
|
nodeMajor: runContext.nodeMajor
|
|
44196
44272
|
};
|
|
@@ -44328,13 +44404,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
44328
44404
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
44329
44405
|
* standard `SENTRY_RELEASE` override.
|
|
44330
44406
|
*/
|
|
44331
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.
|
|
44407
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.6-dev.50999f4`;
|
|
44332
44408
|
/**
|
|
44333
44409
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
44334
44410
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
44335
44411
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
44336
44412
|
*/
|
|
44337
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.
|
|
44413
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.6-dev.50999f4") ? "development" : "production");
|
|
44338
44414
|
/**
|
|
44339
44415
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
44340
44416
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -44398,7 +44474,7 @@ const flushSentry = async () => {
|
|
|
44398
44474
|
const initializeSentry = () => {
|
|
44399
44475
|
if (isInitialized || !shouldEnableSentry()) return;
|
|
44400
44476
|
isInitialized = true;
|
|
44401
|
-
resolvedTracesSampleRate = resolveTracesSampleRate();
|
|
44477
|
+
resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
|
|
44402
44478
|
const { tags, contexts } = buildSentryScope();
|
|
44403
44479
|
Sentry.init({
|
|
44404
44480
|
dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
|
|
@@ -47614,6 +47690,11 @@ const setActiveRunTrace = (trace) => {
|
|
|
47614
47690
|
activeRunTrace = trace;
|
|
47615
47691
|
};
|
|
47616
47692
|
const getActiveRunTrace = () => activeRunTrace;
|
|
47693
|
+
let lastRunTraceId = null;
|
|
47694
|
+
const recordRunTraceId = (traceId) => {
|
|
47695
|
+
lastRunTraceId = traceId;
|
|
47696
|
+
};
|
|
47697
|
+
const getLastRunTraceId = () => lastRunTraceId;
|
|
47617
47698
|
//#endregion
|
|
47618
47699
|
//#region src/cli/utils/to-span-attributes.ts
|
|
47619
47700
|
/**
|
|
@@ -47676,14 +47757,13 @@ const withSentryRunSpan = (run, options = {}) => {
|
|
|
47676
47757
|
op: "cli.inspect",
|
|
47677
47758
|
attributes: toSpanAttributes(tags)
|
|
47678
47759
|
}, (rootSpan) => {
|
|
47679
|
-
|
|
47680
|
-
|
|
47681
|
-
|
|
47682
|
-
|
|
47683
|
-
|
|
47684
|
-
|
|
47685
|
-
|
|
47686
|
-
}
|
|
47760
|
+
const spanContext = rootSpan.spanContext();
|
|
47761
|
+
recordRunTraceId(spanContext.traceId);
|
|
47762
|
+
if (options.concurrentScan !== true) setActiveRunTrace({
|
|
47763
|
+
traceId: spanContext.traceId,
|
|
47764
|
+
spanId: spanContext.spanId,
|
|
47765
|
+
sampled: (spanContext.traceFlags & 1) === 1
|
|
47766
|
+
});
|
|
47687
47767
|
return run(rootSpan);
|
|
47688
47768
|
});
|
|
47689
47769
|
};
|
|
@@ -48116,7 +48196,7 @@ const AGENT_GUIDANCE_LINES = [
|
|
|
48116
48196
|
"Investigate deeply where relevant: race conditions, security-sensitive flows, state propagation, multi-file refactors, and downstream dependency chains.",
|
|
48117
48197
|
"Ignore pure style preferences, theoretical issues without real impact, missing features, and unrelated pre-existing code.",
|
|
48118
48198
|
"Start with high-confidence fixes that preserve behavior. Leave low-confidence or product-dependent changes as notes.",
|
|
48119
|
-
"Run `npx react-doctor@latest --verbose --
|
|
48199
|
+
"Run `npx react-doctor@latest --verbose --scope changed` before and after changes, plus relevant tests after each focused batch.",
|
|
48120
48200
|
"When available, spawn subagents or isolated worktrees for independent rule families, then review and merge only the best safe fixes.",
|
|
48121
48201
|
"Split unrelated, broad, or behavior-changing work into separate PRs/branches instead of one large cleanup.",
|
|
48122
48202
|
"For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.",
|
|
@@ -48191,6 +48271,15 @@ const boxText = (content, innerWidth) => {
|
|
|
48191
48271
|
].join("\n");
|
|
48192
48272
|
};
|
|
48193
48273
|
//#endregion
|
|
48274
|
+
//#region src/cli/utils/resolve-absolute-path.ts
|
|
48275
|
+
/**
|
|
48276
|
+
* Resolves a diagnostic's `filePath` (relative to its project root, or
|
|
48277
|
+
* already absolute) to an absolute path. Shared by the code-frame reader and
|
|
48278
|
+
* the terminal hyperlink builder so both turn a relative path into the same
|
|
48279
|
+
* on-disk location.
|
|
48280
|
+
*/
|
|
48281
|
+
const resolveAbsolutePath = (filePath, rootDirectory) => Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory || ".", filePath);
|
|
48282
|
+
//#endregion
|
|
48194
48283
|
//#region src/cli/utils/build-code-frame.ts
|
|
48195
48284
|
/**
|
|
48196
48285
|
* Renders a syntax-highlighted source excerpt around a diagnostic site
|
|
@@ -48201,7 +48290,7 @@ const boxText = (content, innerWidth) => {
|
|
|
48201
48290
|
*/
|
|
48202
48291
|
const buildCodeFrame = (input) => {
|
|
48203
48292
|
if (input.line <= 0) return null;
|
|
48204
|
-
const absolutePath =
|
|
48293
|
+
const absolutePath = resolveAbsolutePath(input.filePath, input.rootDirectory);
|
|
48205
48294
|
let source;
|
|
48206
48295
|
try {
|
|
48207
48296
|
source = NFS.readFileSync(absolutePath, "utf8");
|
|
@@ -48241,6 +48330,16 @@ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
|
|
|
48241
48330
|
const DIVIDER_INDENT = " ";
|
|
48242
48331
|
const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
|
|
48243
48332
|
//#endregion
|
|
48333
|
+
//#region src/cli/utils/format-hyperlink.ts
|
|
48334
|
+
const OSC = "\x1B]";
|
|
48335
|
+
const ST = "\x1B\\";
|
|
48336
|
+
/**
|
|
48337
|
+
* Wraps `text` in an OSC 8 hyperlink pointing at `uri`. The visible characters
|
|
48338
|
+
* are exactly `text`; the link is carried in escape sequences a capable
|
|
48339
|
+
* terminal turns into a click target.
|
|
48340
|
+
*/
|
|
48341
|
+
const formatHyperlink = (text, uri) => `${OSC}8;;${uri}${ST}${text}${OSC}8;;${ST}`;
|
|
48342
|
+
//#endregion
|
|
48244
48343
|
//#region src/cli/utils/indent-multiline-text.ts
|
|
48245
48344
|
const indentMultilineText = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
48246
48345
|
//#endregion
|
|
@@ -48394,17 +48493,23 @@ const clusterNearbyDiagnostics = (diagnostics) => {
|
|
|
48394
48493
|
}
|
|
48395
48494
|
return clusters;
|
|
48396
48495
|
};
|
|
48397
|
-
const
|
|
48496
|
+
const formatClusterLocationText = (cluster) => {
|
|
48497
|
+
const { filePath } = cluster.diagnostics[0];
|
|
48498
|
+
if (cluster.startLine <= 0) return filePath;
|
|
48499
|
+
if (cluster.endLine > cluster.startLine) return `${filePath}:${cluster.startLine}-${cluster.endLine}`;
|
|
48500
|
+
return `${filePath}:${cluster.startLine}`;
|
|
48501
|
+
};
|
|
48502
|
+
const formatClusterLocation = (cluster, resolveSourceRoot, hyperlinks) => {
|
|
48398
48503
|
const lead = cluster.diagnostics[0];
|
|
48399
48504
|
const contextTag = formatFileContextTag(lead);
|
|
48400
|
-
|
|
48401
|
-
if (
|
|
48402
|
-
return `${lead.filePath
|
|
48505
|
+
const location = formatClusterLocationText(cluster);
|
|
48506
|
+
if (!hyperlinks) return `${location}${contextTag}`;
|
|
48507
|
+
return `${formatHyperlink(location, pathToFileURL(resolveAbsolutePath(lead.filePath, resolveSourceRoot(lead))).href)}${contextTag}`;
|
|
48403
48508
|
};
|
|
48404
|
-
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame) => {
|
|
48509
|
+
const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame, hyperlinks) => {
|
|
48405
48510
|
const lead = cluster.diagnostics[0];
|
|
48406
48511
|
const isMultiSite = cluster.diagnostics.length > 1;
|
|
48407
|
-
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster)}`)];
|
|
48512
|
+
const lines = ["", highlighter.gray(`${TOP_ERROR_DETAIL_INDENT}${formatClusterLocation(cluster, resolveSourceRoot, hyperlinks)}`)];
|
|
48408
48513
|
const codeFrame = renderCodeFrame ? buildCodeFrame({
|
|
48409
48514
|
filePath: lead.filePath,
|
|
48410
48515
|
line: cluster.startLine,
|
|
@@ -48423,7 +48528,7 @@ const buildDiagnosticClusterLines = (cluster, resolveSourceRoot, renderCodeFrame
|
|
|
48423
48528
|
}
|
|
48424
48529
|
return lines;
|
|
48425
48530
|
};
|
|
48426
|
-
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment) => {
|
|
48531
|
+
const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, renderEverySite, isAgentEnvironment, hyperlinks) => {
|
|
48427
48532
|
const representative = pickRepresentativeDiagnostic(ruleDiagnostics);
|
|
48428
48533
|
const { severity } = representative;
|
|
48429
48534
|
const trailingBadge = formatTrailingSiteBadge(ruleDiagnostics.length);
|
|
@@ -48443,7 +48548,7 @@ const buildRuleDetailBlock = (ruleKey, ruleDiagnostics, resolveSourceRoot, rende
|
|
|
48443
48548
|
}
|
|
48444
48549
|
const renderCodeFrame = severity === "error";
|
|
48445
48550
|
const sites = renderEverySite ? ruleDiagnostics : [representative];
|
|
48446
|
-
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame));
|
|
48551
|
+
if (!(isCollapsedWarningGroup && representative.help.includes(representative.filePath))) for (const cluster of clusterNearbyDiagnostics(sites)) lines.push(...buildDiagnosticClusterLines(cluster, resolveSourceRoot, renderCodeFrame, hyperlinks));
|
|
48447
48552
|
return lines;
|
|
48448
48553
|
};
|
|
48449
48554
|
const selectErrorRuleGroups = (diagnostics, rulePriority) => buildSortedRuleGroups(diagnostics.filter((diagnostic) => diagnostic.severity === "error"), rulePriority);
|
|
@@ -48456,7 +48561,7 @@ const buildOverflowSummaryLine = (diagnostics, rulePriority) => {
|
|
|
48456
48561
|
return ` ${highlighter.dim("Run")} ${command} ${highlighter.dim("to list every error and warning")}`;
|
|
48457
48562
|
};
|
|
48458
48563
|
const getTopErrorRuleKeys = (diagnostics, limit, rulePriority) => new Set(selectTopErrorRuleGroups(diagnostics, limit, rulePriority).map(([ruleKey]) => ruleKey));
|
|
48459
|
-
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) => {
|
|
48564
|
+
const buildTopErrorsSection = (diagnostics, resolveSourceRoot, hyperlinks, rulePriority) => {
|
|
48460
48565
|
const topRuleGroups = selectErrorRuleGroups(diagnostics, rulePriority).slice(0, 3);
|
|
48461
48566
|
if (topRuleGroups.length === 0) return {
|
|
48462
48567
|
lines: [],
|
|
@@ -48466,7 +48571,7 @@ const buildTopErrorsSection = (diagnostics, resolveSourceRoot, rulePriority) =>
|
|
|
48466
48571
|
const blockOffsets = [];
|
|
48467
48572
|
for (const [ruleKey, ruleDiagnostics] of topRuleGroups) {
|
|
48468
48573
|
blockOffsets.push(lines.length);
|
|
48469
|
-
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false));
|
|
48574
|
+
lines.push(...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, false, false, hyperlinks));
|
|
48470
48575
|
lines.push("");
|
|
48471
48576
|
}
|
|
48472
48577
|
return {
|
|
@@ -48504,18 +48609,18 @@ const buildOverviewHeaderLines = (diagnostics) => {
|
|
|
48504
48609
|
* single Effect.forEach over Console.log so failures or fiber
|
|
48505
48610
|
* interruption produce predictable partial output.
|
|
48506
48611
|
*/
|
|
48507
|
-
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}) => gen(function* () {
|
|
48612
|
+
const printDiagnostics = (diagnostics, isVerbose, sourceRoot, rulePriority, isAgentEnvironment = false, onboarding = {}, hyperlinks = false) => gen(function* () {
|
|
48508
48613
|
const sectionPause = onboarding.sectionPause ?? void_;
|
|
48509
48614
|
const animateCountUp = onboarding.animateCountUp ?? false;
|
|
48510
48615
|
const resolveSourceRoot = typeof sourceRoot === "function" ? sourceRoot : () => sourceRoot;
|
|
48511
48616
|
let detailLines;
|
|
48512
48617
|
let topErrorBlockOffsets = [];
|
|
48513
48618
|
if (!isVerbose) {
|
|
48514
|
-
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, rulePriority);
|
|
48619
|
+
const topErrors = buildTopErrorsSection(diagnostics, resolveSourceRoot, hyperlinks, rulePriority);
|
|
48515
48620
|
detailLines = topErrors.lines;
|
|
48516
48621
|
topErrorBlockOffsets = topErrors.blockOffsets;
|
|
48517
48622
|
} else detailLines = buildSortedRuleGroups(diagnostics, rulePriority).flatMap(([ruleKey, ruleDiagnostics]) => {
|
|
48518
|
-
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment), ""];
|
|
48623
|
+
return [...buildRuleDetailBlock(ruleKey, ruleDiagnostics, resolveSourceRoot, true, isAgentEnvironment, hyperlinks), ""];
|
|
48519
48624
|
});
|
|
48520
48625
|
const overflowLine = isVerbose ? void 0 : buildOverflowSummaryLine(diagnostics, rulePriority);
|
|
48521
48626
|
const categoryTallies = buildCategoryDiagnosticGroups(diagnostics, rulePriority).map(buildCategoryTally);
|
|
@@ -48576,6 +48681,48 @@ const computeProjectedScore = async (topErrorSource, rescoreSource, currentScore
|
|
|
48576
48681
|
//#endregion
|
|
48577
48682
|
//#region src/cli/utils/filter-diagnostics-by-categories.ts
|
|
48578
48683
|
const filterDiagnosticsByCategories = (diagnostics, categories) => categories.size === 0 ? [...diagnostics] : diagnostics.filter((diagnostic) => categories.has(diagnostic.category));
|
|
48684
|
+
//#endregion
|
|
48685
|
+
//#region src/cli/utils/supports-hyperlinks.ts
|
|
48686
|
+
const HYPERLINK_CAPABLE_TERM_PROGRAMS = new Set([
|
|
48687
|
+
"iTerm.app",
|
|
48688
|
+
"WezTerm",
|
|
48689
|
+
"vscode",
|
|
48690
|
+
"Hyper",
|
|
48691
|
+
"ghostty",
|
|
48692
|
+
"Tabby",
|
|
48693
|
+
"rio"
|
|
48694
|
+
]);
|
|
48695
|
+
const parseVteVersion = (raw) => {
|
|
48696
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
48697
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
48698
|
+
};
|
|
48699
|
+
/**
|
|
48700
|
+
* Whether `stream` is a terminal that renders OSC 8 hyperlinks. Auto-detected
|
|
48701
|
+
* from terminal-identity env vars; the de-facto `FORCE_HYPERLINK` env var
|
|
48702
|
+
* overrides detection (`FORCE_HYPERLINK=0`/`false` forces off, any other value
|
|
48703
|
+
* forces on), mirroring how the ecosystem's terminal libraries gate the same
|
|
48704
|
+
* feature. Off for non-TTYs, `TERM=dumb`, and CI (whose log viewers render the
|
|
48705
|
+
* raw escape rather than a link). Unknown terminals default to off.
|
|
48706
|
+
*/
|
|
48707
|
+
const supportsHyperlinks = (stream = process.stdout, env = process.env) => {
|
|
48708
|
+
const forced = env.FORCE_HYPERLINK;
|
|
48709
|
+
if (forced !== void 0 && forced !== "") return forced !== "0" && forced.toLowerCase() !== "false";
|
|
48710
|
+
if (stream.isTTY !== true) return false;
|
|
48711
|
+
if (env.TERM === "dumb") return false;
|
|
48712
|
+
if (isCiEnvironment(env)) return false;
|
|
48713
|
+
if (env.WT_SESSION) return true;
|
|
48714
|
+
if (env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty") return true;
|
|
48715
|
+
if (parseVteVersion(env.VTE_VERSION) >= 5e3) return true;
|
|
48716
|
+
return Boolean(env.TERM_PROGRAM && HYPERLINK_CAPABLE_TERM_PROGRAMS.has(env.TERM_PROGRAM));
|
|
48717
|
+
};
|
|
48718
|
+
//#endregion
|
|
48719
|
+
//#region src/cli/utils/should-render-hyperlinks.ts
|
|
48720
|
+
/**
|
|
48721
|
+
* Whether to emit OSC 8 clickable `file:line` locations for this run: a
|
|
48722
|
+
* hyperlink-capable terminal AND not a coding agent (whose output parsers
|
|
48723
|
+
* would choke on the escape sequences).
|
|
48724
|
+
*/
|
|
48725
|
+
const shouldRenderHyperlinks = (stream = process.stdout) => supportsHyperlinks(stream) && !isCodingAgentEnvironment();
|
|
48579
48726
|
const FORCE_ONBOARDING_ENV_VAR = "REACT_DOCTOR_FORCE_ONBOARDING";
|
|
48580
48727
|
const FALSY_FLAG_VALUES = new Set([
|
|
48581
48728
|
"",
|
|
@@ -48595,10 +48742,9 @@ const canAnimateOnboarding = (stream = process.stdout) => {
|
|
|
48595
48742
|
};
|
|
48596
48743
|
//#endregion
|
|
48597
48744
|
//#region src/cli/utils/onboarding-state.ts
|
|
48598
|
-
const GLOBAL_CONFIG_PROJECT_NAME$2 = "react-doctor";
|
|
48599
48745
|
const ONBOARDED_AT_KEY = "onboardedAt";
|
|
48600
48746
|
const getOnboardingStore = (options = {}) => new Conf({
|
|
48601
|
-
projectName:
|
|
48747
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
48602
48748
|
cwd: options.cwd
|
|
48603
48749
|
});
|
|
48604
48750
|
const hasCompletedOnboarding = (options = {}) => {
|
|
@@ -49054,6 +49200,78 @@ const resolveCliCategories = (categoryFlag) => {
|
|
|
49054
49200
|
return resolvedCategories.length > 0 ? resolvedCategories : void 0;
|
|
49055
49201
|
};
|
|
49056
49202
|
//#endregion
|
|
49203
|
+
//#region src/cli/utils/git-hook-shared.ts
|
|
49204
|
+
const HOOK_FILE_NAME = "pre-commit";
|
|
49205
|
+
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49206
|
+
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49207
|
+
const HUSKY_HOOKS_PATH = ".husky";
|
|
49208
|
+
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49209
|
+
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49210
|
+
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49211
|
+
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49212
|
+
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49213
|
+
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49214
|
+
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49215
|
+
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49216
|
+
"rm -f \"$react_doctor_output\";",
|
|
49217
|
+
"else",
|
|
49218
|
+
"rm -f \"$react_doctor_output\";",
|
|
49219
|
+
`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;`,
|
|
49220
|
+
"fi"
|
|
49221
|
+
].join(" ");
|
|
49222
|
+
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49223
|
+
const runGit = (projectRoot, args) => {
|
|
49224
|
+
try {
|
|
49225
|
+
return execFileSync("git", [...args], {
|
|
49226
|
+
cwd: projectRoot,
|
|
49227
|
+
encoding: "utf8",
|
|
49228
|
+
stdio: [
|
|
49229
|
+
"ignore",
|
|
49230
|
+
"pipe",
|
|
49231
|
+
"ignore"
|
|
49232
|
+
]
|
|
49233
|
+
}).trim();
|
|
49234
|
+
} catch {
|
|
49235
|
+
return null;
|
|
49236
|
+
}
|
|
49237
|
+
};
|
|
49238
|
+
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
49239
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49240
|
+
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
49241
|
+
const readPackageJson = (projectRoot) => {
|
|
49242
|
+
try {
|
|
49243
|
+
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
49244
|
+
} catch {
|
|
49245
|
+
return null;
|
|
49246
|
+
}
|
|
49247
|
+
};
|
|
49248
|
+
const writeJsonFile$1 = (filePath, value) => {
|
|
49249
|
+
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
49250
|
+
};
|
|
49251
|
+
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
49252
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49253
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49254
|
+
return [
|
|
49255
|
+
"dependencies",
|
|
49256
|
+
"devDependencies",
|
|
49257
|
+
"optionalDependencies"
|
|
49258
|
+
].some((fieldName) => {
|
|
49259
|
+
const dependencies = packageJson[fieldName];
|
|
49260
|
+
return isRecord$1(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
49261
|
+
});
|
|
49262
|
+
};
|
|
49263
|
+
const packageHasRecordKey = (projectRoot, key) => {
|
|
49264
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49265
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson[key]);
|
|
49266
|
+
};
|
|
49267
|
+
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
49268
|
+
const packageJson = readPackageJson(projectRoot);
|
|
49269
|
+
if (!isRecord$1(packageJson)) return false;
|
|
49270
|
+
const value = packageJson[key];
|
|
49271
|
+
return isRecord$1(value) && isRecord$1(value[nestedKey]);
|
|
49272
|
+
};
|
|
49273
|
+
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
49274
|
+
//#endregion
|
|
49057
49275
|
//#region src/cli/utils/scan-result-cache.ts
|
|
49058
49276
|
const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
|
|
49059
49277
|
const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
@@ -49064,7 +49282,7 @@ const TOOLCHAIN_PACKAGE_SPECIFIERS = [
|
|
|
49064
49282
|
"eslint-plugin-react-hooks/package.json"
|
|
49065
49283
|
];
|
|
49066
49284
|
const bundledRequire = createRequire(import.meta.url);
|
|
49067
|
-
const isRecord
|
|
49285
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49068
49286
|
const normalizeForStableJson = (value) => {
|
|
49069
49287
|
if (value === null) return null;
|
|
49070
49288
|
if (value === void 0) return void 0;
|
|
@@ -49093,24 +49311,9 @@ const stringifyStableJson = (value) => {
|
|
|
49093
49311
|
}
|
|
49094
49312
|
};
|
|
49095
49313
|
const hashString = (value) => crypto.createHash("sha1").update(value).digest("hex");
|
|
49096
|
-
const
|
|
49097
|
-
try {
|
|
49098
|
-
return execFileSync("git", [...args], {
|
|
49099
|
-
cwd: directory,
|
|
49100
|
-
encoding: "utf8",
|
|
49101
|
-
stdio: [
|
|
49102
|
-
"ignore",
|
|
49103
|
-
"pipe",
|
|
49104
|
-
"ignore"
|
|
49105
|
-
]
|
|
49106
|
-
}).trim();
|
|
49107
|
-
} catch {
|
|
49108
|
-
return null;
|
|
49109
|
-
}
|
|
49110
|
-
};
|
|
49111
|
-
const readHeadSha = (projectDirectory) => runGit$1(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49314
|
+
const readHeadSha = (projectDirectory) => runGit(projectDirectory, ["rev-parse", "HEAD"]);
|
|
49112
49315
|
const isWorktreeClean = (projectDirectory) => {
|
|
49113
|
-
const status = runGit
|
|
49316
|
+
const status = runGit(projectDirectory, [
|
|
49114
49317
|
"status",
|
|
49115
49318
|
"--porcelain=v1",
|
|
49116
49319
|
"--untracked-files=normal"
|
|
@@ -49118,7 +49321,7 @@ const isWorktreeClean = (projectDirectory) => {
|
|
|
49118
49321
|
return status !== null && status.length === 0;
|
|
49119
49322
|
};
|
|
49120
49323
|
const hasHiddenTrackedFileState = (projectDirectory) => {
|
|
49121
|
-
const output = runGit
|
|
49324
|
+
const output = runGit(projectDirectory, ["ls-files", "-v"]);
|
|
49122
49325
|
if (output === null) return true;
|
|
49123
49326
|
return output.split("\n").some((line) => line.length > 0 && line[0] !== "H");
|
|
49124
49327
|
};
|
|
@@ -49131,7 +49334,7 @@ const resolveCacheFilePath = (projectDirectory) => {
|
|
|
49131
49334
|
const readPersistedCache = (cacheFilePath) => {
|
|
49132
49335
|
try {
|
|
49133
49336
|
const parsed = JSON.parse(fs.readFileSync(cacheFilePath, "utf8"));
|
|
49134
|
-
if (!isRecord
|
|
49337
|
+
if (!isRecord(parsed) || parsed.version !== 1) return {
|
|
49135
49338
|
version: 1,
|
|
49136
49339
|
entries: []
|
|
49137
49340
|
};
|
|
@@ -49141,8 +49344,8 @@ const readPersistedCache = (cacheFilePath) => {
|
|
|
49141
49344
|
};
|
|
49142
49345
|
const entries = [];
|
|
49143
49346
|
for (const entry of parsed.entries) {
|
|
49144
|
-
if (!isRecord
|
|
49145
|
-
if (!isRecord
|
|
49347
|
+
if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.createdAtMs !== "number") continue;
|
|
49348
|
+
if (!isRecord(entry.payload) || !Array.isArray(entry.payload.diagnostics)) continue;
|
|
49146
49349
|
entries.push(entry);
|
|
49147
49350
|
}
|
|
49148
49351
|
return {
|
|
@@ -49674,6 +49877,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49674
49877
|
}
|
|
49675
49878
|
const animateRender = !options.silent && !options.verbose && canAnimateOnboarding(process.stdout);
|
|
49676
49879
|
const pause = onboardingSectionPause(animateRender);
|
|
49880
|
+
const useHyperlinks = shouldRenderHyperlinks(process.stdout);
|
|
49677
49881
|
const demotedDiagnosticCount = diagnostics.length - surfaceDiagnostics.length;
|
|
49678
49882
|
const lintSourceFileCount = options.includePaths.length > 0 ? options.includePaths.length : project.sourceFileCount;
|
|
49679
49883
|
if (printedDiagnostics.length === 0) {
|
|
@@ -49699,7 +49903,7 @@ const finalizeAndRender = (input) => gen(function* () {
|
|
|
49699
49903
|
yield* printDiagnostics([...printedDiagnostics], options.verbose, directory, buildRulePriorityMap([score]), isCodingAgentEnvironment(), {
|
|
49700
49904
|
sectionPause: pause,
|
|
49701
49905
|
animateCountUp: animateRender
|
|
49702
|
-
});
|
|
49906
|
+
}, useHyperlinks);
|
|
49703
49907
|
if (options.isNonInteractiveEnvironment && options.outputSurface !== "prComment") yield* printAgentGuidance();
|
|
49704
49908
|
if (options.categoryFilters.size === 0 && demotedDiagnosticCount > 0) {
|
|
49705
49909
|
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.`));
|
|
@@ -49892,16 +50096,7 @@ const buildHandoffPayload = (input) => {
|
|
|
49892
50096
|
try {
|
|
49893
50097
|
outputDirectory = writeDiagnosticsDirectory([...input.diagnostics], input.outputDirectory);
|
|
49894
50098
|
} catch {}
|
|
49895
|
-
const lines = [
|
|
49896
|
-
"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:",
|
|
49897
|
-
`- It's used by teams at ${CI_TRUST_COMPANIES}.`,
|
|
49898
|
-
"- I don't have to fix everything at once; CI keeps new PRs clean while I incrementally pay down the existing issues.",
|
|
49899
|
-
`- Setup guide: ${CI_URL}`,
|
|
49900
|
-
"If I say yes, run `npx react-doctor install` (or add the GitHub Actions workflow). Either way, then continue with the fixes below.",
|
|
49901
|
-
"",
|
|
49902
|
-
`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.`,
|
|
49903
|
-
""
|
|
49904
|
-
];
|
|
50099
|
+
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.`, ""];
|
|
49905
50100
|
topGroups.forEach(([ruleKey, ruleDiagnostics], index) => {
|
|
49906
50101
|
const representative = ruleDiagnostics[0];
|
|
49907
50102
|
const severityLabel = representative.severity === "error" ? "ERROR" : "WARN";
|
|
@@ -49962,78 +50157,6 @@ const detectAvailableAgents = async () => {
|
|
|
49962
50157
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
49963
50158
|
};
|
|
49964
50159
|
//#endregion
|
|
49965
|
-
//#region src/cli/utils/git-hook-shared.ts
|
|
49966
|
-
const HOOK_FILE_NAME = "pre-commit";
|
|
49967
|
-
const HOOK_RELATIVE_PATH = "hooks/pre-commit";
|
|
49968
|
-
const LEGACY_HOOK_RUNNER_RELATIVE_PATH = ".react-doctor/hooks/pre-commit";
|
|
49969
|
-
const HUSKY_HOOKS_PATH = ".husky";
|
|
49970
|
-
const VITE_PLUS_HOOKS_PATH = ".vite-hooks";
|
|
49971
|
-
const LEFTHOOK_CONFIG_FILES = ["lefthook.yml", "lefthook.yaml"];
|
|
49972
|
-
const PRE_COMMIT_CONFIG_FILE = ".pre-commit-config.yaml";
|
|
49973
|
-
const OVERCOMMIT_CONFIG_FILE = ".overcommit.yml";
|
|
49974
|
-
const REACT_DOCTOR_COMMAND = "react-doctor --staged --blocking warning";
|
|
49975
|
-
const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
49976
|
-
"react_doctor_output=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-hook.XXXXXX\");",
|
|
49977
|
-
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
49978
|
-
"rm -f \"$react_doctor_output\";",
|
|
49979
|
-
"else",
|
|
49980
|
-
"rm -f \"$react_doctor_output\";",
|
|
49981
|
-
`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;`,
|
|
49982
|
-
"fi"
|
|
49983
|
-
].join(" ");
|
|
49984
|
-
const PACKAGE_JSON_FILE_NAME = "package.json";
|
|
49985
|
-
const runGit = (projectRoot, args) => {
|
|
49986
|
-
try {
|
|
49987
|
-
return execFileSync("git", [...args], {
|
|
49988
|
-
cwd: projectRoot,
|
|
49989
|
-
encoding: "utf8",
|
|
49990
|
-
stdio: [
|
|
49991
|
-
"ignore",
|
|
49992
|
-
"pipe",
|
|
49993
|
-
"ignore"
|
|
49994
|
-
]
|
|
49995
|
-
}).trim();
|
|
49996
|
-
} catch {
|
|
49997
|
-
return null;
|
|
49998
|
-
}
|
|
49999
|
-
};
|
|
50000
|
-
const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
|
|
50001
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50002
|
-
const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
|
|
50003
|
-
const readPackageJson = (projectRoot) => {
|
|
50004
|
-
try {
|
|
50005
|
-
return JSON.parse(NFS.readFileSync(getPackageJsonPath(projectRoot), "utf8"));
|
|
50006
|
-
} catch {
|
|
50007
|
-
return null;
|
|
50008
|
-
}
|
|
50009
|
-
};
|
|
50010
|
-
const writeJsonFile$1 = (filePath, value) => {
|
|
50011
|
-
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50012
|
-
};
|
|
50013
|
-
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
50014
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50015
|
-
if (!isRecord(packageJson)) return false;
|
|
50016
|
-
return [
|
|
50017
|
-
"dependencies",
|
|
50018
|
-
"devDependencies",
|
|
50019
|
-
"optionalDependencies"
|
|
50020
|
-
].some((fieldName) => {
|
|
50021
|
-
const dependencies = packageJson[fieldName];
|
|
50022
|
-
return isRecord(dependencies) && typeof dependencies[dependencyName] === "string";
|
|
50023
|
-
});
|
|
50024
|
-
};
|
|
50025
|
-
const packageHasRecordKey = (projectRoot, key) => {
|
|
50026
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50027
|
-
return isRecord(packageJson) && isRecord(packageJson[key]);
|
|
50028
|
-
};
|
|
50029
|
-
const packageHasNestedRecordKey = (projectRoot, key, nestedKey) => {
|
|
50030
|
-
const packageJson = readPackageJson(projectRoot);
|
|
50031
|
-
if (!isRecord(packageJson)) return false;
|
|
50032
|
-
const value = packageJson[key];
|
|
50033
|
-
return isRecord(value) && isRecord(value[nestedKey]);
|
|
50034
|
-
};
|
|
50035
|
-
const ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}\n`;
|
|
50036
|
-
//#endregion
|
|
50037
50160
|
//#region src/cli/utils/install-doctor-script.ts
|
|
50038
50161
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
50039
50162
|
const FALLBACK_DOCTOR_SCRIPT_NAME = "react-doctor";
|
|
@@ -50059,31 +50182,31 @@ const findNearestPackageDirectory = (startDirectory, stopDirectory) => {
|
|
|
50059
50182
|
};
|
|
50060
50183
|
const hasDoctorScript = (projectRoot) => {
|
|
50061
50184
|
const packageJson = readPackageJson(findNearestPackageDirectory(projectRoot) ?? projectRoot);
|
|
50062
|
-
if (!isRecord(packageJson)) return false;
|
|
50185
|
+
if (!isRecord$1(packageJson)) return false;
|
|
50063
50186
|
const scripts = packageJson.scripts;
|
|
50064
|
-
if (!isRecord(scripts)) return false;
|
|
50187
|
+
if (!isRecord$1(scripts)) return false;
|
|
50065
50188
|
return isReactDoctorScriptCommand(scripts[DOCTOR_SCRIPT_NAME]) || isReactDoctorScriptCommand(scripts[FALLBACK_DOCTOR_SCRIPT_NAME]);
|
|
50066
50189
|
};
|
|
50067
50190
|
const hasDoctorDependency = (packageJson) => DEPENDENCY_FIELD_NAMES.some((fieldName) => {
|
|
50068
50191
|
const dependencies = packageJson[fieldName];
|
|
50069
|
-
return isRecord(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50192
|
+
return isRecord$1(dependencies) && Object.hasOwn(dependencies, "react-doctor");
|
|
50070
50193
|
});
|
|
50071
50194
|
const installDoctorScript = (options) => {
|
|
50072
50195
|
const packageDirectory = findNearestPackageDirectory(options.projectRoot) ?? options.projectRoot;
|
|
50073
50196
|
const packageJsonPath = getPackageJsonPath(packageDirectory);
|
|
50074
50197
|
const packageJson = readPackageJson(packageDirectory);
|
|
50075
|
-
if (!isRecord(packageJson)) return {
|
|
50198
|
+
if (!isRecord$1(packageJson)) return {
|
|
50076
50199
|
packageJsonPath,
|
|
50077
50200
|
scriptStatus: "skipped",
|
|
50078
50201
|
scriptReason: "missing-or-invalid-package-json"
|
|
50079
50202
|
};
|
|
50080
50203
|
const scripts = packageJson.scripts;
|
|
50081
50204
|
const scriptTarget = (() => {
|
|
50082
|
-
if (scripts !== void 0 && !isRecord(scripts)) return {
|
|
50205
|
+
if (scripts !== void 0 && !isRecord$1(scripts)) return {
|
|
50083
50206
|
status: "skipped",
|
|
50084
50207
|
reason: "invalid-scripts"
|
|
50085
50208
|
};
|
|
50086
|
-
const scriptRecord = isRecord(scripts) ? scripts : {};
|
|
50209
|
+
const scriptRecord = isRecord$1(scripts) ? scripts : {};
|
|
50087
50210
|
if (isReactDoctorScriptCommand(scriptRecord[DOCTOR_SCRIPT_NAME])) return {
|
|
50088
50211
|
scriptName: DOCTOR_SCRIPT_NAME,
|
|
50089
50212
|
status: "existing"
|
|
@@ -50117,7 +50240,7 @@ const installDoctorScript = (options) => {
|
|
|
50117
50240
|
if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
|
|
50118
50241
|
...packageJson,
|
|
50119
50242
|
scripts: {
|
|
50120
|
-
...isRecord(scripts) ? scripts : {},
|
|
50243
|
+
...isRecord$1(scripts) ? scripts : {},
|
|
50121
50244
|
[scriptTarget.scriptName ?? DOCTOR_SCRIPT_NAME]: DOCTOR_SCRIPT_COMMAND
|
|
50122
50245
|
}
|
|
50123
50246
|
});
|
|
@@ -50271,38 +50394,52 @@ const upgradeReactDoctorWorkflowInPlace = (projectRoot) => {
|
|
|
50271
50394
|
//#region src/cli/utils/hash-project-root.ts
|
|
50272
50395
|
const hashProjectRoot = (projectRoot) => createHash("sha256").update(Path.resolve(projectRoot)).digest("hex");
|
|
50273
50396
|
//#endregion
|
|
50274
|
-
//#region src/cli/utils/
|
|
50275
|
-
const
|
|
50276
|
-
const
|
|
50277
|
-
|
|
50278
|
-
|
|
50279
|
-
});
|
|
50280
|
-
|
|
50281
|
-
|
|
50282
|
-
|
|
50283
|
-
|
|
50284
|
-
|
|
50285
|
-
|
|
50286
|
-
|
|
50287
|
-
};
|
|
50288
|
-
const recordActionUpgradeDecision = (projectRoot, outcome, storeOptions = {}) => {
|
|
50289
|
-
try {
|
|
50290
|
-
const store = getActionUpgradeStore(storeOptions);
|
|
50291
|
-
const upgrades = store.get("actionUpgrades", {});
|
|
50292
|
-
store.set("actionUpgrades", {
|
|
50293
|
-
...upgrades,
|
|
50294
|
-
[hashProjectRoot(projectRoot)]: {
|
|
50295
|
-
rootDirectory: Path.resolve(projectRoot),
|
|
50296
|
-
outcome,
|
|
50297
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50397
|
+
//#region src/cli/utils/project-decision-store.ts
|
|
50398
|
+
const createProjectDecisionStore = (storeKey) => {
|
|
50399
|
+
const getStore = (options = {}) => new Conf({
|
|
50400
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
50401
|
+
cwd: options.cwd
|
|
50402
|
+
});
|
|
50403
|
+
return {
|
|
50404
|
+
getConfigPath: (options = {}) => getStore(options).path,
|
|
50405
|
+
hasHandled: (projectRoot, options = {}) => {
|
|
50406
|
+
try {
|
|
50407
|
+
return Boolean(getStore(options).get(storeKey, {})[hashProjectRoot(projectRoot)]);
|
|
50408
|
+
} catch {
|
|
50409
|
+
return true;
|
|
50298
50410
|
}
|
|
50299
|
-
}
|
|
50300
|
-
|
|
50301
|
-
|
|
50302
|
-
|
|
50303
|
-
|
|
50411
|
+
},
|
|
50412
|
+
record: (projectRoot, outcome, options = {}) => {
|
|
50413
|
+
try {
|
|
50414
|
+
const store = getStore(options);
|
|
50415
|
+
store.set(storeKey, {
|
|
50416
|
+
...store.get(storeKey, {}),
|
|
50417
|
+
[hashProjectRoot(projectRoot)]: {
|
|
50418
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
50419
|
+
outcome,
|
|
50420
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
50421
|
+
}
|
|
50422
|
+
});
|
|
50423
|
+
return true;
|
|
50424
|
+
} catch {
|
|
50425
|
+
return false;
|
|
50426
|
+
}
|
|
50427
|
+
}
|
|
50428
|
+
};
|
|
50304
50429
|
};
|
|
50305
50430
|
//#endregion
|
|
50431
|
+
//#region src/cli/utils/action-upgrade-prompt.ts
|
|
50432
|
+
const store$1 = createProjectDecisionStore("actionUpgrades");
|
|
50433
|
+
store$1.getConfigPath;
|
|
50434
|
+
const hasHandledActionUpgrade = store$1.hasHandled;
|
|
50435
|
+
const recordActionUpgradeDecision = store$1.record;
|
|
50436
|
+
//#endregion
|
|
50437
|
+
//#region src/cli/utils/ci-prompt-decision.ts
|
|
50438
|
+
const store = createProjectDecisionStore("ciPrompts");
|
|
50439
|
+
store.getConfigPath;
|
|
50440
|
+
const hasHandledCiPrompt = store.hasHandled;
|
|
50441
|
+
const recordCiPromptDecision = store.record;
|
|
50442
|
+
//#endregion
|
|
50306
50443
|
//#region src/cli/utils/open-url.ts
|
|
50307
50444
|
const resolveOpenCommand = (url) => {
|
|
50308
50445
|
if (process$1.platform === "darwin") return {
|
|
@@ -50758,22 +50895,22 @@ const buildAgentHookScript = () => [
|
|
|
50758
50895
|
"",
|
|
50759
50896
|
"run_react_doctor() {",
|
|
50760
50897
|
" if [ -x ./node_modules/.bin/react-doctor ]; then",
|
|
50761
|
-
" ./node_modules/.bin/react-doctor --verbose --
|
|
50898
|
+
" ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50762
50899
|
" return",
|
|
50763
50900
|
" fi",
|
|
50764
50901
|
"",
|
|
50765
50902
|
" if command -v react-doctor >/dev/null 2>&1; then",
|
|
50766
|
-
" react-doctor --verbose --
|
|
50903
|
+
" react-doctor --verbose --scope changed --blocking warning --no-score",
|
|
50767
50904
|
" return",
|
|
50768
50905
|
" fi",
|
|
50769
50906
|
"",
|
|
50770
50907
|
" if command -v pnpm >/dev/null 2>&1; then",
|
|
50771
|
-
" pnpm dlx react-doctor@latest --verbose --
|
|
50908
|
+
" pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50772
50909
|
" return",
|
|
50773
50910
|
" fi",
|
|
50774
50911
|
"",
|
|
50775
50912
|
" if command -v npx >/dev/null 2>&1; then",
|
|
50776
|
-
" npx --yes react-doctor@latest --verbose --
|
|
50913
|
+
" npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
|
|
50777
50914
|
" return",
|
|
50778
50915
|
" fi",
|
|
50779
50916
|
"",
|
|
@@ -50931,13 +51068,13 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
50931
51068
|
const packageJsonPath = getPackageJsonPath(options.projectRoot);
|
|
50932
51069
|
const didHookExist = NFS.existsSync(packageJsonPath);
|
|
50933
51070
|
const packageJson = readPackageJson(options.projectRoot);
|
|
50934
|
-
const nextPackageJson = isRecord(packageJson) ? { ...packageJson } : {};
|
|
51071
|
+
const nextPackageJson = isRecord$1(packageJson) ? { ...packageJson } : {};
|
|
50935
51072
|
const parentKeys = strategy.path.slice(0, -1);
|
|
50936
51073
|
const leafKey = strategy.path[strategy.path.length - 1];
|
|
50937
51074
|
let parent = nextPackageJson;
|
|
50938
51075
|
for (const key of parentKeys) {
|
|
50939
51076
|
const existing = parent[key];
|
|
50940
|
-
const cloned = isRecord(existing) ? { ...existing } : {};
|
|
51077
|
+
const cloned = isRecord$1(existing) ? { ...existing } : {};
|
|
50941
51078
|
parent[key] = cloned;
|
|
50942
51079
|
parent = cloned;
|
|
50943
51080
|
}
|
|
@@ -51108,7 +51245,7 @@ const isHuskyProject = (projectRoot) => NFS.existsSync(Path.join(projectRoot, ".
|
|
|
51108
51245
|
const isVitePlusProject = (projectRoot) => packageHasDependency(projectRoot, "vite-plus");
|
|
51109
51246
|
const isSimpleGitHooksProject = (projectRoot) => {
|
|
51110
51247
|
const packageJson = readPackageJson(projectRoot);
|
|
51111
|
-
return isRecord(packageJson) && isRecord(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51248
|
+
return isRecord$1(packageJson) && isRecord$1(packageJson["simple-git-hooks"]) || packageHasDependency(projectRoot, "simple-git-hooks") || NFS.existsSync(Path.join(projectRoot, ".simple-git-hooks.cjs"));
|
|
51112
51249
|
};
|
|
51113
51250
|
const getLefthookConfigPath = (projectRoot) => {
|
|
51114
51251
|
for (const fileName of LEFTHOOK_CONFIG_FILES) {
|
|
@@ -51274,7 +51411,7 @@ const detectPackageManager = (projectRoot) => {
|
|
|
51274
51411
|
let currentDirectory = Path.resolve(projectRoot);
|
|
51275
51412
|
while (true) {
|
|
51276
51413
|
const packageJson = readPackageJson(currentDirectory);
|
|
51277
|
-
if (isRecord(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51414
|
+
if (isRecord$1(packageJson) && typeof packageJson.packageManager === "string") {
|
|
51278
51415
|
const packageManagerName = packageJson.packageManager.split("@")[0];
|
|
51279
51416
|
if (packageManagerName === "pnpm" || packageManagerName === "yarn" || packageManagerName === "bun" || packageManagerName === "npm") return packageManagerName;
|
|
51280
51417
|
}
|
|
@@ -51350,12 +51487,12 @@ const isSupplyChainTrustError = (error) => {
|
|
|
51350
51487
|
const formatInstallCommand = (input) => [input.command, ...input.args].join(" ");
|
|
51351
51488
|
const installReactDoctorDependency = async (options) => {
|
|
51352
51489
|
const packageJson = readPackageJson(options.projectRoot);
|
|
51353
|
-
if (!isRecord(packageJson)) return {
|
|
51490
|
+
if (!isRecord$1(packageJson)) return {
|
|
51354
51491
|
dependencyStatus: "skipped",
|
|
51355
51492
|
dependencyReason: "missing-or-invalid-package-json"
|
|
51356
51493
|
};
|
|
51357
51494
|
if (hasDoctorDependency(packageJson)) return { dependencyStatus: "existing" };
|
|
51358
|
-
if (packageJson.devDependencies !== void 0 && !isRecord(packageJson.devDependencies)) return {
|
|
51495
|
+
if (packageJson.devDependencies !== void 0 && !isRecord$1(packageJson.devDependencies)) return {
|
|
51359
51496
|
dependencyStatus: "skipped",
|
|
51360
51497
|
dependencyReason: "invalid-dev-dependencies"
|
|
51361
51498
|
};
|
|
@@ -51519,10 +51656,12 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
51519
51656
|
const existingWorkflow = readReactDoctorWorkflow(projectRoot);
|
|
51520
51657
|
const canInstallWorkflow = !NFS.existsSync(workflowTargetPath);
|
|
51521
51658
|
const canUpgradeWorkflow = existingWorkflow !== null && workflowUsesV1Action(existingWorkflow.content) && !hasHandledActionUpgrade(projectRoot);
|
|
51522
|
-
const
|
|
51659
|
+
const ciPromptOutcome = canInstallWorkflow && !options.yes && !skipPrompts && !hasHandledCiPrompt(projectRoot) ? await askAddToGitHubActions(prompt) : null;
|
|
51660
|
+
const shouldInstallWorkflow = canInstallWorkflow && (Boolean(options.yes) || ciPromptOutcome === "yes");
|
|
51523
51661
|
const upgradePromptOutcome = canUpgradeWorkflow && !options.yes && !skipPrompts ? await askUpgradeActionVersion(prompt) : null;
|
|
51524
51662
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
51525
51663
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
51664
|
+
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
51526
51665
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
51527
51666
|
type: "multiselect",
|
|
51528
51667
|
name: "agents",
|
|
@@ -51769,18 +51908,24 @@ const handoffToAgent = async (input) => {
|
|
|
51769
51908
|
if (!input.interactive || input.diagnostics.length === 0) return;
|
|
51770
51909
|
cliLogger.break();
|
|
51771
51910
|
const projectRootForCi = findNearestPackageDirectory(input.rootDirectory) ?? input.rootDirectory;
|
|
51772
|
-
|
|
51911
|
+
const isGitHubActionsConfigured = isReactDoctorWorkflowInstalled(projectRootForCi);
|
|
51912
|
+
if (!isGitHubActionsConfigured && !hasHandledCiPrompt(projectRootForCi)) {
|
|
51773
51913
|
const ciOutcome = await askAddToGitHubActions();
|
|
51774
51914
|
recordCount(METRIC.agentHandoff, 1, {
|
|
51775
51915
|
outcome: `ci-${ciOutcome}`,
|
|
51776
51916
|
diagnosticsCount: input.diagnostics.length
|
|
51777
51917
|
});
|
|
51778
51918
|
if (ciOutcome === "cancel") return;
|
|
51919
|
+
recordCiPromptDecision(projectRootForCi, ciOutcome === "yes" ? "accepted" : "declined");
|
|
51779
51920
|
if (ciOutcome === "yes") {
|
|
51780
51921
|
await setUpGitHubActions({ rootDirectory: input.rootDirectory });
|
|
51781
51922
|
cliLogger.break();
|
|
51782
51923
|
}
|
|
51783
|
-
} else await maybeOfferActionUpgrade(projectRootForCi);
|
|
51924
|
+
} else if (isGitHubActionsConfigured) await maybeOfferActionUpgrade(projectRootForCi);
|
|
51925
|
+
else recordCount(METRIC.agentHandoff, 1, {
|
|
51926
|
+
outcome: "ci-suppressed",
|
|
51927
|
+
diagnosticsCount: input.diagnostics.length
|
|
51928
|
+
});
|
|
51784
51929
|
const { handoffTarget } = await prompts({
|
|
51785
51930
|
type: "select",
|
|
51786
51931
|
name: "handoffTarget",
|
|
@@ -52003,6 +52148,7 @@ const reportErrorToSentry = async (error) => {
|
|
|
52003
52148
|
sampled: runTrace.sampled,
|
|
52004
52149
|
sampleRand: Math.random()
|
|
52005
52150
|
});
|
|
52151
|
+
recordRunTraceId(scope.getPropagationContext().traceId);
|
|
52006
52152
|
return Sentry.captureException(error);
|
|
52007
52153
|
});
|
|
52008
52154
|
await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
|
|
@@ -52086,7 +52232,7 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52086
52232
|
yield* log(`${highlighter.success("✔")} Scanned ${totalScannedFileCount} ${totalScannedFileCount === 1 ? "file" : "files"} in ${formatElapsedTime(totalElapsedMilliseconds)}`);
|
|
52087
52233
|
if (displayDiagnostics.length > 0) {
|
|
52088
52234
|
yield* log("");
|
|
52089
|
-
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender });
|
|
52235
|
+
yield* printDiagnostics(displayDiagnostics, verbose, resolveDiagnosticSourceRoot, buildRulePriorityMap(completedScans.map((scan) => scan.result.score)), isCodingAgentEnvironment(), { animateCountUp: animateRender }, shouldRenderHyperlinks(process.stdout));
|
|
52090
52236
|
}
|
|
52091
52237
|
const lowestScoredScan = findLowestScoredScan(completedScans);
|
|
52092
52238
|
const aggregateScore = lowestScoredScan?.result.score ?? null;
|
|
@@ -52124,9 +52270,8 @@ const printMultiProjectSummary = (input) => gen(function* () {
|
|
|
52124
52270
|
});
|
|
52125
52271
|
//#endregion
|
|
52126
52272
|
//#region src/cli/utils/prompt-install-setup.ts
|
|
52127
|
-
const GLOBAL_CONFIG_PROJECT_NAME = "react-doctor";
|
|
52128
52273
|
const getSetupPromptStore = (options = {}) => new Conf({
|
|
52129
|
-
projectName:
|
|
52274
|
+
projectName: REACT_DOCTOR_CONFIG_PROJECT_NAME,
|
|
52130
52275
|
cwd: options.cwd
|
|
52131
52276
|
});
|
|
52132
52277
|
const getSetupPromptProjectKey = (projectRoot) => hashProjectRoot(projectRoot);
|
|
@@ -52137,6 +52282,24 @@ const hasDisabledSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
|
52137
52282
|
return false;
|
|
52138
52283
|
}
|
|
52139
52284
|
};
|
|
52285
|
+
const disableSetupPrompt = (projectRoot, storeOptions = {}) => {
|
|
52286
|
+
try {
|
|
52287
|
+
const store = getSetupPromptStore(storeOptions);
|
|
52288
|
+
const projects = store.get("projects", {});
|
|
52289
|
+
const projectKey = getSetupPromptProjectKey(projectRoot);
|
|
52290
|
+
store.set("projects", {
|
|
52291
|
+
...projects,
|
|
52292
|
+
[projectKey]: {
|
|
52293
|
+
...projects[projectKey] ?? {},
|
|
52294
|
+
rootDirectory: Path.resolve(projectRoot),
|
|
52295
|
+
setupPrompt: false
|
|
52296
|
+
}
|
|
52297
|
+
});
|
|
52298
|
+
return true;
|
|
52299
|
+
} catch {
|
|
52300
|
+
return false;
|
|
52301
|
+
}
|
|
52302
|
+
};
|
|
52140
52303
|
const resolveInstallSetupProjectRoot = (options) => {
|
|
52141
52304
|
if (options.scanDirectories.length === 0) return findNearestPackageDirectory(options.scanRoot) ?? options.scanRoot;
|
|
52142
52305
|
const packageDirectories = /* @__PURE__ */ new Set();
|
|
@@ -52543,6 +52706,14 @@ const runExplain = async (fileLineArgument, context) => {
|
|
|
52543
52706
|
const colorizedRule = colorizeRuleByDiagnostic(ruleIdentifier, diagnostic.severity);
|
|
52544
52707
|
const severityLabel = colorizeRuleByDiagnostic(diagnostic.severity, diagnostic.severity);
|
|
52545
52708
|
cliLogger.log(`${severitySymbol} ${colorizedRule} ${highlighter.dim(`(${severityLabel})`)} — ${diagnostic.message}`);
|
|
52709
|
+
const codeFrame = buildCodeFrame({
|
|
52710
|
+
filePath: diagnostic.filePath,
|
|
52711
|
+
line: diagnostic.line,
|
|
52712
|
+
column: diagnostic.column,
|
|
52713
|
+
endLine: diagnostic.endLine,
|
|
52714
|
+
rootDirectory: targetDirectory
|
|
52715
|
+
});
|
|
52716
|
+
if (codeFrame) cliLogger.log(indentMultilineText(codeFrame, " "));
|
|
52546
52717
|
if (diagnostic.category) cliLogger.dim(` Category: ${diagnostic.category}`);
|
|
52547
52718
|
if (diagnostic.help) cliLogger.dim(` ${diagnostic.help}`);
|
|
52548
52719
|
cliLogger.dim(` If this needs follow-up or looks like a false positive, open: ${buildDiagnosticIssueUrl({
|
|
@@ -52592,6 +52763,10 @@ const validateModeFlags = (flags) => {
|
|
|
52592
52763
|
if (flags.staged && (flags.scope === "full" || flags.scope === "changed")) throw new CliInputError(`Cannot combine --staged with --scope ${flags.scope}; use --scope files or --scope lines, or drop --scope.`);
|
|
52593
52764
|
if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
|
|
52594
52765
|
if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
|
|
52766
|
+
if (flags.debug && (flags.score === false || flags.telemetry === false)) {
|
|
52767
|
+
const disablingFlag = flags.score === false ? "--no-score" : "--no-telemetry";
|
|
52768
|
+
throw new CliInputError(`Cannot combine --debug with ${disablingFlag}; ${disablingFlag} disables the Sentry reporting --debug needs to capture a trace.`);
|
|
52769
|
+
}
|
|
52595
52770
|
};
|
|
52596
52771
|
//#endregion
|
|
52597
52772
|
//#region src/cli/commands/inspect.ts
|
|
@@ -52939,11 +53114,13 @@ const inspectAction = async (directory, flags) => {
|
|
|
52939
53114
|
})) {
|
|
52940
53115
|
printAgentInstallHint();
|
|
52941
53116
|
recordCount(METRIC.agentInstallHintShown, 1);
|
|
53117
|
+
disableSetupPrompt(setupProjectRoot);
|
|
52942
53118
|
}
|
|
52943
53119
|
}
|
|
52944
53120
|
} catch (error) {
|
|
52945
53121
|
const isUserError = isExpectedUserError(error);
|
|
52946
53122
|
const sentryEventId = isUserError ? void 0 : await reportErrorToSentry(error);
|
|
53123
|
+
if (isDebugFlagEnabled()) await flushSentry();
|
|
52947
53124
|
if (isJsonMode) {
|
|
52948
53125
|
writeJsonErrorReport(error, sentryEventId);
|
|
52949
53126
|
process.exitCode = 1;
|
|
@@ -53666,6 +53843,33 @@ const normalizeHelpInvocation = (argv, knownCommands) => {
|
|
|
53666
53843
|
return [...nodeArguments, "--help"];
|
|
53667
53844
|
};
|
|
53668
53845
|
//#endregion
|
|
53846
|
+
//#region src/cli/utils/print-debug-trace.ts
|
|
53847
|
+
/**
|
|
53848
|
+
* The `--debug` end-of-run line, pure so it's testable without the Sentry SDK.
|
|
53849
|
+
* Mirrors the crash-reference phrasing in `handle-error.ts` ("mention this when
|
|
53850
|
+
* reporting") so users learn one habit for both paths. A `null` trace says why,
|
|
53851
|
+
* so `--debug` never silently does nothing.
|
|
53852
|
+
*/
|
|
53853
|
+
const buildDebugTraceMessage = (traceId) => traceId === null ? "Sentry trace unavailable for this run (no trace was recorded)." : `Sentry trace (mention this when reporting): ${traceId}`;
|
|
53854
|
+
/**
|
|
53855
|
+
* Prints the run's Sentry trace id to stderr at the end of a `--debug` run, so
|
|
53856
|
+
* maintainers can pull the full trace from a pasted id. Runs from the process
|
|
53857
|
+
* `exit` handler, so it's the last line on both the success path and the error
|
|
53858
|
+
* funnels (which `process.exit()` before the promise chain could resume).
|
|
53859
|
+
*
|
|
53860
|
+
* Writes straight to `process.stderr` (not `Console`) for three reasons: the
|
|
53861
|
+
* exit handler is synchronous, JSON mode patches the global console to no-ops —
|
|
53862
|
+
* a diagnostic the user explicitly asked for must survive that — and stderr
|
|
53863
|
+
* keeps `--json` / `--score` stdout machine-clean. The write is wrapped because
|
|
53864
|
+
* a diagnostic must never throw out of an exit handler.
|
|
53865
|
+
*/
|
|
53866
|
+
const printDebugTrace = () => {
|
|
53867
|
+
if (!Sentry.isInitialized()) return;
|
|
53868
|
+
try {
|
|
53869
|
+
process.stderr.write(`${highlighter.dim(buildDebugTraceMessage(getLastRunTraceId()))}\n`);
|
|
53870
|
+
} catch {}
|
|
53871
|
+
};
|
|
53872
|
+
//#endregion
|
|
53669
53873
|
//#region src/cli/utils/removed-cli-flags.ts
|
|
53670
53874
|
const REMOVED_FLAGS = new Map([
|
|
53671
53875
|
["--full", "use `--diff false` to force a full scan"],
|
|
@@ -53692,6 +53896,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
53692
53896
|
longOptionsWithoutValues: new Set([
|
|
53693
53897
|
"--color",
|
|
53694
53898
|
"--dead-code",
|
|
53899
|
+
"--debug",
|
|
53695
53900
|
"--help",
|
|
53696
53901
|
"--json",
|
|
53697
53902
|
"--json-compact",
|
|
@@ -53859,6 +54064,9 @@ const stripUnknownCliFlags = (argv) => {
|
|
|
53859
54064
|
initializeSentry();
|
|
53860
54065
|
process.on("SIGINT", exitGracefully);
|
|
53861
54066
|
process.on("SIGTERM", exitGracefully);
|
|
54067
|
+
process.on("exit", () => {
|
|
54068
|
+
if (isDebugFlagEnabled()) printDebugTrace();
|
|
54069
|
+
});
|
|
53862
54070
|
unrefStdin();
|
|
53863
54071
|
guardStdin();
|
|
53864
54072
|
const formatExampleLines = (examples) => {
|
|
@@ -53870,7 +54078,7 @@ ${highlighter.dim("Examples:")}
|
|
|
53870
54078
|
${formatExampleLines([
|
|
53871
54079
|
["react-doctor", "scan the current project"],
|
|
53872
54080
|
["react-doctor ./apps/web", "scan a specific directory"],
|
|
53873
|
-
["react-doctor --
|
|
54081
|
+
["react-doctor --scope changed --base main", "scan only new issues vs. main"],
|
|
53874
54082
|
["react-doctor --project modules/a,modules/b", "score each module separately (names or paths)"],
|
|
53875
54083
|
["react-doctor --staged", "scan staged files (pre-commit hook)"],
|
|
53876
54084
|
["react-doctor --category Security", "show only one diagnostic category"],
|
|
@@ -53903,7 +54111,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
53903
54111
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
53904
54112
|
`;
|
|
53905
54113
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
53906
|
-
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
54114
|
+
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
|
|
53907
54115
|
program.action(inspectAction);
|
|
53908
54116
|
program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
|
|
53909
54117
|
program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
|
|
@@ -53946,4 +54154,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
53946
54154
|
export {};
|
|
53947
54155
|
|
|
53948
54156
|
//# sourceMappingURL=cli.js.map
|
|
53949
|
-
//# debugId=
|
|
54157
|
+
//# debugId=496f49cf-4038-5332-b301-ffedc55210ba
|