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/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]="89eea142-f3f2-5f35-a6b0-e9e10055d0cb")}catch(e){}}();
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 checkSecurityScan = (rootDirectory, options = {}) => {
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
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
40169
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
40170
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
40171
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
40172
- if (seen.has(key)) continue;
40173
- seen.add(key);
40174
- diagnostics.push(diagnostic);
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 result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40700
- rootDirectory,
40701
- entryPatterns,
40702
- tsConfigPath: resolveTsConfigPath(rootDirectory),
40703
- ignorePatterns,
40704
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
40705
- parseConcurrency: options.parseConcurrency
40706
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
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) => scoped(gen(function* () {
41108
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
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
- }), withSpan("git.exec", { attributes: {
41141
- "git.command": input.command,
41142
- "git.subcommand": input.args[0] ?? ""
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 + security scan), collected synchronously
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.f853efd";
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
- process.stdout.write(`${serialized}\n`);
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.f853efd`;
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.f853efd") ? "development" : "production");
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
- outcome: "error",
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
- exitCode: wouldBlock ? 1 : 0,
48954
- wouldBlock,
48955
- blocking: blockingLevel,
48956
- scanClean: isClean,
48957
- totalDiagnostics: summary.totalDiagnosticCount,
48958
- errorCount: summary.errorCount,
48959
- warningCount: summary.warningCount,
48960
- affectedFiles: summary.affectedFileCount,
48961
- diagnosticsInTestFiles,
48962
- diagnosticsInStoryFiles,
48963
- distinctRulesFired: countByRule.size,
48964
- "diag.fixGroups": findingsPerFixGroup.size,
48965
- "diag.fixGroupedFindings": fixGroupedFindings,
48966
- topRule,
48967
- "migration.largestRuleBucketFiles": largestRuleBucket ? largestRuleBucket.fileCount : null,
48968
- "migration.largestRuleBucketSites": largestRuleBucket ? largestRuleBucket.siteCount : null,
48969
- "migration.largestRuleBucketRule": largestRuleBucket ? largestRuleBucket.ruleKey : null,
48970
- scannedFileCount: result.scannedFileCount ?? null,
48971
- lintCacheHitFiles: result.lintCacheHitFileCount ?? null,
48972
- lintCacheTotalFiles: result.lintCacheTotalFileCount ?? null,
48973
- lintCacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null,
48974
- elapsedMs: result.elapsedMilliseconds,
48975
- scanPhaseMs: result.scanElapsedMilliseconds ?? null,
48976
- score: result.score ? result.score.score : null,
48977
- scoreLabel: result.score ? result.score.label : null,
48978
- scoreAvailable: result.score !== null,
48979
- skippedCheckCount: result.skippedChecks.length,
48980
- didLintFail: input.didLintFail ?? null,
48981
- lintFailureReasonKind: input.lintFailureReasonKind ?? null,
48982
- lintPartialFailureCount: input.lintPartialFailureCount ?? null,
48983
- lintDroppedFileCount: input.lintDroppedFileCount ?? null,
48984
- didDeadCodeFail: input.didDeadCodeFail ?? null,
48985
- supplyChainOverlapTimedOut: input.supplyChainOverlapTimedOut ?? null,
48986
- deadCodeOverlapped: input.deadCodeOverlapped ?? null
48987
- };
48988
- for (const [category, count] of countByCategory) attributes[`diag.category.${toCategoryKey(category)}`] = count;
48989
- if (result.baselineDelta) {
48990
- attributes["baseline.new"] = summary.totalDiagnosticCount;
48991
- attributes["baseline.fixed"] = result.baselineDelta.fixedCount;
48992
- attributes["baseline.baseTotal"] = result.baselineDelta.baseTotalCount;
48993
- attributes["baseline.degraded"] = false;
48994
- } else if (input.gateExempt) attributes["baseline.degraded"] = true;
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 buildCiAttributes = () => {
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 buildConfigAttributes = (input) => {
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 flat attribute set for its root span — the canonical
49030
- * per-scan "wide event". Pure and exported so the projection (outcome
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
- ...buildConfigAttributes(input),
49040
- ...buildCiAttributes(),
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.sh";
52035
- const CLAUDE_HOOK_COMMAND = "sh \"$CLAUDE_PROJECT_DIR/.claude/hooks/react-doctor.sh\"";
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.sh";
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 === CURSOR_HOOK_RELATIVE_PATH);
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: CURSOR_HOOK_RELATIVE_PATH,
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
- "#!/bin/sh",
52113
- "set -u",
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
- "input_file=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-agent-hook.XXXXXX\")",
52116
- "output_file=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-agent-hook-output.XXXXXX\")",
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
- "script_dir=$(CDPATH= cd \"$(dirname \"$0\")\" && pwd)",
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
- "should_scan() {",
52130
- " if ! command -v node >/dev/null 2>&1; then",
52131
- " return 0",
52132
- " fi",
52357
+ "const readStdin = () => {",
52358
+ " try {",
52359
+ " return readFileSync(0, 'utf8');",
52360
+ " } catch {",
52361
+ " return '';",
52362
+ " }",
52363
+ "};",
52133
52364
  "",
52134
- " node - \"$input_file\" <<'NODE'",
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
- " process.exit(toolCalls.some((toolCall) => editToolNames.has(toolCall.tool_name)) ? 0 : 10);",
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
- " process.exit(!toolName || editToolNames.has(toolName) ? 0 : 10);",
52147
- "} catch {",
52148
- " process.exit(0);",
52149
- "}",
52150
- "NODE",
52151
- "}",
52372
+ " return !toolName || EDIT_TOOL_NAMES.has(toolName);",
52373
+ "};",
52152
52374
  "",
52153
- "run_react_doctor() {",
52154
- " if [ -x ./node_modules/.bin/react-doctor ]; then",
52155
- " ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
52156
- " return",
52157
- " fi",
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
- " if command -v react-doctor >/dev/null 2>&1; then",
52160
- " react-doctor --verbose --scope changed --blocking warning --no-score",
52161
- " return",
52162
- " fi",
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
- " if command -v pnpm >/dev/null 2>&1; then",
52165
- " pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
52166
- " return",
52167
- " fi",
52397
+ " return 0;",
52398
+ "};",
52168
52399
  "",
52169
- " if command -v npx >/dev/null 2>&1; then",
52170
- " npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
52171
- " return",
52172
- " fi",
52400
+ "const cleanup = (...paths) => {",
52401
+ " for (const path of paths) {",
52402
+ " try { unlinkSync(path); } catch {}",
52403
+ " }",
52404
+ "};",
52173
52405
  "",
52174
- " printf '%s\\n' 'react-doctor: command not found; skipping agent hook scan.'",
52175
- " return 0",
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 ! should_scan; then",
52179
- " exit 0",
52180
- "fi",
52415
+ " if (!shouldScan(input)) {",
52416
+ " process.exit(0);",
52417
+ " }",
52181
52418
  "",
52182
- "if run_react_doctor > \"$output_file\" 2>&1; then",
52183
- " exit 0",
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
- " return JSON.parse(fs.readFileSync(inputPath, 'utf8') || '{}');",
52423
+ " process.chdir(projectRoot);",
52193
52424
  " } catch {",
52194
- " return {};",
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
- "const input = readInput();",
52198
- "const scanOutput = fs.readFileSync(outputPath, 'utf8').trim();",
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, shell = false) => new Promise((resolve, reject) => {
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) enableJsonMode({
54166
- compact: Boolean(flags.jsonCompact),
54167
- directory: requestedDirectory
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=89eea142-f3f2-5f35-a6b0-e9e10055d0cb
55773
+ //# debugId=f1523493-79d1-56b7-9f5f-78f1dd741d1e