react-doctor 0.5.8-dev.970babc → 0.5.8-dev.99f2417

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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]="a201a872-19de-5f85-bec4-8ecc9f0ae2a8")}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]="cf52ec59-cee0-5503-b8f2-a4f98bf77ea4")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -9041,7 +9041,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
9041
9041
  * @since 2.0.0
9042
9042
  */
9043
9043
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
9044
- const setImmediate = "setImmediate" in globalThis ? (f) => {
9044
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
9045
9045
  const timer = globalThis.setImmediate(f);
9046
9046
  return () => globalThis.clearImmediate(timer);
9047
9047
  } : (f) => {
@@ -9085,7 +9085,7 @@ var PriorityBuckets = class {
9085
9085
  var MixedScheduler = class {
9086
9086
  executionMode;
9087
9087
  setImmediate;
9088
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
9088
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
9089
9089
  this.executionMode = executionMode;
9090
9090
  this.setImmediate = setImmediateFn;
9091
9091
  }
@@ -9110,7 +9110,7 @@ var MixedSchedulerDispatcher = class {
9110
9110
  tasks = /* @__PURE__ */ new PriorityBuckets();
9111
9111
  running = void 0;
9112
9112
  setImmediate;
9113
- constructor(setImmediateFn = setImmediate) {
9113
+ constructor(setImmediateFn = setImmediate$1) {
9114
9114
  this.setImmediate = setImmediateFn;
9115
9115
  }
9116
9116
  /**
@@ -36947,6 +36947,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
36947
36947
  const LINT_PHASE_TIMEOUT_MS = 3e5;
36948
36948
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
36949
36949
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
36950
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
36950
36951
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
36951
36952
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
36952
36953
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -38186,6 +38187,7 @@ const collectStringSet = (values) => {
38186
38187
  if (!Array.isArray(values)) return /* @__PURE__ */ new Set();
38187
38188
  return new Set(values.filter((value) => typeof value === "string"));
38188
38189
  };
38190
+ const JSX_NO_UNDEF_IDENTIFIER_PATTERN = /^`([^`]+)`/;
38189
38191
  /**
38190
38192
  * Pre-compiles every stateful filter and returns a single
38191
38193
  * `apply(diagnostic)` closure that runs (in order):
@@ -38219,8 +38221,10 @@ const buildDiagnosticPipeline = (input) => {
38219
38221
  const compiledOverrides = compileIgnoreOverrides(userConfig);
38220
38222
  const textComponentNames = collectStringSet(userConfig?.textComponents);
38221
38223
  const rawTextWrapperComponentNames = collectStringSet(userConfig?.rawTextWrapperComponents);
38224
+ const runtimeGlobalNames = collectStringSet(userConfig?.runtimeGlobals);
38222
38225
  const hasTextComponents = textComponentNames.size > 0;
38223
38226
  const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
38227
+ const hasRuntimeGlobals = runtimeGlobalNames.size > 0;
38224
38228
  const fileLinesCache = /* @__PURE__ */ new Map();
38225
38229
  const fileContextCache = /* @__PURE__ */ new Map();
38226
38230
  const libraryFileCache = /* @__PURE__ */ new Map();
@@ -38272,6 +38276,12 @@ const buildDiagnosticPipeline = (input) => {
38272
38276
  if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return true;
38273
38277
  return false;
38274
38278
  };
38279
+ const isJsxNoUndefSuppressedByConfig = (diagnostic) => {
38280
+ if (!hasRuntimeGlobals) return false;
38281
+ if (diagnostic.rule !== "jsx-no-undef") return false;
38282
+ const identifierMatch = JSX_NO_UNDEF_IDENTIFIER_PATTERN.exec(diagnostic.message);
38283
+ return identifierMatch !== null && runtimeGlobalNames.has(identifierMatch[1]);
38284
+ };
38275
38285
  return { apply: (diagnostic) => {
38276
38286
  if (shouldAutoSuppress(diagnostic)) return null;
38277
38287
  let current = diagnostic;
@@ -38296,6 +38306,7 @@ const buildDiagnosticPipeline = (input) => {
38296
38306
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
38297
38307
  if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return null;
38298
38308
  if (isRnRawTextSuppressedByConfig(current)) return null;
38309
+ if (isJsxNoUndefSuppressedByConfig(current)) return null;
38299
38310
  }
38300
38311
  if (respectInlineDisables && current.line > 0) {
38301
38312
  const lines = getFileLines(current.filePath);
@@ -38557,7 +38568,7 @@ const resolveScanConcurrency = (requested) => {
38557
38568
  if (!Number.isFinite(requested) || requested < 1) return 1;
38558
38569
  return Math.min(Math.floor(requested), 32);
38559
38570
  };
38560
- const readSystemFacts = () => ({
38571
+ const readSystemFacts$1 = () => ({
38561
38572
  availableCores: os.availableParallelism(),
38562
38573
  totalMemoryBytes: os.totalmem(),
38563
38574
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -38578,7 +38589,7 @@ const readSystemFacts = () => ({
38578
38589
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
38579
38590
  * limited, and ceiling cases without mocking `os` or the filesystem.
38580
38591
  */
38581
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
38592
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
38582
38593
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
38583
38594
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
38584
38595
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -38773,6 +38784,7 @@ const STRING_ARRAY_FIELD_NAMES = [
38773
38784
  "projects",
38774
38785
  "textComponents",
38775
38786
  "rawTextWrapperComponents",
38787
+ "runtimeGlobals",
38776
38788
  "serverAuthFunctionNames"
38777
38789
  ];
38778
38790
  const SURFACE_CONTROL_FIELD_NAMES = [
@@ -40139,7 +40151,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
40139
40151
  }
40140
40152
  return true;
40141
40153
  };
40142
- const checkSecurityScan = (rootDirectory, options = {}) => {
40154
+ const yieldToEventLoop = () => new Promise((resolve) => {
40155
+ setImmediate(resolve);
40156
+ });
40157
+ const createSecurityScanSession = (rootDirectory, options) => {
40143
40158
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
40144
40159
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
40145
40160
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -40154,7 +40169,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
40154
40169
  committedFilesOnly: rule.committedFilesOnly === true
40155
40170
  }];
40156
40171
  });
40157
- if (enabledScanRules.length === 0) return [];
40172
+ if (enabledScanRules.length === 0) return null;
40158
40173
  const diagnostics = [];
40159
40174
  const seen = /* @__PURE__ */ new Set();
40160
40175
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -40166,15 +40181,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
40166
40181
  }
40167
40182
  return status === true;
40168
40183
  };
40169
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
40170
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
40171
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
40172
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
40173
- if (seen.has(key)) continue;
40174
- seen.add(key);
40175
- diagnostics.push(diagnostic);
40184
+ const scanFile = (file) => {
40185
+ for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
40186
+ if (committedFilesOnly && isFileGitIgnored(file)) continue;
40187
+ const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
40188
+ const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
40189
+ if (seen.has(key)) continue;
40190
+ seen.add(key);
40191
+ diagnostics.push(diagnostic);
40192
+ }
40193
+ };
40194
+ return {
40195
+ scanFile,
40196
+ diagnostics
40197
+ };
40198
+ };
40199
+ const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
40200
+ const session = createSecurityScanSession(rootDirectory, options);
40201
+ if (session === null) return [];
40202
+ let filesSinceYield = 0;
40203
+ for (const file of collectSecurityScanFiles(rootDirectory)) {
40204
+ session.scanFile(file);
40205
+ filesSinceYield += 1;
40206
+ if (filesSinceYield >= 16) {
40207
+ filesSinceYield = 0;
40208
+ await yieldToEventLoop();
40209
+ }
40176
40210
  }
40177
- return diagnostics;
40211
+ return session.diagnostics;
40178
40212
  };
40179
40213
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
40180
40214
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -40407,6 +40441,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
40407
40441
  return [...seen].filter((pattern) => pattern.length > 0);
40408
40442
  };
40409
40443
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
40444
+ const readSystemFacts = () => ({
40445
+ availableCores: os.availableParallelism(),
40446
+ totalMemoryBytes: os.totalmem(),
40447
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
40448
+ });
40449
+ /**
40450
+ * How many real deslop dead-code child processes may run at once, across the
40451
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
40452
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
40453
+ * workers that fit in available memory, floored at 1.
40454
+ *
40455
+ * On a roomy dev box / CI runner this resolves high enough that every
40456
+ * concurrently-scanned project still spawns its own worker (no serialization vs
40457
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
40458
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
40459
+ * instead of oversubscribing memory with N simultaneous children — the global
40460
+ * cap the per-project spawn path lacked.
40461
+ *
40462
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
40463
+ * heavier dead-code worker. `facts` is injectable for tests.
40464
+ */
40465
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
40466
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
40467
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
40468
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
40469
+ };
40470
+ let availableSlots = -1;
40471
+ const waiters = [];
40472
+ const releaseSlot = () => {
40473
+ const nextWaiter = waiters.shift();
40474
+ if (nextWaiter !== void 0) nextWaiter();
40475
+ else availableSlots += 1;
40476
+ };
40477
+ /**
40478
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
40479
+ * task settles (success or failure). With a high cap (roomy machine) every
40480
+ * caller proceeds immediately; with a low cap (constrained runner) callers
40481
+ * queue and run as slots free.
40482
+ *
40483
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
40484
+ * this caller is queued, the call rejects without acquiring a slot or running
40485
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
40486
+ * then spawn a child only to tear it down. A queued caller that aborts removes
40487
+ * its own waiter so a later release never hands a slot to a dead request.
40488
+ */
40489
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
40490
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
40491
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
40492
+ if (availableSlots > 0) availableSlots -= 1;
40493
+ else await new Promise((resolve, reject) => {
40494
+ const waiter = () => {
40495
+ abortSignal?.removeEventListener("abort", onAbort);
40496
+ resolve();
40497
+ };
40498
+ const onAbort = () => {
40499
+ const queuedIndex = waiters.indexOf(waiter);
40500
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
40501
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
40502
+ };
40503
+ waiters.push(waiter);
40504
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
40505
+ });
40506
+ try {
40507
+ return await task();
40508
+ } finally {
40509
+ releaseSlot();
40510
+ }
40511
+ };
40410
40512
  /**
40411
40513
  * Resolves a path to its canonical, symlink-free form, falling back to
40412
40514
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -40431,6 +40533,11 @@ const toCanonicalPath = (filePath) => {
40431
40533
  };
40432
40534
  const DEAD_CODE_PLUGIN = "deslop";
40433
40535
  const DEAD_CODE_CATEGORY = "Maintainability";
40536
+ const REACT_DOCTOR_TOOLCHAIN_PACKAGES = new Set([
40537
+ "react-doctor",
40538
+ "eslint-plugin-react-doctor",
40539
+ "oxlint-plugin-react-doctor"
40540
+ ]);
40434
40541
  const DEAD_CODE_WORKER_SCRIPT = `
40435
40542
  const inputChunks = [];
40436
40543
  process.stdin.on("data", (chunk) => inputChunks.push(chunk));
@@ -40697,14 +40804,17 @@ const checkDeadCode = async (options) => {
40697
40804
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
40698
40805
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
40699
40806
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
40700
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40701
- rootDirectory,
40702
- entryPatterns,
40703
- tsConfigPath: resolveTsConfigPath(rootDirectory),
40704
- ignorePatterns,
40705
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
40706
- parseConcurrency: options.parseConcurrency
40707
- }), options.workerTimeoutMs ?? 12e4, options.abortSignal));
40807
+ const spawnAndRun = () => {
40808
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40809
+ rootDirectory,
40810
+ entryPatterns,
40811
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
40812
+ ignorePatterns,
40813
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
40814
+ parseConcurrency: options.parseConcurrency
40815
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
40816
+ };
40817
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
40708
40818
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
40709
40819
  const diagnostics = [];
