react-doctor 0.5.8-dev.f853efd → 0.5.8-dev.fff9466
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 +513 -220
- package/dist/index.d.ts +2 -2
- package/dist/index.js +236 -64
- package/dist/lsp.js +176 -65
- 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]="f1523493-79d1-56b7-9f5f-78f1dd741d1e")}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;
|
|
@@ -38556,7 +38558,7 @@ const resolveScanConcurrency = (requested) => {
|
|
|
38556
38558
|
if (!Number.isFinite(requested) || requested < 1) return 1;
|
|
38557
38559
|
return Math.min(Math.floor(requested), 32);
|
|
38558
38560
|
};
|
|
38559
|
-
const readSystemFacts = () => ({
|
|
38561
|
+
const readSystemFacts$1 = () => ({
|
|
38560
38562
|
availableCores: os.availableParallelism(),
|
|
38561
38563
|
totalMemoryBytes: os.totalmem(),
|
|
38562
38564
|
cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
|
|
@@ -38577,7 +38579,7 @@ const readSystemFacts = () => ({
|
|
|
38577
38579
|
* `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
|
|
38578
38580
|
* limited, and ceiling cases without mocking `os` or the filesystem.
|
|
38579
38581
|
*/
|
|
38580
|
-
const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
|
|
38582
|
+
const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
|
|
38581
38583
|
const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
|
|
38582
38584
|
const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
|
|
38583
38585
|
return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
|
|
@@ -40138,7 +40140,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
|
|
|
40138
40140
|
}
|
|
40139
40141
|
return true;
|
|
40140
40142
|
};
|
|
40141
|
-
const
|
|
40143
|
+
const yieldToEventLoop = () => new Promise((resolve) => {
|
|
40144
|
+
setImmediate(resolve);
|
|
40145
|
+
});
|
|
40146
|
+
const createSecurityScanSession = (rootDirectory, options) => {
|
|
40142
40147
|
const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
|
|
40143
40148
|
const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
|
|
40144
40149
|
const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
|
|
@@ -40153,7 +40158,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
40153
40158
|
committedFilesOnly: rule.committedFilesOnly === true
|
|
40154
40159
|
}];
|
|
40155
40160
|
});
|
|
40156
|
-
if (enabledScanRules.length === 0) return
|
|
40161
|
+
if (enabledScanRules.length === 0) return null;
|
|
40157
40162
|
const diagnostics = [];
|
|
40158
40163
|
const seen = /* @__PURE__ */ new Set();
|
|
40159
40164
|
const gitIgnoredCache = /* @__PURE__ */ new Map();
|
|
@@ -40165,15 +40170,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
|
|
|
40165
40170
|
}
|
|
40166
40171
|
return status === true;
|
|
40167
40172
|
};
|
|
40168
|
-
|
|
40169
|
-
|
|
40170
|
-
|
|
40171
|
-
|
|
40172
|
-
|
|
40173
|
-
|
|
40174
|
-
|
|
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
|
+
}
|
|
40175
40199
|
}
|
|
40176
|
-
return diagnostics;
|
|
40200
|
+
return session.diagnostics;
|
|
40177
40201
|
};
|
|
40178
40202
|
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
40179
40203
|
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
@@ -40406,6 +40430,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
|
|
|
40406
40430
|
return [...seen].filter((pattern) => pattern.length > 0);
|
|
40407
40431
|
};
|
|
40408
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
|
+
};
|
|
40409
40501
|
/**
|
|
40410
40502
|
* Resolves a path to its canonical, symlink-free form, falling back to
|
|
40411
40503
|
* the input when it cannot be realpath'd (broken symlink, permission
|
|
@@ -40430,6 +40522,11 @@ const toCanonicalPath = (filePath) => {
|
|
|
40430
40522
|
};
|
|
40431
40523
|
const DEAD_CODE_PLUGIN = "deslop";
|
|
40432
40524
|
const DEAD_CODE_CATEGORY = "Maintainability";
|
|
40525
|
+
const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
|
|
40526
|
+
"react-doctor",
|
|
40527
|
+
"eslint-plugin-react-doctor",
|
|
40528
|
+
"oxlint-plugin-react-doctor"
|
|
40529
|
+
]);
|
|
40433
40530
|
const DEAD_CODE_WORKER_SCRIPT = `
|
|
40434
40531
|
const inputChunks = [];
|
|
40435
40532
|
process.stdin.on("data", (chunk) => inputChunks.push(chunk));
|
|
@@ -40696,14 +40793,17 @@ const checkDeadCode = async (options) => {
|
|
|
40696
40793
|
if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
|
|
40697
40794
|
const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
|
|
40698
40795
|
const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
|
|
40699
|
-
const
|
|
40700
|
-
|
|
40701
|
-
|
|
40702
|
-
|
|
40703
|
-
|
|
40704
|
-
|
|
40705
|
-
|
|
40706
|
-
|
|
40796
|
+
const spawnAndRun = () => {
|
|
40797
|
+
return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
|
|
40798
|
+
rootDirectory,
|
|
40799
|
+
entryPatterns,
|
|
40800
|
+
tsConfigPath: resolveTsConfigPath(rootDirectory),
|
|
40801
|
+
ignorePatterns,
|
|
40802
|
+
deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
|
|
40803
|
+
parseConcurrency: options.parseConcurrency
|
|
40804
|
+
}), options.workerTimeoutMs ?? 12e4, options.abortSignal);
|
|
40805
|
+
};
|
|
40806
|
+
const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
|
|
40707
40807
|
const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
|
|
40708
40808
|
const diagnostics = [];
|
|
40709
40809
|
for (const unusedFile of result.unusedFiles) diagnostics.push({
|
|
@@ -40732,6 +40832,7 @@ const checkDeadCode = async (options) => {
|
|
|
40732
40832
|
});
|
|
40733
40833
|
}
|
|
40734
40834
|
for (const unusedDependency of result.unusedDependencies) {
|
|
40835
|
+
if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
|
|
40735
40836
|
const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
|
|
40736
40837
|
diagnostics.push({
|
|
40737
40838
|
filePath: "package.json",
|
|
@@ -41104,43 +41205,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41104
41205
|
* reason: GitInvocationFailed })` so the rest of the codebase
|
|
41105
41206
|
* sees a single failure channel.
|
|
41106
41207
|
*/
|
|
41107
|
-
const runCommand = (input) =>
|
|
41108
|
-
const
|
|
41109
|
-
cwd: input.directory,
|
|
41110
|
-
env: input.env,
|
|
41111
|
-
extendEnv: true
|
|
41112
|
-
}));
|
|
41113
|
-
const maxStdoutBytes = input.maxStdoutBytes;
|
|
41114
|
-
const stdoutByteCount = yield* make$13(0);
|
|
41115
|
-
const [stdout, stderr, status] = yield* all([
|
|
41116
|
-
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({
|
|
41117
|
-
args: [...input.args],
|
|
41118
|
-
directory: input.directory,
|
|
41119
|
-
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
41120
|
-
}) })) : void_)))))),
|
|
41121
|
-
mkString(decodeText(handle.stderr)),
|
|
41122
|
-
handle.exitCode
|
|
41123
|
-
], { concurrency: 3 });
|
|
41124
|
-
return {
|
|
41125
|
-
status,
|
|
41126
|
-
stdout,
|
|
41127
|
-
stderr
|
|
41128
|
-
};
|
|
41129
|
-
})).pipe(catchTag$1("PlatformError", (cause) => {
|
|
41130
|
-
if (input.command !== "git") return succeed$2({
|
|
41208
|
+
const runCommand = (input) => {
|
|
41209
|
+
const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
|
|
41131
41210
|
status: 127,
|
|
41132
41211
|
stdout: "",
|
|
41133
41212
|
stderr: String(cause)
|
|
41134
|
-
})
|
|
41135
|
-
return new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41213
|
+
}) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
|
|
41136
41214
|
args: [...input.args],
|
|
41137
41215
|
directory: input.directory,
|
|
41138
41216
|
cause
|
|
41139
|
-
}) });
|
|
41140
|
-
|
|
41141
|
-
|
|
41142
|
-
|
|
41143
|
-
|
|
41217
|
+
}) }));
|
|
41218
|
+
return scoped(gen(function* () {
|
|
41219
|
+
if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
|
|
41220
|
+
const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
|
|
41221
|
+
if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
|
|
41222
|
+
const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
|
|
41223
|
+
cwd: input.directory,
|
|
41224
|
+
env: input.env,
|
|
41225
|
+
extendEnv: true
|
|
41226
|
+
}));
|
|
41227
|
+
const maxStdoutBytes = input.maxStdoutBytes;
|
|
41228
|
+
const stdoutByteCount = yield* make$13(0);
|
|
41229
|
+
const [stdout, stderr, status] = yield* all([
|
|
41230
|
+
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({
|
|
41231
|
+
args: [...input.args],
|
|
41232
|
+
directory: input.directory,
|
|
41233
|
+
cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
|
|
41234
|
+
}) })) : void_)))))),
|
|
41235
|
+
mkString(decodeText(handle.stderr)),
|
|
41236
|
+
handle.exitCode
|
|
41237
|
+
], { concurrency: 3 });
|
|
41238
|
+
return {
|
|
41239
|
+
status,
|
|
41240
|
+
stdout,
|
|
41241
|
+
stderr
|
|
41242
|
+
};
|
|
41243
|
+
})).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
|
|
41244
|
+
"git.command": input.command,
|
|
41245
|
+
"git.subcommand": input.args[0] ?? ""
|
|
41246
|
+
} }));
|
|
41247
|
+
};
|
|
41144
41248
|
const runGit = (directory, args) => runCommand({
|
|
41145
41249
|
command: "git",
|
|
41146
41250
|
args,
|
|
@@ -41387,7 +41491,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
|
|
|
41387
41491
|
]);
|
|
41388
41492
|
if (result.status !== 0) return null;
|
|
41389
41493
|
return parseChangedLineRanges(result.stdout);
|
|
41390
|
-
}).pipe(withSpan("Git.changedLineRanges"))
|
|
41494
|
+
}).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
|
|
41391
41495
|
});
|
|
41392
41496
|
})).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
|
|
41393
41497
|
/**
|
|
@@ -43864,7 +43968,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43864
43968
|
* diagnostics).
|
|
43865
43969
|
* 2. beforeLint hook (e.g. CLI renders the project-detection block)
|
|
43866
43970
|
* 3. environment checks (reduced-motion + pnpm hardening +
|
|
43867
|
-
* expo/react-native
|
|
43971
|
+
* expo/react-native), collected synchronously. The heavier
|
|
43972
|
+
* content-regex security scan is forked instead (like supply-chain
|
|
43973
|
+
* below) and joined before the concat, so its CPU overlaps lint
|
|
43974
|
+
* rather than blocking the event loop before it.
|
|
43868
43975
|
* 4. The supply-chain check (Socket.dev) is forked onto a background
|
|
43869
43976
|
* fiber so its ~100% network-bound time overlaps the ~100%
|
|
43870
43977
|
* CPU/subprocess-bound lint pass below, collapsing two serial
|
|
@@ -43884,7 +43991,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
|
|
|
43884
43991
|
* order, so terminal output is identical either way; supply-chain
|
|
43885
43992
|
* rides alongside without a spinner.
|
|
43886
43993
|
* 6. Join the supply-chain fiber, then assemble the diagnostics in a
|
|
43887
|
-
* FIXED order (env, supply-chain, lint, dead-code) so the output is
|
|
43994
|
+
* FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
|
|
43888
43995
|
* byte-identical regardless of which fiber settled first. The
|
|
43889
43996
|
* viewer-permission fiber is joined later, during score-metadata
|
|
43890
43997
|
* assembly (it feeds score metadata, not diagnostics). The per-element
|
|
@@ -43945,12 +44052,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
43945
44052
|
...checkPnpmHardening(scanDirectory),
|
|
43946
44053
|
...checkReactServerComponentsAdvisory(scanDirectory, project),
|
|
43947
44054
|
...checkExpoProject(scanDirectory, project),
|
|
43948
|
-
...checkReactNativeProject(scanDirectory, project)
|
|
43949
|
-
...checkSecurityScan(scanDirectory, {
|
|
43950
|
-
project,
|
|
43951
|
-
ignoredTags: input.ignoredTags
|
|
43952
|
-
})
|
|
44055
|
+
...checkReactNativeProject(scanDirectory, project)
|
|
43953
44056
|
])));
|
|
44057
|
+
const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
|
|
44058
|
+
project,
|
|
44059
|
+
ignoredTags: input.ignoredTags
|
|
44060
|
+
})).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
|
|
43954
44061
|
const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
|
|
43955
44062
|
const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
|
|
43956
44063
|
const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
|
|
@@ -44086,9 +44193,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
|
|
|
44086
44193
|
else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
|
|
44087
44194
|
const supplyChainResult = yield* join(supplyChainFiber);
|
|
44088
44195
|
const supplyChainCollected = supplyChainResult.diagnostics;
|
|
44196
|
+
const securityScanCollected = yield* join(securityScanFiber);
|
|
44089
44197
|
yield* reporterService.finalize;
|
|
44090
44198
|
const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
|
|
44091
44199
|
...envCollected,
|
|
44200
|
+
...securityScanCollected,
|
|
44092
44201
|
...supplyChainCollected,
|
|
44093
44202
|
...lintCollected,
|
|
44094
44203
|
...deadCodeCollected
|
|
@@ -44627,6 +44736,61 @@ const computeConfigFingerprint = (projectDirectory, version) => {
|
|
|
44627
44736
|
}
|
|
44628
44737
|
return crypto.createHash("sha1").update(parts.join("|")).digest("hex");
|
|
44629
44738
|
};
|
|
44739
|
+
const AI_TRAINING_ENV_VARS = [
|
|
44740
|
+
["HF_DATASETS_CACHE", "huggingface"],
|
|
44741
|
+
["HF_HOME", "huggingface"],
|
|
44742
|
+
["HUGGINGFACE_HUB_CACHE", "huggingface"],
|
|
44743
|
+
["SPACE_ID", "huggingface-spaces"],
|
|
44744
|
+
["CUDA_VISIBLE_DEVICES", "cuda"],
|
|
44745
|
+
["NVIDIA_VISIBLE_DEVICES", "nvidia"],
|
|
44746
|
+
["ROCR_VISIBLE_DEVICES", "rocm"],
|
|
44747
|
+
["TPU_NAME", "google-tpu"],
|
|
44748
|
+
["WANDB_RUN_ID", "wandb"],
|
|
44749
|
+
["WANDB_SWEEP_ID", "wandb"],
|
|
44750
|
+
["MLFLOW_RUN_ID", "mlflow"],
|
|
44751
|
+
["MLFLOW_TRACKING_URI", "mlflow"],
|
|
44752
|
+
["COMET_EXPERIMENT_KEY", "comet"],
|
|
44753
|
+
["NEPTUNE_RUN_ID", "neptune"],
|
|
44754
|
+
["CLEARML_TASK_ID", "clearml"],
|
|
44755
|
+
["DVC_STAGE", "dvc"],
|
|
44756
|
+
["RAY_WORKER_PROCESS", "ray"],
|
|
44757
|
+
["RAY_ADDRESS", "ray"],
|
|
44758
|
+
["MUJOCO_GL", "mujoco"],
|
|
44759
|
+
["MUJOCO_PATH", "mujoco"],
|
|
44760
|
+
["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
|
|
44761
|
+
["SM_TRAINING_ENV", "sagemaker"],
|
|
44762
|
+
["TRAINING_JOB_ARN", "sagemaker"],
|
|
44763
|
+
["SAGEMAKER_BASE_DIR", "sagemaker"],
|
|
44764
|
+
["AZUREML_RUN_ID", "azure-ml"],
|
|
44765
|
+
["AZURE_ML_MODEL_DIR", "azure-ml"],
|
|
44766
|
+
["CLOUD_ML_PROJECT_ID", "vertex-ai"],
|
|
44767
|
+
["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
|
|
44768
|
+
["DET_MASTER", "determined-ai"],
|
|
44769
|
+
["LIGHTNING_USER_ID", "lightning-ai"],
|
|
44770
|
+
["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
|
|
44771
|
+
["ARGO_WORKFLOW_NAME", "argo-workflows"],
|
|
44772
|
+
["KFP_POD_NAME", "kubeflow-pipelines"],
|
|
44773
|
+
["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
|
|
44774
|
+
["COLAB_BACKEND_VERSION", "google-colab"],
|
|
44775
|
+
["DATABRICKS_RUNTIME_VERSION", "databricks"],
|
|
44776
|
+
["DAYTONA_WS_ID", "daytona"],
|
|
44777
|
+
["DAYTONA_WS_NAME", "daytona"],
|
|
44778
|
+
["E2B_SANDBOX_ID", "e2b"],
|
|
44779
|
+
["MODAL_FUNCTION_ID", "modal"],
|
|
44780
|
+
["MODAL_TASK_ID", "modal"],
|
|
44781
|
+
["RUNPOD_POD_ID", "runpod"],
|
|
44782
|
+
["REPLICATE_USERNAME", "replicate"],
|
|
44783
|
+
["VAST_CONTAINERLABEL", "vast-ai"],
|
|
44784
|
+
["HARBOR_URL", "harbor"],
|
|
44785
|
+
["HARBOR_HOSTNAME", "harbor"],
|
|
44786
|
+
["SWE_BENCH_TASK", "swe-bench"],
|
|
44787
|
+
["SWEBENCH_TASK", "swe-bench"],
|
|
44788
|
+
["SWE_AGENT_MODEL", "swe-agent"]
|
|
44789
|
+
];
|
|
44790
|
+
const detectAiTrainingEnvironment = () => {
|
|
44791
|
+
for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
|
|
44792
|
+
return null;
|
|
44793
|
+
};
|
|
44630
44794
|
const groupBy = (items, keyFn) => {
|
|
44631
44795
|
const groups = /* @__PURE__ */ new Map();
|
|
44632
44796
|
for (const item of items) {
|
|
@@ -44712,7 +44876,25 @@ const CI_PROVIDER_BY_ENVIRONMENT_VARIABLE = [
|
|
|
44712
44876
|
["TEAMCITY_VERSION", "teamcity"],
|
|
44713
44877
|
["BITBUCKET_BUILD_NUMBER", "bitbucket"],
|
|
44714
44878
|
["TRAVIS", "travis"],
|
|
44715
|
-
["DRONE", "drone"]
|
|
44879
|
+
["DRONE", "drone"],
|
|
44880
|
+
["SEMAPHORE", "semaphore"],
|
|
44881
|
+
["APPVEYOR", "appveyor"],
|
|
44882
|
+
["HARNESS_BUILD_ID", "harness"],
|
|
44883
|
+
["BUDDY", "buddy"],
|
|
44884
|
+
["CF_BUILD_ID", "codefresh"],
|
|
44885
|
+
["NETLIFY", "netlify"],
|
|
44886
|
+
["RAILWAY_SERVICE_ID", "railway"],
|
|
44887
|
+
["VERCEL", "vercel"],
|
|
44888
|
+
["CM_BUILD_ID", "codemagic"],
|
|
44889
|
+
["PROW_JOB_ID", "prow"],
|
|
44890
|
+
["AGOLA_GIT_REF", "agola"],
|
|
44891
|
+
["CIRRUS_CI", "cirrus-ci"],
|
|
44892
|
+
["BLACKSMITH_STICKYDISK_TOKEN", "blacksmith"],
|
|
44893
|
+
["WARPBUILD_RUNNER_VERIFICATION_TOKEN", "warpbuild"],
|
|
44894
|
+
["NSC_CACHE_PATH", "namespace"],
|
|
44895
|
+
["UBICLOUD_CACHE_URL", "ubicloud"],
|
|
44896
|
+
["RENDER", "render"],
|
|
44897
|
+
["FLY_APP_NAME", "fly-io"]
|
|
44716
44898
|
];
|
|
44717
44899
|
const GITHUB_ACTION_MARKER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_GITHUB_ACTION";
|
|
44718
44900
|
const ACTION_INPUT_ENVIRONMENT_VARIABLES = {
|
|
@@ -44730,7 +44912,10 @@ const CODING_AGENT_BY_ENVIRONMENT_VARIABLE = [
|
|
|
44730
44912
|
["CODEX_SANDBOX_NETWORK_DISABLED", "codex"],
|
|
44731
44913
|
["OPENCODE", "opencode"],
|
|
44732
44914
|
["GOOSE_TERMINAL", "goose"],
|
|
44733
|
-
["AMP_THREAD_ID", "amp"]
|
|
44915
|
+
["AMP_THREAD_ID", "amp"],
|
|
44916
|
+
["CLINE_ACTIVE", "cline"],
|
|
44917
|
+
["AUGMENT_AGENT", "augment"],
|
|
44918
|
+
["TRAE_AI_SHELL_ID", "trae-ai"]
|
|
44734
44919
|
];
|
|
44735
44920
|
const GENERIC_CODING_AGENT_ENVIRONMENT_VARIABLES = ["AGENT_SESSION_ID", "AGENT_THREAD_ID"];
|
|
44736
44921
|
const CODING_AGENT_ENVIRONMENT_VALUE_VARIABLES = ["AGENT"];
|
|
@@ -44879,7 +45064,9 @@ const METRIC = {
|
|
|
44879
45064
|
lspSessionStarted: "lsp.session.started",
|
|
44880
45065
|
lspScanCompleted: "lsp.scan.completed",
|
|
44881
45066
|
lspScanDuration: "lsp.scan.duration",
|
|
44882
|
-
lspScanDiagnostics: "lsp.scan.diagnostics"
|
|
45067
|
+
lspScanDiagnostics: "lsp.scan.diagnostics",
|
|
45068
|
+
aiTrainingWarningShown: "ai.training.warning_shown",
|
|
45069
|
+
jsonOutUsed: "json.out_used"
|
|
44883
45070
|
};
|
|
44884
45071
|
//#endregion
|
|
44885
45072
|
//#region src/cli/utils/noop-console.ts
|
|
@@ -44922,7 +45109,7 @@ const makeNoopConsole = () => ({
|
|
|
44922
45109
|
});
|
|
44923
45110
|
//#endregion
|
|
44924
45111
|
//#region src/cli/utils/version.ts
|
|
44925
|
-
const VERSION = "0.5.8-dev.
|
|
45112
|
+
const VERSION = "0.5.8-dev.fff9466";
|
|
44926
45113
|
//#endregion
|
|
44927
45114
|
//#region src/cli/utils/json-mode.ts
|
|
44928
45115
|
let context = null;
|
|
@@ -44960,12 +45147,13 @@ const installSilentConsole = () => {
|
|
|
44960
45147
|
"trace"
|
|
44961
45148
|
]) target[key] = source[key];
|
|
44962
45149
|
};
|
|
44963
|
-
const enableJsonMode = ({ compact, directory }) => {
|
|
45150
|
+
const enableJsonMode = ({ compact, directory, outputFile }) => {
|
|
44964
45151
|
context = {
|
|
44965
45152
|
compact,
|
|
44966
45153
|
directory,
|
|
44967
45154
|
startTime: performance$1.now(),
|
|
44968
|
-
mode: "full"
|
|
45155
|
+
mode: "full",
|
|
45156
|
+
outputFile: outputFile ?? null
|
|
44969
45157
|
};
|
|
44970
45158
|
installSilentConsole();
|
|
44971
45159
|
};
|
|
@@ -44978,7 +45166,11 @@ const setJsonReportMode = (mode) => {
|
|
|
44978
45166
|
};
|
|
44979
45167
|
const writeJsonReport = (report) => {
|
|
44980
45168
|
const serialized = context?.compact ? JSON.stringify(report) : JSON.stringify(report, null, 2);
|
|
44981
|
-
|
|
45169
|
+
if (context?.outputFile) {
|
|
45170
|
+
const resolvedPath = Path.resolve(context.outputFile);
|
|
45171
|
+
NFS.mkdirSync(Path.dirname(resolvedPath), { recursive: true });
|
|
45172
|
+
NFS.writeFileSync(resolvedPath, `${serialized}\n`);
|
|
45173
|
+
} else process.stdout.write(`${serialized}\n`);
|
|
44982
45174
|
};
|
|
44983
45175
|
const writeJsonErrorReport = (error, sentryEventId) => {
|
|
44984
45176
|
if (!context) return;
|
|
@@ -45286,13 +45478,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
|
|
|
45286
45478
|
* uploads source-map artifacts under, so stack frames symbolicate. Honors the
|
|
45287
45479
|
* standard `SENTRY_RELEASE` override.
|
|
45288
45480
|
*/
|
|
45289
|
-
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.
|
|
45481
|
+
const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.fff9466`;
|
|
45290
45482
|
/**
|
|
45291
45483
|
* Deployment environment shown in Sentry's environment filter. Defaults to
|
|
45292
45484
|
* `production` for tagged releases and `development` for dev/unbuilt versions,
|
|
45293
45485
|
* overridable via the standard `SENTRY_ENVIRONMENT` env var.
|
|
45294
45486
|
*/
|
|
45295
|
-
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.
|
|
45487
|
+
const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.fff9466") ? "development" : "production");
|
|
45296
45488
|
/**
|
|
45297
45489
|
* Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
|
|
45298
45490
|
* (set to `0` to disable tracing) and falls back to
|
|
@@ -48888,6 +49080,25 @@ const shouldBlockCi = (diagnostics, blockingLevel) => {
|
|
|
48888
49080
|
*/
|
|
48889
49081
|
const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
48890
49082
|
//#endregion
|
|
49083
|
+
//#region src/cli/utils/with-namespace.ts
|
|
49084
|
+
/**
|
|
49085
|
+
* Prefixes every key in a flat attribute group with `namespace.`, so a logical
|
|
49086
|
+
* group (scan config, diagnostics rollup, score, …) carries one dotted
|
|
49087
|
+
* namespace applied in a single place rather than hand-spelled into each key —
|
|
49088
|
+
* which is how the wide event drifted into a flat, half-namespaced soup. The
|
|
49089
|
+
* dotted prefix is what makes the attributes tree up in Sentry's attribute
|
|
49090
|
+
* browser and stay group-/filter-/aggregate-able in the Spans dataset.
|
|
49091
|
+
*
|
|
49092
|
+
* Value types are preserved verbatim (numbers stay numbers so Sentry infers a
|
|
49093
|
+
* numeric attribute and `p75(...)`/`avg(...)` work; `null` is kept so
|
|
49094
|
+
* `toSpanAttributes` can drop absent signals rather than coerce them).
|
|
49095
|
+
*/
|
|
49096
|
+
const withNamespace = (namespace, attributes) => {
|
|
49097
|
+
const namespaced = {};
|
|
49098
|
+
for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
|
|
49099
|
+
return namespaced;
|
|
49100
|
+
};
|
|
49101
|
+
//#endregion
|
|
48891
49102
|
//#region src/cli/utils/build-run-event.ts
|
|
48892
49103
|
const readEnvBoolean = (name) => {
|
|
48893
49104
|
const value = process.env[name];
|
|
@@ -48909,12 +49120,12 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48909
49120
|
if (input.result === void 0) {
|
|
48910
49121
|
const error = input.error;
|
|
48911
49122
|
const known = isReactDoctorError(error);
|
|
48912
|
-
return {
|
|
48913
|
-
|
|
49123
|
+
return withNamespace("outcome", {
|
|
49124
|
+
status: "error",
|
|
48914
49125
|
exitCode: 1,
|
|
48915
49126
|
knownError: known,
|
|
48916
49127
|
errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
|
|
48917
|
-
};
|
|
49128
|
+
});
|
|
48918
49129
|
}
|
|
48919
49130
|
const result = input.result;
|
|
48920
49131
|
const summary = summarizeDiagnostics(result.diagnostics);
|
|
@@ -48948,66 +49159,82 @@ const buildOutcomeAttributes = (input) => {
|
|
|
48948
49159
|
}
|
|
48949
49160
|
let fixGroupedFindings = 0;
|
|
48950
49161
|
for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
|
|
49162
|
+
const categoryRollup = {};
|
|
49163
|
+
for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
|
|
48951
49164
|
const attributes = {
|
|
48952
|
-
outcome,
|
|
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
|
-
|
|
48984
|
-
|
|
48985
|
-
|
|
48986
|
-
|
|
48987
|
-
|
|
48988
|
-
|
|
48989
|
-
|
|
48990
|
-
|
|
48991
|
-
|
|
48992
|
-
|
|
48993
|
-
|
|
48994
|
-
|
|
49165
|
+
...withNamespace("outcome", {
|
|
49166
|
+
status: outcome,
|
|
49167
|
+
exitCode: wouldBlock ? 1 : 0,
|
|
49168
|
+
wouldBlock,
|
|
49169
|
+
blocking: blockingLevel,
|
|
49170
|
+
clean: isClean,
|
|
49171
|
+
skippedChecks: result.skippedChecks.length
|
|
49172
|
+
}),
|
|
49173
|
+
...withNamespace("diag", {
|
|
49174
|
+
total: summary.totalDiagnosticCount,
|
|
49175
|
+
errors: summary.errorCount,
|
|
49176
|
+
warnings: summary.warningCount,
|
|
49177
|
+
affectedFiles: summary.affectedFileCount,
|
|
49178
|
+
inTestFiles: diagnosticsInTestFiles,
|
|
49179
|
+
inStoryFiles: diagnosticsInStoryFiles,
|
|
49180
|
+
distinctRules: countByRule.size,
|
|
49181
|
+
topRule,
|
|
49182
|
+
fixGroups: findingsPerFixGroup.size,
|
|
49183
|
+
fixGroupedFindings,
|
|
49184
|
+
...categoryRollup
|
|
49185
|
+
}),
|
|
49186
|
+
...withNamespace("score", {
|
|
49187
|
+
value: result.score ? result.score.score : null,
|
|
49188
|
+
label: result.score ? result.score.label : null,
|
|
49189
|
+
available: result.score !== null
|
|
49190
|
+
}),
|
|
49191
|
+
...withNamespace("lint", {
|
|
49192
|
+
failed: input.didLintFail ?? null,
|
|
49193
|
+
failureReasonKind: input.lintFailureReasonKind ?? null,
|
|
49194
|
+
partialFailureCount: input.lintPartialFailureCount ?? null,
|
|
49195
|
+
droppedFileCount: input.lintDroppedFileCount ?? null,
|
|
49196
|
+
cacheHitFiles: result.lintCacheHitFileCount ?? null,
|
|
49197
|
+
cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
|
|
49198
|
+
cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
|
|
49199
|
+
}),
|
|
49200
|
+
...withNamespace("deadCode", {
|
|
49201
|
+
failed: input.didDeadCodeFail ?? null,
|
|
49202
|
+
overlapped: input.deadCodeOverlapped ?? null
|
|
49203
|
+
}),
|
|
49204
|
+
...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
|
|
49205
|
+
...withNamespace("timing", {
|
|
49206
|
+
elapsedMs: result.elapsedMilliseconds,
|
|
49207
|
+
scanMs: result.scanElapsedMilliseconds ?? null
|
|
49208
|
+
}),
|
|
49209
|
+
...withNamespace("migration", {
|
|
49210
|
+
largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
|
|
49211
|
+
largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
|
|
49212
|
+
largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
|
|
49213
|
+
})
|
|
49214
|
+
};
|
|
49215
|
+
if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
|
|
49216
|
+
new: summary.totalDiagnosticCount,
|
|
49217
|
+
fixed: result.baselineDelta.fixedCount,
|
|
49218
|
+
baseTotal: result.baselineDelta.baseTotalCount,
|
|
49219
|
+
degraded: false
|
|
49220
|
+
}));
|
|
49221
|
+
else if (input.gateExempt) attributes["baseline.degraded"] = true;
|
|
48995
49222
|
return attributes;
|
|
48996
49223
|
};
|
|
48997
|
-
const
|
|
49224
|
+
const buildActionAttributes = () => {
|
|
48998
49225
|
const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
|
|
48999
|
-
return {
|
|
49226
|
+
return withNamespace("action", {
|
|
49000
49227
|
actorAssociation: githubActorAssociation ?? null,
|
|
49001
49228
|
runnerOs: detectRunnerOs(),
|
|
49002
49229
|
comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
|
|
49003
49230
|
reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
|
|
49004
49231
|
versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
|
|
49005
|
-
};
|
|
49232
|
+
});
|
|
49006
49233
|
};
|
|
49007
|
-
const
|
|
49234
|
+
const buildScanAttributes = (input) => {
|
|
49008
49235
|
const ruleOverrides = input.userConfig?.rules ?? {};
|
|
49009
49236
|
const ruleKeys = Object.keys(ruleOverrides);
|
|
49010
|
-
return {
|
|
49237
|
+
return withNamespace("scan", {
|
|
49011
49238
|
mode: input.mode,
|
|
49012
49239
|
scope: input.scope,
|
|
49013
49240
|
parallel: input.parallel,
|
|
@@ -49022,12 +49249,18 @@ const buildConfigAttributes = (input) => {
|
|
|
49022
49249
|
ignoredTagCount: input.ignoredTagCount,
|
|
49023
49250
|
hasCustomConfig: input.hasCustomConfig,
|
|
49024
49251
|
rulesConfigured: ruleKeys.length,
|
|
49025
|
-
rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length
|
|
49026
|
-
|
|
49252
|
+
rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
|
|
49253
|
+
fileCount: input.result?.scannedFileCount ?? null
|
|
49254
|
+
});
|
|
49027
49255
|
};
|
|
49028
49256
|
/**
|
|
49029
|
-
* Projects a scan into the
|
|
49030
|
-
* per-scan "wide event".
|
|
49257
|
+
* Projects a scan into the namespaced attribute set for its root span — the
|
|
49258
|
+
* canonical per-scan "wide event". Every attribute carries a dotted namespace
|
|
49259
|
+
* that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
|
|
49260
|
+
* verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
|
|
49261
|
+
* `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
|
|
49262
|
+
* tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
|
|
49263
|
+
* in the Spans dataset. Pure and exported so the projection (outcome
|
|
49031
49264
|
* precedence, rule/category rollups, CI knobs, config shape) is unit-testable
|
|
49032
49265
|
* without a live Sentry client. `null` values are dropped so absent signals
|
|
49033
49266
|
* never become misleading `"null"` attributes. The run + project base context
|
|
@@ -49036,8 +49269,8 @@ const buildConfigAttributes = (input) => {
|
|
|
49036
49269
|
* those don't carry.
|
|
49037
49270
|
*/
|
|
49038
49271
|
const buildRunEventAttributes = (input) => toSpanAttributes({
|
|
49039
|
-
...
|
|
49040
|
-
...
|
|
49272
|
+
...buildScanAttributes(input),
|
|
49273
|
+
...buildActionAttributes(),
|
|
49041
49274
|
...buildOutcomeAttributes(input)
|
|
49042
49275
|
});
|
|
49043
49276
|
/**
|
|
@@ -51085,7 +51318,8 @@ const getStagedSourceFiles = async (directory) => {
|
|
|
51085
51318
|
return [...await runPromise(gen(function* () {
|
|
51086
51319
|
return yield* (yield* StagedFiles).discoverSourceFiles(directory);
|
|
51087
51320
|
}).pipe(provide(stagedFilesLayer)))];
|
|
51088
|
-
} catch {
|
|
51321
|
+
} catch (error) {
|
|
51322
|
+
cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
|
|
51089
51323
|
return [];
|
|
51090
51324
|
}
|
|
51091
51325
|
};
|
|
@@ -52031,10 +52265,11 @@ const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_
|
|
|
52031
52265
|
const CLAUDE_AGENT = "claude-code";
|
|
52032
52266
|
const CURSOR_AGENT = "cursor";
|
|
52033
52267
|
const CLAUDE_SETTINGS_RELATIVE_PATH = ".claude/settings.json";
|
|
52034
|
-
const CLAUDE_HOOK_RELATIVE_PATH = ".claude/hooks/react-doctor.
|
|
52035
|
-
const CLAUDE_HOOK_COMMAND = "
|
|
52268
|
+
const CLAUDE_HOOK_RELATIVE_PATH = ".claude/hooks/react-doctor.mjs";
|
|
52269
|
+
const CLAUDE_HOOK_COMMAND = "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/react-doctor.mjs\"";
|
|
52036
52270
|
const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
|
|
52037
|
-
const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.
|
|
52271
|
+
const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.mjs";
|
|
52272
|
+
const CURSOR_HOOK_COMMAND = "node .cursor/hooks/react-doctor.mjs";
|
|
52038
52273
|
const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
|
|
52039
52274
|
const CURSOR_HOOKS_SCHEMA_VERSION = 1;
|
|
52040
52275
|
const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
|
|
@@ -52066,7 +52301,6 @@ const writeJsonFileWithDirectoryCheck = (filePath, value) => {
|
|
|
52066
52301
|
const writeHookScript = (filePath) => {
|
|
52067
52302
|
ensureDirectoryExists(Path.dirname(filePath));
|
|
52068
52303
|
NFS.writeFileSync(filePath, buildAgentHookScript());
|
|
52069
|
-
NFS.chmodSync(filePath, 493);
|
|
52070
52304
|
};
|
|
52071
52305
|
const hasClaudeHookCommand = (groups) => groups.some((group) => (group.hooks ?? []).some((hook) => hook.command === CLAUDE_HOOK_COMMAND));
|
|
52072
52306
|
const installClaudeHook = (projectRoot) => {
|
|
@@ -52087,7 +52321,7 @@ const installClaudeHook = (projectRoot) => {
|
|
|
52087
52321
|
writeHookScript(hookPath);
|
|
52088
52322
|
return [settingsPath, hookPath];
|
|
52089
52323
|
};
|
|
52090
|
-
const hasCursorHookCommand = (handlers) => handlers.some((handler) => handler.command ===
|
|
52324
|
+
const hasCursorHookCommand = (handlers) => handlers.some((handler) => handler.command === CURSOR_HOOK_COMMAND);
|
|
52091
52325
|
const installCursorHook = (projectRoot) => {
|
|
52092
52326
|
const configPath = Path.join(projectRoot, CURSOR_HOOKS_RELATIVE_PATH);
|
|
52093
52327
|
const hookPath = Path.join(projectRoot, CURSOR_HOOK_RELATIVE_PATH);
|
|
@@ -52095,7 +52329,7 @@ const installCursorHook = (projectRoot) => {
|
|
|
52095
52329
|
const hooks = { ...config.hooks ?? {} };
|
|
52096
52330
|
const postToolUseHooks = [...hooks.postToolUse ?? []];
|
|
52097
52331
|
if (!hasCursorHookCommand(postToolUseHooks)) postToolUseHooks.push({
|
|
52098
|
-
command:
|
|
52332
|
+
command: CURSOR_HOOK_COMMAND,
|
|
52099
52333
|
matcher: CURSOR_HOOK_MATCHER,
|
|
52100
52334
|
timeout: 120
|
|
52101
52335
|
});
|
|
@@ -52109,102 +52343,111 @@ const installCursorHook = (projectRoot) => {
|
|
|
52109
52343
|
return [configPath, hookPath];
|
|
52110
52344
|
};
|
|
52111
52345
|
const buildAgentHookScript = () => [
|
|
52112
|
-
"
|
|
52113
|
-
"
|
|
52346
|
+
"import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';",
|
|
52347
|
+
"import { tmpdir } from 'node:os';",
|
|
52348
|
+
"import { join, dirname } from 'node:path';",
|
|
52349
|
+
"import { fileURLToPath } from 'node:url';",
|
|
52350
|
+
"import { spawnSync } from 'node:child_process';",
|
|
52114
52351
|
"",
|
|
52115
|
-
"
|
|
52116
|
-
"
|
|
52117
|
-
"trap 'rm -f \"$input_file\" \"$output_file\"' EXIT",
|
|
52118
|
-
"cat > \"$input_file\"",
|
|
52352
|
+
"const __filename = fileURLToPath(import.meta.url);",
|
|
52353
|
+
"const __dirname = dirname(__filename);",
|
|
52119
52354
|
"",
|
|
52120
|
-
"
|
|
52121
|
-
"project_root=${CLAUDE_PROJECT_DIR:-}",
|
|
52122
|
-
"if [ -z \"$project_root\" ]; then",
|
|
52123
|
-
" project_root=$(CDPATH= cd \"$script_dir/../..\" && pwd)",
|
|
52124
|
-
"fi",
|
|
52125
|
-
"if ! cd \"$project_root\"; then",
|
|
52126
|
-
" exit 0",
|
|
52127
|
-
"fi",
|
|
52355
|
+
"const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
|
|
52128
52356
|
"",
|
|
52129
|
-
"
|
|
52130
|
-
"
|
|
52131
|
-
" return 0",
|
|
52132
|
-
"
|
|
52357
|
+
"const readStdin = () => {",
|
|
52358
|
+
" try {",
|
|
52359
|
+
" return readFileSync(0, 'utf8');",
|
|
52360
|
+
" } catch {",
|
|
52361
|
+
" return '';",
|
|
52362
|
+
" }",
|
|
52363
|
+
"};",
|
|
52133
52364
|
"",
|
|
52134
|
-
"
|
|
52135
|
-
"const fs = require('node:fs');",
|
|
52136
|
-
"const inputPath = process.argv[2];",
|
|
52137
|
-
"const editToolNames = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
|
|
52138
|
-
"try {",
|
|
52139
|
-
" const input = JSON.parse(fs.readFileSync(inputPath, 'utf8') || '{}');",
|
|
52365
|
+
"const shouldScan = (input) => {",
|
|
52140
52366
|
" const eventName = input.hook_event_name || input.eventName || input.event_name;",
|
|
52141
52367
|
" if (eventName === 'PostToolBatch') {",
|
|
52142
52368
|
" const toolCalls = Array.isArray(input.tool_calls) ? input.tool_calls : [];",
|
|
52143
|
-
"
|
|
52369
|
+
" return toolCalls.some((toolCall) => EDIT_TOOL_NAMES.has(toolCall.tool_name));",
|
|
52144
52370
|
" }",
|
|
52145
52371
|
" const toolName = input.tool_name || input.toolName || input.tool;",
|
|
52146
|
-
"
|
|
52147
|
-
"}
|
|
52148
|
-
" process.exit(0);",
|
|
52149
|
-
"}",
|
|
52150
|
-
"NODE",
|
|
52151
|
-
"}",
|
|
52372
|
+
" return !toolName || EDIT_TOOL_NAMES.has(toolName);",
|
|
52373
|
+
"};",
|
|
52152
52374
|
"",
|
|
52153
|
-
"
|
|
52154
|
-
"
|
|
52155
|
-
"
|
|
52156
|
-
"
|
|
52157
|
-
"
|
|
52375
|
+
"const runReactDoctor = (outputPath) => {",
|
|
52376
|
+
" // Each candidate is a single shell command string (not an args array):",
|
|
52377
|
+
" // `shell: true` is required to run the Windows `.cmd` shims, and an args",
|
|
52378
|
+
" // array with `shell: true` trips Node's DEP0190. A missing command exits",
|
|
52379
|
+
" // 127 via the shell (no ENOENT error), so fall through on that. With no",
|
|
52380
|
+
" // runner found, exit 0 silently — stdout is parsed as the hook's JSON.",
|
|
52381
|
+
" const commands = [",
|
|
52382
|
+
" './node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score',",
|
|
52383
|
+
" 'react-doctor --verbose --scope changed --blocking warning --no-score',",
|
|
52384
|
+
" 'pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
|
|
52385
|
+
" 'npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
|
|
52386
|
+
" ];",
|
|
52158
52387
|
"",
|
|
52159
|
-
"
|
|
52160
|
-
"
|
|
52161
|
-
"
|
|
52162
|
-
"
|
|
52388
|
+
" for (const command of commands) {",
|
|
52389
|
+
" const result = spawnSync(command, { encoding: 'utf8', shell: true });",
|
|
52390
|
+
" if (result.error?.code === 'ENOENT' || result.status === 127) continue;",
|
|
52391
|
+
" try {",
|
|
52392
|
+
" writeFileSync(outputPath, (result.stdout || '') + (result.stderr || ''));",
|
|
52393
|
+
" } catch {}",
|
|
52394
|
+
" return result.status;",
|
|
52395
|
+
" }",
|
|
52163
52396
|
"",
|
|
52164
|
-
"
|
|
52165
|
-
"
|
|
52166
|
-
" return",
|
|
52167
|
-
" fi",
|
|
52397
|
+
" return 0;",
|
|
52398
|
+
"};",
|
|
52168
52399
|
"",
|
|
52169
|
-
"
|
|
52170
|
-
"
|
|
52171
|
-
"
|
|
52172
|
-
"
|
|
52400
|
+
"const cleanup = (...paths) => {",
|
|
52401
|
+
" for (const path of paths) {",
|
|
52402
|
+
" try { unlinkSync(path); } catch {}",
|
|
52403
|
+
" }",
|
|
52404
|
+
"};",
|
|
52173
52405
|
"",
|
|
52174
|
-
"
|
|
52175
|
-
"
|
|
52176
|
-
"
|
|
52406
|
+
"const main = () => {",
|
|
52407
|
+
" let input;",
|
|
52408
|
+
" try {",
|
|
52409
|
+
" const stdinContent = readStdin();",
|
|
52410
|
+
" input = JSON.parse(stdinContent || '{}');",
|
|
52411
|
+
" } catch {",
|
|
52412
|
+
" input = {};",
|
|
52413
|
+
" }",
|
|
52177
52414
|
"",
|
|
52178
|
-
"if !
|
|
52179
|
-
"
|
|
52180
|
-
"
|
|
52415
|
+
" if (!shouldScan(input)) {",
|
|
52416
|
+
" process.exit(0);",
|
|
52417
|
+
" }",
|
|
52181
52418
|
"",
|
|
52182
|
-
"
|
|
52183
|
-
"
|
|
52184
|
-
"fi",
|
|
52419
|
+
" const projectRoot = process.env.CLAUDE_PROJECT_DIR || join(__dirname, '../..');",
|
|
52420
|
+
" const outputPath = join(tmpdir(), `react-doctor-agent-hook-output-${process.pid}.txt`);",
|
|
52185
52421
|
"",
|
|
52186
|
-
"node - \"$input_file\" \"$output_file\" <<'NODE'",
|
|
52187
|
-
"const fs = require('node:fs');",
|
|
52188
|
-
"const inputPath = process.argv[2];",
|
|
52189
|
-
"const outputPath = process.argv[3];",
|
|
52190
|
-
"const readInput = () => {",
|
|
52191
52422
|
" try {",
|
|
52192
|
-
"
|
|
52423
|
+
" process.chdir(projectRoot);",
|
|
52193
52424
|
" } catch {",
|
|
52194
|
-
"
|
|
52425
|
+
" process.exit(0);",
|
|
52426
|
+
" }",
|
|
52427
|
+
"",
|
|
52428
|
+
" const scanResult = runReactDoctor(outputPath);",
|
|
52429
|
+
" if (scanResult === 0) {",
|
|
52430
|
+
" cleanup(outputPath);",
|
|
52431
|
+
" process.exit(0);",
|
|
52432
|
+
" }",
|
|
52433
|
+
"",
|
|
52434
|
+
" const scanOutput = readFileSync(outputPath, 'utf8').trim();",
|
|
52435
|
+
" cleanup(outputPath);",
|
|
52436
|
+
"",
|
|
52437
|
+
" if (!scanOutput) {",
|
|
52438
|
+
" process.exit(0);",
|
|
52439
|
+
" }",
|
|
52440
|
+
"",
|
|
52441
|
+
" 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}`;",
|
|
52442
|
+
"",
|
|
52443
|
+
" if (input.hook_event_name === 'PostToolBatch') {",
|
|
52444
|
+
" console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: message } }));",
|
|
52445
|
+
" } else {",
|
|
52446
|
+
" console.log(JSON.stringify({ additional_context: message }));",
|
|
52195
52447
|
" }",
|
|
52196
52448
|
"};",
|
|
52197
|
-
"
|
|
52198
|
-
"
|
|
52199
|
-
"if (!scanOutput) process.exit(0);",
|
|
52200
|
-
"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}`;",
|
|
52201
|
-
"if (input.hook_event_name === 'PostToolBatch') {",
|
|
52202
|
-
" console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: message } }));",
|
|
52203
|
-
"} else {",
|
|
52204
|
-
" console.log(JSON.stringify({ additional_context: message }));",
|
|
52205
|
-
"}",
|
|
52206
|
-
"NODE",
|
|
52207
|
-
""
|
|
52449
|
+
"",
|
|
52450
|
+
"main();"
|
|
52208
52451
|
].join("\n");
|
|
52209
52452
|
const installReactDoctorAgentHooks = (options) => {
|
|
52210
52453
|
const installedAgents = [];
|
|
@@ -53030,6 +53273,26 @@ const CLI_AGENT_AUTO_FLAGS = {
|
|
|
53030
53273
|
codex: ["--yolo"],
|
|
53031
53274
|
cursor: ["--force"]
|
|
53032
53275
|
};
|
|
53276
|
+
const resolveCursorBundledNode = () => {
|
|
53277
|
+
if (!isWindows) return null;
|
|
53278
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
53279
|
+
if (!localAppData) return null;
|
|
53280
|
+
const cursorAgentRoot = Path.join(localAppData, "cursor-agent", "versions");
|
|
53281
|
+
try {
|
|
53282
|
+
const versions = NFS.readdirSync(cursorAgentRoot);
|
|
53283
|
+
if (versions.length === 0) return null;
|
|
53284
|
+
const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
|
|
53285
|
+
for (const version of sortedVersions) try {
|
|
53286
|
+
const bundledNodePath = Path.join(cursorAgentRoot, version, "node.exe");
|
|
53287
|
+
const entryScriptPath = Path.join(cursorAgentRoot, version, "index.js");
|
|
53288
|
+
if (NFS.statSync(bundledNodePath).isFile() && NFS.statSync(entryScriptPath).isFile()) return {
|
|
53289
|
+
command: bundledNodePath,
|
|
53290
|
+
args: [entryScriptPath]
|
|
53291
|
+
};
|
|
53292
|
+
} catch {}
|
|
53293
|
+
} catch {}
|
|
53294
|
+
return null;
|
|
53295
|
+
};
|
|
53033
53296
|
const resolveWindowsCmdEntryScript = (command) => {
|
|
53034
53297
|
if (!isWindows) return null;
|
|
53035
53298
|
const pathDirectories = (process.env.PATH ?? "").split(Path.delimiter).filter(Boolean);
|
|
@@ -53049,11 +53312,10 @@ const resolveWindowsCmdEntryScript = (command) => {
|
|
|
53049
53312
|
}
|
|
53050
53313
|
return null;
|
|
53051
53314
|
};
|
|
53052
|
-
const spawnAgent = (command, args, cwd
|
|
53315
|
+
const spawnAgent = (command, args, cwd) => new Promise((resolve, reject) => {
|
|
53053
53316
|
const child = spawn(command, [...args], {
|
|
53054
53317
|
cwd,
|
|
53055
|
-
stdio: "inherit"
|
|
53056
|
-
shell
|
|
53318
|
+
stdio: "inherit"
|
|
53057
53319
|
});
|
|
53058
53320
|
child.on("error", reject);
|
|
53059
53321
|
child.on("close", (code) => resolve(code ?? 0));
|
|
@@ -53062,6 +53324,10 @@ const launchCliAgent = async (agentId, prompt, cwd) => {
|
|
|
53062
53324
|
const binary = CLI_AGENT_BINARIES[agentId];
|
|
53063
53325
|
const agentArgs = [...CLI_AGENT_AUTO_FLAGS[agentId], prompt];
|
|
53064
53326
|
if (isWindows) {
|
|
53327
|
+
if (agentId === "cursor") {
|
|
53328
|
+
const cursorResolution = resolveCursorBundledNode();
|
|
53329
|
+
if (cursorResolution) return spawnAgent(cursorResolution.command, [...cursorResolution.args, ...agentArgs], cwd);
|
|
53330
|
+
}
|
|
53065
53331
|
const entryScript = resolveWindowsCmdEntryScript(binary);
|
|
53066
53332
|
if (entryScript) return spawnAgent(process.execPath, [entryScript, ...agentArgs], cwd);
|
|
53067
53333
|
}
|
|
@@ -53972,6 +54238,7 @@ const resolveRequestedProjects = (requestedNames, workspacePackages, rootDirecto
|
|
|
53972
54238
|
return requestedNames.map((requestedName) => {
|
|
53973
54239
|
const matched = workspacePackages.find((workspacePackage) => workspacePackage.name === requestedName || Path.basename(workspacePackage.directory) === requestedName);
|
|
53974
54240
|
if (matched) return matched.directory;
|
|
54241
|
+
if (Path.basename(rootDirectory) === requestedName) return rootDirectory;
|
|
53975
54242
|
const candidateDirectory = Path.resolve(rootDirectory, requestedName);
|
|
53976
54243
|
if (isDirectory(candidateDirectory)) {
|
|
53977
54244
|
recordCount(METRIC.projectPathSelected);
|
|
@@ -54079,6 +54346,17 @@ const warnDeprecatedFailOn = (flags, userConfig) => {
|
|
|
54079
54346
|
cliLogger.warn(`${source} is deprecated; rename it to ${replacement}.`);
|
|
54080
54347
|
};
|
|
54081
54348
|
//#endregion
|
|
54349
|
+
//#region src/cli/utils/warn-ai-training-environment.ts
|
|
54350
|
+
let didWarnAiTraining = false;
|
|
54351
|
+
const warnIfAiTrainingEnvironment = () => {
|
|
54352
|
+
if (didWarnAiTraining) return;
|
|
54353
|
+
const detected = detectAiTrainingEnvironment();
|
|
54354
|
+
if (detected === null) return;
|
|
54355
|
+
didWarnAiTraining = true;
|
|
54356
|
+
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`);
|
|
54357
|
+
recordCount(METRIC.aiTrainingWarningShown, 1, { environment: detected });
|
|
54358
|
+
};
|
|
54359
|
+
//#endregion
|
|
54082
54360
|
//#region src/cli/utils/validate-mode-flags.ts
|
|
54083
54361
|
const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
|
|
54084
54362
|
const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
|
|
@@ -54162,10 +54440,14 @@ const inspectAction = async (directory, flags) => {
|
|
|
54162
54440
|
const isQuiet = isScoreOnly || isJsonMode;
|
|
54163
54441
|
const requestedDirectory = Path.resolve(directory);
|
|
54164
54442
|
const startTime = performance$1.now();
|
|
54165
|
-
if (isJsonMode)
|
|
54166
|
-
|
|
54167
|
-
|
|
54168
|
-
|
|
54443
|
+
if (isJsonMode) {
|
|
54444
|
+
enableJsonMode({
|
|
54445
|
+
compact: Boolean(flags.jsonCompact),
|
|
54446
|
+
directory: requestedDirectory,
|
|
54447
|
+
outputFile: flags.jsonOut
|
|
54448
|
+
});
|
|
54449
|
+
if (flags.jsonOut) recordCount(METRIC.jsonOutUsed, 1);
|
|
54450
|
+
}
|
|
54169
54451
|
recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
|
|
54170
54452
|
try {
|
|
54171
54453
|
validateModeFlags(flags);
|
|
@@ -54179,6 +54461,7 @@ const inspectAction = async (directory, flags) => {
|
|
|
54179
54461
|
setJsonReportDirectory(resolvedDirectory);
|
|
54180
54462
|
warnDeprecatedFailOn(flags, userConfig);
|
|
54181
54463
|
warnDeprecatedDiff(flags, userConfig);
|
|
54464
|
+
warnIfAiTrainingEnvironment();
|
|
54182
54465
|
if (scanTarget.didRedirectViaRootDir && !isQuiet) {
|
|
54183
54466
|
cliLogger.dim(`Redirected to ${highlighter.info(toRelativePath(resolvedDirectory, requestedDirectory))} via react-doctor config "rootDir".`);
|
|
54184
54467
|
cliLogger.break();
|
|
@@ -55102,6 +55385,14 @@ const applyColorPreference = (argv, env = process.env) => {
|
|
|
55102
55385
|
setColorEnabled(enabled);
|
|
55103
55386
|
};
|
|
55104
55387
|
//#endregion
|
|
55388
|
+
//#region src/cli/utils/ensure-windows-utf8-console.ts
|
|
55389
|
+
const ensureWindowsUtf8Console = () => {
|
|
55390
|
+
if (process.platform !== "win32" || !process.stdout.isTTY) return;
|
|
55391
|
+
try {
|
|
55392
|
+
execFileSync("chcp.com", ["65001"], { stdio: "ignore" });
|
|
55393
|
+
} catch {}
|
|
55394
|
+
};
|
|
55395
|
+
//#endregion
|
|
55105
55396
|
//#region src/cli/utils/exit-gracefully.ts
|
|
55106
55397
|
const exitGracefully = () => {
|
|
55107
55398
|
try {
|
|
@@ -55244,6 +55535,7 @@ const ROOT_FLAG_SPEC = {
|
|
|
55244
55535
|
"--category",
|
|
55245
55536
|
"--changed-files-from",
|
|
55246
55537
|
"--blocking",
|
|
55538
|
+
"--json-out",
|
|
55247
55539
|
"--output-dir",
|
|
55248
55540
|
"--fail-on",
|
|
55249
55541
|
"--project",
|
|
@@ -55434,7 +55726,7 @@ ${highlighter.dim("Learn more:")}
|
|
|
55434
55726
|
${highlighter.info(CANONICAL_GITHUB_URL)}
|
|
55435
55727
|
`;
|
|
55436
55728
|
const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
|
|
55437
|
-
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);
|
|
55729
|
+
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);
|
|
55438
55730
|
program.action(inspectAction);
|
|
55439
55731
|
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));
|
|
55440
55732
|
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);
|
|
@@ -55459,6 +55751,7 @@ if (process.argv.includes("-V") && !strippedArgv.includes("-V")) {
|
|
|
55459
55751
|
process.exit(0);
|
|
55460
55752
|
}
|
|
55461
55753
|
applyColorPreference(strippedArgv);
|
|
55754
|
+
ensureWindowsUtf8Console();
|
|
55462
55755
|
const argv = normalizeHelpInvocation(strippedArgv, knownCommands);
|
|
55463
55756
|
Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => program.parseAsync(argv)).then(() => flushSentry()).catch(async (error) => {
|
|
55464
55757
|
const isUserError = isExpectedUserError(error);
|
|
@@ -55477,4 +55770,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
|
|
|
55477
55770
|
export {};
|
|
55478
55771
|
|
|
55479
55772
|
//# sourceMappingURL=cli.js.map
|
|
55480
|
-
//# debugId=
|
|
55773
|
+
//# debugId=f1523493-79d1-56b7-9f5f-78f1dd741d1e
|