react-doctor 0.5.8-dev.ea00b1b → 0.5.8-dev.f69f216

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]="7e6ea254-9fcd-5076-8bd6-01ad63633c24")}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]="fabe3e34-b40f-514e-a5af-fa68a36365ae")}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;
@@ -38557,7 +38558,7 @@ const resolveScanConcurrency = (requested) => {
38557
38558
  if (!Number.isFinite(requested) || requested < 1) return 1;
38558
38559
  return Math.min(Math.floor(requested), 32);
38559
38560
  };
38560
- const readSystemFacts = () => ({
38561
+ const readSystemFacts$1 = () => ({
38561
38562
  availableCores: os.availableParallelism(),
38562
38563
  totalMemoryBytes: os.totalmem(),
38563
38564
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -38578,7 +38579,7 @@ const readSystemFacts = () => ({
38578
38579
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
38579
38580
  * limited, and ceiling cases without mocking `os` or the filesystem.
38580
38581
  */
38581
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
38582
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
38582
38583
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
38583
38584
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
38584
38585
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -40139,7 +40140,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
40139
40140
  }
40140
40141
  return true;
40141
40142
  };
40142
- const checkSecurityScan = (rootDirectory, options = {}) => {
40143
+ const yieldToEventLoop = () => new Promise((resolve) => {
40144
+ setImmediate(resolve);
40145
+ });
40146
+ const createSecurityScanSession = (rootDirectory, options) => {
40143
40147
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
40144
40148
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
40145
40149
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -40154,7 +40158,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
40154
40158
  committedFilesOnly: rule.committedFilesOnly === true
40155
40159
  }];
40156
40160
  });
40157
- if (enabledScanRules.length === 0) return [];
40161
+ if (enabledScanRules.length === 0) return null;
40158
40162
  const diagnostics = [];
40159
40163
  const seen = /* @__PURE__ */ new Set();
40160
40164
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -40166,15 +40170,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
40166
40170
  }
40167
40171
  return status === true;
40168
40172
  };
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);
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
+ }
40176
40199
  }
40177
- return diagnostics;
40200
+ return session.diagnostics;
40178
40201
  };
40179
40202
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
40180
40203
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -40407,6 +40430,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
40407
40430
  return [...seen].filter((pattern) => pattern.length > 0);
40408
40431
  };