40710
40820
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -40733,6 +40843,7 @@ const checkDeadCode = async (options) => {
40733
40843
  });
40734
40844
  }
40735
40845
  for (const unusedDependency of result.unusedDependencies) {
40846
+ if (REACT_DOCTOR_TOOLCHAIN_PACKAGES.has(unusedDependency.name)) continue;
40736
40847
  const label = unusedDependency.isDevDependency ? "devDependency" : "dependency";
40737
40848
  diagnostics.push({
40738
40849
  filePath: "package.json",
@@ -43868,7 +43979,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
43868
43979
  * diagnostics).
43869
43980
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
43870
43981
  * 3. environment checks (reduced-motion + pnpm hardening +
43871
- * expo/react-native + security scan), collected synchronously
43982
+ * expo/react-native), collected synchronously. The heavier
43983
+ * content-regex security scan is forked instead (like supply-chain
43984
+ * below) and joined before the concat, so its CPU overlaps lint
43985
+ * rather than blocking the event loop before it.
43872
43986
  * 4. The supply-chain check (Socket.dev) is forked onto a background
43873
43987
  * fiber so its ~100% network-bound time overlaps the ~100%
43874
43988
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -43888,7 +44002,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
43888
44002
  * order, so terminal output is identical either way; supply-chain
43889
44003
  * rides alongside without a spinner.
43890
44004
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
43891
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
44005
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
43892
44006
  * byte-identical regardless of which fiber settled first. The
43893
44007
  * viewer-permission fiber is joined later, during score-metadata
43894
44008
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -43949,12 +44063,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
43949
44063
  ...checkPnpmHardening(scanDirectory),
43950
44064
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
43951
44065
  ...checkExpoProject(scanDirectory, project),
43952
- ...checkReactNativeProject(scanDirectory, project),
43953
- ...checkSecurityScan(scanDirectory, {
43954
- project,
43955
- ignoredTags: input.ignoredTags
43956
- })
44066
+ ...checkReactNativeProject(scanDirectory, project)
43957
44067
  ])));
44068
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
44069
+ project,
44070
+ ignoredTags: input.ignoredTags
44071
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
43958
44072
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
43959
44073
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
43960
44074
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -44090,9 +44204,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44090
44204
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
44091
44205
  const supplyChainResult = yield* join(supplyChainFiber);
44092
44206
  const supplyChainCollected = supplyChainResult.diagnostics;
44207
+ const securityScanCollected = yield* join(securityScanFiber);
44093
44208
  yield* reporterService.finalize;
44094
44209
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
44095
44210
  ...envCollected,
