react-doctor 0.5.8-dev.9f28454 → 0.5.8-dev.b69f4a7

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]="34e4ad65-2650-5e3e-afd8-191931eb2a47")}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]="bd31e7dc-cfca-56b2-a963-9d798b3cbbe5")}catch(e){}}();
3
3
  import { createRequire } from "node:module";
4
4
  import * as NodeChildProcess from "node:child_process";
5
5
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
@@ -26,7 +26,7 @@ import tty from "node:tty";
26
26
  import { codeFrameColumns } from "@babel/code-frame";
27
27
  import Conf from "conf";
28
28
  import basePrompts from "prompts";
29
- import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource } from "agent-install";
29
+ import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource, isSkillAgentType } from "agent-install";
30
30
  import { generateCode, loadFile, writeFile } from "magicast";
31
31
  import { getConfigFromVariableDeclaration, getDefaultExportOptions } from "magicast/helpers";
32
32
  //#region \0rolldown/runtime.js
@@ -9041,7 +9041,7 @@ const composePassthrough = /* @__PURE__ */ dual(2, (left, right) => (input) => {
9041
9041
  * @since 2.0.0
9042
9042
  */
9043
9043
  const Scheduler = /* @__PURE__ */ Reference("effect/Scheduler", { defaultValue: () => new MixedScheduler() });
9044
- const setImmediate = "setImmediate" in globalThis ? (f) => {
9044
+ const setImmediate$1 = "setImmediate" in globalThis ? (f) => {
9045
9045
  const timer = globalThis.setImmediate(f);
9046
9046
  return () => globalThis.clearImmediate(timer);
9047
9047
  } : (f) => {
@@ -9085,7 +9085,7 @@ var PriorityBuckets = class {
9085
9085
  var MixedScheduler = class {
9086
9086
  executionMode;
9087
9087
  setImmediate;
9088
- constructor(executionMode = "async", setImmediateFn = setImmediate) {
9088
+ constructor(executionMode = "async", setImmediateFn = setImmediate$1) {
9089
9089
  this.executionMode = executionMode;
9090
9090
  this.setImmediate = setImmediateFn;
9091
9091
  }
@@ -9110,7 +9110,7 @@ var MixedSchedulerDispatcher = class {
9110
9110
  tasks = /* @__PURE__ */ new PriorityBuckets();
9111
9111
  running = void 0;
9112
9112
  setImmediate;
9113
- constructor(setImmediateFn = setImmediate) {
9113
+ constructor(setImmediateFn = setImmediate$1) {
9114
9114
  this.setImmediate = setImmediateFn;
9115
9115
  }
9116
9116
  /**
@@ -36862,6 +36862,7 @@ const DOCS_URL = "https://react.doctor/docs";
36862
36862
  const DOCS_RULES_BASE_URL = `${DOCS_URL}/rules`;
36863
36863
  const FETCH_TIMEOUT_MS = 1e4;
36864
36864
  const GITHUB_VIEWER_PERMISSION_TIMEOUT_MS = 2e3;
36865
+ const SPAWN_ARGS_MAX_LENGTH_CHARS = 24e3;
36865
36866
  const PER_WORKER_MEM_BUDGET_BYTES = 1024 * 1024 * 1024;
36866
36867
  const DEFAULT_BRANCH_CANDIDATES = ["main", "master"];
36867
36868
  const ADOPTABLE_LINT_CONFIG_FILENAMES = [".oxlintrc.json", ".eslintrc.json"];
@@ -36946,6 +36947,7 @@ const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
36946
36947
  const LINT_PHASE_TIMEOUT_MS = 3e5;
36947
36948
  const SCAN_TOTAL_DEADLINE_MS = 9e5;
36948
36949
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
36950
+ const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
36949
36951
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
36950
36952
  const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
36951
36953
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
@@ -37679,7 +37681,10 @@ for (const [legacyRuleKey, nativeRuleKey] of Object.entries(LEGACY_RULE_KEY_TO_N
37679
37681
  NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.set(nativeRuleKey, aliases);
37680
37682
  }
37681
37683
  const getLegacyRuleKeysForNative = (ruleKey) => NATIVE_RULE_KEY_TO_LEGACY_RULE_KEYS.get(ruleKey) ?? [];
37682
- const canonicalizeRuleKey = (ruleKey) => LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey] ?? ruleKey;
37684
+ const canonicalizeRuleKey = (ruleKey) => {
37685
+ const nativeRuleKey = LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY[ruleKey];
37686
+ return typeof nativeRuleKey === "string" ? nativeRuleKey : ruleKey;
37687
+ };
37683
37688
  const isReactDoctorShortIdOf = (bareRuleKey, qualifiedRuleKey) => !bareRuleKey.includes("/") && qualifiedRuleKey === `react-doctor/${bareRuleKey}`;
37684
37689
  const isSameRuleKey = (candidateRuleKey, targetRuleKey) => {
37685
37690
  const canonicalCandidate = canonicalizeRuleKey(candidateRuleKey);
@@ -38553,7 +38558,7 @@ const resolveScanConcurrency = (requested) => {
38553
38558
  if (!Number.isFinite(requested) || requested < 1) return 1;
38554
38559
  return Math.min(Math.floor(requested), 32);
38555
38560
  };
38556
- const readSystemFacts = () => ({
38561
+ const readSystemFacts$1 = () => ({
38557
38562
  availableCores: os.availableParallelism(),
38558
38563
  totalMemoryBytes: os.totalmem(),
38559
38564
  cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
@@ -38574,7 +38579,7 @@ const readSystemFacts = () => ({
38574
38579
  * `facts` is injectable so tests exercise core-bound, memory-bound, cgroup-
38575
38580
  * limited, and ceiling cases without mocking `os` or the filesystem.
38576
38581
  */
38577
- const resolveAutoScanConcurrency = (facts = readSystemFacts()) => {
38582
+ const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
38578
38583
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
38579
38584
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
38580
38585
  return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
@@ -38765,6 +38770,12 @@ const BOOLEAN_FIELD_NAMES = [
38765
38770
  "adoptExistingLintConfig"
38766
38771
  ];
38767
38772
  const STRING_FIELD_NAMES = ["rootDir"];
38773
+ const STRING_ARRAY_FIELD_NAMES = [
38774
+ "projects",
38775
+ "textComponents",
38776
+ "rawTextWrapperComponents",
38777
+ "serverAuthFunctionNames"
38778
+ ];
38768
38779
  const SURFACE_CONTROL_FIELD_NAMES = [
38769
38780
  "includeTags",
38770
38781
  "excludeTags",
@@ -38866,6 +38877,7 @@ const validateConfigTypes = (config) => {
38866
38877
  const validated = { ...config };
38867
38878
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
38868
38879
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
38880
+ for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
38869
38881
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
38870
38882
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
38871
38883
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -40128,7 +40140,10 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledBy)
40128
40140
  }
40129
40141
  return true;
40130
40142
  };
40131
- const checkSecurityScan = (rootDirectory, options = {}) => {
40143
+ const yieldToEventLoop = () => new Promise((resolve) => {
40144
+ setImmediate(resolve);
40145
+ });
40146
+ const createSecurityScanSession = (rootDirectory, options) => {
40132
40147
  const capabilities = options.project ? buildCapabilities(options.project) : /* @__PURE__ */ new Set();
40133
40148
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
40134
40149
  const enabledScanRules = REACT_DOCTOR_RULES.flatMap((entry) => {
@@ -40143,7 +40158,7 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
40143
40158
  committedFilesOnly: rule.committedFilesOnly === true
40144
40159
  }];
40145
40160
  });
40146
- if (enabledScanRules.length === 0) return [];
40161
+ if (enabledScanRules.length === 0) return null;
40147
40162
  const diagnostics = [];
40148
40163
  const seen = /* @__PURE__ */ new Set();
40149
40164
  const gitIgnoredCache = /* @__PURE__ */ new Map();
@@ -40155,15 +40170,34 @@ const checkSecurityScan = (rootDirectory, options = {}) => {
40155
40170
  }
40156
40171
  return status === true;
40157
40172
  };
40158
- for (const file of collectSecurityScanFiles(rootDirectory)) for (const { entry, scan, committedFilesOnly } of enabledScanRules) for (const finding of scan(file)) {
40159
- if (committedFilesOnly && isFileGitIgnored(file)) continue;
40160
- const diagnostic = buildSecurityScanDiagnostic(finding, entry, file.relativePath);
40161
- const key = `${diagnostic.rule}:${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}:${diagnostic.message}`;
40162
- if (seen.has(key)) continue;
40163
- seen.add(key);
40164
- 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
+ }
40165
40199
  }
40166
- return diagnostics;
40200
+ return session.diagnostics;
40167
40201
  };
40168
40202
  var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
40169
40203
  let p = process || {}, argv = p.argv || [], env = p.env || {};
@@ -40396,6 +40430,74 @@ const collectDeadCodeIgnorePatterns = (rootDirectory) => {
40396
40430
  return [...seen].filter((pattern) => pattern.length > 0);
40397
40431
  };
40398
40432
  const collectDeadCodeEntryPatterns = (rootDirectory) => [...new Set(collectKnipPatterns(rootDirectory, "entry"))].filter((pattern) => pattern.length > 0);
40433
+ const readSystemFacts = () => ({
40434
+ availableCores: os.availableParallelism(),
40435
+ totalMemoryBytes: os.totalmem(),
40436
+ cgroupMemoryLimitBytes: readCgroupMemoryLimitBytes()
40437
+ });
40438
+ /**
40439
+ * How many real deslop dead-code child processes may run at once, across the
40440
+ * concurrent per-project `runInspect` fibers of one CLI run. The cap is the
40441
+ * smaller of the core count and the number of `DEAD_CODE_WORKER_MEM_BUDGET_BYTES`
40442
+ * workers that fit in available memory, floored at 1.
40443
+ *
40444
+ * On a roomy dev box / CI runner this resolves high enough that every
40445
+ * concurrently-scanned project still spawns its own worker (no serialization vs
40446
+ * the prior uncapped behavior); on a memory-constrained runner it collapses
40447
+ * toward 1, so the `withDeadCodeWorkerSlot` semaphore serializes the spawns
40448
+ * instead of oversubscribing memory with N simultaneous children — the global
40449
+ * cap the per-project spawn path lacked.
40450
+ *
40451
+ * Mirrors `resolveAutoScanConcurrency` (lint), but budgets memory per the
40452
+ * heavier dead-code worker. `facts` is injectable for tests.
40453
+ */
40454
+ const resolveDeadCodeConcurrency = (facts = readSystemFacts()) => {
40455
+ const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
40456
+ const memoryBoundedWorkers = Math.floor(availableMemoryBytes / DEAD_CODE_WORKER_MEM_BUDGET_BYTES);
40457
+ return Math.max(1, Math.min(facts.availableCores, memoryBoundedWorkers));
40458
+ };
40459
+ let availableSlots = -1;
40460
+ const waiters = [];
40461
+ const releaseSlot = () => {
40462
+ const nextWaiter = waiters.shift();
40463
+ if (nextWaiter !== void 0) nextWaiter();
40464
+ else availableSlots += 1;
40465
+ };
40466
+ /**
40467
+ * Runs `task` once a dead-code worker slot is free, releasing the slot when the
40468
+ * task settles (success or failure). With a high cap (roomy machine) every
40469
+ * caller proceeds immediately; with a low cap (constrained runner) callers
40470
+ * queue and run as slots free.
40471
+ *
40472
+ * `abortSignal` short-circuits the WAIT: if it's already aborted, or fires while
40473
+ * this caller is queued, the call rejects without acquiring a slot or running
40474
+ * `task` — so a cancelled scan (e.g. lint failed) doesn't sit in the queue and
40475
+ * then spawn a child only to tear it down. A queued caller that aborts removes
40476
+ * its own waiter so a later release never hands a slot to a dead request.
40477
+ */
40478
+ const withDeadCodeWorkerSlot = async (task, abortSignal) => {
40479
+ if (abortSignal?.aborted) throw new Error("Dead-code worker aborted.");
40480
+ if (availableSlots < 0) availableSlots = resolveDeadCodeConcurrency();
40481
+ if (availableSlots > 0) availableSlots -= 1;
40482
+ else await new Promise((resolve, reject) => {
40483
+ const waiter = () => {
40484
+ abortSignal?.removeEventListener("abort", onAbort);
40485
+ resolve();
40486
+ };
40487
+ const onAbort = () => {
40488
+ const queuedIndex = waiters.indexOf(waiter);
40489
+ if (queuedIndex !== -1) waiters.splice(queuedIndex, 1);
40490
+ reject(/* @__PURE__ */ new Error("Dead-code worker aborted."));
40491
+ };
40492
+ waiters.push(waiter);
40493
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
40494
+ });
40495
+ try {
40496
+ return await task();
40497
+ } finally {
40498
+ releaseSlot();
40499
+ }
40500
+ };
40399
40501
  /**
40400
40502
  * Resolves a path to its canonical, symlink-free form, falling back to
40401
40503
  * the input when it cannot be realpath'd (broken symlink, permission
@@ -40686,14 +40788,17 @@ const checkDeadCode = async (options) => {
40686
40788
  if (!NFS.existsSync(Path.join(rootDirectory, "package.json"))) return [];
40687
40789
  const entryPatterns = collectDeadCodeEntryPatterns(rootDirectory);
40688
40790
  const ignorePatterns = collectDeadCodeIgnorePatterns(rootDirectory);
40689
- const result = parseDeadCodeWorkerResult(await runDeadCodeWorkerWithTimeout((options.createWorker ?? createDeadCodeWorker)({
40690
- rootDirectory,
40691
- entryPatterns,
40692
- tsConfigPath: resolveTsConfigPath(rootDirectory),
40693
- ignorePatterns,
40694
- deslopJsModuleSpecifier: options.deslopJsModuleSpecifier ?? import.meta.resolve("deslop-js"),
40695
- parseConcurrency: options.parseConcurrency
40696
- }), 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());
40697
40802
  const toRelative = (filePath) => toRelativeFilePath(rootDirectory, filePath);
40698
40803
  const diagnostics = [];
40699
40804
  for (const unusedFile of result.unusedFiles) diagnostics.push({
@@ -41094,43 +41199,46 @@ var Git = class Git extends Service()("react-doctor/Git") {
41094
41199
  * reason: GitInvocationFailed })` so the rest of the codebase
41095
41200
  * sees a single failure channel.
41096
41201
  */
41097
- const runCommand = (input) => scoped(gen(function* () {
41098
- const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
41099
- cwd: input.directory,
41100
- env: input.env,
41101
- extendEnv: true
41102
- }));
41103
- const maxStdoutBytes = input.maxStdoutBytes;
41104
- const stdoutByteCount = yield* make$13(0);
41105
- const [stdout, stderr, status] = yield* all([
41106
- mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
41107
- args: [...input.args],
41108
- directory: input.directory,
41109
- cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
41110
- }) })) : void_)))))),
41111
- mkString(decodeText(handle.stderr)),
41112
- handle.exitCode
41113
- ], { concurrency: 3 });
41114
- return {
41115
- status,
41116
- stdout,
41117
- stderr
41118
- };
41119
- })).pipe(catchTag$1("PlatformError", (cause) => {
41120
- if (input.command !== "git") return succeed$2({
41202
+ const runCommand = (input) => {
41203
+ const foldSpawnFailure = (cause) => input.command !== "git" ? succeed$2({
41121
41204
  status: 127,
41122
41205
  stdout: "",
41123
41206
  stderr: String(cause)
41124
- });
41125
- return new ReactDoctorError({ reason: new GitInvocationFailed({
41207
+ }) : fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
41126
41208
  args: [...input.args],
41127
41209
  directory: input.directory,
41128
41210
  cause
41129
- }) });
41130
- }), withSpan("git.exec", { attributes: {
41131
- "git.command": input.command,
41132
- "git.subcommand": input.args[0] ?? ""
41133
- } }));
41211
+ }) }));
41212
+ return scoped(gen(function* () {
41213
+ if (!isDirectory(input.directory)) return yield* foldSpawnFailure(`spawn ENOTDIR (cwd is not a directory: ${input.directory})`);
41214
+ const argvLengthChars = input.command.length + 1 + input.args.reduce((total, arg) => total + arg.length + 1, 0);
41215
+ if (argvLengthChars > 24e3) return yield* foldSpawnFailure(`spawn ENAMETOOLONG (${argvLengthChars} argv chars exceed ${SPAWN_ARGS_MAX_LENGTH_CHARS})`);
41216
+ const handle = yield* spawner.spawn(make$1(input.command, [...input.args], {
41217
+ cwd: input.directory,
41218
+ env: input.env,
41219
+ extendEnv: true
41220
+ }));
41221
+ const maxStdoutBytes = input.maxStdoutBytes;
41222
+ const stdoutByteCount = yield* make$13(0);
41223
+ const [stdout, stderr, status] = yield* all([
41224
+ mkString(decodeText(maxStdoutBytes === void 0 ? handle.stdout : handle.stdout.pipe(tap((chunk) => updateAndGet(stdoutByteCount, (total) => total + chunk.length).pipe(flatMap$2((total) => total > maxStdoutBytes ? fail$4(new ReactDoctorError({ reason: new GitInvocationFailed({
41225
+ args: [...input.args],
41226
+ directory: input.directory,
41227
+ cause: /* @__PURE__ */ new Error(`git stdout exceeded ${maxStdoutBytes} bytes`)
41228
+ }) })) : void_)))))),
41229
+ mkString(decodeText(handle.stderr)),
41230
+ handle.exitCode
41231
+ ], { concurrency: 3 });
41232
+ return {
41233
+ status,
41234
+ stdout,
41235
+ stderr
41236
+ };
41237
+ })).pipe(catchTag$1("PlatformError", foldSpawnFailure), withSpan("git.exec", { attributes: {
41238
+ "git.command": input.command,
41239
+ "git.subcommand": input.args[0] ?? ""
41240
+ } }));
41241
+ };
41134
41242
  const runGit = (directory, args) => runCommand({
41135
41243
  command: "git",
41136
41244
  args,
@@ -41163,7 +41271,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
41163
41271
  "rev-parse",
41164
41272
  "--verify",
41165
41273
  branch
41166
- ]).pipe(map$3((result) => result.status === 0));
41274
+ ]).pipe(map$3((result) => result.status === 0), catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(false) : fail$4(error)));
41167
41275
  const headSha = (directory) => runGit(directory, ["rev-parse", "HEAD"]).pipe(map$3((result) => result.status === 0 ? trimOrNull(result.stdout) : null));
