react-doctor 0.5.8-dev.986557d → 0.5.8-dev.99f2417
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/LICENSE +14 -1
- package/dist/cli.js +616 -241
- package/dist/index.d.ts +11 -2
- package/dist/index.js +259 -66
- package/dist/lsp.js +200 -67
- package/package.json +9 -8
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]="cf52ec59-cee0-5503-b8f2-a4f98bf77ea4")}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";
|
|
@@ -9041,7 +9041,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
|
|
|
9041
9041
|
* @since 2.0.0
|
|
9042
9042
|
*/
|
|
9043
9043
|
const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
|
|
9044
|
-
const setImmediate = "setImmediate" in globalThis ? (f) => {
|
|
9044
|
+
const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
|
|
9045
9045
|
const timer = globalThis.setImmediate(f);
|
|
9046
9046
|
return () => globalThis.clearImmediate(timer);
|
|
9047
9047
|
} : (f) => {
|
|
@@ -9085,7 +9085,7 @@ var PriorityBuckets = class {
|
|
|
9085
9085
|
var MixedScheduler = class {
|
|
9086
9086
|
executionMode;
|
|
9087
9087
|
setImmediate;
|
|
9088
|
-
constructor(executionMode = "async", setImmediateFn = setImmediate) {
|
|
9088
|
+
constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
|
|
9089
9089
|
this.executionMode = executionMode;
|
|
9090
9090
|
this.setImmediate = setImmediateFn;
|
|
9091
9091
|
}
|
|
@@ -9110,7 +9110,7 @@ var MixedSchedulerDispatcher = class {
|
|
|
9110
9110
|
tasks = /* @__PURE__ */ new PriorityBuckets();
|
|
9111
9111
|
running = void 0;
|
|
9112
9112
|
setImmediate;
|
|
9113
|
-
constructor(setImmediateFn = setImmediate) {
|
|
9113
|
+
constructor(setImmediateFn = setImmediate$1) {
|
|
9114
9114
|
this.setImmediate = setImmediateFn;
|
|
9115
9115
|
}
|
|
9116
9116
|
/**
|
|
@@ -36862,6 +36862,7 @@ const DOCS_URL = "https://react.doctor/docs";
|
|
|
36862
36862
|
const DOCS_RULES_BASE_URL = `${DOCS_URL}/rules`;
|
|
36863
36863
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
36864
36864
|
const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
|
|
36865
|
+
const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
|
|
36865
36866
|
const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
|
|
36866
36867
|
const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
|
|
36867
36868
|
const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
|
|
@@ -36946,6 +36947,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
|
|
|
36946
36947
|
const LINT_PHASE_TIMEOUT_MS = 3e5;
|
|
36947
36948
|
const SCAN_TOTAL_DEADLINE_MS = 9e5;
|
|
36948
36949
|
const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
|
|
36950
|
+
const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
|
|
36949
36951
|
const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
|
|
36950
36952
|
const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
|
|
36951
36953
|
const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
|
|
@@ -37679,7 +37681,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
|
|
|
37679
37681
|
NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
|
|
37680
37682
|
}
|
|
37681
37683
|
const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
|
|
37682
|
-
const canonicalizeRuleKey = (ruleKey) =>
|
|
37684
|
+
const canonicalizeRuleKey = (ruleKey) => {
|
|
37685
|
+
const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
|
|
37686
|
+
return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
|
|
37687
|
+
};
|
|
37683
37688
|
const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
|
|
37684
37689
|
const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
|
|
37685
37690
|
const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
|
|
@@ -38182,6 +38187,7 @@ const collectStringSet = (values) => {
|
|
|
38182
38187
|
if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
|
|
38183
38188
|
return new Set(values.filter((value) => typeof value === "string"));
|
|
38184
38189
|
};
|
|
38190
|
+
const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
|
|
38185
38191
|
/**
|
|
38186
38192
|
* Pre-compiles every stateful filter and returns a single
|
|
38187
38193
|
* `apply(diagnostic)` closure that runs (in order):
|
|
@@ -38215,8 +38221,10 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
38215
38221
|
const compiledOverrides = compileIgnoreOverrides(userConfig);
|
|
38216
38222
|
const textComponentNames = collectStringSet(userConfig?.textComponents);
|
|
38217
38223
|
const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
|
|
38224
|
+
const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
|
|
38218
38225
|
const hasTextComponents = textComponentNames.size > 0;
|
|
38219
38226
|
const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
|
|
38227
|
+
const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
|
|
38220
38228
|
const fileLinesCache = /* @__PURE__ */ new Map();
|
|
38221
38229
|
const fileContextCache = /* @__PURE__ */ new Map();
|
|
38222
38230
|
const libraryFileCache = /* @__PURE__ */ new Map();
|
|
@@ -38268,6 +38276,12 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
38268
38276
|
if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
|
|
38269
38277
|
return false;
|
|
38270
38278
|
};
|
|
38279
|
+
const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
|
|
38280
|
+
if (!hasRuntimeGlobals) return false;
|
|
38281
|
+
if (diagnostic.rule !== "jsx-no-undef") return false;
|
|
38282
|
+
const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
|
|
38283
|
+
return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
|
|
38284
|
+
};
|
|
38271
38285
|
return { apply: (diagnostic) => {
|
|
38272
38286
|
if (shouldAutoSuppress(diagnostic)) return null;
|
|
38273
38287
|
let current = diagnostic;
|
|
@@ -38292,6 +38306,7 @@ const buildDiagnosticPipeline = (input) => {
|
|
|
38292
38306
|
if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
|
|
38293
38307
|
if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
|
|
38294
38308
|
if (isRnRawTextSuppressedByConfig(current)) return null;
|
|
38309
|
+
if (isJsxNoUndefSuppressedByConfig(current)) return null;
|
|
38295
38310
|
}
|
|
38296
38311
|
if (respectInlineDisables && current.line > 0) {
|
|
38297
38312
|
const lines = getFileLines(current.filePath);
|
|
@@ -38553,7 +38568,7 @@ const resolveScanConcurrency = (requested) => {
|
|
|
38553
38568
|
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
38554
38569
|
return Math.min(Math.floor(requested), 32);
|
|
38555
38570
|
};
|
|
38556
|
-
const readSystemFacts = () => ({
|
|
38571
|
+
const readSystemFacts$1 = () => ({
|
|
38557
38572
|
availableCores: os.availableParallelism(),
|
|
38558
38573
|
totalMemoryBytes: os.totalmem(),
|
|
38559
38574
|
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
@@ -38574,7 +38589,7 @@ const readSystemFacts = () => ({
|
|
|
38574
38589
|
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
38575
38590
|
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
38576
38591
|
*/
|
|
38577
|
-
const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
|
|
38592
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
|
|
38578
38593
|
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
38579
38594
|
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
38580
38595
|
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
@@ -38765,6 +38780,13 @@ const BOOLEAN_FIELD_NAMES = [
|
|
|
38765
38780
|
"adoptExistingLintConfig"
|
|
38766
38781
|
];
|
|
38767
38782
|
const STRING_FIELD_NAMES = ["rootDir"];
|
|
38783
|
+
const STRING_ARRAY_FIELD_NAMES = [
|
|
38784
|
+
"projects",
|
|
38785
|
+
"textComponents",
|
|
38786
|
+
"rawTextWrapperComponents",
|
|
38787
|
+
"runtimeGlobals",
|
|
38788
|
+
"serverAuthFunctionNames"
|
|
38789
|
+
];
|
|
38768
38790
|
const SURFACE_CONTROL_FIELD_NAMES = [
|
|
38769
38791
|
"includeTags",
|
|
38770
38792
|
"excludeTags",
|
|
@@ -38866,6 +38888,7 @@ const validateConfigTypes = (config) => {
|
|
|
38866
38888
|
const validated = { ...config };
|
|
38867
38889
|
for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
|
|
38868
38890
|
for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
|
|
38891
|
+
for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
|
|
38869
38892
|
applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
|
|
38870
38893
|
for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
|
|
38871
38894
|
applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
|
|
@@ -40128,7 +40151,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
|
|
|
40128
40151
|
}
|
|
40129
40152
|
return true;
|
|
40130
40153
|
};
|
|
40131
|
-
const
|
|
40154
|
+
const yieldToEventLoop = () => new Promise((resolve) => {
|
|
40155
|
+
setImmediate(resolve);
|
|
40156
|
+
});
|
|
40157
|
+
const createSecurityScanSession = (rootDirectory, options) => {
|
|
40132
40158
|
const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
|
|
40133
40159
|
const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
|
|
40134
40160
|
const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
|
|
@@ -40143,7 +40169,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
40143
40169
|
committedFilesOnly: rule.committedFilesOnly === true
|
|
40144
40170
|
}];
|
|
40145
40171
|
});
|
|
40146
|
-
if (enabledScanRules.length === 0) return
|
|
40172
|
+
if (enabledScanRules.length === 0) return null;
|
|
40147
40173
|
const diagnostics = [];
|
|
40148
40174
|
const seen = /* @__PURE__ */ new Set();
|
|
40149
40175
|
const gitIgnoredCache = /* @__PURE__ */ new Map();
|
|
@@ -40155,15 +40181,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
40155
40181
|
}
|
|
40156
40182
|
return status === true;
|
|
40157
40183
|
};
|
|
40158
|
-
|
|
40159
|
-
|
|
40160
|
-
|
|
40161
|
-
|
|
40162
|
-
|
|
40163
|
-
|
|
40164
|
-
|
|
40184
|
+
const scanFile = (file) => {
|
|
40185
|
+
for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
|
|
40186
|
+
if (committedFilesOnly && isFileGitIgnored(file)) continue;
|
|
40187
|
+
const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
|
|
40188
|
+
const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
|
|
40189
|
+
if (seen.has(key)) continue;
|
|
40190
|
+
seen.add(key);
|
|
40191
|
+
diagnostics.push(diagnostic);
|
|
40192
|
+
}
|
|
40193
|
+
};
|
|
40194
|
+
return {
|
|
40195
|
+
scanFile,
|
|
40196
|
+
diagnostics
|
|
40197
|
+
};
|
|
40198
|
+
};
|
|
40199
|
+
const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
|
|
40200
|
+
const session = createSecurityScanSession(rootDirectory, options);
|
|
40201
|
+
if (session === null) return [];
|
|
40202
|
+
let filesSinceYield = 0;
|
|
40203
|
+
for (const file of collectSecurityScanFiles(rootDirectory)) {
|
|
40204
|
+
session.scanFile(file);
|
|
40205
|
+
filesSinceYield += 1;
|
|
40206
|
+
if (filesSinceYield >= 16) {
|
|
40207
|
+
filesSinceYield = 0;
|
|
40208
|
+
await yieldToEventLoop();
|
|
40209
|
+
}
|
|
40165
40210
|
}
|
|
40166
|
-
return diagnostics;
|
|
40211
|
+
return session.diagnostics;
|
|
40167
40212
|
};
|
|
40168
40213
|
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
40169
40214
|
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
@@ -40396,6 +40441,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
40396
40441
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
40397
40442
|
};
|
|
40398
40443
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
40444
|
+
const readSystemFacts = () => ({
|
|
40445
|
+
availableCores: os.availableParallelism(),
|
|
40446
|
+
totalMemoryBytes: os.totalmem(),
|
|
40447
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
40448
|
+
});
|
|
40449
|
+
/**
|
|
40450
|
+
* How many real deslop dead-code child processes may run at once, across the
|
|
40451
|
+
* concurrent per-project `runInspect` fibers of one CLI run. The cap is the
|
|
40452
|
+
* smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
|
|
40453
|
+
* workers that fit in available memory, floored at 1.
|
|
40454
|
+
*
|
|
40455
|
+
* On a roomy dev box / CI runner this resolves high enough that every
|
|
40456
|
+
* concurrently-scanned project still spawns its own worker (no serialization vs
|
|
40457
|
+
* the prior uncapped behavior); on a memory-constrained runner it collapses
|
|
40458
|
+
* toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
|
|
40459
|
+
* instead of oversubscribing memory with N simultaneous children — the global
|
|
40460
|
+
* cap the per-project spawn path lacked.
|
|
40461
|
+
*
|
|
40462
|
+
* Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
|
|
40463
|
+
* heavier dead-code worker. `facts` is injectable for tests.
|
|
40464
|
+
*/
|
|
40465
|
+
const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
|
|
40466
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
40467
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
|
|
40468
|
+
return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
40469
|
+
};
|
|
40470
|
+
let availableSlots = -1;
|
|
40471
|
+
const waiters = [];
|
|
40472
|
+
const releaseSlot = () => {
|
|
40473
|
+
const nextWaiter = waiters.shift();
|
|
40474
|
+
if (nextWaiter !== void 0) nextWaiter();
|
|
40475
|
+
else availableSlots += 1;
|
|
40476
|
+
};
|
|
40477
|
+
/**
|
|
40478
|
+
* Runs `task` once a dead-code worker slot is free, releasing the slot when the
|
|
40479
|
+
* task settles (success or failure). With a high cap (roomy machine) every
|
|
40480
|
+
* caller proceeds immediately; with a low cap (constrained runner) callers
|
|
40481
|
+
* queue and run as slots free.
|
|
40482
|
+
*
|
|
40483
|
+
* `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
|
|
40484
|
+
* this caller is queued, the call rejects without acquiring a slot or running
|
|
40485
|
+
* `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
|
|
40486
|
+
* then spawn a child only to tear it down. A queued caller that aborts removes
|
|
40487
|
+
* its own waiter so a later release never hands a slot to a dead request.
|
|
40488
|
+
*/
|
|
40489
|
+
const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
40490
|
+
if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
|
|
40491
|
+
if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
|
|
40492
|
+
if (availableSlots > 0) availableSlots -= 1;
|
|
40493
|
+
else await new Promise((resolve, reject) => {
|
|
40494
|
+
const waiter = () => {
|
|
40495
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
40496
|
+
resolve();
|
|
40497
|
+
};
|
|
40498
|
+
const onAbort = () => {
|
|
40499
|
+
const queuedIndex = waiters.indexOf(waiter);
|
|
40500
|
+
if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
|
|
40501
|
+
reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
|
|
40502
|
+
};
|
|
40503
|
+
waiters.push(waiter);
|
|
40504
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
40505
|
+
});
|
|
40506
|
+
try {
|
|
40507
|
+
return await task();
|
|
40508
|
+
} finally {
|
|
40509
|
+
releaseSlot();
|
|
40510
|
+
}
|
|
40511
|
+
};
|
|
40399
40512
|
/**
|
|
40400
40513
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
40401
40514
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -40420,6 +40533,11 @@ const toCanonicalPath = (filePath) => {
|
|
|
40420
40533
|
};
|
|
40421
40534
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40422
40535
|
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
40536
|
+
const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
|
|
40537
|
+
"react-doctor",
|
|
40538
|
+
"eslint-plugin-react-doctor",
|
|
40539
|
+
"oxlint-plugin-react-doctor"
|
|
40540
|
+
]);
|
|
40423
40541
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40424
40542
|
const inputChunks = [];
|
|
40425
40543
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40686,14 +40804,17 @@ const checkDeadCode = async (options) => {
|
|
|
40686
40804
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40687
40805
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40688
40806
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
40689
|
-
const
|
|
40690
|
-
|
|
40691
|
-
|
|
40692
|
-
|
|
40693
|
-
|
|
40694
|
-
|
|
40695
|
-
|
|
40696
|
-
|
|
40807
|
+
const spawnAndRun = () => {
|
|
40808
|
+
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40809
|
+
rootDirectory,
|
|
40810
|
+
entryPatterns,
|
|
40811
|
+
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
40812
|
+
ignorePatterns,
|
|
40813
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
40814
|
+
parseConcurrency: options.parseConcurrency
|
|
40815
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
40816
|
+
};
|
|
40817
|
+
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
40697
40818
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
40698
40819
|
const diagnostics = [];
|
|
40699
40820
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -40722,6 +40843,7 @@ const checkDeadCode = async (options) => {
|
|
|
40722
40843
|
});
|
|
40723
40844
|
}
|
|
40724
40845
|
for (const unusedDependency of result.unusedDependencies) {
|
|
40846
|
+
if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
|
|
40725
40847
|
const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
|
|
40726
40848
|
diagnostics.push({
|
|
40727
40849
|
filePath: "package.json",
|
|
@@ -41094,43 +41216,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41094
41216
|
* reason: GitInvocationFailed })` so the rest of the codebase
|
|
41095
41217
|
* sees a single failure channel.
|
|
41096
41218
|
*/
|
|
41097
|
-
const runCommand = (input) =>
|
|
41098
|
-
const
|
|
41099
|
-
cwd: input.directory,
|
|
41100
|
-
env: input.env,
|
|
41101
|
-
extendEnv: true
|
|
41102
|
-
}));
|
|
41103
|
-
const maxStdoutBytes = input.maxStdoutBytes;
|
|
41104
|
-
const stdoutByteCount = yield* make$13(0);
|
|
41105
|
-
const [stdout, stderr, status] = yield* all([
|
|
41106
|
-
mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41107
|
-
args: [...input.args],
|
|
41108
|
-
directory: input.directory,
|
|
41109
|
-
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
41110
|
-
}) })) : void_)))))),
|
|
41111
|
-
mkString(decodeText(handle.stderr)),
|
|
41112
|
-
handle.exitCode
|
|
41113
|
-
], { concurrency: 3 });
|
|
41114
|
-
return {
|
|
41115
|
-
status,
|
|
41116
|
-
stdout,
|
|
41117
|
-
stderr
|
|
41118
|
-
};
|
|
41119
|
-
})).pipe(catchTag$1("PlatformError", (cause) => {
|
|
41120
|
-
if (input.command !== "git") return succeed$2({
|
|
41219
|
+
const runCommand = (input) => {
|
|
41220
|
+
const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
|
|
41121
41221
|
status: 127,
|
|
41122
41222
|
stdout: "",
|
|
41123
41223
|
stderr: String(cause)
|
|
41124
|
-
})
|
|
41125
|
-
return new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41224
|
+
}) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41126
41225
|
args: [...input.args],
|
|
41127
41226
|
directory: input.directory,
|
|
41128
41227
|
cause
|
|
41129
|
-
}) });
|
|
41130
|
-
|
|
41131
|
-
|
|
41132
|
-
|
|
41133
|
-
|
|
41228
|
+
}) }));
|
|
41229
|
+
return scoped(gen(function* () {
|
|
41230
|
+
if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
|
|
41231
|
+
const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
|
|
41232
|
+
if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
|
|
41233
|
+
const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
|
|
41234
|
+
cwd: input.directory,
|
|
41235
|
+
env: input.env,
|
|
41236
|
+
extendEnv: true
|
|
41237
|
+
}));
|
|
41238
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
41239
|
+
const stdoutByteCount = yield* make$13(0);
|
|
41240
|
+
const [stdout, stderr, status] = yield* all([
|
|
41241
|
+
mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41242
|
+
args: [...input.args],
|
|
41243
|
+
directory: input.directory,
|
|
41244
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
41245
|
+
}) })) : void_)))))),
|
|
41246
|
+
mkString(decodeText(handle.stderr)),
|
|
41247
|
+
handle.exitCode
|
|
41248
|
+
], { concurrency: 3 });
|
|
41249
|
+
return {
|
|
41250
|
+
status,
|
|
41251
|
+
stdout,
|
|
41252
|
+
stderr
|
|
41253
|
+
};
|
|
41254
|
+
})).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
|
|
41255
|
+
"git.command": input.command,
|
|
41256
|
+
"git.subcommand": input.args[0] ?? ""
|
|
41257
|
+
} }));
|
|
41258
|
+
};
|
|
41134
41259
|
const runGit = (directory, args) => runCommand({
|
|
41135
41260
|
command: "git",
|
|
41136
41261
|
args,
|
|
@@ -41163,7 +41288,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41163
41288
|
"rev-parse",
|
|
41164
41289
|
"--verify",
|
|
41165
41290
|
branch
|
|
41166
|
-
]).pipe(map$3((result) => result.status === 0));
|
|
41291
|
+
]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
|
|
41167
41292
|
const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
|
|
41168
41293
|
const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
|
|
41169
41294
|
"merge-base",
|
|
@@ -41377,7 +41502,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41377
41502
|
]);
|
|
41378
41503
|
if (result.status !== 0) return null;
|
|
41379
41504
|
return parseChangedLineRanges(result.stdout);
|
|
41380
|
-
}).pipe(withSpan("Git.changedLineRanges"))
|
|
41505
|
+
}).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
|
|
41381
41506
|
});
|
|
41382
41507
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
41383
41508
|
/**
|
|
@@ -43854,7 +43979,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43854
43979
|
* diagnostics).
|
|
43855
43980
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
43856
43981
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
43857
|
-
* expo/react-native
|
|
43982
|
+
* expo/react-native), collected synchronously. The heavier
|
|
43983
|
+
* content-regex security scan is forked instead (like supply-chain
|
|
43984
|
+
* below) and joined before the concat, so its CPU overlaps lint
|
|
43985
|
+
* rather than blocking the event loop before it.
|
|
43858
43986
|
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
43859
43987
|
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
43860
43988
|
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
@@ -43874,7 +44002,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43874
44002
|
* order, so terminal output is identical either way; supply-chain
|
|
43875
44003
|
* rides alongside without a spinner.
|
|
43876
44004
|
* 6. Join the supply-chain fiber, then assemble the diagnostics in a
|
|
43877
|
-
* FIXED order (env, supply-chain, lint, dead-code) so the output is
|
|
44005
|
+
* FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
|
|
43878
44006
|
* byte-identical regardless of which fiber settled first. The
|
|
43879
44007
|
* viewer-permission fiber is joined later, during score-metadata
|
|
43880
44008
|
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
@@ -43935,12 +44063,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43935
44063
|
...checkPnpmHardening(scanDirectory),
|
|
43936
44064
|
...checkReactServerComponentsAdvisory(scanDirectory, project),
|
|
43937
44065
|
...checkExpoProject(scanDirectory, project),
|
|
43938
|
-
...checkReactNativeProject(scanDirectory, project)
|
|
43939
|
-
...checkSecurityScan(scanDirectory, {
|
|
43940
|
-
project,
|
|
43941
|
-
ignoredTags: input.ignoredTags
|
|
43942
|
-
})
|
|
44066
|
+
...checkReactNativeProject(scanDirectory, project)
|
|
43943
44067
|
])));
|
|
44068
|
+
const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
|
|
44069
|
+
project,
|
|
44070
|
+
ignoredTags: input.ignoredTags
|
|
44071
|
+
})).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
|
|
43944
44072
|
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
43945
44073
|
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
43946
44074
|
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
@@ -44076,9 +44204,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44076
44204
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
44077
44205
|
const supplyChainResult = yield* join(supplyChainFiber);
|
|
44078
44206
|
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
44207
|
+
const securityScanCollected = yield* join(securityScanFiber);
|
|
44079
44208
|
yield* reporterService.finalize;
|
|
44080
44209
|
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
44081
44210
|
...envCollected,
|
|
44211
|
+
...securityScanCollected,
|
|
44082
44212
|
...supplyChainCollected,
|
|
44083
44213
|
...lintCollected,
|
|
44084
44214
|
...deadCodeCollected
|
|
@@ -44617,6 +44747,61 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
44617
44747
|
}
|
|
44618
44748
|
return crypto.createHash("sha1").update(parts.join("|")).digest("hex");
|
|
44619
44749
|
};
|
|
44750
|
+
const AI_TRAINING_ENV_VARS = [
|
|
44751
|
+
["HF_DATASETS_CACHE", "huggingface"],
|
|
44752
|
+
["HF_HOME", "huggingface"],
|
|
44753
|
+
["HUGGINGFACE_HUB_CACHE", "huggingface"],
|
|
44754
|
+
["SPACE_ID", "huggingface-spaces"],
|
|
44755
|
+
["CUDA_VISIBLE_DEVICES", "cuda"],
|
|
44756
|
+
["NVIDIA_VISIBLE_DEVICES", "nvidia"],
|
|
44757
|
+
["ROCR_VISIBLE_DEVICES", "rocm"],
|
|
44758
|
+
["TPU_NAME", "google-tpu"],
|
|
44759
|
+
["WANDB_RUN_ID", "wandb"],
|
|
44760
|
+
["WANDB_SWEEP_ID", "wandb"],
|
|
44761
|
+
["MLFLOW_RUN_ID", "mlflow"],
|
|
44762
|
+
["MLFLOW_TRACKING_URI", "mlflow"],
|
|
44763
|
+
["COMET_EXPERIMENT_KEY", "comet"],
|
|
44764
|
+
["NEPTUNE_RUN_ID", "neptune"],
|
|
44765
|
+
["CLEARML_TASK_ID", "clearml"],
|
|
44766
|
+
["DVC_STAGE", "dvc"],
|
|
44767
|
+
["RAY_WORKER_PROCESS", "ray"],
|
|
44768
|
+
["RAY_ADDRESS", "ray"],
|
|
44769
|
+
["MUJOCO_GL", "mujoco"],
|
|
44770
|
+
["MUJOCO_PATH", "mujoco"],
|
|
44771
|
+
["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
|
|
44772
|
+
["SM_TRAINING_ENV", "sagemaker"],
|
|
44773
|
+
["TRAINING_JOB_ARN", "sagemaker"],
|
|
44774
|
+
["SAGEMAKER_BASE_DIR", "sagemaker"],
|
|
44775
|
+
["AZUREML_RUN_ID", "azure-ml"],
|
|
44776
|
+
["AZURE_ML_MODEL_DIR", "azure-ml"],
|
|
44777
|
+
["CLOUD_ML_PROJECT_ID", "vertex-ai"],
|
|
44778
|
+
["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
|
|
44779
|
+
["DET_MASTER", "determined-ai"],
|
|
44780
|
+
["LIGHTNING_USER_ID", "lightning-ai"],
|
|
44781
|
+
["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
|
|
44782
|
+
["ARGO_WORKFLOW_NAME", "argo-workflows"],
|
|
44783
|
+
["KFP_POD_NAME", "kubeflow-pipelines"],
|
|
44784
|
+
["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
|
|
44785
|
+
["COLAB_BACKEND_VERSION", "google-colab"],
|
|
44786
|
+
["DATABRICKS_RUNTIME_VERSION", "databricks"],
|
|
44787
|
+
["DAYTONA_WS_ID", "daytona"],
|
|
44788
|
+
["DAYTONA_WS_NAME", "daytona"],
|
|
44789
|
+
["E2B_SANDBOX_ID", "e2b"],
|
|
44790
|
+
["MODAL_FUNCTION_ID", "modal"],
|
|
44791
|
+
["MODAL_TASK_ID", "modal"],
|
|
44792
|
+
["RUNPOD_POD_ID", "runpod"],
|
|
44793
|
+
["REPLICATE_USERNAME", "replicate"],
|
|
44794
|
+
["VAST_CONTAINERLABEL", "vast-ai"],
|
|
44795
|
+
["HARBOR_URL", "harbor"],
|
|
44796
|
+
["HARBOR_HOSTNAME", "harbor"],
|
|
44797
|
+
["SWE_BENCH_TASK", "swe-bench"],
|
|
44798
|
+
["SWEBENCH_TASK", "swe-bench"],
|
|
44799
|
+
["SWE_AGENT_MODEL", "swe-agent"]
|
|
44800
|
+
];
|
|
44801
|
+
const detectAiTrainingEnvironment = () => {
|
|
44802
|
+
for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
|
|
44803
|
+
return null;
|
|
44804
|
+
};
|
|
44620
44805
|
const groupBy = (items, keyFn) => {
|
|
44621
44806
|
const groups = /* @__PURE__ */ new Map();
|
|
44622
44807
|
for (const item of items) {
|
|
@@ -44702,7 +44887,25 @@ const CI_PROVIDER_BY_ENVIRONMENT_VARIABLE = [
|
|
|
44702
44887
|
["TEAMCITY_VERSION", "teamcity"],
|
|
44703
44888
|
["BITBUCKET_BUILD_NUMBER", "bitbucket"],
|
|
44704
44889
|
["TRAVIS", "travis"],
|
|
44705
|
-
["DRONE", "drone"]
|
|
44890
|
+
["DRONE", "drone"],
|
|
44891
|
+
["SEMAPHORE", "semaphore"],
|
|
44892
|
+
["APPVEYOR", "appveyor"],
|
|
44893
|
+
["HARNESS_BUILD_ID", "harness"],
|
|
44894
|
+
["BUDDY", "buddy"],
|
|
44895
|
+
["CF_BUILD_ID", "codefresh"],
|
|
44896
|
+
["NETLIFY", "netlify"],
|
|
44897
|
+
["RAILWAY_SERVICE_ID", "railway"],
|
|
44898
|
+
["VERCEL", "vercel"],
|
|
44899
|
+
["CM_BUILD_ID", "codemagic"],
|
|
44900
|
+
["PROW_JOB_ID", "prow"],
|
|
44901
|
+
["AGOLA_GIT_REF", "agola"],
|
|
44902
|
+
["CIRRUS_CI", "cirrus-ci"],
|
|
44903
|
+
["BLACKSMITH_STICKYDISK_TOKEN", "blacksmith"],
|
|
44904
|
+
["WARPBUILD_RUNNER_VERIFICATION_TOKEN", "warpbuild"],
|
|
44905
|
+
["NSC_CACHE_PATH", "namespace"],
|
|
44906
|
+
["UBICLOUD_CACHE_URL", "ubicloud"],
|
|
44907
|
+
["RENDER", "render"],
|
|
44908
|
+
["FLY_APP_NAME", "fly-io"]
|
|
44706
44909
|
];
|
|
44707
44910
|
const GITHUB_ACTION_MARKER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_GITHUB_ACTION";
|
|
44708
44911
|
const ACTION_INPUT_ENVIRONMENT_VARIABLES = {
|
|
@@ -44720,7 +44923,10 @@ const CODING_AGENT_BY_ENVIRONMENT_VARIABLE = [
|
|
|
44720
44923
|
["CODEX_SANDBOX_NETWORK_DISABLED", "codex"],
|
|
44721
44924
|
["OPENCODE", "opencode"],
|
|
44722
44925
|
["GOOSE_TERMINAL", "goose"],
|
|
44723
|
-
["AMP_THREAD_ID", "amp"]
|
|
44926
|
+
["AMP_THREAD_ID", "amp"],
|
|
44927
|
+
["CLINE_ACTIVE", "cline"],
|
|
44928
|
+
["AUGMENT_AGENT", "augment"],
|
|
44929
|
+
["TRAE_AI_SHELL_ID", "trae-ai"]
|
|
44724
44930
|
];
|
|
44725
44931
|
const GENERIC_CODING_AGENT_ENVIRONMENT_VARIABLES = ["AGENT_SESSION_ID", "AGENT_THREAD_ID"];
|
|
44726
44932
|
const CODING_AGENT_ENVIRONMENT_VALUE_VARIABLES = ["AGENT"];
|
|
@@ -44839,6 +45045,7 @@ const NANOSECONDS_PER_SECOND = 1000000000n;
|
|
|
44839
45045
|
const METRIC = {
|
|
44840
45046
|
cliInvoked: "cli.invoked",
|
|
44841
45047
|
cliError: "cli.error",
|
|
45048
|
+
cliEnvironmentError: "cli.env_error",
|
|
44842
45049
|
projectDetected: "project.detected",
|
|
44843
45050
|
projectPathSelected: "project.path_selected",
|
|
44844
45051
|
projectConfigSelected: "project.config_selected",
|
|
@@ -44868,7 +45075,9 @@ const METRIC = {
|
|
|
44868
45075
|
lspSessionStarted: "lsp.session.started",
|
|
44869
45076
|
lspScanCompleted: "lsp.scan.completed",
|
|
44870
45077
|
lspScanDuration: "lsp.scan.duration",
|
|
44871
|
-
lspScanDiagnostics: "lsp.scan.diagnostics"
|
|
45078
|
+
lspScanDiagnostics: "lsp.scan.diagnostics",
|
|
45079
|
+
aiTrainingWarningShown: "ai.training.warning_shown",
|
|
45080
|
+
jsonOutUsed: "json.out_used"
|
|
44872
45081
|
};
|
|
44873
45082
|
//#endregion
|
|
44874
45083
|
//#region src/cli/utils/noop-console.ts
|
|
@@ -44911,7 +45120,7 @@ const makeNoopConsole = () => ({
|
|
|
44911
45120
|
});
|
|
44912
45121
|
//#endregion
|
|
44913
45122
|
//#region src/cli/utils/version.ts
|
|
44914
|
-
const VERSION = "0.5.8-dev.
|
|
45123
|
+
const VERSION = "0.5.8-dev.99f2417";
|
|
44915
45124
|
//#endregion
|
|
44916
45125
|
//#region src/cli/utils/json-mode.ts
|
|
44917
45126
|
let context = null;
|
|
@@ -44949,12 +45158,13 @@ const installSilentConsole = () => {
|
|
|
44949
45158
|
"trace"
|
|
44950
45159
|
]) target[key] = source[key];
|
|
44951
45160
|
};
|
|
44952
|
-
const enableJsonMode = ({ compact, directory }) => {
|
|
45161
|
+
const enableJsonMode = ({ compact, directory, outputFile }) => {
|
|
44953
45162
|
context = {
|
|
44954
45163
|
compact,
|
|
44955
45164
|
directory,
|
|
44956
45165
|
startTime: performance$1.now(),
|
|
44957
|
-
mode: "full"
|
|
45166
|
+
mode: "full",
|
|
45167
|
+
outputFile: outputFile ?? null
|
|
44958
45168
|
};
|
|
44959
45169
|
installSilentConsole();
|
|
44960
45170
|
};
|
|
@@ -44967,7 +45177,11 @@ const setJsonReportMode = (mode) => {
|
|
|
44967
45177
|
};
|
|
44968
45178
|
const writeJsonReport = (report) => {
|
|
44969
45179
|
const serialized = context?.compact ? JSON.stringify(report) : JSON.stringify(report, null, 2);
|
|
44970
|
-
|
|
45180
|
+
if (context?.outputFile) {
|
|
45181
|
+
const resolvedPath = Path.resolve(context.outputFile);
|
|
45182
|
+
NFS.mkdirSync(Path.dirname(resolvedPath), { recursive: true });
|
|
45183
|
+
NFS.writeFileSync(resolvedPath, `${serialized}\n`);
|
|
45184
|
+
} else process.stdout.write(`${serialized}\n`);
|
|
44971
45185
|
};
|
|
44972
45186
|
const writeJsonErrorReport = (error, sentryEventId) => {
|
|
44973
45187
|
if (!context) return;
|
|
@@ -45275,13 +45489,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
45275
45489
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
45276
45490
|
* standard `SENTRY_RELEASE` override.
|
|
45277
45491
|
*/
|
|
45278
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.
|
|
45492
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.99f2417`;
|
|
45279
45493
|
/**
|
|
45280
45494
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
45281
45495
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
45282
45496
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
45283
45497
|
*/
|
|
45284
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.
|
|
45498
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.99f2417") ? "development" : "production");
|
|
45285
45499
|
/**
|
|
45286
45500
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
45287
45501
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -48877,6 +49091,25 @@ const shouldBlockCi = (diagnostics, blockingLevel) => {
|
|
|
48877
49091
|
*/
|
|
48878
49092
|
const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
48879
49093
|
//#endregion
|
|
49094
|
+
//#region src/cli/utils/with-namespace.ts
|
|
49095
|
+
/**
|
|
49096
|
+
* Prefixes every key in a flat attribute group with `namespace.`, so a logical
|
|
49097
|
+
* group (scan config, diagnostics rollup, score, …) carries one dotted
|
|
49098
|
+
* namespace applied in a single place rather than hand-spelled into each key —
|
|
49099
|
+
* which is how the wide event drifted into a flat, half-namespaced soup. The
|
|
49100
|
+
* dotted prefix is what makes the attributes tree up in Sentry's attribute
|
|
49101
|
+
* browser and stay group-/filter-/aggregate-able in the Spans dataset.
|
|
49102
|
+
*
|
|
49103
|
+
* Value types are preserved verbatim (numbers stay numbers so Sentry infers a
|
|
49104
|
+
* numeric attribute and `p75(...)`/`avg(...)` work; `null` is kept so
|
|
49105
|
+
* `toSpanAttributes` can drop absent signals rather than coerce them).
|
|
49106
|
+
*/
|
|
49107
|
+
const withNamespace = (namespace, attributes) => {
|
|
49108
|
+
const namespaced = {};
|
|
49109
|
+
for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
|
|
49110
|
+
return namespaced;
|
|
49111
|
+
};
|
|
49112
|
+
//#endregion
|
|
48880
49113
|
//#region src/cli/utils/build-run-event.ts
|
|
48881
49114
|
const readEnvBoolean = (name) => {
|
|
48882
49115
|
const value = process.env[name];
|
|
@@ -48898,12 +49131,12 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48898
49131
|
if (input.result === void 0) {
|
|
48899
49132
|
const error = input.error;
|
|
48900
49133
|
const known = isReactDoctorError(error);
|
|
48901
|
-
return {
|
|
48902
|
-
|
|
49134
|
+
return withNamespace("outcome", {
|
|
49135
|
+
status: "error",
|
|
48903
49136
|
exitCode: 1,
|
|
48904
49137
|
knownError: known,
|
|
48905
49138
|
errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
|
|
48906
|
-
};
|
|
49139
|
+
});
|
|
48907
49140
|
}
|
|
48908
49141
|
const result = input.result;
|
|
48909
49142
|
const summary = summarizeDiagnostics(result.diagnostics);
|
|
@@ -48937,66 +49170,82 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48937
49170
|
}
|
|
48938
49171
|
let fixGroupedFindings = 0;
|
|
48939
49172
|
for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
|
|
49173
|
+
const categoryRollup = {};
|
|
49174
|
+
for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
|
|
48940
49175
|
const attributes = {
|
|
48941
|
-
outcome,
|
|
48942
|
-
|
|
48943
|
-
|
|
48944
|
-
|
|
48945
|
-
|
|
48946
|
-
|
|
48947
|
-
|
|
48948
|
-
|
|
48949
|
-
|
|
48950
|
-
|
|
48951
|
-
|
|
48952
|
-
|
|
48953
|
-
|
|
48954
|
-
|
|
48955
|
-
|
|
48956
|
-
|
|
48957
|
-
|
|
48958
|
-
|
|
48959
|
-
|
|
48960
|
-
|
|
48961
|
-
|
|
48962
|
-
|
|
48963
|
-
|
|
48964
|
-
|
|
48965
|
-
|
|
48966
|
-
|
|
48967
|
-
|
|
48968
|
-
|
|
48969
|
-
|
|
48970
|
-
|
|
48971
|
-
|
|
48972
|
-
|
|
48973
|
-
|
|
48974
|
-
|
|
48975
|
-
|
|
48976
|
-
|
|
48977
|
-
|
|
48978
|
-
|
|
48979
|
-
|
|
48980
|
-
|
|
48981
|
-
|
|
48982
|
-
|
|
48983
|
-
|
|
49176
|
+
...withNamespace("outcome", {
|
|
49177
|
+
status: outcome,
|
|
49178
|
+
exitCode: wouldBlock ? 1 : 0,
|
|
49179
|
+
wouldBlock,
|
|
49180
|
+
blocking: blockingLevel,
|
|
49181
|
+
clean: isClean,
|
|
49182
|
+
skippedChecks: result.skippedChecks.length
|
|
49183
|
+
}),
|
|
49184
|
+
...withNamespace("diag", {
|
|
49185
|
+
total: summary.totalDiagnosticCount,
|
|
49186
|
+
errors: summary.errorCount,
|
|
49187
|
+
warnings: summary.warningCount,
|
|
49188
|
+
affectedFiles: summary.affectedFileCount,
|
|
49189
|
+
inTestFiles: diagnosticsInTestFiles,
|
|
49190
|
+
inStoryFiles: diagnosticsInStoryFiles,
|
|
49191
|
+
distinctRules: countByRule.size,
|
|
49192
|
+
topRule,
|
|
49193
|
+
fixGroups: findingsPerFixGroup.size,
|
|
49194
|
+
fixGroupedFindings,
|
|
49195
|
+
...categoryRollup
|
|
49196
|
+
}),
|
|
49197
|
+
...withNamespace("score", {
|
|
49198
|
+
value: result.score ? result.score.score : null,
|
|
49199
|
+
label: result.score ? result.score.label : null,
|
|
49200
|
+
available: result.score !== null
|
|
49201
|
+
}),
|
|
49202
|
+
...withNamespace("lint", {
|
|
49203
|
+
failed: input.didLintFail ?? null,
|
|
49204
|
+
failureReasonKind: input.lintFailureReasonKind ?? null,
|
|
49205
|
+
partialFailureCount: input.lintPartialFailureCount ?? null,
|
|
49206
|
+
droppedFileCount: input.lintDroppedFileCount ?? null,
|
|
49207
|
+
cacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
49208
|
+
cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
49209
|
+
cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
|
|
49210
|
+
}),
|
|
49211
|
+
...withNamespace("deadCode", {
|
|
49212
|
+
failed: input.didDeadCodeFail ?? null,
|
|
49213
|
+
overlapped: input.deadCodeOverlapped ?? null
|
|
49214
|
+
}),
|
|
49215
|
+
...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
|
|
49216
|
+
...withNamespace("timing", {
|
|
49217
|
+
elapsedMs: result.elapsedMilliseconds,
|
|
49218
|
+
scanMs: result.scanElapsedMilliseconds ?? null
|
|
49219
|
+
}),
|
|
49220
|
+
...withNamespace("migration", {
|
|
49221
|
+
largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
|
|
49222
|
+
largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
49223
|
+
largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
|
|
49224
|
+
})
|
|
49225
|
+
};
|
|
49226
|
+
if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
|
|
49227
|
+
new: summary.totalDiagnosticCount,
|
|
49228
|
+
fixed: result.baselineDelta.fixedCount,
|
|
49229
|
+
baseTotal: result.baselineDelta.baseTotalCount,
|
|
49230
|
+
degraded: false
|
|
49231
|
+
}));
|
|
49232
|
+
else if (input.gateExempt) attributes["baseline.degraded"] = true;
|
|
48984
49233
|
return attributes;
|
|
48985
49234
|
};
|
|
48986
|
-
const
|
|
49235
|
+
const buildActionAttributes = () => {
|
|
48987
49236
|
const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
|
|
48988
|
-
return {
|
|
49237
|
+
return withNamespace("action", {
|
|
48989
49238
|
actorAssociation: githubActorAssociation ?? null,
|
|
48990
49239
|
runnerOs: detectRunnerOs(),
|
|
48991
49240
|
comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
|
|
48992
49241
|
reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
|
|
48993
49242
|
versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
|
|
48994
|
-
};
|
|
49243
|
+
});
|
|
48995
49244
|
};
|
|
48996
|
-
const
|
|
49245
|
+
const buildScanAttributes = (input) => {
|
|
48997
49246
|
const ruleOverrides = input.userConfig?.rules ?? {};
|
|
48998
49247
|
const ruleKeys = Object.keys(ruleOverrides);
|
|
48999
|
-
return {
|
|
49248
|
+
return withNamespace("scan", {
|
|
49000
49249
|
mode: input.mode,
|
|
49001
49250
|
scope: input.scope,
|
|
49002
49251
|
parallel: input.parallel,
|
|
@@ -49011,12 +49260,18 @@ const buildConfigAttributes = (input) => {
|
|
|
49011
49260
|
ignoredTagCount: input.ignoredTagCount,
|
|
49012
49261
|
hasCustomConfig: input.hasCustomConfig,
|
|
49013
49262
|
rulesConfigured: ruleKeys.length,
|
|
49014
|
-
rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length
|
|
49015
|
-
|
|
49263
|
+
rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
|
|
49264
|
+
fileCount: input.result?.scannedFileCount ?? null
|
|
49265
|
+
});
|
|
49016
49266
|
};
|
|
49017
49267
|
/**
|
|
49018
|
-
* Projects a scan into the
|
|
49019
|
-
* per-scan "wide event".
|
|
49268
|
+
* Projects a scan into the namespaced attribute set for its root span — the
|
|
49269
|
+
* canonical per-scan "wide event". Every attribute carries a dotted namespace
|
|
49270
|
+
* that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
|
|
49271
|
+
* verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
|
|
49272
|
+
* `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
|
|
49273
|
+
* tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
|
|
49274
|
+
* in the Spans dataset. Pure and exported so the projection (outcome
|
|
49020
49275
|
* precedence, rule/category rollups, CI knobs, config shape) is unit-testable
|
|
49021
49276
|
* without a live Sentry client. `null` values are dropped so absent signals
|
|
49022
49277
|
* never become misleading `"null"` attributes. The run + project base context
|
|
@@ -49025,8 +49280,8 @@ const buildConfigAttributes = (input) => {
|
|
|
49025
49280
|
* those don't carry.
|
|
49026
49281
|
*/
|
|
49027
49282
|
const buildRunEventAttributes = (input) => toSpanAttributes({
|
|
49028
|
-
...
|
|
49029
|
-
...
|
|
49283
|
+
...buildScanAttributes(input),
|
|
49284
|
+
...buildActionAttributes(),
|
|
49030
49285
|
...buildOutcomeAttributes(input)
|
|
49031
49286
|
});
|
|
49032
49287
|
/**
|
|
@@ -50324,6 +50579,7 @@ const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
|
|
|
50324
50579
|
`if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
|
|
50325
50580
|
"rm -f \"$react_doctor_output\";",
|
|
50326
50581
|
"else",
|
|
50582
|
+
"cat \"$react_doctor_output\" >&2;",
|
|
50327
50583
|
"rm -f \"$react_doctor_output\";",
|
|
50328
50584
|
`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;`,
|
|
50329
50585
|
"fi"
|
|
@@ -50354,7 +50610,7 @@ const readPackageJson = (projectRoot) => {
|
|
|
50354
50610
|
return null;
|
|
50355
50611
|
}
|
|
50356
50612
|
};
|
|
50357
|
-
const writeJsonFile
|
|
50613
|
+
const writeJsonFile = (filePath, value) => {
|
|
50358
50614
|
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50359
50615
|
};
|
|
50360
50616
|
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
@@ -51074,7 +51330,8 @@ const getStagedSourceFiles = async (directory) => {
|
|
|
51074
51330
|
return [...await runPromise(gen(function* () {
|
|
51075
51331
|
return yield* (yield* StagedFiles).discoverSourceFiles(directory);
|
|
51076
51332
|
}).pipe(provide(stagedFilesLayer)))];
|
|
51077
|
-
} catch {
|
|
51333
|
+
} catch (error) {
|
|
51334
|
+
cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
|
|
51078
51335
|
return [];
|
|
51079
51336
|
}
|
|
51080
51337
|
};
|
|
@@ -51093,6 +51350,35 @@ const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) =>
|
|
|
51093
51350
|
};
|
|
51094
51351
|
};
|
|
51095
51352
|
//#endregion
|
|
51353
|
+
//#region src/cli/utils/is-environment-error.ts
|
|
51354
|
+
const isNodeSystemError = (error) => error instanceof Error && typeof error.code === "string";
|
|
51355
|
+
const ENVIRONMENT_ERROR_CODES = new Set([
|
|
51356
|
+
"ENOSPC",
|
|
51357
|
+
"EIO",
|
|
51358
|
+
"EROFS",
|
|
51359
|
+
"EACCES",
|
|
51360
|
+
"EPERM",
|
|
51361
|
+
"ENOTDIR"
|
|
51362
|
+
]);
|
|
51363
|
+
const isEnvironmentError = (error) => {
|
|
51364
|
+
if (!isNodeSystemError(error)) return false;
|
|
51365
|
+
if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
|
|
51366
|
+
return error.code !== void 0 && ENVIRONMENT_ERROR_CODES.has(error.code);
|
|
51367
|
+
};
|
|
51368
|
+
const formatEnvironmentError = (error) => {
|
|
51369
|
+
if (!isNodeSystemError(error)) return error instanceof Error ? error.message : String(error);
|
|
51370
|
+
switch (error.code) {
|
|
51371
|
+
case "ENOSPC": return "No space left on device. Free up disk space and try again.";
|
|
51372
|
+
case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
|
|
51373
|
+
case "EROFS": return "Read-only filesystem: cannot write to this location.";
|
|
51374
|
+
case "EACCES":
|
|
51375
|
+
case "EPERM": return error.path ? `Permission denied accessing ${error.path}. Check file permissions and try again.` : "Permission denied. Check file permissions and try again.";
|
|
51376
|
+
case "ENOTDIR": return error.path ? `A file exists at ${error.path} or one of its parent paths where a directory was expected.` : "A file exists where a directory was expected.";
|
|
51377
|
+
case "ENOENT": return "Required command not found. Ensure the tool (e.g. git) is installed and on your PATH.";
|
|
51378
|
+
default: return error.message;
|
|
51379
|
+
}
|
|
51380
|
+
};
|
|
51381
|
+
//#endregion
|
|
51096
51382
|
//#region src/cli/utils/handle-error.ts
|
|
51097
51383
|
const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
|
|
51098
51384
|
const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
|
|
@@ -51175,15 +51461,19 @@ const handleError = (error, options = {}) => {
|
|
|
51175
51461
|
process.exitCode = 1;
|
|
51176
51462
|
};
|
|
51177
51463
|
/**
|
|
51178
|
-
* Renderer for expected, user-actionable failures — a bad `--diff` value
|
|
51179
|
-
* a base branch that isn't fetched
|
|
51180
|
-
*
|
|
51181
|
-
*
|
|
51464
|
+
* Renderer for expected, user-actionable failures — a bad `--diff` value,
|
|
51465
|
+
* a base branch that isn't fetched, or environment errors like disk-full or
|
|
51466
|
+
* permission-denied. Prints just the (already human-readable) message — no
|
|
51467
|
+
* "Something went wrong", prefilled issue, Discord link, or Sentry reference
|
|
51468
|
+
* — because there is no bug to report.
|
|
51182
51469
|
*/
|
|
51183
51470
|
const handleUserError = (error, options = {}) => {
|
|
51471
|
+
const isEnvError = isEnvironmentError(error);
|
|
51472
|
+
if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: error.code ?? "unknown" });
|
|
51473
|
+
const message = isEnvError ? formatEnvironmentError(error) : formatErrorForReport(error);
|
|
51184
51474
|
runSync(gen(function* () {
|
|
51185
51475
|
yield* error$1("");
|
|
51186
|
-
yield* error$1(highlighter.error(
|
|
51476
|
+
yield* error$1(highlighter.error(message));
|
|
51187
51477
|
yield* error$1("");
|
|
51188
51478
|
}));
|
|
51189
51479
|
if (options.shouldExit !== false) process.exit(1);
|
|
@@ -51198,7 +51488,7 @@ const handleUserError = (error, options = {}) => {
|
|
|
51198
51488
|
* `handleUserError` (a plain message — no "Something went wrong", prefilled
|
|
51199
51489
|
* issue, Discord link, or Sentry reference), since there is no bug to report.
|
|
51200
51490
|
*
|
|
51201
|
-
*
|
|
51491
|
+
* Four distinct shapes reach the CLI's catch blocks:
|
|
51202
51492
|
*
|
|
51203
51493
|
* - **Project-discovery failures** (`NoReactDependencyError`,
|
|
51204
51494
|
* `ProjectNotFoundError`, `PackageJsonNotFoundError`, `NotADirectoryError`,
|
|
@@ -51211,12 +51501,19 @@ const handleUserError = (error, options = {}) => {
|
|
|
51211
51501
|
* `--project` name.
|
|
51212
51502
|
* - **Bad `--diff` input** (`GitBaseBranchInvalid` / `GitBaseBranchMissing`)
|
|
51213
51503
|
* stays the tagged `ReactDoctorError`, so dispatch on the reason `_tag`.
|
|
51504
|
+
* - **Environment failures** (`ENOSPC`, `EIO`, `EROFS`, `EACCES`, `EPERM`,
|
|
51505
|
+
* `ENOTDIR`, plus a `spawn`-scoped `ENOENT` for a missing binary) — disk
|
|
51506
|
+
* full / failing / read-only, permission denied, or a path blocked by a
|
|
51507
|
+
* file. React Doctor cannot fix the user's environment; exit cleanly with an
|
|
51508
|
+
* actionable message instead of crashing. See `is-environment-error.ts` for
|
|
51509
|
+
* why the set stays narrow (codes that usually mean our bug keep reaching
|
|
51510
|
+
* Sentry).
|
|
51214
51511
|
*
|
|
51215
51512
|
* This composes the existing core narrowers rather than introducing a new
|
|
51216
51513
|
* error-shape helper (AGENTS.md): it encodes CLI-layer reporting policy, not
|
|
51217
51514
|
* knowledge of the `ReactDoctorError` shape.
|
|
51218
51515
|
*/
|
|
51219
|
-
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
51516
|
+
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isEnvironmentError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
51220
51517
|
//#endregion
|
|
51221
51518
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
51222
51519
|
const buildHandoffPayload = (input) => {
|
|
@@ -51392,7 +51689,7 @@ const installDoctorScript = (options) => {
|
|
|
51392
51689
|
};
|
|
51393
51690
|
})();
|
|
51394
51691
|
const scriptStatus = scriptTarget.status;
|
|
51395
|
-
if (scriptStatus === "created") writeJsonFile
|
|
51692
|
+
if (scriptStatus === "created") writeJsonFile(packageJsonPath, {
|
|
51396
51693
|
...packageJson,
|
|
51397
51694
|
scripts: {
|
|
51398
51695
|
...isRecord$1(scripts) ? scripts : {},
|
|
@@ -51980,28 +52277,42 @@ const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_
|
|
|
51980
52277
|
const CLAUDE_AGENT = "claude-code";
|
|
51981
52278
|
const CURSOR_AGENT = "cursor";
|
|
51982
52279
|
const CLAUDE_SETTINGS_RELATIVE_PATH = ".claude/settings.json";
|
|
51983
|
-
const CLAUDE_HOOK_RELATIVE_PATH = ".claude/hooks/react-doctor.
|
|
51984
|
-
const CLAUDE_HOOK_COMMAND = "
|
|
52280
|
+
const CLAUDE_HOOK_RELATIVE_PATH = ".claude/hooks/react-doctor.mjs";
|
|
52281
|
+
const CLAUDE_HOOK_COMMAND = "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/react-doctor.mjs\"";
|
|
51985
52282
|
const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
|
|
51986
|
-
const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.
|
|
52283
|
+
const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.mjs";
|
|
52284
|
+
const CURSOR_HOOK_COMMAND = "node .cursor/hooks/react-doctor.mjs";
|
|
51987
52285
|
const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
|
|
51988
52286
|
const CURSOR_HOOKS_SCHEMA_VERSION = 1;
|
|
51989
|
-
const JSON_INDENT_SPACES$1 = 2;
|
|
51990
52287
|
const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
|
|
51991
52288
|
const readJsonFile = (filePath, fallback) => {
|
|
51992
52289
|
if (!NFS.existsSync(filePath)) return fallback;
|
|
51993
52290
|
const content = NFS.readFileSync(filePath, "utf8").trim();
|
|
51994
52291
|
if (content.length === 0) return fallback;
|
|
51995
|
-
|
|
52292
|
+
try {
|
|
52293
|
+
return JSON.parse(content);
|
|
52294
|
+
} catch (error) {
|
|
52295
|
+
if (error instanceof SyntaxError) throw new CliInputError(`Could not parse ${filePath}: the file contains invalid JSON. Fix the syntax errors in this file and re-run the install command.`);
|
|
52296
|
+
throw error;
|
|
52297
|
+
}
|
|
51996
52298
|
};
|
|
51997
|
-
const
|
|
51998
|
-
|
|
51999
|
-
|
|
52299
|
+
const ensureDirectoryExists = (directoryPath) => {
|
|
52300
|
+
try {
|
|
52301
|
+
NFS.mkdirSync(directoryPath, { recursive: true });
|
|
52302
|
+
} catch (error) {
|
|
52303
|
+
const code = error.code;
|
|
52304
|
+
if (code === "EACCES" || code === "EPERM") throw new CliInputError(`Could not create directory ${directoryPath}: permission denied. Ensure you have write permissions for this location and re-run the install command.`);
|
|
52305
|
+
if (code === "ENOTDIR" || code === "EEXIST") throw new CliInputError(`Could not create directory ${directoryPath}: a file exists at this path or one of its parent paths. Remove the conflicting file and re-run the install command.`);
|
|
52306
|
+
throw error;
|
|
52307
|
+
}
|
|
52308
|
+
};
|
|
52309
|
+
const writeJsonFileWithDirectoryCheck = (filePath, value) => {
|
|
52310
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
52311
|
+
writeJsonFile(filePath, value);
|
|
52000
52312
|
};
|
|
52001
52313
|
const writeHookScript = (filePath) => {
|
|
52002
|
-
|
|
52314
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
52003
52315
|
NFS.writeFileSync(filePath, buildAgentHookScript());
|
|
52004
|
-
NFS.chmodSync(filePath, 493);
|
|
52005
52316
|
};
|
|
52006
52317
|
const hasClaudeHookCommand = (groups) => groups.some((group) => (group.hooks ?? []).some((hook) => hook.command === CLAUDE_HOOK_COMMAND));
|
|
52007
52318
|
const installClaudeHook = (projectRoot) => {
|
|
@@ -52015,14 +52326,14 @@ const installClaudeHook = (projectRoot) => {
|
|
|
52015
52326
|
command: CLAUDE_HOOK_COMMAND
|
|
52016
52327
|
}] });
|
|
52017
52328
|
hooks.PostToolBatch = postToolBatchHooks;
|
|
52018
|
-
|
|
52329
|
+
writeJsonFileWithDirectoryCheck(settingsPath, {
|
|
52019
52330
|
...settings,
|
|
52020
52331
|
hooks
|
|
52021
52332
|
});
|
|
52022
52333
|
writeHookScript(hookPath);
|
|
52023
52334
|
return [settingsPath, hookPath];
|
|
52024
52335
|
};
|
|
52025
|
-
const hasCursorHookCommand = (handlers) => handlers.some((handler) => handler.command ===
|
|
52336
|
+
const hasCursorHookCommand = (handlers) => handlers.some((handler) => handler.command === CURSOR_HOOK_COMMAND);
|
|
52026
52337
|
const installCursorHook = (projectRoot) => {
|
|
52027
52338
|
const configPath = Path.join(projectRoot, CURSOR_HOOKS_RELATIVE_PATH);
|
|
52028
52339
|
const hookPath = Path.join(projectRoot, CURSOR_HOOK_RELATIVE_PATH);
|
|
@@ -52030,12 +52341,12 @@ const installCursorHook = (projectRoot) => {
|
|
|
52030
52341
|
const hooks = { ...config.hooks ?? {} };
|
|
52031
52342
|
const postToolUseHooks = [...hooks.postToolUse ?? []];
|
|
52032
52343
|
if (!hasCursorHookCommand(postToolUseHooks)) postToolUseHooks.push({
|
|
52033
|
-
command:
|
|
52344
|
+
command: CURSOR_HOOK_COMMAND,
|
|
52034
52345
|
matcher: CURSOR_HOOK_MATCHER,
|
|
52035
52346
|
timeout: 120
|
|
52036
52347
|
});
|
|
52037
52348
|
hooks.postToolUse = postToolUseHooks;
|
|
52038
|
-
|
|
52349
|
+
writeJsonFileWithDirectoryCheck(configPath, {
|
|
52039
52350
|
...config,
|
|
52040
52351
|
version: config.version ?? CURSOR_HOOKS_SCHEMA_VERSION,
|
|
52041
52352
|
hooks
|
|
@@ -52044,102 +52355,111 @@ const installCursorHook = (projectRoot) => {
|
|
|
52044
52355
|
return [configPath, hookPath];
|
|
52045
52356
|
};
|
|
52046
52357
|
const buildAgentHookScript = () => [
|
|
52047
|
-
"
|
|
52048
|
-
"
|
|
52358
|
+
"import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';",
|
|
52359
|
+
"import { tmpdir } from 'node:os';",
|
|
52360
|
+
"import { join, dirname } from 'node:path';",
|
|
52361
|
+
"import { fileURLToPath } from 'node:url';",
|
|
52362
|
+
"import { spawnSync } from 'node:child_process';",
|
|
52049
52363
|
"",
|
|
52050
|
-
"
|
|
52051
|
-
"
|
|
52052
|
-
"trap 'rm -f \"$input_file\" \"$output_file\"' EXIT",
|
|
52053
|
-
"cat > \"$input_file\"",
|
|
52364
|
+
"const __filename = fileURLToPath(import.meta.url);",
|
|
52365
|
+
"const __dirname = dirname(__filename);",
|
|
52054
52366
|
"",
|
|
52055
|
-
"
|
|
52056
|
-
"project_root=${CLAUDE_PROJECT_DIR:-}",
|
|
52057
|
-
"if [ -z \"$project_root\" ]; then",
|
|
52058
|
-
" project_root=$(CDPATH= cd \"$script_dir/../..\" && pwd)",
|
|
52059
|
-
"fi",
|
|
52060
|
-
"if ! cd \"$project_root\"; then",
|
|
52061
|
-
" exit 0",
|
|
52062
|
-
"fi",
|
|
52367
|
+
"const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
|
|
52063
52368
|
"",
|
|
52064
|
-
"
|
|
52065
|
-
"
|
|
52066
|
-
" return 0",
|
|
52067
|
-
"
|
|
52369
|
+
"const readStdin = () => {",
|
|
52370
|
+
" try {",
|
|
52371
|
+
" return readFileSync(0, 'utf8');",
|
|
52372
|
+
" } catch {",
|
|
52373
|
+
" return '';",
|
|
52374
|
+
" }",
|
|
52375
|
+
"};",
|
|
52068
52376
|
"",
|
|
52069
|
-
"
|
|
52070
|
-
"const fs = require('node:fs');",
|
|
52071
|
-
"const inputPath = process.argv[2];",
|
|
52072
|
-
"const editToolNames = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
|
|
52073
|
-
"try {",
|
|
52074
|
-
" const input = JSON.parse(fs.readFileSync(inputPath, 'utf8') || '{}');",
|
|
52377
|
+
"const shouldScan = (input) => {",
|
|
52075
52378
|
" const eventName = input.hook_event_name || input.eventName || input.event_name;",
|
|
52076
52379
|
" if (eventName === 'PostToolBatch') {",
|
|
52077
52380
|
" const toolCalls = Array.isArray(input.tool_calls) ? input.tool_calls : [];",
|
|
52078
|
-
"
|
|
52381
|
+
" return toolCalls.some((toolCall) => EDIT_TOOL_NAMES.has(toolCall.tool_name));",
|
|
52079
52382
|
" }",
|
|
52080
52383
|
" const toolName = input.tool_name || input.toolName || input.tool;",
|
|
52081
|
-
"
|
|
52082
|
-
"}
|
|
52083
|
-
" process.exit(0);",
|
|
52084
|
-
"}",
|
|
52085
|
-
"NODE",
|
|
52086
|
-
"}",
|
|
52384
|
+
" return !toolName || EDIT_TOOL_NAMES.has(toolName);",
|
|
52385
|
+
"};",
|
|
52087
52386
|
"",
|
|
52088
|
-
"
|
|
52089
|
-
"
|
|
52090
|
-
"
|
|
52091
|
-
"
|
|
52092
|
-
"
|
|
52387
|
+
"const runReactDoctor = (outputPath) => {",
|
|
52388
|
+
" // Each candidate is a single shell command string (not an args array):",
|
|
52389
|
+
" // `shell: true` is required to run the Windows `.cmd` shims, and an args",
|
|
52390
|
+
" // array with `shell: true` trips Node's DEP0190. A missing command exits",
|
|
52391
|
+
" // 127 via the shell (no ENOENT error), so fall through on that. With no",
|
|
52392
|
+
" // runner found, exit 0 silently — stdout is parsed as the hook's JSON.",
|
|
52393
|
+
" const commands = [",
|
|
52394
|
+
" './node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score',",
|
|
52395
|
+
" 'react-doctor --verbose --scope changed --blocking warning --no-score',",
|
|
52396
|
+
" 'pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
|
|
52397
|
+
" 'npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
|
|
52398
|
+
" ];",
|
|
52093
52399
|
"",
|
|
52094
|
-
"
|
|
52095
|
-
"
|
|
52096
|
-
"
|
|
52097
|
-
"
|
|
52400
|
+
" for (const command of commands) {",
|
|
52401
|
+
" const result = spawnSync(command, { encoding: 'utf8', shell: true });",
|
|
52402
|
+
" if (result.error?.code === 'ENOENT' || result.status === 127) continue;",
|
|
52403
|
+
" try {",
|
|
52404
|
+
" writeFileSync(outputPath, (result.stdout || '') + (result.stderr || ''));",
|
|
52405
|
+
" } catch {}",
|
|
52406
|
+
" return result.status;",
|
|
52407
|
+
" }",
|
|
52098
52408
|
"",
|
|
52099
|
-
"
|
|
52100
|
-
"
|
|
52101
|
-
" return",
|
|
52102
|
-
" fi",
|
|
52409
|
+
" return 0;",
|
|
52410
|
+
"};",
|
|
52103
52411
|
"",
|
|
52104
|
-
"
|
|
52105
|
-
"
|
|
52106
|
-
"
|
|
52107
|
-
"
|
|
52412
|
+
"const cleanup = (...paths) => {",
|
|
52413
|
+
" for (const path of paths) {",
|
|
52414
|
+
" try { unlinkSync(path); } catch {}",
|
|
52415
|
+
" }",
|
|
52416
|
+
"};",
|
|
52108
52417
|
"",
|
|
52109
|
-
"
|
|
52110
|
-
"
|
|
52111
|
-
"
|
|
52418
|
+
"const main = () => {",
|
|
52419
|
+
" let input;",
|
|
52420
|
+
" try {",
|
|
52421
|
+
" const stdinContent = readStdin();",
|
|
52422
|
+
" input = JSON.parse(stdinContent || '{}');",
|
|
52423
|
+
" } catch {",
|
|
52424
|
+
" input = {};",
|
|
52425
|
+
" }",
|
|
52112
52426
|
"",
|
|
52113
|
-
"if !
|
|
52114
|
-
"
|
|
52115
|
-
"
|
|
52427
|
+
" if (!shouldScan(input)) {",
|
|
52428
|
+
" process.exit(0);",
|
|
52429
|
+
" }",
|
|
52116
52430
|
"",
|
|
52117
|
-
"
|
|
52118
|
-
"
|
|
52119
|
-
"fi",
|
|
52431
|
+
" const projectRoot = process.env.CLAUDE_PROJECT_DIR || join(__dirname, '../..');",
|
|
52432
|
+
" const outputPath = join(tmpdir(), `react-doctor-agent-hook-output-${process.pid}.txt`);",
|
|
52120
52433
|
"",
|
|
52121
|
-
"node - \"$input_file\" \"$output_file\" <<'NODE'",
|
|
52122
|
-
"const fs = require('node:fs');",
|
|
52123
|
-
"const inputPath = process.argv[2];",
|
|
52124
|
-
"const outputPath = process.argv[3];",
|
|
52125
|
-
"const readInput = () => {",
|
|
52126
52434
|
" try {",
|
|
52127
|
-
"
|
|
52435
|
+
" process.chdir(projectRoot);",
|
|
52128
52436
|
" } catch {",
|
|
52129
|
-
"
|
|
52437
|
+
" process.exit(0);",
|
|
52438
|
+
" }",
|
|
52439
|
+
"",
|
|
52440
|
+
" const scanResult = runReactDoctor(outputPath);",
|
|
52441
|
+
" if (scanResult === 0) {",
|
|
52442
|
+
" cleanup(outputPath);",
|
|
52443
|
+
" process.exit(0);",
|
|
52444
|
+
" }",
|
|
52445
|
+
"",
|
|
52446
|
+
" const scanOutput = readFileSync(outputPath, 'utf8').trim();",
|
|
52447
|
+
" cleanup(outputPath);",
|
|
52448
|
+
"",
|
|
52449
|
+
" if (!scanOutput) {",
|
|
52450
|
+
" process.exit(0);",
|
|
52451
|
+
" }",
|
|
52452
|
+
"",
|
|
52453
|
+
" const message = `React Doctor found issues in the changed files. Review this output and fix the regressions before finishing. For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.\\n\\n${scanOutput}`;",
|
|
52454
|
+
"",
|
|
52455
|
+
" if (input.hook_event_name === 'PostToolBatch') {",
|
|
52456
|
+
" console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: message } }));",
|
|
52457
|
+
" } else {",
|
|
52458
|
+
" console.log(JSON.stringify({ additional_context: message }));",
|
|
52130
52459
|
" }",
|
|
52131
52460
|
"};",
|
|
52132
|
-
"
|
|
52133
|
-
"
|
|
52134
|
-
"if (!scanOutput) process.exit(0);",
|
|
52135
|
-
"const message = `React Doctor found issues in the changed files. Review this output and fix the regressions before finishing. For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.\\n\\n${scanOutput}`;",
|
|
52136
|
-
"if (input.hook_event_name === 'PostToolBatch') {",
|
|
52137
|
-
" console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: message } }));",
|
|
52138
|
-
"} else {",
|
|
52139
|
-
" console.log(JSON.stringify({ additional_context: message }));",
|
|
52140
|
-
"}",
|
|
52141
|
-
"NODE",
|
|
52142
|
-
""
|
|
52461
|
+
"",
|
|
52462
|
+
"main();"
|
|
52143
52463
|
].join("\n");
|
|
52144
52464
|
const installReactDoctorAgentHooks = (options) => {
|
|
52145
52465
|
const installedAgents = [];
|
|
@@ -52200,6 +52520,7 @@ const buildReactDoctorHookBlock = () => [
|
|
|
52200
52520
|
"if react_doctor_scan_staged_files > \"$react_doctor_output\" 2>&1; then",
|
|
52201
52521
|
" rm -f \"$react_doctor_output\"",
|
|
52202
52522
|
"else",
|
|
52523
|
+
" cat \"$react_doctor_output\" >&2",
|
|
52203
52524
|
" rm -f \"$react_doctor_output\"",
|
|
52204
52525
|
` 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`,
|
|
52205
52526
|
"fi",
|
|
@@ -52271,7 +52592,7 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
52271
52592
|
parent = cloned;
|
|
52272
52593
|
}
|
|
52273
52594
|
parent[leafKey] = strategy.leafShape === "array" ? appendArrayCommand(parent[leafKey]) : appendStringCommand(parent[leafKey]);
|
|
52274
|
-
writeJsonFile
|
|
52595
|
+
writeJsonFile(packageJsonPath, nextPackageJson);
|
|
52275
52596
|
removeLegacyManagedRunner(options.projectRoot);
|
|
52276
52597
|
return {
|
|
52277
52598
|
hookPath: packageJsonPath,
|
|
@@ -52965,6 +53286,26 @@ const CLI_AGENT_AUTO_FLAGS = {
|
|
|
52965
53286
|
codex: ["--yolo"],
|
|
52966
53287
|
cursor: ["--force"]
|
|
52967
53288
|
};
|
|
53289
|
+
const resolveCursorBundledNode = () => {
|
|
53290
|
+
if (!isWindows) return null;
|
|
53291
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
53292
|
+
if (!localAppData) return null;
|
|
53293
|
+
const cursorAgentRoot = Path.join(localAppData, "cursor-agent", "versions");
|
|
53294
|
+
try {
|
|
53295
|
+
const versions = NFS.readdirSync(cursorAgentRoot);
|
|
53296
|
+
if (versions.length === 0) return null;
|
|
53297
|
+
const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
|
|
53298
|
+
for (const version of sortedVersions) try {
|
|
53299
|
+
const bundledNodePath = Path.join(cursorAgentRoot, version, "node.exe");
|
|
53300
|
+
const entryScriptPath = Path.join(cursorAgentRoot, version, "index.js");
|
|
53301
|
+
if (NFS.statSync(bundledNodePath).isFile() && NFS.statSync(entryScriptPath).isFile()) return {
|
|
53302
|
+
command: bundledNodePath,
|
|
53303
|
+
args: [entryScriptPath]
|
|
53304
|
+
};
|
|
53305
|
+
} catch {}
|
|
53306
|
+
} catch {}
|
|
53307
|
+
return null;
|
|
53308
|
+
};
|
|
52968
53309
|
const resolveWindowsCmdEntryScript = (command) => {
|
|
52969
53310
|
if (!isWindows) return null;
|
|
52970
53311
|
const pathDirectories = (process.env.PATH ?? "").split(Path.delimiter).filter(Boolean);
|
|
@@ -52984,11 +53325,10 @@ const resolveWindowsCmdEntryScript = (command) => {
|
|
|
52984
53325
|
}
|
|
52985
53326
|
return null;
|
|
52986
53327
|
};
|
|
52987
|
-
const spawnAgent = (command, args, cwd
|
|
53328
|
+
const spawnAgent = (command, args, cwd) => new Promise((resolve, reject) => {
|
|
52988
53329
|
const child = spawn(command, [...args], {
|
|
52989
53330
|
cwd,
|
|
52990
|
-
stdio: "inherit"
|
|
52991
|
-
shell
|
|
53331
|
+
stdio: "inherit"
|
|
52992
53332
|
});
|
|
52993
53333
|
child.on("error", reject);
|
|
52994
53334
|
child.on("close", (code) => resolve(code ?? 0));
|
|
@@ -52997,6 +53337,10 @@ const launchCliAgent = async (agentId, prompt, cwd) => {
|
|
|
52997
53337
|
const binary = CLI_AGENT_BINARIES[agentId];
|
|
52998
53338
|
const agentArgs = [...CLI_AGENT_AUTO_FLAGS[agentId], prompt];
|
|
52999
53339
|
if (isWindows) {
|
|
53340
|
+
if (agentId === "cursor") {
|
|
53341
|
+
const cursorResolution = resolveCursorBundledNode();
|
|
53342
|
+
if (cursorResolution) return spawnAgent(cursorResolution.command, [...cursorResolution.args, ...agentArgs], cwd);
|
|
53343
|
+
}
|
|
53000
53344
|
const entryScript = resolveWindowsCmdEntryScript(binary);
|
|
53001
53345
|
if (entryScript) return spawnAgent(process.execPath, [entryScript, ...agentArgs], cwd);
|
|
53002
53346
|
}
|
|
@@ -53710,7 +54054,7 @@ const warnDeprecatedDiff = (flags, userConfig) => {
|
|
|
53710
54054
|
};
|
|
53711
54055
|
const warnDiffUnavailable = (requested, isQuiet) => {
|
|
53712
54056
|
if (isQuiet) return;
|
|
53713
|
-
if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (
|
|
54057
|
+
if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (git unavailable, ref not found, or merge-base failed). Running full scan.`);
|
|
53714
54058
|
else cliLogger.warn("No feature branch or uncommitted changes detected. Running full scan.");
|
|
53715
54059
|
cliLogger.break();
|
|
53716
54060
|
};
|
|
@@ -53907,6 +54251,7 @@ const resolveRequestedProjects = (requestedNames, workspacePackages, rootDirecto
|
|
|
53907
54251
|
return requestedNames.map((requestedName) => {
|
|
53908
54252
|
const matched = workspacePackages.find((workspacePackage) => workspacePackage.name === requestedName || Path.basename(workspacePackage.directory) === requestedName);
|
|
53909
54253
|
if (matched) return matched.directory;
|
|
54254
|
+
if (Path.basename(rootDirectory) === requestedName) return rootDirectory;
|
|
53910
54255
|
const candidateDirectory = Path.resolve(rootDirectory, requestedName);
|
|
53911
54256
|
if (isDirectory(candidateDirectory)) {
|
|
53912
54257
|
recordCount(METRIC.projectPathSelected);
|
|
@@ -54014,6 +54359,17 @@ const warnDeprecatedFailOn = (flags, userConfig) => {
|
|
|
54014
54359
|
cliLogger.warn(`${source} is deprecated; rename it to ${replacement}.`);
|
|
54015
54360
|
};
|
|
54016
54361
|
//#endregion
|
|
54362
|
+
//#region src/cli/utils/warn-ai-training-environment.ts
|
|
54363
|
+
let didWarnAiTraining = false;
|
|
54364
|
+
const warnIfAiTrainingEnvironment = () => {
|
|
54365
|
+
if (didWarnAiTraining) return;
|
|
54366
|
+
const detected = detectAiTrainingEnvironment();
|
|
54367
|
+
if (detected === null) return;
|
|
54368
|
+
didWarnAiTraining = true;
|
|
54369
|
+
process.stderr.write(`${highlighter.warn("react-doctor detected use in an AI or ML pipeline. This use requires written permission under the react-doctor license — contact founders@million.dev to request access.")}\n`);
|
|
54370
|
+
recordCount(METRIC.aiTrainingWarningShown, 1, { environment: detected });
|
|
54371
|
+
};
|
|
54372
|
+
//#endregion
|
|
54017
54373
|
//#region src/cli/utils/validate-mode-flags.ts
|
|
54018
54374
|
const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
|
|
54019
54375
|
const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
|
|
@@ -54097,10 +54453,14 @@ const inspectAction = async (directory, flags) => {
|
|
|
54097
54453
|
const isQuiet = isScoreOnly || isJsonMode;
|
|
54098
54454
|
const requestedDirectory = Path.resolve(directory);
|
|
54099
54455
|
const startTime = performance$1.now();
|
|
54100
|
-
if (isJsonMode)
|
|
54101
|
-
|
|
54102
|
-
|
|
54103
|
-
|
|
54456
|
+
if (isJsonMode) {
|
|
54457
|
+
enableJsonMode({
|
|
54458
|
+
compact: Boolean(flags.jsonCompact),
|
|
54459
|
+
directory: requestedDirectory,
|
|
54460
|
+
outputFile: flags.jsonOut
|
|
54461
|
+
});
|
|
54462
|
+
if (flags.jsonOut) recordCount(METRIC.jsonOutUsed, 1);
|
|
54463
|
+
}
|
|
54104
54464
|
recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
|
|
54105
54465
|
try {
|
|
54106
54466
|
validateModeFlags(flags);
|
|
@@ -54114,6 +54474,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
54114
54474
|
setJsonReportDirectory(resolvedDirectory);
|
|
54115
54475
|
warnDeprecatedFailOn(flags, userConfig);
|
|
54116
54476
|
warnDeprecatedDiff(flags, userConfig);
|
|
54477
|
+
warnIfAiTrainingEnvironment();
|
|
54117
54478
|
if (scanTarget.didRedirectViaRootDir && !isQuiet) {
|
|
54118
54479
|
cliLogger.dim(`Redirected to ${highlighter.info(toRelativePath(resolvedDirectory, requestedDirectory))} via react-doctor config "rootDir".`);
|
|
54119
54480
|
cliLogger.break();
|
|
@@ -54401,6 +54762,10 @@ const installAction = async (options, command) => {
|
|
|
54401
54762
|
projectRoot: options.cwd ?? process.cwd()
|
|
54402
54763
|
});
|
|
54403
54764
|
} catch (error) {
|
|
54765
|
+
if (isExpectedUserError(error)) {
|
|
54766
|
+
handleUserError(error);
|
|
54767
|
+
return;
|
|
54768
|
+
}
|
|
54404
54769
|
handleError(error, { sentryEventId: await reportErrorToSentry(error) });
|
|
54405
54770
|
}
|
|
54406
54771
|
};
|
|
@@ -55033,6 +55398,14 @@ const applyColorPreference = (argv, env = process.env) => {
|
|
|
55033
55398
|
setColorEnabled(enabled);
|
|
55034
55399
|
};
|
|
55035
55400
|
//#endregion
|
|
55401
|
+
//#region src/cli/utils/ensure-windows-utf8-console.ts
|
|
55402
|
+
const ensureWindowsUtf8Console = () => {
|
|
55403
|
+
if (process.platform !== "win32" || !process.stdout.isTTY) return;
|
|
55404
|
+
try {
|
|
55405
|
+
execFileSync("chcp.com", ["65001"], { stdio: "ignore" });
|
|
55406
|
+
} catch {}
|
|
55407
|
+
};
|
|
55408
|
+
//#endregion
|
|
55036
55409
|
//#region src/cli/utils/exit-gracefully.ts
|
|
55037
55410
|
const exitGracefully = () => {
|
|
55038
55411
|
try {
|
|
@@ -55175,6 +55548,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
55175
55548
|
"--category",
|
|
55176
55549
|
"--changed-files-from",
|
|
55177
55550
|
"--blocking",
|
|
55551
|
+
"--json-out",
|
|
55178
55552
|
"--output-dir",
|
|
55179
55553
|
"--fail-on",
|
|
55180
55554
|
"--project",
|
|
@@ -55365,7 +55739,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
55365
55739
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
55366
55740
|
`;
|
|
55367
55741
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
55368
|
-
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);
|
|
55742
|
+
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("--json-out <path>", "with --json, write the report to a file instead of stdout").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);
|
|
55369
55743
|
program.action(inspectAction);
|
|
55370
55744
|
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));
|
|
55371
55745
|
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);
|
|
@@ -55390,6 +55764,7 @@ if (process.argv.includes("-V") && !strippedArgv.includes("-V")) {
|
|
|
55390
55764
|
process.exit(0);
|
|
55391
55765
|
}
|
|
55392
55766
|
applyColorPreference(strippedArgv);
|
|
55767
|
+
ensureWindowsUtf8Console();
|
|
55393
55768
|
const argv = normalizeHelpInvocation(strippedArgv, knownCommands);
|
|
55394
55769
|
Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => program.parseAsync(argv)).then(() => flushSentry()).catch(async (error) => {
|
|
55395
55770
|
const isUserError = isExpectedUserError(error);
|
|
@@ -55408,4 +55783,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
55408
55783
|
export {};
|
|
55409
55784
|
|
|
55410
55785
|
//# sourceMappingURL=cli.js.map
|
|
55411
|
-
//# debugId=
|
|
55786
|
+
//# debugId=cf52ec59-cee0-5503-b8f2-a4f98bf77ea4
|