40409
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
+ };
40410
40501
  /**
40411
40502
  * Resolves a path to its canonical, symlink-free form, falling back to
40412
40503
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -40697,14 +40788,17 @@ const checkDeadCode = async (options) => {
40697
40788
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
40698
40789
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
40699
40790
  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));
40791
+ const spawnAndRun = () => {
40792
+ return runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40793
+ rootDirectory,
40794
+ entryPatterns,
40795
+ tsConfigPath: resolveTsConfigPath(rootDirectory),
40796
+ ignorePatterns,
40797
+ deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
40798
+ parseConcurrency: options.parseConcurrency
40799
+ }), options.workerTimeoutMs ?? 12e4, options.abortSignal);
40800
+ };
40801
+ const result = parseDeadCodeWorkerResult(options.createWorker === void 0 ? await withDeadCodeWorkerSlot(spawnAndRun, options.abortSignal) : await spawnAndRun());
40708
40802
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
40709
40803
  const diagnostics = [];
40710
40804
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -43868,7 +43962,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
43868
43962
  * diagnostics).
43869
43963
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
43870
43964
  * 3. environment checks (reduced-motion + pnpm hardening +
43871
- * expo/react-native + security scan), collected synchronously
43965
+ * expo/react-native), collected synchronously. The heavier
43966
+ * content-regex security scan is forked instead (like supply-chain
43967
+ * below) and joined before the concat, so its CPU overlaps lint
43968
+ * rather than blocking the event loop before it.
43872
43969
  * 4. The supply-chain check (Socket.dev) is forked onto a background
43873
43970
  * fiber so its ~100% network-bound time overlaps the ~100%
43874
43971
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -43888,7 +43985,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
43888
43985
  * order, so terminal output is identical either way; supply-chain
43889
43986
  * rides alongside without a spinner.
43890
43987
  * 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
43988
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
43892
43989
  * byte-identical regardless of which fiber settled first. The
43893
43990
  * viewer-permission fiber is joined later, during score-metadata
43894
43991
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -43949,12 +44046,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
43949
44046
  ...checkPnpmHardening(scanDirectory),
43950
44047
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
43951
44048
  ...checkExpoProject(scanDirectory, project),
43952
- ...checkReactNativeProject(scanDirectory, project),
43953
- ...checkSecurityScan(scanDirectory, {
43954
- project,
43955
- ignoredTags: input.ignoredTags
43956
- })
44049
+ ...checkReactNativeProject(scanDirectory, project)
43957
44050
  ])));
44051
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
44052
+ project,
44053
+ ignoredTags: input.ignoredTags
44054
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
43958
44055
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
43959
44056
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
43960
44057
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -44090,9 +44187,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44090
44187
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
44091
44188
  const supplyChainResult = yield* join(supplyChainFiber);
44092
44189
  const supplyChainCollected = supplyChainResult.diagnostics;
44190
+ const securityScanCollected = yield* join(securityScanFiber);
44093
44191
  yield* reporterService.finalize;
44094
44192
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
44095
44193
  ...envCollected,
44194
+ ...securityScanCollected,
44096
44195
  ...supplyChainCollected,
44097
44196
  ...lintCollected,
44098
44197
  ...deadCodeCollected
@@ -44631,6 +44730,61 @@ const computeConfigFingerprint = (projectDirectory, version) => {
44631
44730
  }
44632
44731
  return crypto.createHash("sha1").update(parts.join("|")).digest("hex");
44633
44732
  };
44733
+ const AI_TRAINING_ENV_VARS = [
44734
+ ["HF_DATASETS_CACHE", "huggingface"],
44735
+ ["HF_HOME", "huggingface"],
44736
+ ["HUGGINGFACE_HUB_CACHE", "huggingface"],
44737
+ ["SPACE_ID", "huggingface-spaces"],
44738
+ ["CUDA_VISIBLE_DEVICES", "cuda"],
44739
+ ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
44740
+ ["ROCR_VISIBLE_DEVICES", "rocm"],
44741
+ ["TPU_NAME", "google-tpu"],
44742
+ ["WANDB_RUN_ID", "wandb"],
44743
+ ["WANDB_SWEEP_ID", "wandb"],
44744
+ ["MLFLOW_RUN_ID", "mlflow"],
44745
+ ["MLFLOW_TRACKING_URI", "mlflow"],
44746
+ ["COMET_EXPERIMENT_KEY", "comet"],
44747
+ ["NEPTUNE_RUN_ID", "neptune"],
44748
+ ["CLEARML_TASK_ID", "clearml"],
44749
+ ["DVC_STAGE", "dvc"],
44750
+ ["RAY_WORKER_PROCESS", "ray"],
44751
+ ["RAY_ADDRESS", "ray"],
44752
+ ["MUJOCO_GL", "mujoco"],
44753
+ ["MUJOCO_PATH", "mujoco"],
44754
+ ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
44755
+ ["SM_TRAINING_ENV", "sagemaker"],
44756
+ ["TRAINING_JOB_ARN", "sagemaker"],
44757
+ ["SAGEMAKER_BASE_DIR", "sagemaker"],
44758
+ ["AZUREML_RUN_ID", "azure-ml"],
44759
+ ["AZURE_ML_MODEL_DIR", "azure-ml"],
44760
+ ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
44761
+ ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
44762
+ ["DET_MASTER", "determined-ai"],
44763
+ ["LIGHTNING_USER_ID", "lightning-ai"],
44764
+ ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
44765
+ ["ARGO_WORKFLOW_NAME", "argo-workflows"],
44766
+ ["KFP_POD_NAME", "kubeflow-pipelines"],
44767
+ ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
44768
+ ["COLAB_BACKEND_VERSION", "google-colab"],
44769
+ ["DATABRICKS_RUNTIME_VERSION", "databricks"],
44770
+ ["DAYTONA_WS_ID", "daytona"],
44771
+ ["DAYTONA_WS_NAME", "daytona"],
44772
+ ["E2B_SANDBOX_ID", "e2b"],
44773
+ ["MODAL_FUNCTION_ID", "modal"],
44774
+ ["MODAL_TASK_ID", "modal"],
44775
+ ["RUNPOD_POD_ID", "runpod"],
44776
+ ["REPLICATE_USERNAME", "replicate"],
44777
+ ["VAST_CONTAINERLABEL", "vast-ai"],
44778
+ ["HARBOR_URL", "harbor"],
44779
+ ["HARBOR_HOSTNAME", "harbor"],
44780
+ ["SWE_BENCH_TASK", "swe-bench"],
44781
+ ["SWEBENCH_TASK", "swe-bench"],
44782
+ ["SWE_AGENT_MODEL", "swe-agent"]
44783
+ ];
44784
+ const detectAiTrainingEnvironment = () => {
44785
+ for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
44786
+ return null;
44787
+ };
44634
44788
  const groupBy = (items, keyFn) => {
44635
44789
  const groups = /* @__PURE__ */ new Map();
