react-doctor 0.5.8-dev.9f28454 → 0.5.8-dev.a7ad969
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 -8
- package/dist/cli.js +605 -300
- package/dist/index.d.ts +2 -2
- package/dist/index.js +235 -119
- package/dist/lsp.js +183 -67
- package/package.json +6 -5
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]="eb7a01bc-f737-5207-b430-b8fee7a1895f")}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";
|
|
@@ -26,7 +26,7 @@ import tty from "node:tty";
|
|
|
26
26
|
import { codeFrameColumns } from "@babel/code-frame";
|
|
27
27
|
import Conf from "conf";
|
|
28
28
|
import basePrompts from "prompts";
|
|
29
|
-
import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource } from "agent-install";
|
|
29
|
+
import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource, isSkillAgentType } from "agent-install";
|
|
30
30
|
import { generateCode, loadFile, writeFile } from "magicast";
|
|
31
31
|
import { getConfigFromVariableDeclaration, getDefaultExportOptions } from "magicast/helpers";
|
|
32
32
|
//#region \0rolldown/runtime.js
|
|
@@ -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);
|
|
@@ -38553,7 +38558,7 @@ const resolveScanConcurrency = (requested) => {
|
|
|
38553
38558
|
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
38554
38559
|
return Math.min(Math.floor(requested), 32);
|
|
38555
38560
|
};
|
|
38556
|
-
const readSystemFacts = () => ({
|
|
38561
|
+
const readSystemFacts$1 = () => ({
|
|
38557
38562
|
availableCores: os.availableParallelism(),
|
|
38558
38563
|
totalMemoryBytes: os.totalmem(),
|
|
38559
38564
|
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
@@ -38574,7 +38579,7 @@ const readSystemFacts = () => ({
|
|
|
38574
38579
|
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
38575
38580
|
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
38576
38581
|
*/
|
|
38577
|
-
const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
|
|
38582
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
|
|
38578
38583
|
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
38579
38584
|
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
38580
38585
|
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
@@ -38765,6 +38770,12 @@ const BOOLEAN_FIELD_NAMES = [
|
|
|
38765
38770
|
"adoptExistingLintConfig"
|
|
38766
38771
|
];
|
|
38767
38772
|
const STRING_FIELD_NAMES = ["rootDir"];
|
|
38773
|
+
const STRING_ARRAY_FIELD_NAMES = [
|
|
38774
|
+
"projects",
|
|
38775
|
+
"textComponents",
|
|
38776
|
+
"rawTextWrapperComponents",
|
|
38777
|
+
"serverAuthFunctionNames"
|
|
38778
|
+
];
|
|
38768
38779
|
const SURFACE_CONTROL_FIELD_NAMES = [
|
|
38769
38780
|
"includeTags",
|
|
38770
38781
|
"excludeTags",
|
|
@@ -38866,6 +38877,7 @@ const validateConfigTypes = (config) => {
|
|
|
38866
38877
|
const validated = { ...config };
|
|
38867
38878
|
for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
|
|
38868
38879
|
for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
|
|
38880
|
+
for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
|
|
38869
38881
|
applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
|
|
38870
38882
|
for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
|
|
38871
38883
|
applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
|
|
@@ -40128,7 +40140,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
|
|
|
40128
40140
|
}
|
|
40129
40141
|
return true;
|
|
40130
40142
|
};
|
|
40131
|
-
const
|
|
40143
|
+
const yieldToEventLoop = () => new Promise((resolve) => {
|
|
40144
|
+
setImmediate(resolve);
|
|
40145
|
+
});
|
|
40146
|
+
const createSecurityScanSession = (rootDirectory, options) => {
|
|
40132
40147
|
const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
|
|
40133
40148
|
const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
|
|
40134
40149
|
const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
|
|
@@ -40143,7 +40158,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
40143
40158
|
committedFilesOnly: rule.committedFilesOnly === true
|
|
40144
40159
|
}];
|
|
40145
40160
|
});
|
|
40146
|
-
if (enabledScanRules.length === 0) return
|
|
40161
|
+
if (enabledScanRules.length === 0) return null;
|
|
40147
40162
|
const diagnostics = [];
|
|
40148
40163
|
const seen = /* @__PURE__ */ new Set();
|
|
40149
40164
|
const gitIgnoredCache = /* @__PURE__ */ new Map();
|
|
@@ -40155,15 +40170,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
40155
40170
|
}
|
|
40156
40171
|
return status === true;
|
|
40157
40172
|
};
|
|
40158
|
-
|
|
40159
|
-
|
|
40160
|
-
|
|
40161
|
-
|
|
40162
|
-
|
|
40163
|
-
|
|
40164
|
-
|
|
40173
|
+
const scanFile = (file) => {
|
|
40174
|
+
for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
|
|
40175
|
+
if (committedFilesOnly && isFileGitIgnored(file)) continue;
|
|
40176
|
+
const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
|
|
40177
|
+
const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
|
|
40178
|
+
if (seen.has(key)) continue;
|
|
40179
|
+
seen.add(key);
|
|
40180
|
+
diagnostics.push(diagnostic);
|
|
40181
|
+
}
|
|
40182
|
+
};
|
|
40183
|
+
return {
|
|
40184
|
+
scanFile,
|
|
40185
|
+
diagnostics
|
|
40186
|
+
};
|
|
40187
|
+
};
|
|
40188
|
+
const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
|
|
40189
|
+
const session = createSecurityScanSession(rootDirectory, options);
|
|
40190
|
+
if (session === null) return [];
|
|
40191
|
+
let filesSinceYield = 0;
|
|
40192
|
+
for (const file of collectSecurityScanFiles(rootDirectory)) {
|
|
40193
|
+
session.scanFile(file);
|
|
40194
|
+
filesSinceYield += 1;
|
|
40195
|
+
if (filesSinceYield >= 16) {
|
|
40196
|
+
filesSinceYield = 0;
|
|
40197
|
+
await yieldToEventLoop();
|
|
40198
|
+
}
|
|
40165
40199
|
}
|
|
40166
|
-
return diagnostics;
|
|
40200
|
+
return session.diagnostics;
|
|
40167
40201
|
};
|
|
40168
40202
|
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
40169
40203
|
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
@@ -40396,6 +40430,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
40396
40430
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
40397
40431
|
};
|
|
40398
40432
|
const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
|
|
40433
|
+
const readSystemFacts = () => ({
|
|
40434
|
+
availableCores: os.availableParallelism(),
|
|
40435
|
+
totalMemoryBytes: os.totalmem(),
|
|
40436
|
+
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
40437
|
+
});
|
|
40438
|
+
/**
|
|
40439
|
+
* How many real deslop dead-code child processes may run at once, across the
|
|
40440
|
+
* concurrent per-project `runInspect` fibers of one CLI run. The cap is the
|
|
40441
|
+
* smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
|
|
40442
|
+
* workers that fit in available memory, floored at 1.
|
|
40443
|
+
*
|
|
40444
|
+
* On a roomy dev box / CI runner this resolves high enough that every
|
|
40445
|
+
* concurrently-scanned project still spawns its own worker (no serialization vs
|
|
40446
|
+
* the prior uncapped behavior); on a memory-constrained runner it collapses
|
|
40447
|
+
* toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
|
|
40448
|
+
* instead of oversubscribing memory with N simultaneous children — the global
|
|
40449
|
+
* cap the per-project spawn path lacked.
|
|
40450
|
+
*
|
|
40451
|
+
* Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
|
|
40452
|
+
* heavier dead-code worker. `facts` is injectable for tests.
|
|
40453
|
+
*/
|
|
40454
|
+
const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
|
|
40455
|
+
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
40456
|
+
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
|
|
40457
|
+
return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
40458
|
+
};
|
|
40459
|
+
let availableSlots = -1;
|
|
40460
|
+
const waiters = [];
|
|
40461
|
+
const releaseSlot = () => {
|
|
40462
|
+
const nextWaiter = waiters.shift();
|
|
40463
|
+
if (nextWaiter !== void 0) nextWaiter();
|
|
40464
|
+
else availableSlots += 1;
|
|
40465
|
+
};
|
|
40466
|
+
/**
|
|
40467
|
+
* Runs `task` once a dead-code worker slot is free, releasing the slot when the
|
|
40468
|
+
* task settles (success or failure). With a high cap (roomy machine) every
|
|
40469
|
+
* caller proceeds immediately; with a low cap (constrained runner) callers
|
|
40470
|
+
* queue and run as slots free.
|
|
40471
|
+
*
|
|
40472
|
+
* `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
|
|
40473
|
+
* this caller is queued, the call rejects without acquiring a slot or running
|
|
40474
|
+
* `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
|
|
40475
|
+
* then spawn a child only to tear it down. A queued caller that aborts removes
|
|
40476
|
+
* its own waiter so a later release never hands a slot to a dead request.
|
|
40477
|
+
*/
|
|
40478
|
+
const withDeadCodeWorkerSlot = async (task, abortSignal) => {
|
|
40479
|
+
if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
|
|
40480
|
+
if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
|
|
40481
|
+
if (availableSlots > 0) availableSlots -= 1;
|
|
40482
|
+
else await new Promise((resolve, reject) => {
|
|
40483
|
+
const waiter = () => {
|
|
40484
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
40485
|
+
resolve();
|
|
40486
|
+
};
|
|
40487
|
+
const onAbort = () => {
|
|
40488
|
+
const queuedIndex = waiters.indexOf(waiter);
|
|
40489
|
+
if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
|
|
40490
|
+
reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
|
|
40491
|
+
};
|
|
40492
|
+
waiters.push(waiter);
|
|
40493
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
40494
|
+
});
|
|
40495
|
+
try {
|
|
40496
|
+
return await task();
|
|
40497
|
+
} finally {
|
|
40498
|
+
releaseSlot();
|
|
40499
|
+
}
|
|
40500
|
+
};
|
|
40399
40501
|
/**
|
|
40400
40502
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
40401
40503
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -40686,14 +40788,17 @@ const checkDeadCode = async (options) => {
|
|
|
40686
40788
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40687
40789
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40688
40790
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
40689
|
-
const
|
|
40690
|
-
|
|
40691
|
-
|
|
40692
|
-
|
|
40693
|
-
|
|
40694
|
-
|
|
40695
|
-
|
|
40696
|
-
|
|
40791
|
+
const spawnAndRun = () => {
|
|
40792
|
+
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40793
|
+
rootDirectory,
|
|
40794
|
+
entryPatterns,
|
|
40795
|
+
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
40796
|
+
ignorePatterns,
|
|
40797
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
40798
|
+
parseConcurrency: options.parseConcurrency
|
|
40799
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
40800
|
+
};
|
|
40801
|
+
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
40697
40802
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
40698
40803
|
const diagnostics = [];
|
|
40699
40804
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -41094,43 +41199,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41094
41199
|
* reason: GitInvocationFailed })` so the rest of the codebase
|
|
41095
41200
|
* sees a single failure channel.
|
|
41096
41201
|
*/
|
|
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({
|
|
41202
|
+
const runCommand = (input) => {
|
|
41203
|
+
const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
|
|
41121
41204
|
status: 127,
|
|
41122
41205
|
stdout: "",
|
|
41123
41206
|
stderr: String(cause)
|
|
41124
|
-
})
|
|
41125
|
-
return new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41207
|
+
}) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41126
41208
|
args: [...input.args],
|
|
41127
41209
|
directory: input.directory,
|
|
41128
41210
|
cause
|
|
41129
|
-
}) });
|
|
41130
|
-
|
|
41131
|
-
|
|
41132
|
-
|
|
41133
|
-
|
|
41211
|
+
}) }));
|
|
41212
|
+
return scoped(gen(function* () {
|
|
41213
|
+
if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
|
|
41214
|
+
const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
|
|
41215
|
+
if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
|
|
41216
|
+
const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
|
|
41217
|
+
cwd: input.directory,
|
|
41218
|
+
env: input.env,
|
|
41219
|
+
extendEnv: true
|
|
41220
|
+
}));
|
|
41221
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
41222
|
+
const stdoutByteCount = yield* make$13(0);
|
|
41223
|
+
const [stdout, stderr, status] = yield* all([
|
|
41224
|
+
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({
|
|
41225
|
+
args: [...input.args],
|
|
41226
|
+
directory: input.directory,
|
|
41227
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
41228
|
+
}) })) : void_)))))),
|
|
41229
|
+
mkString(decodeText(handle.stderr)),
|
|
41230
|
+
handle.exitCode
|
|
41231
|
+
], { concurrency: 3 });
|
|
41232
|
+
return {
|
|
41233
|
+
status,
|
|
41234
|
+
stdout,
|
|
41235
|
+
stderr
|
|
41236
|
+
};
|
|
41237
|
+
})).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
|
|
41238
|
+
"git.command": input.command,
|
|
41239
|
+
"git.subcommand": input.args[0] ?? ""
|
|
41240
|
+
} }));
|
|
41241
|
+
};
|
|
41134
41242
|
const runGit = (directory, args) => runCommand({
|
|
41135
41243
|
command: "git",
|
|
41136
41244
|
args,
|
|
@@ -41163,7 +41271,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41163
41271
|
"rev-parse",
|
|
41164
41272
|
"--verify",
|
|
41165
41273
|
branch
|
|
41166
|
-
]).pipe(map$3((result) => result.status === 0));
|
|
41274
|
+
]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
|
|
41167
41275
|
const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
|
|
41168
41276
|
const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
|
|
41169
41277
|
"merge-base",
|
|
@@ -41377,7 +41485,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41377
41485
|
]);
|
|
41378
41486
|
if (result.status !== 0) return null;
|
|
41379
41487
|
return parseChangedLineRanges(result.stdout);
|
|
41380
|
-
}).pipe(withSpan("Git.changedLineRanges"))
|
|
41488
|
+
}).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
|
|
41381
41489
|
});
|
|
41382
41490
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
41383
41491
|
/**
|
|
@@ -43854,7 +43962,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43854
43962
|
* diagnostics).
|
|
43855
43963
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
43856
43964
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
43857
|
-
* expo/react-native
|
|
43965
|
+
* expo/react-native), collected synchronously. The heavier
|
|
43966
|
+
* content-regex security scan is forked instead (like supply-chain
|
|
43967
|
+
* below) and joined before the concat, so its CPU overlaps lint
|
|
43968
|
+
* rather than blocking the event loop before it.
|
|
43858
43969
|
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
43859
43970
|
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
43860
43971
|
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
@@ -43874,7 +43985,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43874
43985
|
* order, so terminal output is identical either way; supply-chain
|
|
43875
43986
|
* rides alongside without a spinner.
|
|
43876
43987
|
* 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
|
|
43988
|
+
* FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
|
|
43878
43989
|
* byte-identical regardless of which fiber settled first. The
|
|
43879
43990
|
* viewer-permission fiber is joined later, during score-metadata
|
|
43880
43991
|
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
@@ -43935,12 +44046,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43935
44046
|
...checkPnpmHardening(scanDirectory),
|
|
43936
44047
|
...checkReactServerComponentsAdvisory(scanDirectory, project),
|
|
43937
44048
|
...checkExpoProject(scanDirectory, project),
|
|
43938
|
-
...checkReactNativeProject(scanDirectory, project)
|
|
43939
|
-
...checkSecurityScan(scanDirectory, {
|
|
43940
|
-
project,
|
|
43941
|
-
ignoredTags: input.ignoredTags
|
|
43942
|
-
})
|
|
44049
|
+
...checkReactNativeProject(scanDirectory, project)
|
|
43943
44050
|
])));
|
|
44051
|
+
const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
|
|
44052
|
+
project,
|
|
44053
|
+
ignoredTags: input.ignoredTags
|
|
44054
|
+
})).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
|
|
43944
44055
|
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
43945
44056
|
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
43946
44057
|
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
@@ -44076,9 +44187,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44076
44187
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
44077
44188
|
const supplyChainResult = yield* join(supplyChainFiber);
|
|
44078
44189
|
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
44190
|
+
const securityScanCollected = yield* join(securityScanFiber);
|
|
44079
44191
|
yield* reporterService.finalize;
|
|
44080
44192
|
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
44081
44193
|
...envCollected,
|
|
44194
|
+
...securityScanCollected,
|
|
44082
44195
|
...supplyChainCollected,
|
|
44083
44196
|
...lintCollected,
|
|
44084
44197
|
...deadCodeCollected
|
|
@@ -44617,6 +44730,61 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
44617
44730
|
}
|
|
44618
44731
|
return crypto.createHash("sha1").update(parts.join("|")).digest("hex");
|
|
44619
44732
|
};
|
|
44733
|
+
const AI_TRAINING_ENV_VARS = [
|
|
44734
|
+
["HF_DATASETS_CACHE", "huggingface"],
|
|
44735
|
+
["HF_HOME", "huggingface"],
|
|
44736
|
+
["HUGGINGFACE_HUB_CACHE", "huggingface"],
|
|
44737
|
+
["SPACE_ID", "huggingface-spaces"],
|
|
44738
|
+
["CUDA_VISIBLE_DEVICES", "cuda"],
|
|
44739
|
+
["NVIDIA_VISIBLE_DEVICES", "nvidia"],
|
|
44740
|
+
["ROCR_VISIBLE_DEVICES", "rocm"],
|
|
44741
|
+
["TPU_NAME", "google-tpu"],
|
|
44742
|
+
["WANDB_RUN_ID", "wandb"],
|
|
44743
|
+
["WANDB_SWEEP_ID", "wandb"],
|
|
44744
|
+
["MLFLOW_RUN_ID", "mlflow"],
|
|
44745
|
+
["MLFLOW_TRACKING_URI", "mlflow"],
|
|
44746
|
+
["COMET_EXPERIMENT_KEY", "comet"],
|
|
44747
|
+
["NEPTUNE_RUN_ID", "neptune"],
|
|
44748
|
+
["CLEARML_TASK_ID", "clearml"],
|
|
44749
|
+
["DVC_STAGE", "dvc"],
|
|
44750
|
+
["RAY_WORKER_PROCESS", "ray"],
|
|
44751
|
+
["RAY_ADDRESS", "ray"],
|
|
44752
|
+
["MUJOCO_GL", "mujoco"],
|
|
44753
|
+
["MUJOCO_PATH", "mujoco"],
|
|
44754
|
+
["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
|
|
44755
|
+
["SM_TRAINING_ENV", "sagemaker"],
|
|
44756
|
+
["TRAINING_JOB_ARN", "sagemaker"],
|
|
44757
|
+
["SAGEMAKER_BASE_DIR", "sagemaker"],
|
|
44758
|
+
["AZUREML_RUN_ID", "azure-ml"],
|
|
44759
|
+
["AZURE_ML_MODEL_DIR", "azure-ml"],
|
|
44760
|
+
["CLOUD_ML_PROJECT_ID", "vertex-ai"],
|
|
44761
|
+
["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
|
|
44762
|
+
["DET_MASTER", "determined-ai"],
|
|
44763
|
+
["LIGHTNING_USER_ID", "lightning-ai"],
|
|
44764
|
+
["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
|
|
44765
|
+
["ARGO_WORKFLOW_NAME", "argo-workflows"],
|
|
44766
|
+
["KFP_POD_NAME", "kubeflow-pipelines"],
|
|
44767
|
+
["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
|
|
44768
|
+
["COLAB_BACKEND_VERSION", "google-colab"],
|
|
44769
|
+
["DATABRICKS_RUNTIME_VERSION", "databricks"],
|
|
44770
|
+
["DAYTONA_WS_ID", "daytona"],
|
|
44771
|
+
["DAYTONA_WS_NAME", "daytona"],
|
|
44772
|
+
["E2B_SANDBOX_ID", "e2b"],
|
|
44773
|
+
["MODAL_FUNCTION_ID", "modal"],
|
|
44774
|
+
["MODAL_TASK_ID", "modal"],
|
|
44775
|
+
["RUNPOD_POD_ID", "runpod"],
|
|
44776
|
+
["REPLICATE_USERNAME", "replicate"],
|
|
44777
|
+
["VAST_CONTAINERLABEL", "vast-ai"],
|
|
44778
|
+
["HARBOR_URL", "harbor"],
|
|
44779
|
+
["HARBOR_HOSTNAME", "harbor"],
|
|
44780
|
+
["SWE_BENCH_TASK", "swe-bench"],
|
|
44781
|
+
["SWEBENCH_TASK", "swe-bench"],
|
|
44782
|
+
["SWE_AGENT_MODEL", "swe-agent"]
|
|
44783
|
+
];
|
|
44784
|
+
const detectAiTrainingEnvironment = () => {
|
|
44785
|
+
for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
|
|
44786
|
+
return null;
|
|
44787
|
+
};
|
|
44620
44788
|
const groupBy = (items, keyFn) => {
|
|
44621
44789
|
const groups = /* @__PURE__ */ new Map();
|
|
44622
44790
|
for (const item of items) {
|
|
@@ -44860,6 +45028,7 @@ const NANOSECONDS_PER_SECOND = 1000000000n;
|
|
|
44860
45028
|
const METRIC = {
|
|
44861
45029
|
cliInvoked: "cli.invoked",
|
|
44862
45030
|
cliError: "cli.error",
|
|
45031
|
+
cliEnvironmentError: "cli.env_error",
|
|
44863
45032
|
projectDetected: "project.detected",
|
|
44864
45033
|
projectPathSelected: "project.path_selected",
|
|
44865
45034
|
projectConfigSelected: "project.config_selected",
|
|
@@ -44889,7 +45058,9 @@ const METRIC = {
|
|
|
44889
45058
|
lspSessionStarted: "lsp.session.started",
|
|
44890
45059
|
lspScanCompleted: "lsp.scan.completed",
|
|
44891
45060
|
lspScanDuration: "lsp.scan.duration",
|
|
44892
|
-
lspScanDiagnostics: "lsp.scan.diagnostics"
|
|
45061
|
+
lspScanDiagnostics: "lsp.scan.diagnostics",
|
|
45062
|
+
aiTrainingWarningShown: "ai.training.warning_shown",
|
|
45063
|
+
jsonOutUsed: "json.out_used"
|
|
44893
45064
|
};
|
|
44894
45065
|
//#endregion
|
|
44895
45066
|
//#region src/cli/utils/noop-console.ts
|
|
@@ -44932,7 +45103,7 @@ const makeNoopConsole = () => ({
|
|
|
44932
45103
|
});
|
|
44933
45104
|
//#endregion
|
|
44934
45105
|
//#region src/cli/utils/version.ts
|
|
44935
|
-
const VERSION = "0.5.8-dev.
|
|
45106
|
+
const VERSION = "0.5.8-dev.a7ad969";
|
|
44936
45107
|
//#endregion
|
|
44937
45108
|
//#region src/cli/utils/json-mode.ts
|
|
44938
45109
|
let context = null;
|
|
@@ -44970,12 +45141,13 @@ const installSilentConsole = () => {
|
|
|
44970
45141
|
"trace"
|
|
44971
45142
|
]) target[key] = source[key];
|
|
44972
45143
|
};
|
|
44973
|
-
const enableJsonMode = ({ compact, directory }) => {
|
|
45144
|
+
const enableJsonMode = ({ compact, directory, outputFile }) => {
|
|
44974
45145
|
context = {
|
|
44975
45146
|
compact,
|
|
44976
45147
|
directory,
|
|
44977
45148
|
startTime: performance$1.now(),
|
|
44978
|
-
mode: "full"
|
|
45149
|
+
mode: "full",
|
|
45150
|
+
outputFile: outputFile ?? null
|
|
44979
45151
|
};
|
|
44980
45152
|
installSilentConsole();
|
|
44981
45153
|
};
|
|
@@ -44988,7 +45160,11 @@ const setJsonReportMode = (mode) => {
|
|
|
44988
45160
|
};
|
|
44989
45161
|
const writeJsonReport = (report) => {
|
|
44990
45162
|
const serialized = context?.compact ? JSON.stringify(report) : JSON.stringify(report, null, 2);
|
|
44991
|
-
|
|
45163
|
+
if (context?.outputFile) {
|
|
45164
|
+
const resolvedPath = Path.resolve(context.outputFile);
|
|
45165
|
+
NFS.mkdirSync(Path.dirname(resolvedPath), { recursive: true });
|
|
45166
|
+
NFS.writeFileSync(resolvedPath, `${serialized}\n`);
|
|
45167
|
+
} else process.stdout.write(`${serialized}\n`);
|
|
44992
45168
|
};
|
|
44993
45169
|
const writeJsonErrorReport = (error, sentryEventId) => {
|
|
44994
45170
|
if (!context) return;
|
|
@@ -45296,13 +45472,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
45296
45472
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
45297
45473
|
* standard `SENTRY_RELEASE` override.
|
|
45298
45474
|
*/
|
|
45299
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.
|
|
45475
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.a7ad969`;
|
|
45300
45476
|
/**
|
|
45301
45477
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
45302
45478
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
45303
45479
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
45304
45480
|
*/
|
|
45305
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.
|
|
45481
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.a7ad969") ? "development" : "production");
|
|
45306
45482
|
/**
|
|
45307
45483
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
45308
45484
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -48898,6 +49074,25 @@ const shouldBlockCi = (diagnostics, blockingLevel) => {
|
|
|
48898
49074
|
*/
|
|
48899
49075
|
const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
48900
49076
|
//#endregion
|
|
49077
|
+
//#region src/cli/utils/with-namespace.ts
|
|
49078
|
+
/**
|
|
49079
|
+
* Prefixes every key in a flat attribute group with `namespace.`, so a logical
|
|
49080
|
+
* group (scan config, diagnostics rollup, score, …) carries one dotted
|
|
49081
|
+
* namespace applied in a single place rather than hand-spelled into each key —
|
|
49082
|
+
* which is how the wide event drifted into a flat, half-namespaced soup. The
|
|
49083
|
+
* dotted prefix is what makes the attributes tree up in Sentry's attribute
|
|
49084
|
+
* browser and stay group-/filter-/aggregate-able in the Spans dataset.
|
|
49085
|
+
*
|
|
49086
|
+
* Value types are preserved verbatim (numbers stay numbers so Sentry infers a
|
|
49087
|
+
* numeric attribute and `p75(...)`/`avg(...)` work; `null` is kept so
|
|
49088
|
+
* `toSpanAttributes` can drop absent signals rather than coerce them).
|
|
49089
|
+
*/
|
|
49090
|
+
const withNamespace = (namespace, attributes) => {
|
|
49091
|
+
const namespaced = {};
|
|
49092
|
+
for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
|
|
49093
|
+
return namespaced;
|
|
49094
|
+
};
|
|
49095
|
+
//#endregion
|
|
48901
49096
|
//#region src/cli/utils/build-run-event.ts
|
|
48902
49097
|
const readEnvBoolean = (name) => {
|
|
48903
49098
|
const value = process.env[name];
|
|
@@ -48919,12 +49114,12 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48919
49114
|
if (input.result === void 0) {
|
|
48920
49115
|
const error = input.error;
|
|
48921
49116
|
const known = isReactDoctorError(error);
|
|
48922
|
-
return {
|
|
48923
|
-
|
|
49117
|
+
return withNamespace("outcome", {
|
|
49118
|
+
status: "error",
|
|
48924
49119
|
exitCode: 1,
|
|
48925
49120
|
knownError: known,
|
|
48926
49121
|
errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
|
|
48927
|
-
};
|
|
49122
|
+
});
|
|
48928
49123
|
}
|
|
48929
49124
|
const result = input.result;
|
|
48930
49125
|
const summary = summarizeDiagnostics(result.diagnostics);
|
|
@@ -48958,66 +49153,82 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48958
49153
|
}
|
|
48959
49154
|
let fixGroupedFindings = 0;
|
|
48960
49155
|
for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
|
|
49156
|
+
const categoryRollup = {};
|
|
49157
|
+
for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
|
|
48961
49158
|
const attributes = {
|
|
48962
|
-
outcome,
|
|
48963
|
-
|
|
48964
|
-
|
|
48965
|
-
|
|
48966
|
-
|
|
48967
|
-
|
|
48968
|
-
|
|
48969
|
-
|
|
48970
|
-
|
|
48971
|
-
|
|
48972
|
-
|
|
48973
|
-
|
|
48974
|
-
|
|
48975
|
-
|
|
48976
|
-
|
|
48977
|
-
|
|
48978
|
-
|
|
48979
|
-
|
|
48980
|
-
|
|
48981
|
-
|
|
48982
|
-
|
|
48983
|
-
|
|
48984
|
-
|
|
48985
|
-
|
|
48986
|
-
|
|
48987
|
-
|
|
48988
|
-
|
|
48989
|
-
|
|
48990
|
-
|
|
48991
|
-
|
|
48992
|
-
|
|
48993
|
-
|
|
48994
|
-
|
|
48995
|
-
|
|
48996
|
-
|
|
48997
|
-
|
|
48998
|
-
|
|
48999
|
-
|
|
49000
|
-
|
|
49001
|
-
|
|
49002
|
-
|
|
49003
|
-
|
|
49004
|
-
|
|
49159
|
+
...withNamespace("outcome", {
|
|
49160
|
+
status: outcome,
|
|
49161
|
+
exitCode: wouldBlock ? 1 : 0,
|
|
49162
|
+
wouldBlock,
|
|
49163
|
+
blocking: blockingLevel,
|
|
49164
|
+
clean: isClean,
|
|
49165
|
+
skippedChecks: result.skippedChecks.length
|
|
49166
|
+
}),
|
|
49167
|
+
...withNamespace("diag", {
|
|
49168
|
+
total: summary.totalDiagnosticCount,
|
|
49169
|
+
errors: summary.errorCount,
|
|
49170
|
+
warnings: summary.warningCount,
|
|
49171
|
+
affectedFiles: summary.affectedFileCount,
|
|
49172
|
+
inTestFiles: diagnosticsInTestFiles,
|
|
49173
|
+
inStoryFiles: diagnosticsInStoryFiles,
|
|
49174
|
+
distinctRules: countByRule.size,
|
|
49175
|
+
topRule,
|
|
49176
|
+
fixGroups: findingsPerFixGroup.size,
|
|
49177
|
+
fixGroupedFindings,
|
|
49178
|
+
...categoryRollup
|
|
49179
|
+
}),
|
|
49180
|
+
...withNamespace("score", {
|
|
49181
|
+
value: result.score ? result.score.score : null,
|
|
49182
|
+
label: result.score ? result.score.label : null,
|
|
49183
|
+
available: result.score !== null
|
|
49184
|
+
}),
|
|
49185
|
+
...withNamespace("lint", {
|
|
49186
|
+
failed: input.didLintFail ?? null,
|
|
49187
|
+
failureReasonKind: input.lintFailureReasonKind ?? null,
|
|
49188
|
+
partialFailureCount: input.lintPartialFailureCount ?? null,
|
|
49189
|
+
droppedFileCount: input.lintDroppedFileCount ?? null,
|
|
49190
|
+
cacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
49191
|
+
cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
49192
|
+
cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
|
|
49193
|
+
}),
|
|
49194
|
+
...withNamespace("deadCode", {
|
|
49195
|
+
failed: input.didDeadCodeFail ?? null,
|
|
49196
|
+
overlapped: input.deadCodeOverlapped ?? null
|
|
49197
|
+
}),
|
|
49198
|
+
...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
|
|
49199
|
+
...withNamespace("timing", {
|
|
49200
|
+
elapsedMs: result.elapsedMilliseconds,
|
|
49201
|
+
scanMs: result.scanElapsedMilliseconds ?? null
|
|
49202
|
+
}),
|
|
49203
|
+
...withNamespace("migration", {
|
|
49204
|
+
largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
|
|
49205
|
+
largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
49206
|
+
largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
|
|
49207
|
+
})
|
|
49208
|
+
};
|
|
49209
|
+
if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
|
|
49210
|
+
new: summary.totalDiagnosticCount,
|
|
49211
|
+
fixed: result.baselineDelta.fixedCount,
|
|
49212
|
+
baseTotal: result.baselineDelta.baseTotalCount,
|
|
49213
|
+
degraded: false
|
|
49214
|
+
}));
|
|
49215
|
+
else if (input.gateExempt) attributes["baseline.degraded"] = true;
|
|
49005
49216
|
return attributes;
|
|
49006
49217
|
};
|
|
49007
|
-
const
|
|
49218
|
+
const buildActionAttributes = () => {
|
|
49008
49219
|
const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
|
|
49009
|
-
return {
|
|
49220
|
+
return withNamespace("action", {
|
|
49010
49221
|
actorAssociation: githubActorAssociation ?? null,
|
|
49011
49222
|
runnerOs: detectRunnerOs(),
|
|
49012
49223
|
comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
|
|
49013
49224
|
reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
|
|
49014
49225
|
versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
|
|
49015
|
-
};
|
|
49226
|
+
});
|
|
49016
49227
|
};
|
|
49017
|
-
const
|
|
49228
|
+
const buildScanAttributes = (input) => {
|
|
49018
49229
|
const ruleOverrides = input.userConfig?.rules ?? {};
|
|
49019
49230
|
const ruleKeys = Object.keys(ruleOverrides);
|
|
49020
|
-
return {
|
|
49231
|
+
return withNamespace("scan", {
|
|
49021
49232
|
mode: input.mode,
|
|
49022
49233
|
scope: input.scope,
|
|
49023
49234
|
parallel: input.parallel,
|
|
@@ -49032,12 +49243,18 @@ const buildConfigAttributes = (input) => {
|
|
|
49032
49243
|
ignoredTagCount: input.ignoredTagCount,
|
|
49033
49244
|
hasCustomConfig: input.hasCustomConfig,
|
|
49034
49245
|
rulesConfigured: ruleKeys.length,
|
|
49035
|
-
rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length
|
|
49036
|
-
|
|
49246
|
+
rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
|
|
49247
|
+
fileCount: input.result?.scannedFileCount ?? null
|
|
49248
|
+
});
|
|
49037
49249
|
};
|
|
49038
49250
|
/**
|
|
49039
|
-
* Projects a scan into the
|
|
49040
|
-
* per-scan "wide event".
|
|
49251
|
+
* Projects a scan into the namespaced attribute set for its root span — the
|
|
49252
|
+
* canonical per-scan "wide event". Every attribute carries a dotted namespace
|
|
49253
|
+
* that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
|
|
49254
|
+
* verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
|
|
49255
|
+
* `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
|
|
49256
|
+
* tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
|
|
49257
|
+
* in the Spans dataset. Pure and exported so the projection (outcome
|
|
49041
49258
|
* precedence, rule/category rollups, CI knobs, config shape) is unit-testable
|
|
49042
49259
|
* without a live Sentry client. `null` values are dropped so absent signals
|
|
49043
49260
|
* never become misleading `"null"` attributes. The run + project base context
|
|
@@ -49046,8 +49263,8 @@ const buildConfigAttributes = (input) => {
|
|
|
49046
49263
|
* those don't carry.
|
|
49047
49264
|
*/
|
|
49048
49265
|
const buildRunEventAttributes = (input) => toSpanAttributes({
|
|
49049
|
-
...
|
|
49050
|
-
...
|
|
49266
|
+
...buildScanAttributes(input),
|
|
49267
|
+
...buildActionAttributes(),
|
|
49051
49268
|
...buildOutcomeAttributes(input)
|
|
49052
49269
|
});
|
|
49053
49270
|
/**
|
|
@@ -49711,6 +49928,7 @@ const CI_PITCH_EVENT = "ci-pitch";
|
|
|
49711
49928
|
const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
|
|
49712
49929
|
const SETUP_HINT_EVENT = "setup-hint";
|
|
49713
49930
|
const HANDOFF_TARGET_PREFERENCE_ID = "handoff-target";
|
|
49931
|
+
const INSTALL_AGENTS_PREFERENCE_ID = "install-agents";
|
|
49714
49932
|
const foldLegacyDecisions = (projects, legacy, eventId) => {
|
|
49715
49933
|
for (const [hash, record] of Object.entries(legacy ?? {})) {
|
|
49716
49934
|
const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
|
|
@@ -50374,7 +50592,7 @@ const readPackageJson = (projectRoot) => {
|
|
|
50374
50592
|
return null;
|
|
50375
50593
|
}
|
|
50376
50594
|
};
|
|
50377
|
-
const writeJsonFile
|
|
50595
|
+
const writeJsonFile = (filePath, value) => {
|
|
50378
50596
|
NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50379
50597
|
};
|
|
50380
50598
|
const packageHasDependency = (projectRoot, dependencyName) => {
|
|
@@ -51094,7 +51312,8 @@ const getStagedSourceFiles = async (directory) => {
|
|
|
51094
51312
|
return [...await runPromise(gen(function* () {
|
|
51095
51313
|
return yield* (yield* StagedFiles).discoverSourceFiles(directory);
|
|
51096
51314
|
}).pipe(provide(stagedFilesLayer)))];
|
|
51097
|
-
} catch {
|
|
51315
|
+
} catch (error) {
|
|
51316
|
+
cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
|
|
51098
51317
|
return [];
|
|
51099
51318
|
}
|
|
51100
51319
|
};
|
|
@@ -51113,6 +51332,35 @@ const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) =>
|
|
|
51113
51332
|
};
|
|
51114
51333
|
};
|
|
51115
51334
|
//#endregion
|
|
51335
|
+
//#region src/cli/utils/is-environment-error.ts
|
|
51336
|
+
const isNodeSystemError = (error) => error instanceof Error && typeof error.code === "string";
|
|
51337
|
+
const ENVIRONMENT_ERROR_CODES = new Set([
|
|
51338
|
+
"ENOSPC",
|
|
51339
|
+
"EIO",
|
|
51340
|
+
"EROFS",
|
|
51341
|
+
"EACCES",
|
|
51342
|
+
"EPERM",
|
|
51343
|
+
"ENOTDIR"
|
|
51344
|
+
]);
|
|
51345
|
+
const isEnvironmentError = (error) => {
|
|
51346
|
+
if (!isNodeSystemError(error)) return false;
|
|
51347
|
+
if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
|
|
51348
|
+
return error.code !== void 0 && ENVIRONMENT_ERROR_CODES.has(error.code);
|
|
51349
|
+
};
|
|
51350
|
+
const formatEnvironmentError = (error) => {
|
|
51351
|
+
if (!isNodeSystemError(error)) return error instanceof Error ? error.message : String(error);
|
|
51352
|
+
switch (error.code) {
|
|
51353
|
+
case "ENOSPC": return "No space left on device. Free up disk space and try again.";
|
|
51354
|
+
case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
|
|
51355
|
+
case "EROFS": return "Read-only filesystem: cannot write to this location.";
|
|
51356
|
+
case "EACCES":
|
|
51357
|
+
case "EPERM": return error.path ? `Permission denied accessing ${error.path}. Check file permissions and try again.` : "Permission denied. Check file permissions and try again.";
|
|
51358
|
+
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.";
|
|
51359
|
+
case "ENOENT": return "Required command not found. Ensure the tool (e.g. git) is installed and on your PATH.";
|
|
51360
|
+
default: return error.message;
|
|
51361
|
+
}
|
|
51362
|
+
};
|
|
51363
|
+
//#endregion
|
|
51116
51364
|
//#region src/cli/utils/handle-error.ts
|
|
51117
51365
|
const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
|
|
51118
51366
|
const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
|
|
@@ -51195,15 +51443,19 @@ const handleError = (error, options = {}) => {
|
|
|
51195
51443
|
process.exitCode = 1;
|
|
51196
51444
|
};
|
|
51197
51445
|
/**
|
|
51198
|
-
* Renderer for expected, user-actionable failures — a bad `--diff` value
|
|
51199
|
-
* a base branch that isn't fetched
|
|
51200
|
-
*
|
|
51201
|
-
*
|
|
51446
|
+
* Renderer for expected, user-actionable failures — a bad `--diff` value,
|
|
51447
|
+
* a base branch that isn't fetched, or environment errors like disk-full or
|
|
51448
|
+
* permission-denied. Prints just the (already human-readable) message — no
|
|
51449
|
+
* "Something went wrong", prefilled issue, Discord link, or Sentry reference
|
|
51450
|
+
* — because there is no bug to report.
|
|
51202
51451
|
*/
|
|
51203
51452
|
const handleUserError = (error, options = {}) => {
|
|
51453
|
+
const isEnvError = isEnvironmentError(error);
|
|
51454
|
+
if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: error.code ?? "unknown" });
|
|
51455
|
+
const message = isEnvError ? formatEnvironmentError(error) : formatErrorForReport(error);
|
|
51204
51456
|
runSync(gen(function* () {
|
|
51205
51457
|
yield* error$1("");
|
|
51206
|
-
yield* error$1(highlighter.error(
|
|
51458
|
+
yield* error$1(highlighter.error(message));
|
|
51207
51459
|
yield* error$1("");
|
|
51208
51460
|
}));
|
|
51209
51461
|
if (options.shouldExit !== false) process.exit(1);
|
|
@@ -51218,7 +51470,7 @@ const handleUserError = (error, options = {}) => {
|
|
|
51218
51470
|
* `handleUserError` (a plain message — no "Something went wrong", prefilled
|
|
51219
51471
|
* issue, Discord link, or Sentry reference), since there is no bug to report.
|
|
51220
51472
|
*
|
|
51221
|
-
*
|
|
51473
|
+
* Four distinct shapes reach the CLI's catch blocks:
|
|
51222
51474
|
*
|
|
51223
51475
|
* - **Project-discovery failures** (`NoReactDependencyError`,
|
|
51224
51476
|
* `ProjectNotFoundError`, `PackageJsonNotFoundError`, `NotADirectoryError`,
|
|
@@ -51231,12 +51483,19 @@ const handleUserError = (error, options = {}) => {
|
|
|
51231
51483
|
* `--project` name.
|
|
51232
51484
|
* - **Bad `--diff` input** (`GitBaseBranchInvalid` / `GitBaseBranchMissing`)
|
|
51233
51485
|
* stays the tagged `ReactDoctorError`, so dispatch on the reason `_tag`.
|
|
51486
|
+
* - **Environment failures** (`ENOSPC`, `EIO`, `EROFS`, `EACCES`, `EPERM`,
|
|
51487
|
+
* `ENOTDIR`, plus a `spawn`-scoped `ENOENT` for a missing binary) — disk
|
|
51488
|
+
* full / failing / read-only, permission denied, or a path blocked by a
|
|
51489
|
+
* file. React Doctor cannot fix the user's environment; exit cleanly with an
|
|
51490
|
+
* actionable message instead of crashing. See `is-environment-error.ts` for
|
|
51491
|
+
* why the set stays narrow (codes that usually mean our bug keep reaching
|
|
51492
|
+
* Sentry).
|
|
51234
51493
|
*
|
|
51235
51494
|
* This composes the existing core narrowers rather than introducing a new
|
|
51236
51495
|
* error-shape helper (AGENTS.md): it encodes CLI-layer reporting policy, not
|
|
51237
51496
|
* knowledge of the `ReactDoctorError` shape.
|
|
51238
51497
|
*/
|
|
51239
|
-
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
51498
|
+
const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isEnvironmentError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
|
|
51240
51499
|
//#endregion
|
|
51241
51500
|
//#region src/cli/utils/build-handoff-payload.ts
|
|
51242
51501
|
const buildHandoffPayload = (input) => {
|
|
@@ -51317,6 +51576,20 @@ const detectAvailableAgents = async () => {
|
|
|
51317
51576
|
const detected = new Set([...detectPathAvailableAgents(), ...await detectInstalledSkillAgents()]);
|
|
51318
51577
|
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
51319
51578
|
};
|
|
51579
|
+
const DEFAULT_INSTALL_AGENTS = [
|
|
51580
|
+
"claude-code",
|
|
51581
|
+
"cursor",
|
|
51582
|
+
"codex",
|
|
51583
|
+
"opencode"
|
|
51584
|
+
];
|
|
51585
|
+
const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
|
|
51586
|
+
const detected = new Set(detectedAgents);
|
|
51587
|
+
const remembered = rememberedAgents.filter((agent) => detected.has(agent));
|
|
51588
|
+
if (remembered.length > 0) return remembered;
|
|
51589
|
+
const defaults = DEFAULT_INSTALL_AGENTS.filter((agent) => detected.has(agent));
|
|
51590
|
+
if (defaults.length > 0) return defaults;
|
|
51591
|
+
return detectedAgents.length === 1 ? [...detectedAgents] : [];
|
|
51592
|
+
};
|
|
51320
51593
|
//#endregion
|
|
51321
51594
|
//#region src/cli/utils/install-doctor-script.ts
|
|
51322
51595
|
const DOCTOR_SCRIPT_NAME = "doctor";
|
|
@@ -51398,7 +51671,7 @@ const installDoctorScript = (options) => {
|
|
|
51398
51671
|
};
|
|
51399
51672
|
})();
|
|
51400
51673
|
const scriptStatus = scriptTarget.status;
|
|
51401
|
-
if (scriptStatus === "created") writeJsonFile
|
|
51674
|
+
if (scriptStatus === "created") writeJsonFile(packageJsonPath, {
|
|
51402
51675
|
...packageJson,
|
|
51403
51676
|
scripts: {
|
|
51404
51677
|
...isRecord$1(scripts) ? scripts : {},
|
|
@@ -51969,32 +52242,59 @@ const setUpGitHubActions = async (options) => {
|
|
|
51969
52242
|
return didCreateWorkflow;
|
|
51970
52243
|
};
|
|
51971
52244
|
//#endregion
|
|
52245
|
+
//#region src/cli/utils/install-agents-preference.ts
|
|
52246
|
+
const INSTALL_AGENTS_PREFERENCE = {
|
|
52247
|
+
id: INSTALL_AGENTS_PREFERENCE_ID,
|
|
52248
|
+
scope: "global"
|
|
52249
|
+
};
|
|
52250
|
+
const PREFERENCE_SEPARATOR = ",";
|
|
52251
|
+
const readInstallAgents = (options = {}) => {
|
|
52252
|
+
const stored = readPreference(INSTALL_AGENTS_PREFERENCE, {}, options);
|
|
52253
|
+
if (stored === null) return [];
|
|
52254
|
+
return stored.split(PREFERENCE_SEPARATOR).map((entry) => entry.trim()).filter((entry) => isSkillAgentType(entry));
|
|
52255
|
+
};
|
|
52256
|
+
const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_AGENTS_PREFERENCE, agents.join(PREFERENCE_SEPARATOR), {}, options);
|
|
52257
|
+
//#endregion
|
|
51972
52258
|
//#region src/cli/utils/install-agent-hooks.ts
|
|
51973
52259
|
const CLAUDE_AGENT = "claude-code";
|
|
51974
52260
|
const CURSOR_AGENT = "cursor";
|
|
51975
52261
|
const CLAUDE_SETTINGS_RELATIVE_PATH = ".claude/settings.json";
|
|
51976
|
-
const CLAUDE_HOOK_RELATIVE_PATH = ".claude/hooks/react-doctor.
|
|
51977
|
-
const CLAUDE_HOOK_COMMAND = "
|
|
52262
|
+
const CLAUDE_HOOK_RELATIVE_PATH = ".claude/hooks/react-doctor.mjs";
|
|
52263
|
+
const CLAUDE_HOOK_COMMAND = "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/react-doctor.mjs\"";
|
|
51978
52264
|
const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
|
|
51979
|
-
const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.
|
|
52265
|
+
const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.mjs";
|
|
52266
|
+
const CURSOR_HOOK_COMMAND = "node .cursor/hooks/react-doctor.mjs";
|
|
51980
52267
|
const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
|
|
51981
52268
|
const CURSOR_HOOKS_SCHEMA_VERSION = 1;
|
|
51982
|
-
const JSON_INDENT_SPACES$1 = 2;
|
|
51983
52269
|
const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
|
|
51984
52270
|
const readJsonFile = (filePath, fallback) => {
|
|
51985
52271
|
if (!NFS.existsSync(filePath)) return fallback;
|
|
51986
52272
|
const content = NFS.readFileSync(filePath, "utf8").trim();
|
|
51987
52273
|
if (content.length === 0) return fallback;
|
|
51988
|
-
|
|
52274
|
+
try {
|
|
52275
|
+
return JSON.parse(content);
|
|
52276
|
+
} catch (error) {
|
|
52277
|
+
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.`);
|
|
52278
|
+
throw error;
|
|
52279
|
+
}
|
|
51989
52280
|
};
|
|
51990
|
-
const
|
|
51991
|
-
|
|
51992
|
-
|
|
52281
|
+
const ensureDirectoryExists = (directoryPath) => {
|
|
52282
|
+
try {
|
|
52283
|
+
NFS.mkdirSync(directoryPath, { recursive: true });
|
|
52284
|
+
} catch (error) {
|
|
52285
|
+
const code = error.code;
|
|
52286
|
+
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.`);
|
|
52287
|
+
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.`);
|
|
52288
|
+
throw error;
|
|
52289
|
+
}
|
|
52290
|
+
};
|
|
52291
|
+
const writeJsonFileWithDirectoryCheck = (filePath, value) => {
|
|
52292
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
52293
|
+
writeJsonFile(filePath, value);
|
|
51993
52294
|
};
|
|
51994
52295
|
const writeHookScript = (filePath) => {
|
|
51995
|
-
|
|
52296
|
+
ensureDirectoryExists(Path.dirname(filePath));
|
|
51996
52297
|
NFS.writeFileSync(filePath, buildAgentHookScript());
|
|
51997
|
-
NFS.chmodSync(filePath, 493);
|
|
51998
52298
|
};
|
|
51999
52299
|
const hasClaudeHookCommand = (groups) => groups.some((group) => (group.hooks ?? []).some((hook) => hook.command === CLAUDE_HOOK_COMMAND));
|
|
52000
52300
|
const installClaudeHook = (projectRoot) => {
|
|
@@ -52008,14 +52308,14 @@ const installClaudeHook = (projectRoot) => {
|
|
|
52008
52308
|
command: CLAUDE_HOOK_COMMAND
|
|
52009
52309
|
}] });
|
|
52010
52310
|
hooks.PostToolBatch = postToolBatchHooks;
|
|
52011
|
-
|
|
52311
|
+
writeJsonFileWithDirectoryCheck(settingsPath, {
|
|
52012
52312
|
...settings,
|
|
52013
52313
|
hooks
|
|
52014
52314
|
});
|
|
52015
52315
|
writeHookScript(hookPath);
|
|
52016
52316
|
return [settingsPath, hookPath];
|
|
52017
52317
|
};
|
|
52018
|
-
const hasCursorHookCommand = (handlers) => handlers.some((handler) => handler.command ===
|
|
52318
|
+
const hasCursorHookCommand = (handlers) => handlers.some((handler) => handler.command === CURSOR_HOOK_COMMAND);
|
|
52019
52319
|
const installCursorHook = (projectRoot) => {
|
|
52020
52320
|
const configPath = Path.join(projectRoot, CURSOR_HOOKS_RELATIVE_PATH);
|
|
52021
52321
|
const hookPath = Path.join(projectRoot, CURSOR_HOOK_RELATIVE_PATH);
|
|
@@ -52023,12 +52323,12 @@ const installCursorHook = (projectRoot) => {
|
|
|
52023
52323
|
const hooks = { ...config.hooks ?? {} };
|
|
52024
52324
|
const postToolUseHooks = [...hooks.postToolUse ?? []];
|
|
52025
52325
|
if (!hasCursorHookCommand(postToolUseHooks)) postToolUseHooks.push({
|
|
52026
|
-
command:
|
|
52326
|
+
command: CURSOR_HOOK_COMMAND,
|
|
52027
52327
|
matcher: CURSOR_HOOK_MATCHER,
|
|
52028
52328
|
timeout: 120
|
|
52029
52329
|
});
|
|
52030
52330
|
hooks.postToolUse = postToolUseHooks;
|
|
52031
|
-
|
|
52331
|
+
writeJsonFileWithDirectoryCheck(configPath, {
|
|
52032
52332
|
...config,
|
|
52033
52333
|
version: config.version ?? CURSOR_HOOKS_SCHEMA_VERSION,
|
|
52034
52334
|
hooks
|
|
@@ -52037,102 +52337,111 @@ const installCursorHook = (projectRoot) => {
|
|
|
52037
52337
|
return [configPath, hookPath];
|
|
52038
52338
|
};
|
|
52039
52339
|
const buildAgentHookScript = () => [
|
|
52040
|
-
"
|
|
52041
|
-
"
|
|
52340
|
+
"import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';",
|
|
52341
|
+
"import { tmpdir } from 'node:os';",
|
|
52342
|
+
"import { join, dirname } from 'node:path';",
|
|
52343
|
+
"import { fileURLToPath } from 'node:url';",
|
|
52344
|
+
"import { spawnSync } from 'node:child_process';",
|
|
52042
52345
|
"",
|
|
52043
|
-
"
|
|
52044
|
-
"
|
|
52045
|
-
"trap 'rm -f \"$input_file\" \"$output_file\"' EXIT",
|
|
52046
|
-
"cat > \"$input_file\"",
|
|
52346
|
+
"const __filename = fileURLToPath(import.meta.url);",
|
|
52347
|
+
"const __dirname = dirname(__filename);",
|
|
52047
52348
|
"",
|
|
52048
|
-
"
|
|
52049
|
-
"project_root=${CLAUDE_PROJECT_DIR:-}",
|
|
52050
|
-
"if [ -z \"$project_root\" ]; then",
|
|
52051
|
-
" project_root=$(CDPATH= cd \"$script_dir/../..\" && pwd)",
|
|
52052
|
-
"fi",
|
|
52053
|
-
"if ! cd \"$project_root\"; then",
|
|
52054
|
-
" exit 0",
|
|
52055
|
-
"fi",
|
|
52349
|
+
"const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
|
|
52056
52350
|
"",
|
|
52057
|
-
"
|
|
52058
|
-
"
|
|
52059
|
-
" return 0",
|
|
52060
|
-
"
|
|
52351
|
+
"const readStdin = () => {",
|
|
52352
|
+
" try {",
|
|
52353
|
+
" return readFileSync(0, 'utf8');",
|
|
52354
|
+
" } catch {",
|
|
52355
|
+
" return '';",
|
|
52356
|
+
" }",
|
|
52357
|
+
"};",
|
|
52061
52358
|
"",
|
|
52062
|
-
"
|
|
52063
|
-
"const fs = require('node:fs');",
|
|
52064
|
-
"const inputPath = process.argv[2];",
|
|
52065
|
-
"const editToolNames = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
|
|
52066
|
-
"try {",
|
|
52067
|
-
" const input = JSON.parse(fs.readFileSync(inputPath, 'utf8') || '{}');",
|
|
52359
|
+
"const shouldScan = (input) => {",
|
|
52068
52360
|
" const eventName = input.hook_event_name || input.eventName || input.event_name;",
|
|
52069
52361
|
" if (eventName === 'PostToolBatch') {",
|
|
52070
52362
|
" const toolCalls = Array.isArray(input.tool_calls) ? input.tool_calls : [];",
|
|
52071
|
-
"
|
|
52363
|
+
" return toolCalls.some((toolCall) => EDIT_TOOL_NAMES.has(toolCall.tool_name));",
|
|
52072
52364
|
" }",
|
|
52073
52365
|
" const toolName = input.tool_name || input.toolName || input.tool;",
|
|
52074
|
-
"
|
|
52075
|
-
"}
|
|
52076
|
-
" process.exit(0);",
|
|
52077
|
-
"}",
|
|
52078
|
-
"NODE",
|
|
52079
|
-
"}",
|
|
52366
|
+
" return !toolName || EDIT_TOOL_NAMES.has(toolName);",
|
|
52367
|
+
"};",
|
|
52080
52368
|
"",
|
|
52081
|
-
"
|
|
52082
|
-
"
|
|
52083
|
-
"
|
|
52084
|
-
"
|
|
52085
|
-
"
|
|
52369
|
+
"const runReactDoctor = (outputPath) => {",
|
|
52370
|
+
" // Each candidate is a single shell command string (not an args array):",
|
|
52371
|
+
" // `shell: true` is required to run the Windows `.cmd` shims, and an args",
|
|
52372
|
+
" // array with `shell: true` trips Node's DEP0190. A missing command exits",
|
|
52373
|
+
" // 127 via the shell (no ENOENT error), so fall through on that. With no",
|
|
52374
|
+
" // runner found, exit 0 silently — stdout is parsed as the hook's JSON.",
|
|
52375
|
+
" const commands = [",
|
|
52376
|
+
" './node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score',",
|
|
52377
|
+
" 'react-doctor --verbose --scope changed --blocking warning --no-score',",
|
|
52378
|
+
" 'pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
|
|
52379
|
+
" 'npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
|
|
52380
|
+
" ];",
|
|
52086
52381
|
"",
|
|
52087
|
-
"
|
|
52088
|
-
"
|
|
52089
|
-
"
|
|
52090
|
-
"
|
|
52382
|
+
" for (const command of commands) {",
|
|
52383
|
+
" const result = spawnSync(command, { encoding: 'utf8', shell: true });",
|
|
52384
|
+
" if (result.error?.code === 'ENOENT' || result.status === 127) continue;",
|
|
52385
|
+
" try {",
|
|
52386
|
+
" writeFileSync(outputPath, (result.stdout || '') + (result.stderr || ''));",
|
|
52387
|
+
" } catch {}",
|
|
52388
|
+
" return result.status;",
|
|
52389
|
+
" }",
|
|
52091
52390
|
"",
|
|
52092
|
-
"
|
|
52093
|
-
"
|
|
52094
|
-
" return",
|
|
52095
|
-
" fi",
|
|
52391
|
+
" return 0;",
|
|
52392
|
+
"};",
|
|
52096
52393
|
"",
|
|
52097
|
-
"
|
|
52098
|
-
"
|
|
52099
|
-
"
|
|
52100
|
-
"
|
|
52394
|
+
"const cleanup = (...paths) => {",
|
|
52395
|
+
" for (const path of paths) {",
|
|
52396
|
+
" try { unlinkSync(path); } catch {}",
|
|
52397
|
+
" }",
|
|
52398
|
+
"};",
|
|
52101
52399
|
"",
|
|
52102
|
-
"
|
|
52103
|
-
"
|
|
52104
|
-
"
|
|
52400
|
+
"const main = () => {",
|
|
52401
|
+
" let input;",
|
|
52402
|
+
" try {",
|
|
52403
|
+
" const stdinContent = readStdin();",
|
|
52404
|
+
" input = JSON.parse(stdinContent || '{}');",
|
|
52405
|
+
" } catch {",
|
|
52406
|
+
" input = {};",
|
|
52407
|
+
" }",
|
|
52105
52408
|
"",
|
|
52106
|
-
"if !
|
|
52107
|
-
"
|
|
52108
|
-
"
|
|
52409
|
+
" if (!shouldScan(input)) {",
|
|
52410
|
+
" process.exit(0);",
|
|
52411
|
+
" }",
|
|
52109
52412
|
"",
|
|
52110
|
-
"
|
|
52111
|
-
"
|
|
52112
|
-
"fi",
|
|
52413
|
+
" const projectRoot = process.env.CLAUDE_PROJECT_DIR || join(__dirname, '../..');",
|
|
52414
|
+
" const outputPath = join(tmpdir(), `react-doctor-agent-hook-output-${process.pid}.txt`);",
|
|
52113
52415
|
"",
|
|
52114
|
-
"node - \"$input_file\" \"$output_file\" <<'NODE'",
|
|
52115
|
-
"const fs = require('node:fs');",
|
|
52116
|
-
"const inputPath = process.argv[2];",
|
|
52117
|
-
"const outputPath = process.argv[3];",
|
|
52118
|
-
"const readInput = () => {",
|
|
52119
52416
|
" try {",
|
|
52120
|
-
"
|
|
52417
|
+
" process.chdir(projectRoot);",
|
|
52121
52418
|
" } catch {",
|
|
52122
|
-
"
|
|
52419
|
+
" process.exit(0);",
|
|
52420
|
+
" }",
|
|
52421
|
+
"",
|
|
52422
|
+
" const scanResult = runReactDoctor(outputPath);",
|
|
52423
|
+
" if (scanResult === 0) {",
|
|
52424
|
+
" cleanup(outputPath);",
|
|
52425
|
+
" process.exit(0);",
|
|
52426
|
+
" }",
|
|
52427
|
+
"",
|
|
52428
|
+
" const scanOutput = readFileSync(outputPath, 'utf8').trim();",
|
|
52429
|
+
" cleanup(outputPath);",
|
|
52430
|
+
"",
|
|
52431
|
+
" if (!scanOutput) {",
|
|
52432
|
+
" process.exit(0);",
|
|
52433
|
+
" }",
|
|
52434
|
+
"",
|
|
52435
|
+
" 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}`;",
|
|
52436
|
+
"",
|
|
52437
|
+
" if (input.hook_event_name === 'PostToolBatch') {",
|
|
52438
|
+
" console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: message } }));",
|
|
52439
|
+
" } else {",
|
|
52440
|
+
" console.log(JSON.stringify({ additional_context: message }));",
|
|
52123
52441
|
" }",
|
|
52124
52442
|
"};",
|
|
52125
|
-
"
|
|
52126
|
-
"
|
|
52127
|
-
"if (!scanOutput) process.exit(0);",
|
|
52128
|
-
"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}`;",
|
|
52129
|
-
"if (input.hook_event_name === 'PostToolBatch') {",
|
|
52130
|
-
" console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: message } }));",
|
|
52131
|
-
"} else {",
|
|
52132
|
-
" console.log(JSON.stringify({ additional_context: message }));",
|
|
52133
|
-
"}",
|
|
52134
|
-
"NODE",
|
|
52135
|
-
""
|
|
52443
|
+
"",
|
|
52444
|
+
"main();"
|
|
52136
52445
|
].join("\n");
|
|
52137
52446
|
const installReactDoctorAgentHooks = (options) => {
|
|
52138
52447
|
const installedAgents = [];
|
|
@@ -52264,7 +52573,7 @@ const installPackageJsonHook = (options, strategy) => {
|
|
|
52264
52573
|
parent = cloned;
|
|
52265
52574
|
}
|
|
52266
52575
|
parent[leafKey] = strategy.leafShape === "array" ? appendArrayCommand(parent[leafKey]) : appendStringCommand(parent[leafKey]);
|
|
52267
|
-
writeJsonFile
|
|
52576
|
+
writeJsonFile(packageJsonPath, nextPackageJson);
|
|
52268
52577
|
removeLegacyManagedRunner(options.projectRoot);
|
|
52269
52578
|
return {
|
|
52270
52579
|
hookPath: packageJsonPath,
|
|
@@ -52847,6 +53156,9 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
52847
53156
|
const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
|
|
52848
53157
|
if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
|
|
52849
53158
|
if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
|
|
53159
|
+
const rememberedAgents = options.lastSelectedAgents ?? readInstallAgents();
|
|
53160
|
+
const defaultSelectedAgents = computeDefaultSelectedAgents(detectedAgents, rememberedAgents);
|
|
53161
|
+
const usedRememberedAgents = rememberedAgents.some((agent) => detectedAgents.includes(agent));
|
|
52850
53162
|
const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
|
|
52851
53163
|
type: "multiselect",
|
|
52852
53164
|
name: "agents",
|
|
@@ -52854,12 +53166,13 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
52854
53166
|
choices: detectedAgents.map((agent) => ({
|
|
52855
53167
|
title: getSkillAgentConfig(agent).displayName,
|
|
52856
53168
|
value: agent,
|
|
52857
|
-
selected:
|
|
53169
|
+
selected: defaultSelectedAgents.includes(agent)
|
|
52858
53170
|
})),
|
|
52859
53171
|
instructions: false,
|
|
52860
53172
|
min: 1
|
|
52861
53173
|
}, promptOptions)).agents ?? [];
|
|
52862
53174
|
if (selectedAgents.length === 0) return;
|
|
53175
|
+
if (!skipPrompts && !options.dryRun) rememberInstallAgents(selectedAgents);
|
|
52863
53176
|
let dependencyResult;
|
|
52864
53177
|
if (!options.dryRun) {
|
|
52865
53178
|
await installReactDoctorSkillStep(sourceDir, selectedAgents, projectRoot);
|
|
@@ -52918,6 +53231,8 @@ const runInstallReactDoctor = async (options = {}) => {
|
|
|
52918
53231
|
}
|
|
52919
53232
|
recordCount(METRIC.installCompleted, 1, {
|
|
52920
53233
|
agentsCount: selectedAgents.length,
|
|
53234
|
+
agentsDetected: detectedAgents.length,
|
|
53235
|
+
usedRememberedAgents,
|
|
52921
53236
|
gitHook: shouldInstallGitHook,
|
|
52922
53237
|
agentHooks: shouldInstallAgentHooks,
|
|
52923
53238
|
workflow: didInstallWorkflow,
|
|
@@ -52952,6 +53267,26 @@ const CLI_AGENT_AUTO_FLAGS = {
|
|
|
52952
53267
|
codex: ["--yolo"],
|
|
52953
53268
|
cursor: ["--force"]
|
|
52954
53269
|
};
|
|
53270
|
+
const resolveCursorBundledNode = () => {
|
|
53271
|
+
if (!isWindows) return null;
|
|
53272
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
53273
|
+
if (!localAppData) return null;
|
|
53274
|
+
const cursorAgentRoot = Path.join(localAppData, "cursor-agent", "versions");
|
|
53275
|
+
try {
|
|
53276
|
+
const versions = NFS.readdirSync(cursorAgentRoot);
|
|
53277
|
+
if (versions.length === 0) return null;
|
|
53278
|
+
const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
|
|
53279
|
+
for (const version of sortedVersions) try {
|
|
53280
|
+
const bundledNodePath = Path.join(cursorAgentRoot, version, "node.exe");
|
|
53281
|
+
const entryScriptPath = Path.join(cursorAgentRoot, version, "index.js");
|
|
53282
|
+
if (NFS.statSync(bundledNodePath).isFile() && NFS.statSync(entryScriptPath).isFile()) return {
|
|
53283
|
+
command: bundledNodePath,
|
|
53284
|
+
args: [entryScriptPath]
|
|
53285
|
+
};
|
|
53286
|
+
} catch {}
|
|
53287
|
+
} catch {}
|
|
53288
|
+
return null;
|
|
53289
|
+
};
|
|
52955
53290
|
const resolveWindowsCmdEntryScript = (command) => {
|
|
52956
53291
|
if (!isWindows) return null;
|
|
52957
53292
|
const pathDirectories = (process.env.PATH ?? "").split(Path.delimiter).filter(Boolean);
|
|
@@ -52971,11 +53306,10 @@ const resolveWindowsCmdEntryScript = (command) => {
|
|
|
52971
53306
|
}
|
|
52972
53307
|
return null;
|
|
52973
53308
|
};
|
|
52974
|
-
const spawnAgent = (command, args, cwd
|
|
53309
|
+
const spawnAgent = (command, args, cwd) => new Promise((resolve, reject) => {
|
|
52975
53310
|
const child = spawn(command, [...args], {
|
|
52976
53311
|
cwd,
|
|
52977
|
-
stdio: "inherit"
|
|
52978
|
-
shell
|
|
53312
|
+
stdio: "inherit"
|
|
52979
53313
|
});
|
|
52980
53314
|
child.on("error", reject);
|
|
52981
53315
|
child.on("close", (code) => resolve(code ?? 0));
|
|
@@ -52984,6 +53318,10 @@ const launchCliAgent = async (agentId, prompt, cwd) => {
|
|
|
52984
53318
|
const binary = CLI_AGENT_BINARIES[agentId];
|
|
52985
53319
|
const agentArgs = [...CLI_AGENT_AUTO_FLAGS[agentId], prompt];
|
|
52986
53320
|
if (isWindows) {
|
|
53321
|
+
if (agentId === "cursor") {
|
|
53322
|
+
const cursorResolution = resolveCursorBundledNode();
|
|
53323
|
+
if (cursorResolution) return spawnAgent(cursorResolution.command, [...cursorResolution.args, ...agentArgs], cwd);
|
|
53324
|
+
}
|
|
52987
53325
|
const entryScript = resolveWindowsCmdEntryScript(binary);
|
|
52988
53326
|
if (entryScript) return spawnAgent(process.execPath, [entryScript, ...agentArgs], cwd);
|
|
52989
53327
|
}
|
|
@@ -53697,7 +54035,7 @@ const warnDeprecatedDiff = (flags, userConfig) => {
|
|
|
53697
54035
|
};
|
|
53698
54036
|
const warnDiffUnavailable = (requested, isQuiet) => {
|
|
53699
54037
|
if (isQuiet) return;
|
|
53700
|
-
if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (
|
|
54038
|
+
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.`);
|
|
53701
54039
|
else cliLogger.warn("No feature branch or uncommitted changes detected. Running full scan.");
|
|
53702
54040
|
cliLogger.break();
|
|
53703
54041
|
};
|
|
@@ -53894,6 +54232,7 @@ const resolveRequestedProjects = (requestedNames, workspacePackages, rootDirecto
|
|
|
53894
54232
|
return requestedNames.map((requestedName) => {
|
|
53895
54233
|
const matched = workspacePackages.find((workspacePackage) => workspacePackage.name === requestedName || Path.basename(workspacePackage.directory) === requestedName);
|
|
53896
54234
|
if (matched) return matched.directory;
|
|
54235
|
+
if (Path.basename(rootDirectory) === requestedName) return rootDirectory;
|
|
53897
54236
|
const candidateDirectory = Path.resolve(rootDirectory, requestedName);
|
|
53898
54237
|
if (isDirectory(candidateDirectory)) {
|
|
53899
54238
|
recordCount(METRIC.projectPathSelected);
|
|
@@ -54001,67 +54340,15 @@ const warnDeprecatedFailOn = (flags, userConfig) => {
|
|
|
54001
54340
|
cliLogger.warn(`${source} is deprecated; rename it to ${replacement}.`);
|
|
54002
54341
|
};
|
|
54003
54342
|
//#endregion
|
|
54004
|
-
//#region src/cli/utils/detect-ai-training-environment.ts
|
|
54005
|
-
const AI_TRAINING_ENV_VARS = [
|
|
54006
|
-
["HF_DATASETS_CACHE", "huggingface"],
|
|
54007
|
-
["HF_HOME", "huggingface"],
|
|
54008
|
-
["HUGGINGFACE_HUB_CACHE", "huggingface"],
|
|
54009
|
-
["CUDA_VISIBLE_DEVICES", "cuda"],
|
|
54010
|
-
["NVIDIA_VISIBLE_DEVICES", "nvidia"],
|
|
54011
|
-
["WANDB_RUN_ID", "wandb"],
|
|
54012
|
-
["MLFLOW_RUN_ID", "mlflow"],
|
|
54013
|
-
["MLFLOW_TRACKING_URI", "mlflow"],
|
|
54014
|
-
["COMET_EXPERIMENT_KEY", "comet"],
|
|
54015
|
-
["NEPTUNE_RUN_ID", "neptune"],
|
|
54016
|
-
["RAY_WORKER_PROCESS", "ray"],
|
|
54017
|
-
["RAY_ADDRESS", "ray"],
|
|
54018
|
-
["MUJOCO_GL", "mujoco"],
|
|
54019
|
-
["MUJOCO_PATH", "mujoco"],
|
|
54020
|
-
["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
|
|
54021
|
-
["SAGEMAKER_BASE_DIR", "sagemaker"],
|
|
54022
|
-
["AZURE_ML_MODEL_DIR", "azure-ml"],
|
|
54023
|
-
["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
|
|
54024
|
-
["DAYTONA_WS_ID", "daytona"],
|
|
54025
|
-
["DAYTONA_WS_NAME", "daytona"],
|
|
54026
|
-
["E2B_SANDBOX_ID", "e2b"],
|
|
54027
|
-
["MODAL_FUNCTION_ID", "modal"],
|
|
54028
|
-
["MODAL_TASK_ID", "modal"],
|
|
54029
|
-
["RUNPOD_POD_ID", "runpod"],
|
|
54030
|
-
["HARBOR_URL", "harbor"],
|
|
54031
|
-
["HARBOR_HOSTNAME", "harbor"],
|
|
54032
|
-
["SWE_BENCH_TASK", "swe-bench"],
|
|
54033
|
-
["SWEBENCH_TASK", "swe-bench"],
|
|
54034
|
-
["SWE_AGENT_MODEL", "swe-agent"],
|
|
54035
|
-
["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
|
|
54036
|
-
["COLAB_BACKEND_VERSION", "google-colab"],
|
|
54037
|
-
["DATABRICKS_RUNTIME_VERSION", "databricks"],
|
|
54038
|
-
["SM_TRAINING_ENV", "sagemaker"],
|
|
54039
|
-
["TRAINING_JOB_ARN", "sagemaker"],
|
|
54040
|
-
["AZUREML_RUN_ID", "azure-ml"],
|
|
54041
|
-
["CLOUD_ML_PROJECT_ID", "vertex-ai"],
|
|
54042
|
-
["WANDB_SWEEP_ID", "wandb"],
|
|
54043
|
-
["DVC_STAGE", "dvc"],
|
|
54044
|
-
["CLEARML_TASK_ID", "clearml"],
|
|
54045
|
-
["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
|
|
54046
|
-
["DET_MASTER", "determined-ai"],
|
|
54047
|
-
["LIGHTNING_USER_ID", "lightning-ai"],
|
|
54048
|
-
["ARGO_WORKFLOW_NAME", "argo-workflows"],
|
|
54049
|
-
["KFP_POD_NAME", "kubeflow-pipelines"],
|
|
54050
|
-
["SPACE_ID", "huggingface-spaces"],
|
|
54051
|
-
["REPLICATE_USERNAME", "replicate"],
|
|
54052
|
-
["VAST_CONTAINERLABEL", "vast-ai"],
|
|
54053
|
-
["TPU_NAME", "google-tpu"],
|
|
54054
|
-
["ROCR_VISIBLE_DEVICES", "rocm"]
|
|
54055
|
-
];
|
|
54056
|
-
const detectAiTrainingEnvironment = () => {
|
|
54057
|
-
for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar] !== void 0) return label;
|
|
54058
|
-
return null;
|
|
54059
|
-
};
|
|
54060
|
-
//#endregion
|
|
54061
54343
|
//#region src/cli/utils/warn-ai-training-environment.ts
|
|
54344
|
+
let didWarnAiTraining = false;
|
|
54062
54345
|
const warnIfAiTrainingEnvironment = () => {
|
|
54063
|
-
if (
|
|
54064
|
-
|
|
54346
|
+
if (didWarnAiTraining) return;
|
|
54347
|
+
const detected = detectAiTrainingEnvironment();
|
|
54348
|
+
if (detected === null) return;
|
|
54349
|
+
didWarnAiTraining = true;
|
|
54350
|
+
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`);
|
|
54351
|
+
recordCount(METRIC.aiTrainingWarningShown, 1, { environment: detected });
|
|
54065
54352
|
};
|
|
54066
54353
|
//#endregion
|
|
54067
54354
|
//#region src/cli/utils/validate-mode-flags.ts
|
|
@@ -54147,10 +54434,14 @@ const inspectAction = async (directory, flags) => {
|
|
|
54147
54434
|
const isQuiet = isScoreOnly || isJsonMode;
|
|
54148
54435
|
const requestedDirectory = Path.resolve(directory);
|
|
54149
54436
|
const startTime = performance$1.now();
|
|
54150
|
-
if (isJsonMode)
|
|
54151
|
-
|
|
54152
|
-
|
|
54153
|
-
|
|
54437
|
+
if (isJsonMode) {
|
|
54438
|
+
enableJsonMode({
|
|
54439
|
+
compact: Boolean(flags.jsonCompact),
|
|
54440
|
+
directory: requestedDirectory,
|
|
54441
|
+
outputFile: flags.jsonOut
|
|
54442
|
+
});
|
|
54443
|
+
if (flags.jsonOut) recordCount(METRIC.jsonOutUsed, 1);
|
|
54444
|
+
}
|
|
54154
54445
|
recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
|
|
54155
54446
|
try {
|
|
54156
54447
|
validateModeFlags(flags);
|
|
@@ -54452,6 +54743,10 @@ const installAction = async (options, command) => {
|
|
|
54452
54743
|
projectRoot: options.cwd ?? process.cwd()
|
|
54453
54744
|
});
|
|
54454
54745
|
} catch (error) {
|
|
54746
|
+
if (isExpectedUserError(error)) {
|
|
54747
|
+
handleUserError(error);
|
|
54748
|
+
return;
|
|
54749
|
+
}
|
|
54455
54750
|
handleError(error, { sentryEventId: await reportErrorToSentry(error) });
|
|
54456
54751
|
}
|
|
54457
54752
|
};
|
|
@@ -55084,6 +55379,14 @@ const applyColorPreference = (argv, env = process.env) => {
|
|
|
55084
55379
|
setColorEnabled(enabled);
|
|
55085
55380
|
};
|
|
55086
55381
|
//#endregion
|
|
55382
|
+
//#region src/cli/utils/ensure-windows-utf8-console.ts
|
|
55383
|
+
const ensureWindowsUtf8Console = () => {
|
|
55384
|
+
if (process.platform !== "win32" || !process.stdout.isTTY) return;
|
|
55385
|
+
try {
|
|
55386
|
+
execFileSync("chcp.com", ["65001"], { stdio: "ignore" });
|
|
55387
|
+
} catch {}
|
|
55388
|
+
};
|
|
55389
|
+
//#endregion
|
|
55087
55390
|
//#region src/cli/utils/exit-gracefully.ts
|
|
55088
55391
|
const exitGracefully = () => {
|
|
55089
55392
|
try {
|
|
@@ -55226,6 +55529,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
55226
55529
|
"--category",
|
|
55227
55530
|
"--changed-files-from",
|
|
55228
55531
|
"--blocking",
|
|
55532
|
+
"--json-out",
|
|
55229
55533
|
"--output-dir",
|
|
55230
55534
|
"--fail-on",
|
|
55231
55535
|
"--project",
|
|
@@ -55416,7 +55720,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
55416
55720
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
55417
55721
|
`;
|
|
55418
55722
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
55419
|
-
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);
|
|
55723
|
+
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);
|
|
55420
55724
|
program.action(inspectAction);
|
|
55421
55725
|
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));
|
|
55422
55726
|
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);
|
|
@@ -55441,6 +55745,7 @@ if (process.argv.includes("-V") && !strippedArgv.includes("-V")) {
|
|
|
55441
55745
|
process.exit(0);
|
|
55442
55746
|
}
|
|
55443
55747
|
applyColorPreference(strippedArgv);
|
|
55748
|
+
ensureWindowsUtf8Console();
|
|
55444
55749
|
const argv = normalizeHelpInvocation(strippedArgv, knownCommands);
|
|
55445
55750
|
Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => program.parseAsync(argv)).then(() => flushSentry()).catch(async (error) => {
|
|
55446
55751
|
const isUserError = isExpectedUserError(error);
|
|
@@ -55459,4 +55764,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
55459
55764
|
export {};
|
|
55460
55765
|
|
|
55461
55766
|
//# sourceMappingURL=cli.js.map
|
|
55462
|
-
//# debugId=
|
|
55767
|
+
//# debugId=eb7a01bc-f737-5207-b430-b8fee7a1895f
|