41168
41276
  const mergeBase = (input) => isSafeGitRevision(input.ref) ? runGit(input.directory, [
41169
41277
  "merge-base",
@@ -41377,7 +41485,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
41377
41485
  ]);
41378
41486
  if (result.status !== 0) return null;
41379
41487
  return parseChangedLineRanges(result.stdout);
41380
- }).pipe(withSpan("Git.changedLineRanges"))
41488
+ }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
41381
41489
  });
41382
41490
  })).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
41383
41491
  /**
@@ -43854,7 +43962,10 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
43854
43962
  * diagnostics).
43855
43963
  * 2. beforeLint hook (e.g. CLI renders the project-detection block)
43856
43964
  * 3. environment checks (reduced-motion + pnpm hardening +
43857
- * expo/react-native + 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.
43858
43969
  * 4. The supply-chain check (Socket.dev) is forked onto a background
43859
43970
  * fiber so its ~100% network-bound time overlaps the ~100%
43860
43971
  * CPU/subprocess-bound lint pass below, collapsing two serial
@@ -43874,7 +43985,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
43874
43985
  * order, so terminal output is identical either way; supply-chain
43875
43986
  * rides alongside without a spinner.
43876
43987
  * 6. Join the supply-chain fiber, then assemble the diagnostics in a
43877
- * FIXED order (env, supply-chain, lint, dead-code) so the output is
43988
+ * FIXED order (env, security-scan, supply-chain, lint, dead-code) so the output is
43878
43989
  * byte-identical regardless of which fiber settled first. The
43879
43990
  * viewer-permission fiber is joined later, during score-metadata
43880
43991
  * assembly (it feeds score metadata, not diagnostics). The per-element
@@ -43935,12 +44046,12 @@ const runInspect = (input, hooks = {}) => gen(function* () {
43935
44046
  ...checkPnpmHardening(scanDirectory),
43936
44047
  ...checkReactServerComponentsAdvisory(scanDirectory, project),
43937
44048
  ...checkExpoProject(scanDirectory, project),
43938
- ...checkReactNativeProject(scanDirectory, project),
43939
- ...checkSecurityScan(scanDirectory, {
43940
- project,
43941
- ignoredTags: input.ignoredTags
43942
- })
44049
+ ...checkReactNativeProject(scanDirectory, project)
43943
44050
  ])));
44051
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(promise(() => checkSecurityScanCooperative(scanDirectory, {
44052
+ project,
44053
+ ignoredTags: input.ignoredTags
44054
+ })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)))))).pipe(withSpan("SecurityScan.run")));
43944
44055
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
43945
44056
  const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
43946
44057
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
@@ -44076,9 +44187,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
44076
44187
  else yield* scanProgress.succeed(`Scanned ${scannedFilesLabel} in ${scanElapsedSeconds}s${workerCountSuffix}`);
44077
44188
  const supplyChainResult = yield* join(supplyChainFiber);
44078
44189
  const supplyChainCollected = supplyChainResult.diagnostics;
44190
+ const securityScanCollected = yield* join(securityScanFiber);
44079
44191
  yield* reporterService.finalize;
44080
44192
  const finalDiagnostics = sortDiagnosticsStable(assignFixGroups([
44081
44193
  ...envCollected,
44194
+ ...securityScanCollected,
44082
44195
  ...supplyChainCollected,
44083
44196
  ...lintCollected,
44084
44197
  ...deadCodeCollected
@@ -44617,6 +44730,61 @@ const computeConfigFingerprint = (projectDirectory, version) => {
44617
44730
  }
44618
44731
  return crypto.createHash("sha1").update(parts.join("|")).digest("hex");
44619
44732
  };
44733
+ const AI_TRAINING_ENV_VARS = [
44734
+ ["HF_DATASETS_CACHE", "huggingface"],
44735
+ ["HF_HOME", "huggingface"],
44736
+ ["HUGGINGFACE_HUB_CACHE", "huggingface"],
44737
+ ["SPACE_ID", "huggingface-spaces"],
44738
+ ["CUDA_VISIBLE_DEVICES", "cuda"],
44739
+ ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
44740
+ ["ROCR_VISIBLE_DEVICES", "rocm"],
44741
+ ["TPU_NAME", "google-tpu"],
44742
+ ["WANDB_RUN_ID", "wandb"],
44743
+ ["WANDB_SWEEP_ID", "wandb"],
44744
+ ["MLFLOW_RUN_ID", "mlflow"],
44745
+ ["MLFLOW_TRACKING_URI", "mlflow"],
44746
+ ["COMET_EXPERIMENT_KEY", "comet"],
44747
+ ["NEPTUNE_RUN_ID", "neptune"],
44748
+ ["CLEARML_TASK_ID", "clearml"],
44749
+ ["DVC_STAGE", "dvc"],
44750
+ ["RAY_WORKER_PROCESS", "ray"],
44751
+ ["RAY_ADDRESS", "ray"],
44752
+ ["MUJOCO_GL", "mujoco"],
44753
+ ["MUJOCO_PATH", "mujoco"],
44754
+ ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
44755
+ ["SM_TRAINING_ENV", "sagemaker"],
44756
+ ["TRAINING_JOB_ARN", "sagemaker"],
44757
+ ["SAGEMAKER_BASE_DIR", "sagemaker"],
44758
+ ["AZUREML_RUN_ID", "azure-ml"],
44759
+ ["AZURE_ML_MODEL_DIR", "azure-ml"],
44760
+ ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
44761
+ ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
44762
+ ["DET_MASTER", "determined-ai"],
44763
+ ["LIGHTNING_USER_ID", "lightning-ai"],
44764
+ ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
44765
+ ["ARGO_WORKFLOW_NAME", "argo-workflows"],
44766
+ ["KFP_POD_NAME", "kubeflow-pipelines"],
44767
+ ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
44768
+ ["COLAB_BACKEND_VERSION", "google-colab"],
44769
+ ["DATABRICKS_RUNTIME_VERSION", "databricks"],
44770
+ ["DAYTONA_WS_ID", "daytona"],
44771
+ ["DAYTONA_WS_NAME", "daytona"],
44772
+ ["E2B_SANDBOX_ID", "e2b"],
44773
+ ["MODAL_FUNCTION_ID", "modal"],
44774
+ ["MODAL_TASK_ID", "modal"],
44775
+ ["RUNPOD_POD_ID", "runpod"],
44776
+ ["REPLICATE_USERNAME", "replicate"],
44777
+ ["VAST_CONTAINERLABEL", "vast-ai"],
44778
+ ["HARBOR_URL", "harbor"],
44779
+ ["HARBOR_HOSTNAME", "harbor"],
44780
+ ["SWE_BENCH_TASK", "swe-bench"],
44781
+ ["SWEBENCH_TASK", "swe-bench"],
44782
+ ["SWE_AGENT_MODEL", "swe-agent"]
44783
+ ];
44784
+ const detectAiTrainingEnvironment = () => {
44785
+ for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
44786
+ return null;
44787
+ };
44620
44788
  const groupBy = (items, keyFn) => {
44621
44789
  const groups = /* @__PURE__ */ new Map();