44636
44790
  for (const item of items) {
@@ -44716,7 +44870,25 @@ const CI_PROVIDER_BY_ENVIRONMENT_VARIABLE = [
44716
44870
  ["TEAMCITY_VERSION", "teamcity"],
44717
44871
  ["BITBUCKET_BUILD_NUMBER", "bitbucket"],
44718
44872
  ["TRAVIS", "travis"],
44719
- ["DRONE", "drone"]
44873
+ ["DRONE", "drone"],
44874
+ ["SEMAPHORE", "semaphore"],
44875
+ ["APPVEYOR", "appveyor"],
44876
+ ["HARNESS_BUILD_ID", "harness"],
44877
+ ["BUDDY", "buddy"],
44878
+ ["CF_BUILD_ID", "codefresh"],
44879
+ ["NETLIFY", "netlify"],
44880
+ ["RAILWAY_SERVICE_ID", "railway"],
44881
+ ["VERCEL", "vercel"],
44882
+ ["CM_BUILD_ID", "codemagic"],
44883
+ ["PROW_JOB_ID", "prow"],
44884
+ ["AGOLA_GIT_REF", "agola"],
44885
+ ["CIRRUS_CI", "cirrus-ci"],
44886
+ ["BLACKSMITH_STICKYDISK_TOKEN", "blacksmith"],
44887
+ ["WARPBUILD_RUNNER_VERIFICATION_TOKEN", "warpbuild"],
44888
+ ["NSC_CACHE_PATH", "namespace"],
44889
+ ["UBICLOUD_CACHE_URL", "ubicloud"],
44890
+ ["RENDER", "render"],
44891
+ ["FLY_APP_NAME", "fly-io"]
44720
44892
  ];
44721
44893
  const GITHUB_ACTION_MARKER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_GITHUB_ACTION";
44722
44894
  const ACTION_INPUT_ENVIRONMENT_VARIABLES = {
@@ -44734,7 +44906,10 @@ const CODING_AGENT_BY_ENVIRONMENT_VARIABLE = [
44734
44906
  ["CODEX_SANDBOX_NETWORK_DISABLED", "codex"],
44735
44907
  ["OPENCODE", "opencode"],
44736
44908
  ["GOOSE_TERMINAL", "goose"],
44737
- ["AMP_THREAD_ID", "amp"]
44909
+ ["AMP_THREAD_ID", "amp"],
44910
+ ["CLINE_ACTIVE", "cline"],
44911
+ ["AUGMENT_AGENT", "augment"],
44912
+ ["TRAE_AI_SHELL_ID", "trae-ai"]
44738
44913
  ];
44739
44914
  const GENERIC_CODING_AGENT_ENVIRONMENT_VARIABLES = ["AGENT_SESSION_ID", "AGENT_THREAD_ID"];
44740
44915
  const CODING_AGENT_ENVIRONMENT_VALUE_VARIABLES = ["AGENT"];
@@ -44883,7 +45058,9 @@ const METRIC = {
44883
45058
  lspSessionStarted: "lsp.session.started",
44884
45059
  lspScanCompleted: "lsp.scan.completed",
44885
45060
  lspScanDuration: "lsp.scan.duration",
44886
- lspScanDiagnostics: "lsp.scan.diagnostics"
45061
+ lspScanDiagnostics: "lsp.scan.diagnostics",
45062
+ aiTrainingWarningShown: "ai.training.warning_shown",
45063
+ jsonOutUsed: "json.out_used"
44887
45064
  };
44888
45065
  //#endregion
44889
45066
  //#region src/cli/utils/noop-console.ts
@@ -44926,7 +45103,7 @@ const makeNoopConsole = () => ({
44926
45103
  });
44927
45104
  //#endregion
44928
45105
  //#region src/cli/utils/version.ts
44929
- const VERSION = "0.5.8-dev.ea00b1b";
45106
+ const VERSION = "0.5.8-dev.f69f216";
44930
45107
  //#endregion
44931
45108
  //#region src/cli/utils/json-mode.ts
44932
45109
  let context = null;
@@ -44964,12 +45141,13 @@ const installSilentConsole = () => {
44964
45141
  "trace"
44965
45142
  ]) target[key] = source[key];
44966
45143
  };