44211
+ ...securityScanCollected,
44096
44212
  ...supplyChainCollected,
44097
44213
  ...lintCollected,
44098
44214
  ...deadCodeCollected
@@ -44631,6 +44747,61 @@ const computeConfigFingerprint = (projectDirectory, version) => {
44631
44747
  }
44632
44748
  return crypto.createHash("sha1").update(parts.join("|")).digest("hex");
44633
44749
  };
44750
+ const AI_TRAINING_ENV_VARS = [
44751
+ ["HF_DATASETS_CACHE", "huggingface"],
44752
+ ["HF_HOME", "huggingface"],
44753
+ ["HUGGINGFACE_HUB_CACHE", "huggingface"],
44754
+ ["SPACE_ID", "huggingface-spaces"],
44755
+ ["CUDA_VISIBLE_DEVICES", "cuda"],
44756
+ ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
44757
+ ["ROCR_VISIBLE_DEVICES", "rocm"],
44758
+ ["TPU_NAME", "google-tpu"],
44759
+ ["WANDB_RUN_ID", "wandb"],
44760
+ ["WANDB_SWEEP_ID", "wandb"],
44761
+ ["MLFLOW_RUN_ID", "mlflow"],
44762
+ ["MLFLOW_TRACKING_URI", "mlflow"],
44763
+ ["COMET_EXPERIMENT_KEY", "comet"],
44764
+ ["NEPTUNE_RUN_ID", "neptune"],
44765
+ ["CLEARML_TASK_ID", "clearml"],
44766
+ ["DVC_STAGE", "dvc"],
44767
+ ["RAY_WORKER_PROCESS", "ray"],
44768
+ ["RAY_ADDRESS", "ray"],
44769
+ ["MUJOCO_GL", "mujoco"],
44770
+ ["MUJOCO_PATH", "mujoco"],
44771
+ ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
44772
+ ["SM_TRAINING_ENV", "sagemaker"],
44773
+ ["TRAINING_JOB_ARN", "sagemaker"],
44774
+ ["SAGEMAKER_BASE_DIR", "sagemaker"],
44775
+ ["AZUREML_RUN_ID", "azure-ml"],
44776
+ ["AZURE_ML_MODEL_DIR", "azure-ml"],
44777
+ ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
44778
+ ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
44779
+ ["DET_MASTER", "determined-ai"],
44780
+ ["LIGHTNING_USER_ID", "lightning-ai"],
44781
+ ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
44782
+ ["ARGO_WORKFLOW_NAME", "argo-workflows"],
44783
+ ["KFP_POD_NAME", "kubeflow-pipelines"],
44784
+ ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
44785
+ ["COLAB_BACKEND_VERSION", "google-colab"],
44786
+ ["DATABRICKS_RUNTIME_VERSION", "databricks"],
44787
+ ["DAYTONA_WS_ID", "daytona"],
44788
+ ["DAYTONA_WS_NAME", "daytona"],
44789
+ ["E2B_SANDBOX_ID", "e2b"],
44790
+ ["MODAL_FUNCTION_ID", "modal"],
44791
+ ["MODAL_TASK_ID", "modal"],
44792
+ ["RUNPOD_POD_ID", "runpod"],
44793
+ ["REPLICATE_USERNAME", "replicate"],
44794
+ ["VAST_CONTAINERLABEL", "vast-ai"],
44795
+ ["HARBOR_URL", "harbor"],
44796
+ ["HARBOR_HOSTNAME", "harbor"],
44797
+ ["SWE_BENCH_TASK", "swe-bench"],
44798
+ ["SWEBENCH_TASK", "swe-bench"],
44799
+ ["SWE_AGENT_MODEL", "swe-agent"]
44800
+ ];
44801
+ const detectAiTrainingEnvironment = () => {
44802
+ for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
44803
+ return null;
44804
+ };
44634
44805
  const groupBy = (items, keyFn) => {
44635
44806
  const groups = /* @__PURE__ */ new Map();
44636
44807
  for (const item of items) {
@@ -44716,7 +44887,25 @@ const CI_PROVIDER_BY_ENVIRONMENT_VARIABLE = [
44716
44887
  ["TEAMCITY_VERSION", "teamcity"],
44717
44888
  ["BITBUCKET_BUILD_NUMBER", "bitbucket"],
44718
44889
  ["TRAVIS", "travis"],
44719
- ["DRONE", "drone"]
44890
+ ["DRONE", "drone"],
44891
+ ["SEMAPHORE", "semaphore"],
44892
+ ["APPVEYOR", "appveyor"],
44893
+ ["HARNESS_BUILD_ID", "harness"],
44894
+ ["BUDDY", "buddy"],
44895
+ ["CF_BUILD_ID", "codefresh"],
44896
+ ["NETLIFY", "netlify"],
44897
+ ["RAILWAY_SERVICE_ID", "railway"],
44898
+ ["VERCEL", "vercel"],
44899
+ ["CM_BUILD_ID", "codemagic"],
44900
+ ["PROW_JOB_ID", "prow"],
44901
+ ["AGOLA_GIT_REF", "agola"],
44902
+ ["CIRRUS_CI", "cirrus-ci"],
44903
+ ["BLACKSMITH_STICKYDISK_TOKEN", "blacksmith"],
44904
+ ["WARPBUILD_RUNNER_VERIFICATION_TOKEN", "warpbuild"],
44905
+ ["NSC_CACHE_PATH", "namespace"],
44906
+ ["UBICLOUD_CACHE_URL", "ubicloud"],
44907
+ ["RENDER", "render"],
44908
+ ["FLY_APP_NAME", "fly-io"]
44720
44909
  ];
44721
44910
  const GITHUB_ACTION_MARKER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_GITHUB_ACTION";
44722
44911
  const ACTION_INPUT_ENVIRONMENT_VARIABLES = {
@@ -44734,7 +44923,10 @@ const CODING_AGENT_BY_ENVIRONMENT_VARIABLE = [
44734
44923
  ["CODEX_SANDBOX_NETWORK_DISABLED", "codex"],
44735
44924
  ["OPENCODE", "opencode"],
44736
44925
  ["GOOSE_TERMINAL", "goose"],
44737
- ["AMP_THREAD_ID", "amp"]
44926
+ ["AMP_THREAD_ID", "amp"],
44927
+ ["CLINE_ACTIVE", "cline"],
44928
+ ["AUGMENT_AGENT", "augment"],
44929
+ ["TRAE_AI_SHELL_ID", "trae-ai"]
44738
44930
  ];
44739
44931
  const GENERIC_CODING_AGENT_ENVIRONMENT_VARIABLES = ["AGENT_SESSION_ID", "AGENT_THREAD_ID"];
44740
44932
  const CODING_AGENT_ENVIRONMENT_VALUE_VARIABLES = ["AGENT"];
@@ -44883,7 +45075,9 @@ const METRIC = {
44883
45075
  lspSessionStarted: "lsp.session.started",
44884
45076
  lspScanCompleted: "lsp.scan.completed",
44885
45077
  lspScanDuration: "lsp.scan.duration",
44886
- lspScanDiagnostics: "lsp.scan.diagnostics"
45078
+ lspScanDiagnostics: "lsp.scan.diagnostics",
45079
+ aiTrainingWarningShown: "ai.training.warning_shown",
45080
+ jsonOutUsed: "json.out_used"
44887
45081
  };
44888
45082
  //#endregion
44889
45083
  //#region src/cli/utils/noop-console.ts
@@ -44926,7 +45120,7 @@ const makeNoopConsole = () => ({
44926
45120
  });
44927
45121
  //#endregion
44928
45122
  //#region src/cli/utils/version.ts
44929
- const VERSION = "0.5.8-dev.970babc";
45123
+ const VERSION = "0.5.8-dev.99f2417";
44930
45124
  //#endregion
44931
45125
  //#region src/cli/utils/json-mode.ts
44932
45126
  let context = null;
@@ -44964,12 +45158,13 @@ const installSilentConsole = () => {
44964
45158
  "trace"
44965
45159
  ]) target[key] = source[key];
44966
45160
  };
44967
- const enableJsonMode = ({ compact, directory }) => {
45161
+ const enableJsonMode = ({ compact, directory, outputFile }) => {
44968
45162
  context = {
44969
45163
  compact,
44970
45164
  directory,
44971
45165
  startTime: performance$1.now(),
44972
- mode: "full"
45166
+ mode: "full",
45167
+ outputFile: outputFile ?? null
44973
45168
  };
44974
45169
  installSilentConsole();
44975
45170
  };
@@ -44982,7 +45177,11 @@ const setJsonReportMode = (mode) => {
44982
45177
  };
44983
45178
  const writeJsonReport = (report) => {
44984
45179
  const serialized = context?.compact ? JSON.stringify(report) : JSON.stringify(report, null, 2);
44985
- process.stdout.write(`${serialized}\n`);
45180
+ if (context?.outputFile) {
45181
+ const resolvedPath = Path.resolve(context.outputFile);
45182
+ NFS.mkdirSync(Path.dirname(resolvedPath), { recursive: true });
45183
+ NFS.writeFileSync(resolvedPath, `${serialized}\n`);
45184
+ } else process.stdout.write(`${serialized}\n`);
44986
45185
  };
44987
45186
  const writeJsonErrorReport = (error, sentryEventId) => {
44988
45187
  if (!context) return;
@@ -45290,13 +45489,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45290
45489
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45291
45490
  * standard `SENTRY_RELEASE` override.
45292
45491
  */
45293
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.970babc`;
45492
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.99f2417`;
45294
45493
  /**
45295
45494
  * Deployment environment shown in Sentry's environment filter. Defaults to
45296
45495
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45297
45496
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45298
45497
  */
45299
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.970babc") ? "development" : "production");
45498
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.99f2417") ? "development" : "production");
45300
45499
  /**
45301
45500
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45302
45501
  * (set to `0` to disable tracing) and falls back to
@@ -48892,6 +49091,25 @@ const shouldBlockCi = (diagnostics, blockingLevel) => {
48892
49091
  */
48893
49092
  const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
48894
49093
  //#endregion
49094
+ //#region src/cli/utils/with-namespace.ts
49095
+ /**
49096
+ * Prefixes every key in a flat attribute group with `namespace.`, so a logical
49097
+ * group (scan config, diagnostics rollup, score, …) carries one dotted
49098
+ * namespace applied in a single place rather than hand-spelled into each key —
49099
+ * which is how the wide event drifted into a flat, half-namespaced soup. The
49100
+ * dotted prefix is what makes the attributes tree up in Sentry's attribute
49101
+ * browser and stay group-/filter-/aggregate-able in the Spans dataset.
49102
+ *
49103
+ * Value types are preserved verbatim (numbers stay numbers so Sentry infers a
49104
+ * numeric attribute and `p75(...)`/`avg(...)` work; `null` is kept so
49105
+ * `toSpanAttributes` can drop absent signals rather than coerce them).
49106
+ */
49107
+ const withNamespace = (namespace, attributes) => {
49108
+ const namespaced = {};
49109
+ for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
49110
+ return namespaced;
49111
+ };
49112
+ //#endregion
48895
49113
  //#region src/cli/utils/build-run-event.ts
48896
49114
  const readEnvBoolean = (name) => {
48897
49115
  const value = process.env[name];
@@ -48913,12 +49131,12 @@ const buildOutcomeAttributes = (input) => {
48913
49131
  if (input.result === void 0) {
48914
49132
  const error = input.error;
48915
49133
  const known = isReactDoctorError(error);
48916
- return {
48917
- outcome: "error",
49134
+ return withNamespace("outcome", {
49135
+ status: "error",
48918
49136
  exitCode: 1,
48919
49137
  knownError: known,
48920
49138
  errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
48921
- };
49139
+ });
48922
49140
  }
48923
49141
  const result = input.result;
48924
49142
  const summary = summarizeDiagnostics(result.diagnostics);
@@ -48952,66 +49170,82 @@ const buildOutcomeAttributes = (input) => {
48952
49170
  }
48953
49171
  let fixGroupedFindings = 0;
48954
49172
  for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
49173
+ const categoryRollup = {};
49174
+ for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
48955
49175
  const attributes = {
48956
- outcome,
48957
- exitCode: wouldBlock ? 1 : 0,
48958
- wouldBlock,
48959
- blocking: blockingLevel,
48960
- scanClean: isClean,
48961
- totalDiagnostics: summary.totalDiagnosticCount,
48962
- errorCount: summary.errorCount,
48963
- warningCount: summary.warningCount,
48964
- affectedFiles: summary.affectedFileCount,
48965
- diagnosticsInTestFiles,
48966
- diagnosticsInStoryFiles,
48967
- distinctRulesFired: countByRule.size,
48968
- "diag.fixGroups": findingsPerFixGroup.size,
48969
- "diag.fixGroupedFindings": fixGroupedFindings,
48970
- topRule,
48971
- "migration.largestRuleBucketFiles": largestRuleBucket ? largestRuleBucket.fileCount : null,
48972
- "migration.largestRuleBucketSites": largestRuleBucket ? largestRuleBucket.siteCount : null,
48973
- "migration.largestRuleBucketRule": largestRuleBucket ? largestRuleBucket.ruleKey : null,
48974
- scannedFileCount: result.scannedFileCount ?? null,
48975
- lintCacheHitFiles: result.lintCacheHitFileCount ?? null,
48976
- lintCacheTotalFiles: result.lintCacheTotalFileCount ?? null,
48977
- lintCacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null,
48978
- elapsedMs: result.elapsedMilliseconds,
48979
- scanPhaseMs: result.scanElapsedMilliseconds ?? null,
48980
- score: result.score ? result.score.score : null,
48981
- scoreLabel: result.score ? result.score.label : null,
48982
- scoreAvailable: result.score !== null,
48983
- skippedCheckCount: result.skippedChecks.length,
48984
- didLintFail: input.didLintFail ?? null,
48985
- lintFailureReasonKind: input.lintFailureReasonKind ?? null,
48986
- lintPartialFailureCount: input.lintPartialFailureCount ?? null,
48987
- lintDroppedFileCount: input.lintDroppedFileCount ?? null,
48988
- didDeadCodeFail: input.didDeadCodeFail ?? null,
48989
- supplyChainOverlapTimedOut: input.supplyChainOverlapTimedOut ?? null,
48990
- deadCodeOverlapped: input.deadCodeOverlapped ?? null
48991
- };
48992
- for (const [category, count] of countByCategory) attributes[`diag.category.${toCategoryKey(category)}`] = count;
48993
- if (result.baselineDelta) {
48994
- attributes["baseline.new"] = summary.totalDiagnosticCount;
48995
- attributes["baseline.fixed"] = result.baselineDelta.fixedCount;
48996
- attributes["baseline.baseTotal"] = result.baselineDelta.baseTotalCount;
48997
- attributes["baseline.degraded"] = false;
48998
- } else if (input.gateExempt) attributes["baseline.degraded"] = true;
49176
+ ...withNamespace("outcome", {
49177
+ status: outcome,
49178
+ exitCode: wouldBlock ? 1 : 0,
49179
+ wouldBlock,
49180
+ blocking: blockingLevel,
49181
+ clean: isClean,
49182
+ skippedChecks: result.skippedChecks.length
49183
+ }),
49184
+ ...withNamespace("diag", {
49185
+ total: summary.totalDiagnosticCount,
49186
+ errors: summary.errorCount,
49187
+ warnings: summary.warningCount,
49188
+ affectedFiles: summary.affectedFileCount,
49189
+ inTestFiles: diagnosticsInTestFiles,
49190
+ inStoryFiles: diagnosticsInStoryFiles,
49191
+ distinctRules: countByRule.size,
49192
+ topRule,
49193
+ fixGroups: findingsPerFixGroup.size,
49194
+ fixGroupedFindings,
49195
+ ...categoryRollup
49196
+ }),
49197
+ ...withNamespace("score", {
49198
+ value: result.score ? result.score.score : null,
49199
+ label: result.score ? result.score.label : null,
49200
+ available: result.score !== null
49201
+ }),
49202
+ ...withNamespace("lint", {
49203
+ failed: input.didLintFail ?? null,
49204
+ failureReasonKind: input.lintFailureReasonKind ?? null,
49205
+ partialFailureCount: input.lintPartialFailureCount ?? null,
49206
+ droppedFileCount: input.lintDroppedFileCount ?? null,
49207
+ cacheHitFiles: result.lintCacheHitFileCount ?? null,
49208
+ cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
49209
+ cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
49210
+ }),
49211
+ ...withNamespace("deadCode", {
49212
+ failed: input.didDeadCodeFail ?? null,
49213
+ overlapped: input.deadCodeOverlapped ?? null
49214
+ }),
49215
+ ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
49216
+ ...withNamespace("timing", {
49217
+ elapsedMs: result.elapsedMilliseconds,
49218
+ scanMs: result.scanElapsedMilliseconds ?? null
49219
+ }),
49220
+ ...withNamespace("migration", {
49221
+ largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
49222
+ largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
49223
+ largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
49224
+ })
49225
+ };
49226
+ if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
49227
+ new: summary.totalDiagnosticCount,
49228
+ fixed: result.baselineDelta.fixedCount,
49229
+ baseTotal: result.baselineDelta.baseTotalCount,
49230
+ degraded: false
49231
+ }));
49232
+ else if (input.gateExempt) attributes["baseline.degraded"] = true;
48999
49233
  return attributes;
49000
49234
  };