44622
44790
  for (const item of items) {
@@ -44860,6 +45028,7 @@ const NANOSECONDS_PER_SECOND = 1000000000n;
44860
45028
  const METRIC = {
44861
45029
  cliInvoked: "cli.invoked",
44862
45030
  cliError: "cli.error",
45031
+ cliEnvironmentError: "cli.env_error",
44863
45032
  projectDetected: "project.detected",
44864
45033
  projectPathSelected: "project.path_selected",
44865
45034
  projectConfigSelected: "project.config_selected",
@@ -44889,7 +45058,8 @@ const METRIC = {
44889
45058
  lspSessionStarted: "lsp.session.started",
44890
45059
  lspScanCompleted: "lsp.scan.completed",
44891
45060
  lspScanDuration: "lsp.scan.duration",
44892
- lspScanDiagnostics: "lsp.scan.diagnostics"
45061
+ lspScanDiagnostics: "lsp.scan.diagnostics",
45062
+ aiTrainingWarningShown: "ai.training.warning_shown"
44893
45063
  };
44894
45064
  //#endregion
44895
45065
  //#region src/cli/utils/noop-console.ts
@@ -44932,7 +45102,7 @@ const makeNoopConsole = () => ({
44932
45102
  });
44933
45103
  //#endregion
44934
45104
  //#region src/cli/utils/version.ts
44935
- const VERSION = "0.5.8-dev.9f28454";
45105
+ const VERSION = "0.5.8-dev.b69f4a7";
44936
45106
  //#endregion
44937
45107
  //#region src/cli/utils/json-mode.ts
44938
45108
  let context = null;
@@ -45296,13 +45466,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
45296
45466
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
45297
45467
  * standard `SENTRY_RELEASE` override.
45298
45468
  */
45299
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.9f28454`;
45469
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.5.8-dev.b69f4a7`;
45300
45470
  /**
45301
45471
  * Deployment environment shown in Sentry's environment filter. Defaults to
45302
45472
  * `production` for tagged releases and `development` for dev/unbuilt versions,
45303
45473
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
45304
45474
  */
45305
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.9f28454") ? "development" : "production");
45475
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.5.8-dev.b69f4a7") ? "development" : "production");
45306
45476
  /**
45307
45477
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
45308
45478
  * (set to `0` to disable tracing) and falls back to
@@ -48898,6 +49068,25 @@ const shouldBlockCi = (diagnostics, blockingLevel) => {
48898
49068
  */
48899
49069
  const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
48900
49070
  //#endregion
49071
+ //#region src/cli/utils/with-namespace.ts
49072
+ /**
49073
+ * Prefixes every key in a flat attribute group with `namespace.`, so a logical
49074
+ * group (scan config, diagnostics rollup, score, …) carries one dotted
49075
+ * namespace applied in a single place rather than hand-spelled into each key —
49076
+ * which is how the wide event drifted into a flat, half-namespaced soup. The
49077
+ * dotted prefix is what makes the attributes tree up in Sentry's attribute
49078
+ * browser and stay group-/filter-/aggregate-able in the Spans dataset.
49079
+ *
49080
+ * Value types are preserved verbatim (numbers stay numbers so Sentry infers a
49081
+ * numeric attribute and `p75(...)`/`avg(...)` work; `null` is kept so
49082
+ * `toSpanAttributes` can drop absent signals rather than coerce them).
49083
+ */
49084
+ const withNamespace = (namespace, attributes) => {
49085
+ const namespaced = {};
49086
+ for (const [key, value] of Object.entries(attributes)) namespaced[`${namespace}.${key}`] = value;
49087
+ return namespaced;
49088
+ };
49089
+ //#endregion
48901
49090
  //#region src/cli/utils/build-run-event.ts
48902
49091
  const readEnvBoolean = (name) => {
48903
49092
  const value = process.env[name];
@@ -48919,12 +49108,12 @@ const buildOutcomeAttributes = (input) => {
48919
49108
  if (input.result === void 0) {
48920
49109
  const error = input.error;
48921
49110
  const known = isReactDoctorError(error);
48922
- return {
48923
- outcome: "error",
49111
+ return withNamespace("outcome", {
49112
+ status: "error",
48924
49113
  exitCode: 1,
48925
49114
  knownError: known,
48926
49115
  errorTag: known ? error.reason._tag : error instanceof Error ? error.name : null
48927
- };
49116
+ });
48928
49117
  }
48929
49118
  const result = input.result;
48930
49119
  const summary = summarizeDiagnostics(result.diagnostics);
@@ -48958,66 +49147,82 @@ const buildOutcomeAttributes = (input) => {
48958
49147
  }
48959
49148
  let fixGroupedFindings = 0;
48960
49149
  for (const count of findingsPerFixGroup.values()) fixGroupedFindings += count;
49150
+ const categoryRollup = {};
49151
+ for (const [category, count] of countByCategory) categoryRollup[`category.${toCategoryKey(category)}`] = count;
48961
49152
  const attributes = {
48962
- outcome,
48963
- exitCode: wouldBlock ? 1 : 0,
48964
- wouldBlock,
48965
- blocking: blockingLevel,
48966
- scanClean: isClean,
48967
- totalDiagnostics: summary.totalDiagnosticCount,
48968
- errorCount: summary.errorCount,
48969
- warningCount: summary.warningCount,
48970
- affectedFiles: summary.affectedFileCount,
48971
- diagnosticsInTestFiles,
48972
- diagnosticsInStoryFiles,
48973
- distinctRulesFired: countByRule.size,
48974
- "diag.fixGroups": findingsPerFixGroup.size,
48975
- "diag.fixGroupedFindings": fixGroupedFindings,
48976
- topRule,
48977
- "migration.largestRuleBucketFiles": largestRuleBucket ? largestRuleBucket.fileCount : null,
48978
- "migration.largestRuleBucketSites": largestRuleBucket ? largestRuleBucket.siteCount : null,
48979
- "migration.largestRuleBucketRule": largestRuleBucket ? largestRuleBucket.ruleKey : null,
48980
- scannedFileCount: result.scannedFileCount ?? null,
48981
- lintCacheHitFiles: result.lintCacheHitFileCount ?? null,
48982
- lintCacheTotalFiles: result.lintCacheTotalFileCount ?? null,
48983
- lintCacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null,
48984
- elapsedMs: result.elapsedMilliseconds,
48985
- scanPhaseMs: result.scanElapsedMilliseconds ?? null,
48986
- score: result.score ? result.score.score : null,
48987
- scoreLabel: result.score ? result.score.label : null,
48988
- scoreAvailable: result.score !== null,
48989
- skippedCheckCount: result.skippedChecks.length,
48990
- didLintFail: input.didLintFail ?? null,
48991
- lintFailureReasonKind: input.lintFailureReasonKind ?? null,
48992
- lintPartialFailureCount: input.lintPartialFailureCount ?? null,
48993
- lintDroppedFileCount: input.lintDroppedFileCount ?? null,
48994
- didDeadCodeFail: input.didDeadCodeFail ?? null,
48995
- supplyChainOverlapTimedOut: input.supplyChainOverlapTimedOut ?? null,
48996
- deadCodeOverlapped: input.deadCodeOverlapped ?? null
48997
- };
48998
- for (const [category, count] of countByCategory) attributes[`diag.category.${toCategoryKey(category)}`] = count;
48999
- if (result.baselineDelta) {
49000
- attributes["baseline.new"] = summary.totalDiagnosticCount;
49001
- attributes["baseline.fixed"] = result.baselineDelta.fixedCount;
49002
- attributes["baseline.baseTotal"] = result.baselineDelta.baseTotalCount;
49003
- attributes["baseline.degraded"] = false;
49004
- } else if (input.gateExempt) attributes["baseline.degraded"] = true;
49153
+ ...withNamespace("outcome", {
49154
+ status: outcome,
49155
+ exitCode: wouldBlock ? 1 : 0,
49156
+ wouldBlock,
49157
+ blocking: blockingLevel,
49158
+ clean: isClean,
49159
+ skippedChecks: result.skippedChecks.length
49160
+ }),
49161
+ ...withNamespace("diag", {
49162
+ total: summary.totalDiagnosticCount,
49163
+ errors: summary.errorCount,
49164
+ warnings: summary.warningCount,
49165
+ affectedFiles: summary.affectedFileCount,
49166
+ inTestFiles: diagnosticsInTestFiles,
49167
+ inStoryFiles: diagnosticsInStoryFiles,
49168
+ distinctRules: countByRule.size,
49169
+ topRule,
49170
+ fixGroups: findingsPerFixGroup.size,
49171
+ fixGroupedFindings,
49172
+ ...categoryRollup
49173
+ }),
49174
+ ...withNamespace("score", {
49175
+ value: result.score ? result.score.score : null,
49176
+ label: result.score ? result.score.label : null,
49177
+ available: result.score !== null
49178
+ }),
49179
+ ...withNamespace("lint", {
49180
+ failed: input.didLintFail ?? null,
49181
+ failureReasonKind: input.lintFailureReasonKind ?? null,
49182
+ partialFailureCount: input.lintPartialFailureCount ?? null,
49183
+ droppedFileCount: input.lintDroppedFileCount ?? null,
49184
+ cacheHitFiles: result.lintCacheHitFileCount ?? null,
49185
+ cacheTotalFiles: result.lintCacheTotalFileCount ?? null,
49186
+ cacheHitRatio: result.lintCacheTotalFileCount != null && result.lintCacheTotalFileCount > 0 ? (result.lintCacheHitFileCount ?? 0) / result.lintCacheTotalFileCount : null
49187
+ }),
49188
+ ...withNamespace("deadCode", {
49189
+ failed: input.didDeadCodeFail ?? null,
49190
+ overlapped: input.deadCodeOverlapped ?? null
49191
+ }),
49192
+ ...withNamespace("supplyChain", { overlapTimedOut: input.supplyChainOverlapTimedOut ?? null }),
49193
+ ...withNamespace("timing", {
49194
+ elapsedMs: result.elapsedMilliseconds,
49195
+ scanMs: result.scanElapsedMilliseconds ?? null
49196
+ }),
49197
+ ...withNamespace("migration", {
49198
+ largestRuleBucketFiles: largestRuleBucket ? largestRuleBucket.fileCount : null,
49199
+ largestRuleBucketSites: largestRuleBucket ? largestRuleBucket.siteCount : null,
49200
+ largestRuleBucketRule: largestRuleBucket ? largestRuleBucket.ruleKey : null
49201
+ })
49202
+ };
49203
+ if (result.baselineDelta) Object.assign(attributes, withNamespace("baseline", {
49204
+ new: summary.totalDiagnosticCount,
49205
+ fixed: result.baselineDelta.fixedCount,
49206
+ baseTotal: result.baselineDelta.baseTotalCount,
49207
+ degraded: false
49208
+ }));
49209
+ else if (input.gateExempt) attributes["baseline.degraded"] = true;
49005
49210
  return attributes;
49006
49211
  };
49007
- const buildCiAttributes = () => {
49212
+ const buildActionAttributes = () => {
49008
49213
  const { githubActorAssociation } = resolveGithubActionsScoreMetadata();
49009
- return {
49214
+ return withNamespace("action", {
49010
49215
  actorAssociation: githubActorAssociation ?? null,
49011
49216
  runnerOs: detectRunnerOs(),
49012
49217
  comment: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.comment),
49013
49218
  reviewComments: readEnvBoolean(ACTION_INPUT_ENVIRONMENT_VARIABLES.reviewComments),
49014
49219
  versionPin: resolveVersionPin(process.env[ACTION_INPUT_ENVIRONMENT_VARIABLES.version])
49015
- };
49220
+ });
49016
49221
  };
49017
- const buildConfigAttributes = (input) => {
49222
+ const buildScanAttributes = (input) => {
49018
49223
  const ruleOverrides = input.userConfig?.rules ?? {};
49019
49224
  const ruleKeys = Object.keys(ruleOverrides);
49020
- return {
49225
+ return withNamespace("scan", {
49021
49226
  mode: input.mode,
49022
49227
  scope: input.scope,
49023
49228
  parallel: input.parallel,
@@ -49032,12 +49237,18 @@ const buildConfigAttributes = (input) => {
49032
49237
  ignoredTagCount: input.ignoredTagCount,
49033
49238
  hasCustomConfig: input.hasCustomConfig,
49034
49239
  rulesConfigured: ruleKeys.length,
49035
- rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length
49036
- };
49240
+ rulesDisabled: ruleKeys.filter((key) => ruleOverrides[key] === "off").length,
49241
+ fileCount: input.result?.scannedFileCount ?? null
49242
+ });
49037
49243
  };
49038
49244
  /**
49039
- * Projects a scan into the flat attribute set for its root span — the canonical
49040
- * per-scan "wide event". Pure and exported so the projection (outcome
49245
+ * Projects a scan into the namespaced attribute set for its root span — the
49246
+ * canonical per-scan "wide event". Every attribute carries a dotted namespace
49247
+ * that groups it by concept (`scan.*` config, `action.*` CI knobs, `outcome.*`
49248
+ * verdict, `diag.*` findings, `score.*`, `lint.*`, `deadCode.*`,
49249
+ * `supplyChain.*`, `timing.*`, `migration.*`, `baseline.*`) so the attributes
49250
+ * tree up in Sentry's attribute browser and stay filter-/group-/aggregate-able
49251
+ * in the Spans dataset. Pure and exported so the projection (outcome
49041
49252
  * precedence, rule/category rollups, CI knobs, config shape) is unit-testable
49042
49253
  * without a live Sentry client. `null` values are dropped so absent signals
49043
49254
  * never become misleading `"null"` attributes. The run + project base context
@@ -49046,8 +49257,8 @@ const buildConfigAttributes = (input) => {
49046
49257
  * those don't carry.
49047
49258
  */
49048
49259
  const buildRunEventAttributes = (input) => toSpanAttributes({
49049
- ...buildConfigAttributes(input),
49050
- ...buildCiAttributes(),
49260
+ ...buildScanAttributes(input),
49261
+ ...buildActionAttributes(),
49051
49262
  ...buildOutcomeAttributes(input)
49052
49263
  });
49053
49264
  /**
@@ -49711,6 +49922,7 @@ const CI_PITCH_EVENT = "ci-pitch";
49711
49922
  const ACTION_UPGRADE_EVENT = "action-upgrade-v2";
49712
49923
  const SETUP_HINT_EVENT = "setup-hint";
49713
49924
  const HANDOFF_TARGET_PREFERENCE_ID = "handoff-target";
49925
+ const INSTALL_AGENTS_PREFERENCE_ID = "install-agents";
49714
49926
  const foldLegacyDecisions = (projects, legacy, eventId) => {
49715
49927
  for (const [hash, record] of Object.entries(legacy ?? {})) {
49716
49928
  const existing = projects[hash] ?? { rootDirectory: record.rootDirectory ?? "" };
@@ -50374,7 +50586,7 @@ const readPackageJson = (projectRoot) => {
50374
50586
  return null;
50375
50587
  }
50376
50588
  };
50377
- const writeJsonFile$1 = (filePath, value) => {
50589
+ const writeJsonFile = (filePath, value) => {
50378
50590
  NFS.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
50379
50591
  };
50380
50592
  const packageHasDependency = (projectRoot, dependencyName) => {
@@ -51094,7 +51306,8 @@ const getStagedSourceFiles = async (directory) => {
51094
51306
  return [...await runPromise(gen(function* () {
51095
51307
  return yield* (yield* StagedFiles).discoverSourceFiles(directory);
51096
51308
  }).pipe(provide(stagedFilesLayer)))];
51097
- } catch {
51309
+ } catch (error) {
51310
+ cliLogger.warn(`Failed to discover staged files: ${error instanceof Error ? error.message : String(error)}`);
51098
51311
  return [];
51099
51312
  }
51100
51313
  };
@@ -51113,6 +51326,35 @@ const materializeStagedFiles = async (directory, stagedFiles, tempDirectory) =>
51113
51326
  };
51114
51327
  };
51115
51328
  //#endregion
51329
+ //#region src/cli/utils/is-environment-error.ts
51330
+ const isNodeSystemError = (error) => error instanceof Error && typeof error.code === "string";
51331
+ const ENVIRONMENT_ERROR_CODES = new Set([
51332
+ "ENOSPC",
51333
+ "EIO",
51334
+ "EROFS",
51335
+ "EACCES",
51336
+ "EPERM",
51337
+ "ENOTDIR"
51338
+ ]);
51339
+ const isEnvironmentError = (error) => {
51340
+ if (!isNodeSystemError(error)) return false;
51341
+ if (error.code === "ENOENT") return error.syscall?.startsWith("spawn") ?? false;
51342
+ return error.code !== void 0 && ENVIRONMENT_ERROR_CODES.has(error.code);
51343
+ };
51344
+ const formatEnvironmentError = (error) => {
51345
+ if (!isNodeSystemError(error)) return error instanceof Error ? error.message : String(error);
51346
+ switch (error.code) {
51347
+ case "ENOSPC": return "No space left on device. Free up disk space and try again.";
51348
+ case "EIO": return "I/O error: the filesystem or disk may be failing. Check your system logs.";
51349
+ case "EROFS": return "Read-only filesystem: cannot write to this location.";
51350
+ case "EACCES":
51351
+ case "EPERM": return error.path ? `Permission denied accessing ${error.path}. Check file permissions and try again.` : "Permission denied. Check file permissions and try again.";
51352
+ case "ENOTDIR": return error.path ? `A file exists at ${error.path} or one of its parent paths where a directory was expected.` : "A file exists where a directory was expected.";
51353
+ case "ENOENT": return "Required command not found. Ensure the tool (e.g. git) is installed and on your PATH.";
51354
+ default: return error.message;
51355
+ }
51356
+ };
51357
+ //#endregion
51116
51358
  //#region src/cli/utils/handle-error.ts
51117
51359
  const OTLP_ENDPOINT_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_ENDPOINT";
51118
51360
  const OTLP_AUTH_HEADER_ENVIRONMENT_VARIABLE = "REACT_DOCTOR_OTLP_AUTH_HEADER";
@@ -51195,15 +51437,19 @@ const handleError = (error, options = {}) => {
51195
51437
  process.exitCode = 1;
51196
51438
  };
51197
51439
  /**
51198
- * Renderer for expected, user-actionable failures — a bad `--diff` value or
51199
- * a base branch that isn't fetched. Prints just the (already human-readable)
51200
- * message — no "Something went wrong", prefilled issue, Discord link, or
51201
- * Sentry reference — because there is no bug to report.
51440
+ * Renderer for expected, user-actionable failures — a bad `--diff` value,
51441
+ * a base branch that isn't fetched, or environment errors like disk-full or
51442
+ * permission-denied. Prints just the (already human-readable) message — no
51443
+ * "Something went wrong", prefilled issue, Discord link, or Sentry reference
51444
+ * — because there is no bug to report.
51202
51445
  */
51203
51446
  const handleUserError = (error, options = {}) => {
51447
+ const isEnvError = isEnvironmentError(error);
51448
+ if (isEnvError) recordCount(METRIC.cliEnvironmentError, 1, { code: error.code ?? "unknown" });
51449
+ const message = isEnvError ? formatEnvironmentError(error) : formatErrorForReport(error);
51204
51450
  runSync(gen(function* () {
51205
51451
  yield* error$1("");
51206
- yield* error$1(highlighter.error(formatErrorForReport(error)));
51452
+ yield* error$1(highlighter.error(message));
51207
51453
  yield* error$1("");
51208
51454
  }));
51209
51455
  if (options.shouldExit !== false) process.exit(1);
@@ -51218,7 +51464,7 @@ const handleUserError = (error, options = {}) => {
51218
51464
  * `handleUserError` (a plain message — no "Something went wrong", prefilled
51219
51465
  * issue, Discord link, or Sentry reference), since there is no bug to report.
51220
51466
  *
51221
- * Three distinct shapes reach the CLI's catch blocks:
51467
+ * Four distinct shapes reach the CLI's catch blocks:
51222
51468
  *
51223
51469
  * - **Project-discovery failures** (`NoReactDependencyError`,
51224
51470
  * `ProjectNotFoundError`, `PackageJsonNotFoundError`, `NotADirectoryError`,
@@ -51231,12 +51477,19 @@ const handleUserError = (error, options = {}) => {
51231
51477
  * `--project` name.
51232
51478
  * - **Bad `--diff` input** (`GitBaseBranchInvalid` / `GitBaseBranchMissing`)
51233
51479
  * stays the tagged `ReactDoctorError`, so dispatch on the reason `_tag`.
51480
+ * - **Environment failures** (`ENOSPC`, `EIO`, `EROFS`, `EACCES`, `EPERM`,
51481
+ * `ENOTDIR`, plus a `spawn`-scoped `ENOENT` for a missing binary) — disk
51482
+ * full / failing / read-only, permission denied, or a path blocked by a
51483
+ * file. React Doctor cannot fix the user's environment; exit cleanly with an
51484
+ * actionable message instead of crashing. See `is-environment-error.ts` for
51485
+ * why the set stays narrow (codes that usually mean our bug keep reaching
51486
+ * Sentry).
51234
51487
  *
51235
51488
  * This composes the existing core narrowers rather than introducing a new
51236
51489
  * error-shape helper (AGENTS.md): it encodes CLI-layer reporting policy, not
51237
51490
  * knowledge of the `ReactDoctorError` shape.
51238
51491
  */
51239
- const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
51492
+ const isExpectedUserError = (error) => error instanceof CliInputError || isProjectDiscoveryError(error) || isEnvironmentError(error) || isReactDoctorError(error) && (error.reason._tag === "GitBaseBranchInvalid" || error.reason._tag === "GitBaseBranchMissing");
51240
51493
  //#endregion
51241
51494
  //#region src/cli/utils/build-handoff-payload.ts
51242
51495
  const buildHandoffPayload = (input) => {
@@ -51317,6 +51570,20 @@ const detectAvailableAgents = async () => {
51317
51570
  const detected = new Set([...detectPathAvailableAgents(), ...await detectInstalledSkillAgents()]);
51318
51571
  return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
51319
51572
  };
51573
+ const DEFAULT_INSTALL_AGENTS = [
51574
+ "claude-code",
51575
+ "cursor",
51576
+ "codex",
51577
+ "opencode"
51578
+ ];
51579
+ const computeDefaultSelectedAgents = (detectedAgents, rememberedAgents) => {
51580
+ const detected = new Set(detectedAgents);
51581
+ const remembered = rememberedAgents.filter((agent) => detected.has(agent));
51582
+ if (remembered.length > 0) return remembered;
51583
+ const defaults = DEFAULT_INSTALL_AGENTS.filter((agent) => detected.has(agent));
51584
+ if (defaults.length > 0) return defaults;
51585
+ return detectedAgents.length === 1 ? [...detectedAgents] : [];
51586
+ };
51320
51587
  //#endregion
51321
51588
  //#region src/cli/utils/install-doctor-script.ts
51322
51589
  const DOCTOR_SCRIPT_NAME = "doctor";
@@ -51398,7 +51665,7 @@ const installDoctorScript = (options) => {
51398
51665
  };
51399
51666
  })();
51400
51667
  const scriptStatus = scriptTarget.status;
51401
- if (scriptStatus === "created") writeJsonFile$1(packageJsonPath, {
51668
+ if (scriptStatus === "created") writeJsonFile(packageJsonPath, {
51402
51669
  ...packageJson,
51403
51670
  scripts: {
51404
51671
  ...isRecord$1(scripts) ? scripts : {},
@@ -51969,6 +52236,19 @@ const setUpGitHubActions = async (options) => {
51969
52236
  return didCreateWorkflow;
51970
52237
  };
51971
52238
  //#endregion
52239
+ //#region src/cli/utils/install-agents-preference.ts
52240
+ const INSTALL_AGENTS_PREFERENCE = {
52241
+ id: INSTALL_AGENTS_PREFERENCE_ID,
52242
+ scope: "global"
52243
+ };
52244
+ const PREFERENCE_SEPARATOR = ",";
52245
+ const readInstallAgents = (options = {}) => {
52246
+ const stored = readPreference(INSTALL_AGENTS_PREFERENCE, {}, options);
52247
+ if (stored === null) return [];
52248
+ return stored.split(PREFERENCE_SEPARATOR).map((entry) => entry.trim()).filter((entry) => isSkillAgentType(entry));
52249
+ };
52250
+ const rememberInstallAgents = (agents, options = {}) => writePreference(INSTALL_AGENTS_PREFERENCE, agents.join(PREFERENCE_SEPARATOR), {}, options);
52251
+ //#endregion
51972
52252
  //#region src/cli/utils/install-agent-hooks.ts
51973
52253
  const CLAUDE_AGENT = "claude-code";
51974
52254
  const CURSOR_AGENT = "cursor";
@@ -51979,20 +52259,34 @@ const CURSOR_HOOKS_RELATIVE_PATH = ".cursor/hooks.json";
51979
52259
  const CURSOR_HOOK_RELATIVE_PATH = ".cursor/hooks/react-doctor.sh";
51980
52260
  const CURSOR_HOOK_MATCHER = "Write|Edit|MultiEdit|ApplyPatch";
51981
52261
  const CURSOR_HOOKS_SCHEMA_VERSION = 1;
51982
- const JSON_INDENT_SPACES$1 = 2;
51983
52262
  const isSupportedAgent = (agent) => agent === CLAUDE_AGENT || agent === CURSOR_AGENT;
51984
52263
  const readJsonFile = (filePath, fallback) => {
51985
52264
  if (!NFS.existsSync(filePath)) return fallback;
51986
52265
  const content = NFS.readFileSync(filePath, "utf8").trim();
51987
52266
  if (content.length === 0) return fallback;
51988
- return JSON.parse(content);
52267
+ try {
52268
+ return JSON.parse(content);
52269
+ } catch (error) {
52270
+ if (error instanceof SyntaxError) throw new CliInputError(`Could not parse ${filePath}: the file contains invalid JSON. Fix the syntax errors in this file and re-run the install command.`);
52271
+ throw error;
52272
+ }
51989
52273
  };
51990
- const writeJsonFile = (filePath, value) => {
51991
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
51992
- NFS.writeFileSync(filePath, `${JSON.stringify(value, null, JSON_INDENT_SPACES$1)}\n`);
52274
+ const ensureDirectoryExists = (directoryPath) => {
52275
+ try {
52276
+ NFS.mkdirSync(directoryPath, { recursive: true });
52277
+ } catch (error) {
52278
+ const code = error.code;
52279
+ if (code === "EACCES" || code === "EPERM") throw new CliInputError(`Could not create directory ${directoryPath}: permission denied. Ensure you have write permissions for this location and re-run the install command.`);
52280
+ if (code === "ENOTDIR" || code === "EEXIST") throw new CliInputError(`Could not create directory ${directoryPath}: a file exists at this path or one of its parent paths. Remove the conflicting file and re-run the install command.`);
52281
+ throw error;
52282
+ }
52283
+ };
52284
+ const writeJsonFileWithDirectoryCheck = (filePath, value) => {
52285
+ ensureDirectoryExists(Path.dirname(filePath));
52286
+ writeJsonFile(filePath, value);
51993
52287
  };
51994
52288
  const writeHookScript = (filePath) => {
51995
- NFS.mkdirSync(Path.dirname(filePath), { recursive: true });
52289
+ ensureDirectoryExists(Path.dirname(filePath));
51996
52290
  NFS.writeFileSync(filePath, buildAgentHookScript());
51997
52291
  NFS.chmodSync(filePath, 493);
51998
52292
  };
@@ -52008,7 +52302,7 @@ const installClaudeHook = (projectRoot) => {
52008
52302
  command: CLAUDE_HOOK_COMMAND
52009
52303
  }] });
52010
52304
  hooks.PostToolBatch = postToolBatchHooks;
52011
- writeJsonFile(settingsPath, {
52305
+ writeJsonFileWithDirectoryCheck(settingsPath, {
52012
52306
  ...settings,
52013
52307
  hooks
52014
52308
  });
@@ -52028,7 +52322,7 @@ const installCursorHook = (projectRoot) => {
52028
52322
  timeout: 120
52029
52323
  });
52030
52324
  hooks.postToolUse = postToolUseHooks;
52031
- writeJsonFile(configPath, {
52325
+ writeJsonFileWithDirectoryCheck(configPath, {
52032
52326
  ...config,
52033
52327
  version: config.version ?? CURSOR_HOOKS_SCHEMA_VERSION,
52034
52328
  hooks
@@ -52264,7 +52558,7 @@ const installPackageJsonHook = (options, strategy) => {
52264
52558
  parent = cloned;
52265
52559
  }
52266
52560
  parent[leafKey] = strategy.leafShape === "array" ? appendArrayCommand(parent[leafKey]) : appendStringCommand(parent[leafKey]);
52267
- writeJsonFile$1(packageJsonPath, nextPackageJson);
52561
+ writeJsonFile(packageJsonPath, nextPackageJson);
52268
52562
  removeLegacyManagedRunner(options.projectRoot);
52269
52563
  return {
52270
52564
  hookPath: packageJsonPath,
@@ -52847,6 +53141,9 @@ const runInstallReactDoctor = async (options = {}) => {
52847
53141
  const shouldUpgradeWorkflow = canUpgradeWorkflow && (Boolean(options.yes) || upgradePromptOutcome === "yes");
52848
53142
  if (upgradePromptOutcome === "no" && !options.dryRun) recordActionUpgradeDecision(projectRoot, "declined");
52849
53143
  if ((ciPromptOutcome === "yes" || ciPromptOutcome === "no") && !options.dryRun) recordCiPromptDecision(projectRoot, ciPromptOutcome === "yes" ? "accepted" : "declined");
53144
+ const rememberedAgents = options.lastSelectedAgents ?? readInstallAgents();
53145
+ const defaultSelectedAgents = computeDefaultSelectedAgents(detectedAgents, rememberedAgents);
53146
+ const usedRememberedAgents = rememberedAgents.some((agent) => detectedAgents.includes(agent));
52850
53147
  const selectedAgents = skipPrompts ? detectedAgents : (await prompt({
52851
53148
  type: "multiselect",
52852
53149
  name: "agents",
@@ -52854,12 +53151,13 @@ const runInstallReactDoctor = async (options = {}) => {
52854
53151
  choices: detectedAgents.map((agent) => ({
52855
53152
  title: getSkillAgentConfig(agent).displayName,
52856
53153
  value: agent,
52857
- selected: true
53154
+ selected: defaultSelectedAgents.includes(agent)
52858
53155
  })),
52859
53156
  instructions: false,
52860
53157
  min: 1
52861
53158
  }, promptOptions)).agents ?? [];
52862
53159
  if (selectedAgents.length === 0) return;
53160
+ if (!skipPrompts && !options.dryRun) rememberInstallAgents(selectedAgents);
52863
53161
  let dependencyResult;
52864
53162
  if (!options.dryRun) {
52865
53163
  await installReactDoctorSkillStep(sourceDir, selectedAgents, projectRoot);
@@ -52918,6 +53216,8 @@ const runInstallReactDoctor = async (options = {}) => {
52918
53216
  }
52919
53217
  recordCount(METRIC.installCompleted, 1, {
52920
53218
  agentsCount: selectedAgents.length,
53219
+ agentsDetected: detectedAgents.length,
53220
+ usedRememberedAgents,
52921
53221
  gitHook: shouldInstallGitHook,
52922
53222
  agentHooks: shouldInstallAgentHooks,
52923
53223
  workflow: didInstallWorkflow,
@@ -53697,7 +53997,7 @@ const warnDeprecatedDiff = (flags, userConfig) => {
53697
53997
  };
53698
53998
  const warnDiffUnavailable = (requested, isQuiet) => {
53699
53999
  if (isQuiet) return;
53700
- if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (merge-base failed or HEAD has no history). Running full scan.`);
54000
+ if (typeof requested.base === "string") cliLogger.warn(`Could not compute diff against "${requested.base}" (git unavailable, ref not found, or merge-base failed). Running full scan.`);
53701
54001
  else cliLogger.warn("No feature branch or uncommitted changes detected. Running full scan.");
53702
54002
  cliLogger.break();
53703
54003
  };
@@ -53894,6 +54194,7 @@ const resolveRequestedProjects = (requestedNames, workspacePackages, rootDirecto
53894
54194
  return requestedNames.map((requestedName) => {
53895
54195
  const matched = workspacePackages.find((workspacePackage) => workspacePackage.name === requestedName || Path.basename(workspacePackage.directory) === requestedName);
53896
54196
  if (matched) return matched.directory;
54197
+ if (Path.basename(rootDirectory) === requestedName) return rootDirectory;
53897
54198
  const candidateDirectory = Path.resolve(rootDirectory, requestedName);
53898
54199
  if (isDirectory(candidateDirectory)) {
53899
54200
  recordCount(METRIC.projectPathSelected);
@@ -54001,67 +54302,15 @@ const warnDeprecatedFailOn = (flags, userConfig) => {
54001
54302
  cliLogger.warn(`${source} is deprecated; rename it to ${replacement}.`);
54002
54303
  };
54003
54304
  //#endregion
54004
- //#region src/cli/utils/detect-ai-training-environment.ts
54005
- const AI_TRAINING_ENV_VARS = [
54006
- ["HF_DATASETS_CACHE", "huggingface"],
54007
- ["HF_HOME", "huggingface"],
54008
- ["HUGGINGFACE_HUB_CACHE", "huggingface"],
54009
- ["CUDA_VISIBLE_DEVICES", "cuda"],
54010
- ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
54011
- ["WANDB_RUN_ID", "wandb"],
54012
- ["MLFLOW_RUN_ID", "mlflow"],
54013
- ["MLFLOW_TRACKING_URI", "mlflow"],
54014
- ["COMET_EXPERIMENT_KEY", "comet"],
54015
- ["NEPTUNE_RUN_ID", "neptune"],
54016
- ["RAY_WORKER_PROCESS", "ray"],
54017
- ["RAY_ADDRESS", "ray"],
54018
- ["MUJOCO_GL", "mujoco"],
54019
- ["MUJOCO_PATH", "mujoco"],
54020
- ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
54021
- ["SAGEMAKER_BASE_DIR", "sagemaker"],
54022
- ["AZURE_ML_MODEL_DIR", "azure-ml"],
54023
- ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
54024
- ["DAYTONA_WS_ID", "daytona"],
54025
- ["DAYTONA_WS_NAME", "daytona"],
54026
- ["E2B_SANDBOX_ID", "e2b"],
54027
- ["MODAL_FUNCTION_ID", "modal"],
54028
- ["MODAL_TASK_ID", "modal"],
54029
- ["RUNPOD_POD_ID", "runpod"],
54030
- ["HARBOR_URL", "harbor"],
54031
- ["HARBOR_HOSTNAME", "harbor"],
54032
- ["SWE_BENCH_TASK", "swe-bench"],
54033
- ["SWEBENCH_TASK", "swe-bench"],
54034
- ["SWE_AGENT_MODEL", "swe-agent"],
54035
- ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
54036
- ["COLAB_BACKEND_VERSION", "google-colab"],
54037
- ["DATABRICKS_RUNTIME_VERSION", "databricks"],
54038
- ["SM_TRAINING_ENV", "sagemaker"],
54039
- ["TRAINING_JOB_ARN", "sagemaker"],
54040
- ["AZUREML_RUN_ID", "azure-ml"],
54041
- ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
54042
- ["WANDB_SWEEP_ID", "wandb"],
54043
- ["DVC_STAGE", "dvc"],
54044
- ["CLEARML_TASK_ID", "clearml"],
54045
- ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
54046
- ["DET_MASTER", "determined-ai"],
54047
- ["LIGHTNING_USER_ID", "lightning-ai"],
54048
- ["ARGO_WORKFLOW_NAME", "argo-workflows"],
54049
- ["KFP_POD_NAME", "kubeflow-pipelines"],
54050
- ["SPACE_ID", "huggingface-spaces"],
54051
- ["REPLICATE_USERNAME", "replicate"],
54052
- ["VAST_CONTAINERLABEL", "vast-ai"],
54053
- ["TPU_NAME", "google-tpu"],
54054
- ["ROCR_VISIBLE_DEVICES", "rocm"]
54055
- ];
54056
- const detectAiTrainingEnvironment = () => {
54057
- for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar] !== void 0) return label;
54058
- return null;
54059
- };
54060
- //#endregion
54061
54305
  //#region src/cli/utils/warn-ai-training-environment.ts
54306
+ let didWarnAiTraining = false;
54062
54307
  const warnIfAiTrainingEnvironment = () => {
54063
- if (detectAiTrainingEnvironment() === null) return;
54064
- cliLogger.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.");
54308
+ if (didWarnAiTraining) return;
54309
+ const detected = detectAiTrainingEnvironment();
54310
+ if (detected === null) return;
54311
+ didWarnAiTraining = true;
54312
+ 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`);
54313
+ recordCount(METRIC.aiTrainingWarningShown, 1, { environment: detected });
54065
54314
  };
54066
54315
  //#endregion
54067
54316
  //#region src/cli/utils/validate-mode-flags.ts
@@ -54452,6 +54701,10 @@ const installAction = async (options, command) => {
54452
54701
  projectRoot: options.cwd ?? process.cwd()
54453
54702
  });
54454
54703
  } catch (error) {
54704
+ if (isExpectedUserError(error)) {
54705
+ handleUserError(error);
54706
+ return;
54707
+ }
54455
54708
  handleError(error, { sentryEventId: await reportErrorToSentry(error) });
54456
54709
  }
54457
54710
  };
@@ -55459,4 +55712,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
55459
55712
  export {};
55460
55713
 
55461
55714
  //# sourceMappingURL=cli.js.map
55462
- //# debugId=34e4ad65-2650-5e3e-afd8-191931eb2a47
55715
+ //# debugId=bd31e7dc-cfca-56b2-a963-9d798b3cbbe5