44967
- const enableJsonMode = ({ compact, directory }) => {
45144
+ const enableJsonMode = ({ compact, directory, outputFile }) => {
44968
45145
  context = {
44969
45146
  compact,
44970
45147
  directory,
44971
45148
  startTime: performance$1.now(),
44972
- mode: "full"
45149
+ mode: "full",
45150
+ outputFile: outputFile ?? null
44973
45151
  };
44974
45152
  installSilentConsole();
44975
45153
  };
@@ -44982,7 +45160,11 @@ const setJsonReportMode = (mode) => {
44982
45160
  };
44983
45161
  const writeJsonReport = (report) => {
44984
45162
  const serialized = context?.compact ? JSON.stringify(report) : JSON.stringify(report, null, 2);
44985
- process.stdout.write(`${serialized}\n`);
45163
+ if (context?.outputFile) {
45164
+ const resolvedPath = Path.resolve(context.outputFile);
45165
+ NFS.mkdirSync(Path.dirname(resolvedPath), { recursive: true });
45166
+ NFS.writeFileSync(resolvedPath, `${serialized}\n`);
45167
+ } else process.stdout.write(`${serialized}\n`);
44986
45168
  };
44987
45169
  const writeJsonErrorReport = (error, sentryEventId) => {
44988
45170
  if (!context) return;
@@ -45290,13 +45472,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45290
45472
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45291
45473
  * standard `SENTRY_RELEASE` override.
45292
45474
  */
45293
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.ea00b1b`;
45475
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.f69f216`;
45294
45476
  /**
45295
45477
  * Deployment environment shown in Sentry's environment filter. Defaults to
45296
45478
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45297
45479
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45298
45480
  */
45299
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.ea00b1b") ? "development" : "production");
45481
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.f69f216") ? "development" : "production");
45300
45482
  /**
45301
45483
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45302
45484
  * (set to `0` to disable tracing) and falls back to
@@ -48892,6 +49074,25 @@ const shouldBlockCi = (diagnostics, blockingLevel) => {
48892
49074
  */
48893
49075
  const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
48894
49076
  //#endregion
49077
+ //#region src/cli/utils/with-namespace.ts
49078
+ /**
49079
+ * Prefixes every key in a flat attribute group with `namespace.`, so a logical
49080
+ * group (scan config, diagnostics rollup, score, …) carries one dotted
49081
+ * namespace applied in a single place rather than hand-spelled into each key —
49082
+ * which is how the wide event drifted into a flat, half-namespaced soup. The
49083
+ * dotted prefix is what makes the attributes tree up in Sentry's attribute
49084
+ * browser and stay group-/filter-/aggregate-able in the Spans dataset.
49085
+ *
49086
+ * Value types are preserved verbatim (numbers stay numbers so Sentry infers a
49087
+ * numeric attribute and `p75(...)`/`avg(...)` work; `null` is kept so
49088
+ * `toSpanAttributes` can drop absent signals rather than coerce them).
49089
+ */
49090
+ const withNamespace = (namespace, attributes) => {
49091
+ const namespaced = {};
49092
+ for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
49093
+ return namespaced;
49094
+ };
49095
+ //#endregion
48895
49096
  //#region src/cli/utils/build-run-event.ts
48896
49097
  const readEnvBoolean = (name) => {
48897
49098
  const value = process.env[name];
@@ -48913,12 +49114,12 @@ const buildOutcomeAttributes = (input) => {
48913
49114
  if (input.result === void 0) {
48914
49115
  const error = input.error;
48915
49116
  const known = isReactDoctorError(error);
48916
- return {
48917
- outcome: "error",
49117
+ return withNamespace("outcome", {
49118
+ status: "error",
48918
49119
  exitCode: 1,
48919
49120
  knownError: known,
48920
49121
  errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
48921
- };
49122
+ });
48922
49123
  }
48923
49124
  const result = input.result;
48924
49125
  const summary = summarizeDiagnostics(result.diagnostics);
@@ -48952,66 +49153,82 @@ const buildOutcomeAttributes = (input) => {
48952
49153
  }
48953
49154
  let fixGroupedFindings = 0;
48954
49155
  for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
49156
+ const categoryRollup = {};
49157
+ for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
48955
49158
  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;
49159
+ ...withNamespace("outcome", {
49160
+ status: outcome,
49161
+ exitCode: wouldBlock ? 1 : 0,
49162
+ wouldBlock,
49163
+ blocking: blockingLevel,
49164
+ clean: isClean,
49165
+ skippedChecks: result.skippedChecks.length
49166
+ }),
49167
+ ...withNamespace("diag", {
49168
+ total: summary.totalDiagnosticCount,
49169
+ errors: summary.errorCount,
49170
+ warnings: summary.warningCount,
49171
+ affectedFiles: summary.affectedFileCount,
49172
+ inTestFiles: diagnosticsInTestFiles,
49173
+ inStoryFiles: diagnosticsInStoryFiles,
49174
+ distinctRules: countByRule.size,
49175
+ topRule,
49176
+ fixGroups: findingsPerFixGroup.size,
49177
+ fixGroupedFindings,
49178
+ ...categoryRollup
49179
+ }),
49180
+ ...withNamespace("score", {
49181
+ value: result.score ? result.score.score : null,
49182
+ label: result.score ? result.score.label : null,
49183
+ available: result.score !== null
49184
+ }),
49185
+ ...withNamespace("lint", {
49186
+ failed: input.didLintFail ?? null,
49187
+ failureReasonKind: input.lintFailureReasonKind ?? null,
49188
+ partialFailureCount: input.lintPartialFailureCount ?? null,
49189
+ droppedFileCount: input.lintDroppedFileCount ?? null,
49190
+ cacheHitFiles: result.lintCacheHitFileCount ?? null,
49191
+ cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
49192
+ cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
49193
+ }),
49194
+ ...withNamespace("deadCode", {
49195
+ failed: input.didDeadCodeFail ?? null,
49196
+ overlapped: input.deadCodeOverlapped ?? null
49197
+ }),
49198
+ ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
49199
+ ...withNamespace("timing", {
49200
+ elapsedMs: result.elapsedMilliseconds,
49201
+ scanMs: result.scanElapsedMilliseconds ?? null
49202
+ }),
49203
+ ...withNamespace("migration", {
49204
+ largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
49205
+ largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
49206
+ largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
49207
+ })
49208
+ };
49209
+ if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
49210
+ new: summary.totalDiagnosticCount,
49211
+ fixed: result.baselineDelta.fixedCount,
49212
+ baseTotal: result.baselineDelta.baseTotalCount,
49213
+ degraded: false
49214
+ }));
49215
+ else if (input.gateExempt) attributes["baseline.degraded"] = true;
48999
49216
  return attributes;