49001
- const buildCiAttributes = () => {
49235
+ const buildActionAttributes = () => {
49002
49236
  const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
49003
- return {
49237
+ return withNamespace("action", {
49004
49238
  actorAssociation: githubActorAssociation ?? null,
49005
49239
  runnerOs: detectRunnerOs(),
49006
49240
  comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
49007
49241
  reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
49008
49242
  versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
49009
- };
49243
+ });
49010
49244
  };
49011
- const buildConfigAttributes = (input) => {
49245
+ const buildScanAttributes = (input) => {
49012
49246
  const ruleOverrides = input.userConfig?.rules ?? {};
49013
49247
  const ruleKeys = Object.keys(ruleOverrides);
49014
- return {
49248
+ return withNamespace("scan", {
49015
49249
  mode: input.mode,
49016
49250
  scope: input.scope,
49017
49251
  parallel: input.parallel,
@@ -49026,12 +49260,18 @@ const buildConfigAttributes = (input) => {
49026
49260
  ignoredTagCount: input.ignoredTagCount,
49027
49261
  hasCustomConfig: input.hasCustomConfig,
49028
49262
  rulesConfigured: ruleKeys.length,
49029
- rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length
49030
- };
49263
+ rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
49264
+ fileCount: input.result?.scannedFileCount ?? null
49265
+ });
49031
49266
  };
49032
49267
  /**
49033
- * Projects a scan into the flat attribute set for its root span — the canonical
49034
- * per-scan "wide event". Pure and exported so the projection (outcome
49268
+ * Projects a scan into the namespaced attribute set for its root span — the
49269
+ * canonical per-scan "wide event". Every attribute carries a dotted namespace
49270
+ * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
49271
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
49272
+ * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
49273
+ * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
49274
+ * in the Spans dataset. Pure and exported so the projection (outcome
49035
49275
  * precedence, rule/category rollups, CI knobs, config shape) is unit-testable
49036
49276
  * without a live Sentry client. `null` values are dropped so absent signals
49037
49277
  * never become misleading `"null"` attributes. The run + project base context
@@ -49040,8 +49280,8 @@ const buildConfigAttributes = (input) => {
49040
49280
  * those don't carry.
49041
49281
  */
49042
49282
  const buildRunEventAttributes = (input) => toSpanAttributes({
49043
- ...buildConfigAttributes(input),
49044
- ...buildCiAttributes(),
49283
+ ...buildScanAttributes(input),
49284
+ ...buildActionAttributes(),
49045
49285
  ...buildOutcomeAttributes(input)
49046
49286
  });
49047
49287
  /**
@@ -50339,6 +50579,7 @@ const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
50339
50579
  `if ${REACT_DOCTOR_COMMAND} > "$react_doctor_output" 2>&1; then`,
50340
50580
  "rm -f \"$react_doctor_output\";",
50341
50581
  "else",
50582
+ "cat \"$react_doctor_output\" >&2;",
50342
50583
  "rm -f \"$react_doctor_output\";",
50343
50584
  `printf "%s\\n" "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2;`,
50344
50585
  "fi"
@@ -52036,10 +52277,11 @@ const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_
52036
52277
  const CLAUDE_AGENT = "claude-code";
52037
52278
  const CURSOR_AGENT = "cursor";
52038
52279
  const CLAUDE_SETTINGS_RELATIVE_PATH = ".claude/settings.json";
52039
- const CLAUDE_HOOK_RELATIVE_PATH = ".claude/hooks/react-doctor.sh";
52040
- const CLAUDE_HOOK_COMMAND = "sh \"$CLAUDE_PROJECT_DIR/.claude/hooks/react-doctor.sh\"";
52280
+ const CLAUDE_HOOK_RELATIVE_PATH = ".claude/hooks/react-doctor.mjs";
52281
+ const CLAUDE_HOOK_COMMAND = "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/react-doctor.mjs\"";
52041
52282
  const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
52042
- const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.sh";
52283
+ const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.mjs";
52284
+ const CURSOR_HOOK_COMMAND = "node .cursor/hooks/react-doctor.mjs";
52043
52285
  const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
52044
52286
  const CURSOR_HOOKS_SCHEMA_VERSION = 1;
52045
52287
  const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
@@ -52071,7 +52313,6 @@ const writeJsonFileWithDirectoryCheck = (filePath, value) => {
52071
52313
  const writeHookScript = (filePath) => {
52072
52314
  ensureDirectoryExists(Path.dirname(filePath));
52073
52315
  NFS.writeFileSync(filePath, buildAgentHookScript());
52074
- NFS.chmodSync(filePath, 493);
52075
52316
  };
52076
52317
  const hasClaudeHookCommand = (groups) => groups.some((group) => (group.hooks ?? []).some((hook) => hook.command === CLAUDE_HOOK_COMMAND));
52077
52318
  const installClaudeHook = (projectRoot) => {
@@ -52092,7 +52333,7 @@ const installClaudeHook = (projectRoot) => {
52092
52333
  writeHookScript(hookPath);
52093
52334
  return [settingsPath, hookPath];
52094
52335
  };
52095
- const hasCursorHookCommand = (handlers) => handlers.some((handler) => handler.command === CURSOR_HOOK_RELATIVE_PATH);
52336
+ const hasCursorHookCommand = (handlers) => handlers.some((handler) => handler.command === CURSOR_HOOK_COMMAND);
52096
52337
  const installCursorHook = (projectRoot) => {
52097
52338
  const configPath = Path.join(projectRoot, CURSOR_HOOKS_RELATIVE_PATH);
52098
52339
  const hookPath = Path.join(projectRoot, CURSOR_HOOK_RELATIVE_PATH);
@@ -52100,7 +52341,7 @@ const installCursorHook = (projectRoot) => {
52100
52341
  const hooks = { ...config.hooks ?? {} };
52101
52342
  const postToolUseHooks = [...hooks.postToolUse ?? []];
52102
52343
  if (!hasCursorHookCommand(postToolUseHooks)) postToolUseHooks.push({
52103
- command: CURSOR_HOOK_RELATIVE_PATH,
52344
+ command: CURSOR_HOOK_COMMAND,
52104
52345
  matcher: CURSOR_HOOK_MATCHER,
52105
52346
  timeout: 120
52106
52347
  });
@@ -52114,102 +52355,111 @@ const installCursorHook = (projectRoot) => {
52114
52355
  return [configPath, hookPath];
52115
52356
  };
52116
52357
  const buildAgentHookScript = () => [
52117
- "#!/bin/sh",
52118
- "set -u",
52358
+ "import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';",
52359
+ "import { tmpdir } from 'node:os';",
52360
+ "import { join, dirname } from 'node:path';",
52361
+ "import { fileURLToPath } from 'node:url';",
52362
+ "import { spawnSync } from 'node:child_process';",
52119
52363
  "",
52120
- "input_file=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-agent-hook.XXXXXX\")",
52121
- "output_file=$(mktemp \"${TMPDIR:-/tmp}/react-doctor-agent-hook-output.XXXXXX\")",
52122
- "trap 'rm -f \"$input_file\" \"$output_file\"' EXIT",
52123
- "cat > \"$input_file\"",
52364
+ "const __filename = fileURLToPath(import.meta.url);",
52365
+ "const __dirname = dirname(__filename);",
52124
52366
  "",
52125
- "script_dir=$(CDPATH= cd \"$(dirname \"$0\")\" && pwd)",
52126
- "project_root=${CLAUDE_PROJECT_DIR:-}",
52127
- "if [ -z \"$project_root\" ]; then",
52128
- " project_root=$(CDPATH= cd \"$script_dir/../..\" && pwd)",
52129
- "fi",
52130
- "if ! cd \"$project_root\"; then",
52131
- " exit 0",
52132
- "fi",
52367
+ "const EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
52133
52368
  "",
52134
- "should_scan() {",
52135
- " if ! command -v node >/dev/null 2>&1; then",
52136
- " return 0",
52137
- " fi",
52369
+ "const readStdin = () => {",
52370
+ " try {",
52371
+ " return readFileSync(0, 'utf8');",
52372
+ " } catch {",
52373
+ " return '';",
52374
+ " }",
52375
+ "};",
52138
52376
  "",
52139
- " node - \"$input_file\" <<'NODE'",
52140
- "const fs = require('node:fs');",
52141
- "const inputPath = process.argv[2];",
52142
- "const editToolNames = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);",
52143
- "try {",
52144
- " const input = JSON.parse(fs.readFileSync(inputPath, 'utf8') || '{}');",
52377
+ "const shouldScan = (input) => {",
52145
52378
  " const eventName = input.hook_event_name || input.eventName || input.event_name;",
52146
52379
  " if (eventName === 'PostToolBatch') {",
52147
52380
  " const toolCalls = Array.isArray(input.tool_calls) ? input.tool_calls : [];",
52148
- " process.exit(toolCalls.some((toolCall) => editToolNames.has(toolCall.tool_name)) ? 0 : 10);",
52381
+ " return toolCalls.some((toolCall) => EDIT_TOOL_NAMES.has(toolCall.tool_name));",
52149
52382
  " }",
52150
52383
  " const toolName = input.tool_name || input.toolName || input.tool;",
52151
- " process.exit(!toolName || editToolNames.has(toolName) ? 0 : 10);",
52152
- "} catch {",
52153
- " process.exit(0);",
52154
- "}",
52155
- "NODE",
52156
- "}",
52384
+ " return !toolName || EDIT_TOOL_NAMES.has(toolName);",
52385
+ "};",
52157
52386
  "",
52158
- "run_react_doctor() {",
52159
- " if [ -x ./node_modules/.bin/react-doctor ]; then",
52160
- " ./node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score",
52161
- " return",
52162
- " fi",
52387
+ "const runReactDoctor = (outputPath) => {",
52388
+ " // Each candidate is a single shell command string (not an args array):",
52389
+ " // `shell: true` is required to run the Windows `.cmd` shims, and an args",
52390
+ " // array with `shell: true` trips Node's DEP0190. A missing command exits",
52391
+ " // 127 via the shell (no ENOENT error), so fall through on that. With no",
52392
+ " // runner found, exit 0 silently — stdout is parsed as the hook's JSON.",
52393
+ " const commands = [",
52394
+ " './node_modules/.bin/react-doctor --verbose --scope changed --blocking warning --no-score',",
52395
+ " 'react-doctor --verbose --scope changed --blocking warning --no-score',",
52396
+ " 'pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
52397
+ " 'npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score',",
52398
+ " ];",
52163
52399
  "",
52164
- " if command -v react-doctor >/dev/null 2>&1; then",
52165
- " react-doctor --verbose --scope changed --blocking warning --no-score",
52166
- " return",
52167
- " fi",
52400
+ " for (const command of commands) {",
52401
+ " const result = spawnSync(command, { encoding: 'utf8', shell: true });",
52402
+ " if (result.error?.code === 'ENOENT' || result.status === 127) continue;",
52403
+ " try {",
52404
+ " writeFileSync(outputPath, (result.stdout || '') + (result.stderr || ''));",
52405
+ " } catch {}",
52406
+ " return result.status;",
52407
+ " }",
52168
52408
  "",
52169
- " if command -v pnpm >/dev/null 2>&1; then",
52170
- " pnpm dlx react-doctor@latest --verbose --scope changed --blocking warning --no-score",
52171
- " return",
52172
- " fi",
52409
+ " return 0;",
52410
+ "};",
52173
52411
  "",
52174
- " if command -v npx >/dev/null 2>&1; then",
52175
- " npx --yes react-doctor@latest --verbose --scope changed --blocking warning --no-score",
52176
- " return",
52177
- " fi",
52412
+ "const cleanup = (...paths) => {",
52413
+ " for (const path of paths) {",
52414
+ " try { unlinkSync(path); } catch {}",
52415
+ " }",
52416
+ "};",
52178
52417
  "",
52179
- " printf '%s\\n' 'react-doctor: command not found; skipping agent hook scan.'",
52180
- " return 0",
52181
- "}",
52418
+ "const main = () => {",
52419
+ " let input;",
52420
+ " try {",
52421
+ " const stdinContent = readStdin();",
52422
+ " input = JSON.parse(stdinContent || '{}');",
52423
+ " } catch {",
52424
+ " input = {};",
52425
+ " }",
52182
52426
  "",
52183
- "if ! should_scan; then",
52184
- " exit 0",
52185
- "fi",
52427
+ " if (!shouldScan(input)) {",
52428
+ " process.exit(0);",
52429
+ " }",
52186
52430
  "",
52187
- "if run_react_doctor > \"$output_file\" 2>&1; then",
52188
- " exit 0",
52189
- "fi",
52431
+ " const projectRoot = process.env.CLAUDE_PROJECT_DIR || join(__dirname, '../..');",
52432
+ " const outputPath = join(tmpdir(), `react-doctor-agent-hook-output-${process.pid}.txt`);",
52190
52433
  "",
52191
- "node - \"$input_file\" \"$output_file\" <<'NODE'",
52192
- "const fs = require('node:fs');",
52193
- "const inputPath = process.argv[2];",
52194
- "const outputPath = process.argv[3];",
52195
- "const readInput = () => {",
52196
52434
  " try {",
52197
- " return JSON.parse(fs.readFileSync(inputPath, 'utf8') || '{}');",
52435
+ " process.chdir(projectRoot);",
52198
52436
  " } catch {",
52199
- " return {};",
52437
+ " process.exit(0);",
52438
+ " }",
52439
+ "",
52440
+ " const scanResult = runReactDoctor(outputPath);",
52441
+ " if (scanResult === 0) {",
52442
+ " cleanup(outputPath);",
52443
+ " process.exit(0);",
52444
+ " }",
52445
+ "",
52446
+ " const scanOutput = readFileSync(outputPath, 'utf8').trim();",
52447
+ " cleanup(outputPath);",
52448
+ "",
52449
+ " if (!scanOutput) {",
52450
+ " process.exit(0);",
52451
+ " }",
52452
+ "",
52453
+ " const message = `React Doctor found issues in the changed files. Review this output and fix the regressions before finishing. For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.\\n\\n${scanOutput}`;",
52454
+ "",
52455
+ " if (input.hook_event_name === 'PostToolBatch') {",
52456
+ " console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: message } }));",
52457
+ " } else {",
52458
+ " console.log(JSON.stringify({ additional_context: message }));",
52200
52459
  " }",
52201
52460
  "};",
52202
- "const input = readInput();",
52203
- "const scanOutput = fs.readFileSync(outputPath, 'utf8').trim();",
52204
- "if (!scanOutput) process.exit(0);",
52205
- "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}`;",
52206
- "if (input.hook_event_name === 'PostToolBatch') {",
52207
- " console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: message } }));",
52208
- "} else {",
52209
- " console.log(JSON.stringify({ additional_context: message }));",
52210
- "}",
52211
- "NODE",
52212
- ""
52461
+ "",
52462
+ "main();"
52213
52463
  ].join("\n");
52214
52464
  const installReactDoctorAgentHooks = (options) => {
52215
52465
  const installedAgents = [];
@@ -52270,6 +52520,7 @@ const buildReactDoctorHookBlock = () => [
52270
52520
  "if react_doctor_scan_staged_files > \"$react_doctor_output\" 2>&1; then",
52271
52521
  " rm -f \"$react_doctor_output\"",
52272
52522
  "else",
52523
+ " cat \"$react_doctor_output\" >&2",
52273
52524
  " rm -f \"$react_doctor_output\"",
52274
52525
  ` printf '%s\\n' "React Doctor found staged regressions." "Run ${REACT_DOCTOR_COMMAND} to inspect." "Want them fixed? Ask your agent to run that command and resolve the findings." >&2`,
52275
52526
  "fi",
@@ -53035,6 +53286,26 @@ const CLI_AGENT_AUTO_FLAGS = {
53035
53286
  codex: ["--yolo"],
53036
53287
  cursor: ["--force"]
53037
53288
  };
53289
+ const resolveCursorBundledNode = () => {
53290
+ if (!isWindows) return null;
53291
+ const localAppData = process.env.LOCALAPPDATA;
53292
+ if (!localAppData) return null;
53293
+ const cursorAgentRoot = Path.join(localAppData, "cursor-agent", "versions");
53294
+ try {
53295
+ const versions = NFS.readdirSync(cursorAgentRoot);
53296
+ if (versions.length === 0) return null;
53297
+ const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
53298
+ for (const version of sortedVersions) try {
53299
+ const bundledNodePath = Path.join(cursorAgentRoot, version, "node.exe");
53300
+ const entryScriptPath = Path.join(cursorAgentRoot, version, "index.js");
53301
+ if (NFS.statSync(bundledNodePath).isFile() && NFS.statSync(entryScriptPath).isFile()) return {
53302
+ command: bundledNodePath,
53303
+ args: [entryScriptPath]
53304
+ };
53305
+ } catch {}
53306
+ } catch {}
53307
+ return null;
53308
+ };
53038
53309
  const resolveWindowsCmdEntryScript = (command) => {
53039
53310
  if (!isWindows) return null;
53040
53311
  const pathDirectories = (process.env.PATH ?? "").split(Path.delimiter).filter(Boolean);
@@ -53054,11 +53325,10 @@ const resolveWindowsCmdEntryScript = (command) => {
53054
53325
  }
53055
53326
  return null;
53056
53327
  };
53057
- const spawnAgent = (command, args, cwd, shell = false) => new Promise((resolve, reject) => {
53328
+ const spawnAgent = (command, args, cwd) => new Promise((resolve, reject) => {
53058
53329
  const child = spawn(command, [...args], {
53059
53330
  cwd,
53060
- stdio: "inherit",
53061
- shell
53331
+ stdio: "inherit"
53062
53332
  });
53063
53333
  child.on("error", reject);
53064
53334
  child.on("close", (code) => resolve(code ?? 0));
@@ -53067,6 +53337,10 @@ const launchCliAgent = async (agentId, prompt, cwd) => {
53067
53337
  const binary = CLI_AGENT_BINARIES[agentId];
53068
53338
  const agentArgs = [...CLI_AGENT_AUTO_FLAGS[agentId], prompt];
53069
53339
  if (isWindows) {
53340
+ if (agentId === "cursor") {
53341
+ const cursorResolution = resolveCursorBundledNode();
53342
+ if (cursorResolution) return spawnAgent(cursorResolution.command, [...cursorResolution.args, ...agentArgs], cwd);
53343
+ }
53070
53344
  const entryScript = resolveWindowsCmdEntryScript(binary);
53071
53345
  if (entryScript) return spawnAgent(process.execPath, [entryScript, ...agentArgs], cwd);
53072
53346
  }
@@ -54085,6 +54359,17 @@ const warnDeprecatedFailOn = (flags, userConfig) => {
54085
54359
  cliLogger.warn(`${source} is deprecated; rename it to ${replacement}.`);
54086
54360
  };
54087
54361
  //#endregion
54362
+ //#region src/cli/utils/warn-ai-training-environment.ts
54363
+ let didWarnAiTraining = false;
54364
+ const warnIfAiTrainingEnvironment = () => {
54365
+ if (didWarnAiTraining) return;
54366
+ const detected = detectAiTrainingEnvironment();
54367
+ if (detected === null) return;
54368
+ didWarnAiTraining = true;
54369
+ process.stderr.write(`${highlighter.warn("react-doctor detected use in an AI or ML pipeline. This use requires written permission under the react-doctor license — contact founders@million.dev to request access.")}\n`);
54370
+ recordCount(METRIC.aiTrainingWarningShown, 1, { environment: detected });
54371
+ };
54372
+ //#endregion
54088
54373
  //#region src/cli/utils/validate-mode-flags.ts
54089
54374
  const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
54090
54375
  const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
@@ -54168,10 +54453,14 @@ const inspectAction = async (directory, flags) => {
54168
54453
  const isQuiet = isScoreOnly || isJsonMode;
54169
54454
  const requestedDirectory = Path.resolve(directory);
54170
54455
  const startTime = performance$1.now();
54171
- if (isJsonMode) enableJsonMode({
54172
- compact: Boolean(flags.jsonCompact),
54173
- directory: requestedDirectory
54174
- });
54456
+ if (isJsonMode) {
54457
+ enableJsonMode({
54458
+ compact: Boolean(flags.jsonCompact),
54459
+ directory: requestedDirectory,
54460
+ outputFile: flags.jsonOut
54461
+ });
54462
+ if (flags.jsonOut) recordCount(METRIC.jsonOutUsed, 1);
54463
+ }
54175
54464
  recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
54176
54465
  try {
54177
54466
  validateModeFlags(flags);
@@ -54185,6 +54474,7 @@ const inspectAction = async (directory, flags) => {
54185
54474
  setJsonReportDirectory(resolvedDirectory);
54186
54475
  warnDeprecatedFailOn(flags, userConfig);
54187
54476
  warnDeprecatedDiff(flags, userConfig);
54477
+ warnIfAiTrainingEnvironment();
54188
54478
  if (scanTarget.didRedirectViaRootDir && !isQuiet) {
54189
54479
  cliLogger.dim(`Redirected to ${highlighter.info(toRelativePath(resolvedDirectory, requestedDirectory))} via react-doctor config "rootDir".`);
54190
54480
  cliLogger.break();
@@ -55108,6 +55398,14 @@ const applyColorPreference = (argv, env = process.env) => {
55108
55398
  setColorEnabled(enabled);
55109
55399
  };
55110
55400
  //#endregion
55401
+ //#region src/cli/utils/ensure-windows-utf8-console.ts
55402
+ const ensureWindowsUtf8Console = () => {
55403
+ if (process.platform !== "win32" || !process.stdout.isTTY) return;
55404
+ try {
55405
+ execFileSync("chcp.com", ["65001"], { stdio: "ignore" });
55406
+ } catch {}
55407
+ };
55408
+ //#endregion
55111
55409
  //#region src/cli/utils/exit-gracefully.ts
55112
55410
  const exitGracefully = () => {
55113
55411
  try {
@@ -55250,6 +55548,7 @@ const ROOT_FLAG_SPEC = {
55250
55548
  "--category",
55251
55549
  "--changed-files-from",
55252
55550
  "--blocking",
55551
+ "--json-out",
55253
55552
  "--output-dir",
55254
55553
  "--fail-on",
55255
55554
  "--project",
@@ -55440,7 +55739,7 @@ ${highlighter.dim("Learn more:")}
55440
55739
  ${highlighter.info(CANONICAL_GITHUB_URL)}
55441
55740
  `;
55442
55741
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
55443
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
55742
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--debug", "force a Sentry trace and print its id at the end (paste it into a bug report)").option("--output-dir <dir>", "directory for the full diagnostics dump (default: a temp folder)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("--json-out <path>", "with --json, write the report to a file instead of stdout").option("-y, --yes", "skip prompts, scan all workspace projects").option("--no-parallel", "lint serially with one worker (default: parallel across CPU cores; set the worker count with REACT_DOCTOR_PARALLEL)").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple); overrides the `projects` config field").option("--scope <value>", "how much to scan/report: full (default), files, changed (only new issues vs base), or lines (only changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").addOption(new Option("--diff [base]", "[deprecated] alias for --scope changed (pass `false` to force a full scan)").hideHelp()).addOption(new Option("--changed-files-from <file>", "scan source files listed in a newline-delimited changed-files file").hideHelp()).option("--no-score", "skip the score API, the share URL, and crash reporting").addOption(new Option("--category <category>", "only show diagnostics in a category (repeatable; e.g. Security)").argParser(collectCategoryOption)).option("--no-telemetry", "alias for --no-score (skip the score API, share URL, and crash reporting)").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--blocking <level>", "severity that fails CI: error (default), warning, or none (advisory)").addOption(new Option("--fail-on <level>", "[deprecated] alias for --blocking <level>").hideHelp()).option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (default)").option("--no-warnings", "hide warning-severity diagnostics (errors only)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
55444
55743
  program.action(inspectAction);
55445
55744
  program.command("why <location>").description("Explain why a rule fired (or why a suppression didn't apply) at a file:line").option("--project <name>", "select projects: workspace names or directory paths (comma-separated for multiple)").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action((location, options) => whyAction(location, options));
55446
55745
  program.command("install").alias("setup").description("Install the react-doctor skill into your coding agents and optional git hook").option("-y, --yes", "skip prompts, install for all detected agents").option("--dry-run", "show what would be installed without writing files").option("--agent-hooks", "install native non-blocking agent hooks for Claude Code and Cursor").option("-c, --cwd <cwd>", "working directory", process.cwd()).option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderInstallHelpEpilog).action(installAction);
@@ -55465,6 +55764,7 @@ if (process.argv.includes("-V") && !strippedArgv.includes("-V")) {
55465
55764
  process.exit(0);
55466
55765
  }
55467
55766
  applyColorPreference(strippedArgv);
55767
+ ensureWindowsUtf8Console();
55468
55768
  const argv = normalizeHelpInvocation(strippedArgv, knownCommands);
55469
55769
  Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => program.parseAsync(argv)).then(() => flushSentry()).catch(async (error) => {
55470
55770
  const isUserError = isExpectedUserError(error);
@@ -55483,4 +55783,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
55483
55783
  export {};
55484
55784
 
55485
55785
  //# sourceMappingURL=cli.js.map
55486
- //# debugId=a201a872-19de-5f85-bec4-8ecc9f0ae2a8
55786
+ //# debugId=cf52ec59-cee0-5503-b8f2-a4f98bf77ea4