49000
49217
  };
49001
- const buildCiAttributes = () => {
49218
+ const buildActionAttributes = () => {
49002
49219
  const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
49003
- return {
49220
+ return withNamespace("action", {
49004
49221
  actorAssociation: githubActorAssociation ?? null,
49005
49222
  runnerOs: detectRunnerOs(),
49006
49223
  comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
49007
49224
  reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
49008
49225
  versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
49009
- };
49226
+ });
49010
49227
  };
49011
- const buildConfigAttributes = (input) => {
49228
+ const buildScanAttributes = (input) => {
49012
49229
  const ruleOverrides = input.userConfig?.rules ?? {};
49013
49230
  const ruleKeys = Object.keys(ruleOverrides);
49014
- return {
49231
+ return withNamespace("scan", {
49015
49232
  mode: input.mode,
49016
49233
  scope: input.scope,
49017
49234
  parallel: input.parallel,
@@ -49026,12 +49243,18 @@ const buildConfigAttributes = (input) => {
49026
49243
  ignoredTagCount: input.ignoredTagCount,
49027
49244
  hasCustomConfig: input.hasCustomConfig,
49028
49245
  rulesConfigured: ruleKeys.length,
49029
- rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length
49030
- };
49246
+ rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
49247
+ fileCount: input.result?.scannedFileCount ?? null
49248
+ });
49031
49249
  };
49032
49250
  /**
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
49251
+ * Projects a scan into the namespaced attribute set for its root span — the
49252
+ * canonical per-scan "wide event". Every attribute carries a dotted namespace
49253
+ * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
49254
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
49255
+ * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
49256
+ * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
49257
+ * in the Spans dataset. Pure and exported so the projection (outcome
49035
49258
  * precedence, rule/category rollups, CI knobs, config shape) is unit-testable
49036
49259
  * without a live Sentry client. `null` values are dropped so absent signals
49037
49260
  * never become misleading `"null"` attributes. The run + project base context
@@ -49040,8 +49263,8 @@ const buildConfigAttributes = (input) => {
49040
49263
  * those don't carry.
49041
49264
  */
49042
49265
  const buildRunEventAttributes = (input) => toSpanAttributes({
49043
- ...buildConfigAttributes(input),
49044
- ...buildCiAttributes(),
49266
+ ...buildScanAttributes(input),
49267
+ ...buildActionAttributes(),
49045
49268
  ...buildOutcomeAttributes(input)
49046
49269
  });
49047
49270
  /**
@@ -51089,7 +51312,8 @@ const getStagedSourceFiles = async (directory) => {
51089
51312
  return [...await runPromise(gen(function* () {
51090
51313
  return yield* (yield* StagedFiles).discoverSourceFiles(directory);
51091
51314
  }).pipe(provide(stagedFilesLayer)))];
51092
- } catch {
51315
+ } catch (error) {
51316
+ cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
51093
51317
  return [];
51094
51318
  }
51095
51319
  };
@@ -53034,6 +53258,26 @@ const CLI_AGENT_AUTO_FLAGS = {
53034
53258
  codex: ["--yolo"],
53035
53259
  cursor: ["--force"]
53036
53260
  };
53261
+ const resolveCursorBundledNode = () => {
53262
+ if (!isWindows) return null;
53263
+ const localAppData = process.env.LOCALAPPDATA;
53264
+ if (!localAppData) return null;
53265
+ const cursorAgentRoot = Path.join(localAppData, "cursor-agent", "versions");
53266
+ try {
53267
+ const versions = NFS.readdirSync(cursorAgentRoot);
53268
+ if (versions.length === 0) return null;
53269
+ const sortedVersions = versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
53270
+ for (const version of sortedVersions) try {
53271
+ const bundledNodePath = Path.join(cursorAgentRoot, version, "node.exe");
53272
+ const entryScriptPath = Path.join(cursorAgentRoot, version, "index.js");
53273
+ if (NFS.statSync(bundledNodePath).isFile() && NFS.statSync(entryScriptPath).isFile()) return {
53274
+ command: bundledNodePath,
53275
+ args: [entryScriptPath]
53276
+ };
53277
+ } catch {}
53278
+ } catch {}
53279
+ return null;
53280
+ };
53037
53281
  const resolveWindowsCmdEntryScript = (command) => {
53038
53282
  if (!isWindows) return null;
53039
53283
  const pathDirectories = (process.env.PATH ?? "").split(Path.delimiter).filter(Boolean);
@@ -53053,11 +53297,10 @@ const resolveWindowsCmdEntryScript = (command) => {
53053
53297
  }
53054
53298
  return null;
53055
53299
  };
53056
- const spawnAgent = (command, args, cwd, shell = false) => new Promise((resolve, reject) => {
53300
+ const spawnAgent = (command, args, cwd) => new Promise((resolve, reject) => {
53057
53301
  const child = spawn(command, [...args], {
53058
53302
  cwd,
53059
- stdio: "inherit",
53060
- shell
53303
+ stdio: "inherit"
53061
53304
  });
53062
53305
  child.on("error", reject);
53063
53306
  child.on("close", (code) => resolve(code ?? 0));
@@ -53066,6 +53309,10 @@ const launchCliAgent = async (agentId, prompt, cwd) => {
53066
53309
  const binary = CLI_AGENT_BINARIES[agentId];
53067
53310
  const agentArgs = [...CLI_AGENT_AUTO_FLAGS[agentId], prompt];
53068
53311
  if (isWindows) {
53312
+ if (agentId === "cursor") {
53313
+ const cursorResolution = resolveCursorBundledNode();
53314
+ if (cursorResolution) return spawnAgent(cursorResolution.command, [...cursorResolution.args, ...agentArgs], cwd);
53315
+ }
53069
53316
  const entryScript = resolveWindowsCmdEntryScript(binary);
53070
53317
  if (entryScript) return spawnAgent(process.execPath, [entryScript, ...agentArgs], cwd);
53071
53318
  }
@@ -53976,6 +54223,7 @@ const resolveRequestedProjects = (requestedNames, workspacePackages, rootDirecto
53976
54223
  return requestedNames.map((requestedName) => {
53977
54224
  const matched = workspacePackages.find((workspacePackage) => workspacePackage.name === requestedName || Path.basename(workspacePackage.directory) === requestedName);
53978
54225
  if (matched) return matched.directory;
54226
+ if (Path.basename(rootDirectory) === requestedName) return rootDirectory;
53979
54227
  const candidateDirectory = Path.resolve(rootDirectory, requestedName);
53980
54228
  if (isDirectory(candidateDirectory)) {
53981
54229
  recordCount(METRIC.projectPathSelected);
@@ -54083,6 +54331,17 @@ const warnDeprecatedFailOn = (flags, userConfig) => {
54083
54331
  cliLogger.warn(`${source} is deprecated; rename it to ${replacement}.`);
54084
54332
  };
54085
54333
  //#endregion
54334
+ //#region src/cli/utils/warn-ai-training-environment.ts
54335
+ let didWarnAiTraining = false;
54336
+ const warnIfAiTrainingEnvironment = () => {
54337
+ if (didWarnAiTraining) return;
54338
+ const detected = detectAiTrainingEnvironment();
54339
+ if (detected === null) return;
54340
+ didWarnAiTraining = true;
54341
+ 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`);
54342
+ recordCount(METRIC.aiTrainingWarningShown, 1, { environment: detected });
54343
+ };
54344
+ //#endregion
54086
54345
  //#region src/cli/utils/validate-mode-flags.ts
54087
54346
  const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
54088
54347
  const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
@@ -54166,10 +54425,14 @@ const inspectAction = async (directory, flags) => {
54166
54425
  const isQuiet = isScoreOnly || isJsonMode;
54167
54426
  const requestedDirectory = Path.resolve(directory);
54168
54427
  const startTime = performance$1.now();
54169
- if (isJsonMode) enableJsonMode({
54170
- compact: Boolean(flags.jsonCompact),
54171
- directory: requestedDirectory
54172
- });
54428
+ if (isJsonMode) {
54429
+ enableJsonMode({
54430
+ compact: Boolean(flags.jsonCompact),
54431
+ directory: requestedDirectory,
54432
+ outputFile: flags.jsonOut
54433
+ });
54434
+ if (flags.jsonOut) recordCount(METRIC.jsonOutUsed, 1);
54435
+ }
54173
54436
  recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
54174
54437
  try {
54175
54438
  validateModeFlags(flags);
@@ -54183,6 +54446,7 @@ const inspectAction = async (directory, flags) => {
54183
54446
  setJsonReportDirectory(resolvedDirectory);
54184
54447
  warnDeprecatedFailOn(flags, userConfig);
54185
54448
  warnDeprecatedDiff(flags, userConfig);
54449
+ warnIfAiTrainingEnvironment();
54186
54450
  if (scanTarget.didRedirectViaRootDir && !isQuiet) {
54187
54451
  cliLogger.dim(`Redirected to ${highlighter.info(toRelativePath(resolvedDirectory, requestedDirectory))} via react-doctor config "rootDir".`);
54188
54452
  cliLogger.break();
@@ -55248,6 +55512,7 @@ const ROOT_FLAG_SPEC = {
55248
55512
  "--category",
55249
55513
  "--changed-files-from",
55250
55514
  "--blocking",
55515
+ "--json-out",
55251
55516
  "--output-dir",
55252
55517
  "--fail-on",
55253
55518
  "--project",
@@ -55438,7 +55703,7 @@ ${highlighter.dim("Learn more:")}
55438
55703
  ${highlighter.info(CANONICAL_GITHUB_URL)}
55439
55704
  `;
55440
55705
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
55441
- 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);
55706
+ 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);
55442
55707
  program.action(inspectAction);
55443
55708
  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));
55444
55709
  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);
@@ -55481,4 +55746,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
55481
55746
  export {};
55482
55747
 
55483
55748
  //# sourceMappingURL=cli.js.map
55484
- //# debugId=7e6ea254-9fcd-5076-8bd6-01ad63633c24
55749
+ //# debugId=fabe3e34-b40f-514e-a5af-fa68a36365ae