react-doctor 0.7.6-dev.a8cd24e → 0.7.6-dev.b36e439

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]="9a5c1a3c-7706-5a23-bca4-ddddd2f085af")}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]="58e54b02-7ba2-5f13-a535-98c236467a86")}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";
@@ -41823,6 +41823,7 @@ const parseGithubViewerPermission = (stdout) => {
41823
41823
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
41824
41824
  };
41825
41825
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
41826
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
41826
41827
  /**
41827
41828
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
41828
41829
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -41893,6 +41894,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
41893
41894
  args,
41894
41895
  directory
41895
41896
  });
41897
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
41898
+ "ls-files",
41899
+ "-z",
41900
+ "--others",
41901
+ "--exclude-standard",
41902
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
41903
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
41904
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
41896
41905
  const currentBranch = (directory) => runGit(directory, [
41897
41906
  "rev-parse",
41898
41907
  "--abbrev-ref",
@@ -42015,7 +42024,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42015
42024
  githubViewerPermission,
42016
42025
  branchExists,
42017
42026
  mergeBase,
42018
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
42027
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
42019
42028
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
42020
42029
  if (explicitBaseBranch !== void 0) {
42021
42030
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -42044,7 +42053,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42044
42053
  "HEAD"
42045
42054
  ]);
42046
42055
  if (uncommitted.status !== 0) return null;
42047
- const files = splitNullSeparated(uncommitted.stdout);
42056
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
42048
42057
  if (files.length === 0) return null;
42049
42058
  return {
42050
42059
  currentBranch: resolvedCurrentBranch,
@@ -42074,7 +42083,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42074
42083
  currentBranch: resolvedCurrentBranch,
42075
42084
  baseBranch,
42076
42085
  diffBaseRef: mergeBaseRef,
42077
- changedFiles: splitNullSeparated(diff.stdout),
42086
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
42078
42087
  isCurrentChanges: false
42079
42088
  };
42080
42089
  }).pipe(withSpan("Git.diffSelection")),
@@ -42120,7 +42129,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
42120
42129
  stdout: result.stdout
42121
42130
  };
42122
42131
  }).pipe(withSpan("Git.grep")),
42123
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
42132
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
42124
42133
  if (files.length === 0) return [];
42125
42134
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
42126
42135
  const result = yield* runGit(directory, [
@@ -42134,7 +42143,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
42134
42143
  ...files
42135
42144
  ]);
42136
42145
  if (result.status !== 0) return null;
42137
- return parseChangedLineRanges(result.stdout);
42146
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
42147
+ if (cached || !includeUntracked) return changedLineRanges;
42148
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
42149
+ if (untrackedFilePaths === null) return changedLineRanges;
42150
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
42151
+ file,
42152
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
42153
+ }))];
42138
42154
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
42139
42155
  });
42140
42156
  })).pipe(provide$2(layer$3.pipe(provide$2(mergeAll$1(layer$2, layer$1)))));
@@ -43643,11 +43659,11 @@ const buildRelatedLocations = (labels, filePath) => {
43643
43659
  }
43644
43660
  return related;
43645
43661
  };
43646
- const isOxlintOutput = (value) => {
43647
- if (typeof value !== "object" || value === null) return false;
43648
- const candidate = value;
43649
- return Array.isArray(candidate.diagnostics);
43650
- };
43662
+ const isOxlintSpan = (value) => isRecord$1(value) && typeof value.offset === "number" && typeof value.length === "number" && typeof value.line === "number" && typeof value.column === "number";
43663
+ const isOxlintLabel = (value) => isRecord$1(value) && isOxlintSpan(value.span);
43664
+ const isMappableOxlintDiagnostic = (value) => isRecord$1(value) && typeof value.code === "string" && value.code.length > 0 && typeof value.filename === "string" && value.filename.length > 0 && (value.severity === "warning" || value.severity === "error") && Array.isArray(value.labels) && value.labels.every(isOxlintLabel);
43665
+ const isEngineFailureDiagnostic = (value) => !isRecord$1(value) || typeof value.filename !== "string" || value.filename.length === 0;
43666
+ const isOxlintOutput = (value) => isRecord$1(value) && Array.isArray(value.diagnostics);
43651
43667
  /**
43652
43668
  * Parses one oxlint subprocess's stdout into a flat `Diagnostic[]`.
43653
43669
  * Tolerates the leading-notice-line shape oxlint sometimes prints
@@ -43666,6 +43682,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43666
43682
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
43667
43683
  }
43668
43684
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
43685
+ const engineFailureDiagnostic = parsed.diagnostics.find(isEngineFailureDiagnostic);
43686
+ if (engineFailureDiagnostic !== void 0) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: JSON.stringify(engineFailureDiagnostic).slice(0, 600) }) });
43669
43687
  const minifiedFileCache = /* @__PURE__ */ new Map();
43670
43688
  const sourceBufferCache = /* @__PURE__ */ new Map();
43671
43689
  const sourceLinesCache = /* @__PURE__ */ new Map();
@@ -43700,7 +43718,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
43700
43718
  minifiedFileCache.set(absolutePath, minified);
43701
43719
  return minified;
43702
43720
  };
43703
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
43721
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
43704
43722
  const { plugin, rule } = parseRuleCode(diagnostic.code);
43705
43723
  const primaryLabel = diagnostic.labels[0];
43706
43724
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -46266,10 +46284,11 @@ const computeDiagnosticDelta = (input) => {
46266
46284
  * render diff-base mistakes (`GitBaseBranchInvalid` /
46267
46285
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
46268
46286
  */
46269
- const getDiffInfo = (directory, explicitBaseBranch) => runPromise(gen(function* () {
46287
+ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runPromise(gen(function* () {
46270
46288
  const selection = yield* (yield* Git).diffSelection({
46271
46289
  directory,
46272
- explicitBaseBranch
46290
+ explicitBaseBranch,
46291
+ includeUntracked
46273
46292
  });
46274
46293
  if (selection === null) return null;
46275
46294
  return {
@@ -46605,6 +46624,43 @@ const isNonInteractiveEnvironment = () => NON_INTERACTIVE_ENVIRONMENT_VARIABLES.
46605
46624
  //#region src/cli/utils/constants.ts
46606
46625
  const REACT_DOCTOR_CONFIG_PROJECT_NAME = "react-doctor";
46607
46626
  const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
46627
+ const STAGED_SNAPSHOT_ADDITIONAL_CONFIG_FILENAMES = [
46628
+ ".babelrc",
46629
+ ".babelrc.json",
46630
+ ".eslintignore",
46631
+ ".eslintrc.json",
46632
+ ".oxlintignore",
46633
+ "app.config.cjs",
46634
+ "app.config.js",
46635
+ "app.config.json",
46636
+ "app.config.mjs",
46637
+ "app.config.ts",
46638
+ "app.json",
46639
+ "babel.config.cjs",
46640
+ "babel.config.js",
46641
+ "babel.config.json",
46642
+ "babel.config.mjs",
46643
+ "eslint.config.cjs",
46644
+ "eslint.config.cts",
46645
+ "eslint.config.js",
46646
+ "eslint.config.mjs",
46647
+ "eslint.config.mts",
46648
+ "eslint.config.ts",
46649
+ "next.config.cjs",
46650
+ "next.config.js",
46651
+ "next.config.mjs",
46652
+ "next.config.ts",
46653
+ "oxlint.config.json",
46654
+ "react-doctor.config.json",
46655
+ "vite.config.cjs",
46656
+ "vite.config.cts",
46657
+ "vite.config.js",
46658
+ "vite.config.mjs",
46659
+ "vite.config.mts",
46660
+ "vite.config.ts",
46661
+ "vitest.config.js",
46662
+ "vitest.config.ts"
46663
+ ];
46608
46664
  const BASELINE_FILES_TEMP_DIR_PREFIX = "react-doctor-baseline-";
46609
46665
  const SCAN_RESULT_CACHE_FILENAME = "scan-cache.json";
46610
46666
  const RUN_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
@@ -46619,6 +46675,7 @@ const METRIC = {
46619
46675
  cliInvoked: "cli.invoked",
46620
46676
  cliError: "cli.error",
46621
46677
  cliEnvironmentError: "cli.env_error",
46678
+ stagedSnapshotDivergence: "staged.snapshot_divergence",
46622
46679
  projectDetected: "project.detected",
46623
46680
  projectPathSelected: "project.path_selected",
46624
46681
  projectConfigSelected: "project.config_selected",
@@ -46699,7 +46756,7 @@ const makeNoopConsole = () => ({
46699
46756
  });
46700
46757
  //#endregion
46701
46758
  //#region src/cli/utils/version.ts
46702
- const VERSION = "0.7.6-dev.a8cd24e";
46759
+ const VERSION = "0.7.6-dev.b36e439";
46703
46760
  //#endregion
46704
46761
  //#region src/cli/utils/json-mode.ts
46705
46762
  let context = null;
@@ -47068,13 +47125,13 @@ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
47068
47125
  * uploads source-map artifacts under, so stack frames symbolicate. Honors the
47069
47126
  * standard `SENTRY_RELEASE` override.
47070
47127
  */
47071
- const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.a8cd24e`;
47128
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.7.6-dev.b36e439`;
47072
47129
  /**
47073
47130
  * Deployment environment shown in Sentry's environment filter. Defaults to
47074
47131
  * `production` for tagged releases and `development` for dev/unbuilt versions,
47075
47132
  * overridable via the standard `SENTRY_ENVIRONMENT` env var.
47076
47133
  */
47077
- const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.a8cd24e") ? "development" : "production");
47134
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.7.6-dev.b36e439") ? "development" : "production");
47078
47135
  /**
47079
47136
  * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
47080
47137
  * (set to `0` to disable tracing) and falls back to
@@ -47266,7 +47323,7 @@ const NON_BLOCKING_REACT_DOCTOR_COMMAND = [
47266
47323
  "fi"
47267
47324
  ].join(" ");
47268
47325
  const PACKAGE_JSON_FILE_NAME = "package.json";
47269
- const runGit = (projectRoot, args) => {
47326
+ const runGitRaw = (projectRoot, args) => {
47270
47327
  try {
47271
47328
  return execFileSync("git", [...args], {
47272
47329
  cwd: projectRoot,
@@ -47277,11 +47334,12 @@ const runGit = (projectRoot, args) => {
47277
47334
  "ignore"
47278
47335
  ],
47279
47336
  maxBuffer: RUN_GIT_MAX_BUFFER_BYTES
47280
- }).trim();
47337
+ });
47281
47338
  } catch {
47282
47339
  return null;
47283
47340
  }
47284
47341
  };
47342
+ const runGit = (projectRoot, args) => runGitRaw(projectRoot, args)?.trim() ?? null;
47285
47343
  const resolveGitPath = (baseDirectory, value) => Path.isAbsolute(value) ? value : Path.resolve(baseDirectory, value);
47286
47344
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
47287
47345
  const getPackageJsonPath = (projectRoot) => Path.join(projectRoot, PACKAGE_JSON_FILE_NAME);
@@ -59152,6 +59210,17 @@ const buildRuleBlastRadii = (diagnostics) => buildSortedRuleGroups(diagnostics).
59152
59210
  })).toSorted((left, right) => right.fileCount - left.fileCount);
59153
59211
  const findMigrationScaleBuckets = (diagnostics) => buildRuleBlastRadii(diagnostics).filter((bucket) => bucket.fileCount >= 40);
59154
59212
  //#endregion
59213
+ //#region src/cli/utils/has-lint-hard-failure.ts
59214
+ const hasLintHardFailure = (result) => result.skippedChecks.includes("lint");
59215
+ //#endregion
59216
+ //#region src/cli/utils/is-inspect-result-complete.ts
59217
+ const isInspectResultComplete = (result) => isScanComplete({
59218
+ analyzedFileCount: result.analyzedFiles?.length,
59219
+ scannedFileCount: result.scannedFileCount,
59220
+ skippedCheckCount: result.skippedChecks.length,
59221
+ skippedCheckReasonCount: Object.keys(result.skippedCheckReasons ?? {}).length
59222
+ });
59223
+ //#endregion
59155
59224
  //#region src/cli/utils/scan-result-cache.ts
59156
59225
  const CACHE_DISABLED_VALUES = new Set(["1", "true"]);
59157
59226
  const TOOLCHAIN_PACKAGE_SPECIFIERS = [
@@ -59557,15 +59626,12 @@ const buildOutcomeAttributes = (input) => {
59557
59626
  const summary = summarizeDiagnostics(result.diagnostics);
59558
59627
  const blockingLevel = resolveTelemetryBlocking(input.userConfig);
59559
59628
  const gateDiagnostics = filterDiagnosticsForSurface(result.diagnostics, "ciFailure", input.userConfig);
59629
+ const complete = isInspectResultComplete(result);
59630
+ const didLintHardFail = hasLintHardFailure(result);
59631
+ const failsOnHardFailure = didLintHardFail && blockingLevel !== "none";
59560
59632
  const wouldBlock = !input.scoreOnly && !input.gateExempt && shouldBlockCi(gateDiagnostics, blockingLevel);
59561
- const complete = isScanComplete({
59562
- analyzedFileCount: result.analyzedFiles?.length,
59563
- scannedFileCount: result.scannedFileCount,
59564
- skippedCheckCount: result.skippedChecks.length,
59565
- skippedCheckReasonCount: Object.keys(result.skippedCheckReasons ?? {}).length
59566
- });
59567
59633
  const isClean = result.diagnostics.length === 0 && complete;
59568
- const outcome = wouldBlock ? "blocked" : isClean ? "clean" : "ok";
59634
+ const outcome = didLintHardFail ? "error" : wouldBlock ? "blocked" : isClean ? "clean" : "ok";
59569
59635
  const firings = summarizeRuleFirings(result.diagnostics);
59570
59636
  const countByRule = /* @__PURE__ */ new Map();
59571
59637
  const countByCategory = /* @__PURE__ */ new Map();
@@ -59610,7 +59676,7 @@ const buildOutcomeAttributes = (input) => {
59610
59676
  const attributes = {
59611
59677
  ...withNamespace("outcome", {
59612
59678
  status: outcome,
59613
- exitCode: wouldBlock ? 1 : 0,
59679
+ exitCode: failsOnHardFailure || wouldBlock ? 1 : 0,
59614
59680
  wouldBlock,
59615
59681
  blocking: blockingLevel,
59616
59682
  clean: isClean,
@@ -63792,10 +63858,15 @@ const warnIfAiTrainingEnvironment = () => {
63792
63858
  //#region src/cli/utils/validate-mode-flags.ts
63793
63859
  const usedDiffAlias = (flags) => flags.diff !== void 0 && flags.diff !== false && flags.diff !== "false" && flags.diff !== "";
63794
63860
  const usedScope = (flags) => typeof flags.scope === "string" && flags.scope.length > 0;
63861
+ const validateIncludeUntrackedScope = (includeUntracked, scope) => {
63862
+ if (!includeUntracked || scope !== void 0 && scope !== "full") return;
63863
+ throw new CliInputError("--include-untracked requires the files, changed, or lines scope (via --scope, --diff, or config).");
63864
+ };
63795
63865
  const validateModeFlags = (flags) => {
63796
63866
  if (usedScope(flags) && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --scope and --diff; --diff is the deprecated alias.");
63797
63867
  if (flags.staged && usedDiffAlias(flags)) throw new CliInputError("Cannot combine --staged and --diff; pick one mode.");
63798
63868
  if (flags.staged && (flags.scope === "full" || flags.scope === "changed")) throw new CliInputError(`Cannot combine --staged with --scope ${flags.scope}; use --scope files or --scope lines, or drop --scope.`);
63869
+ if (flags.includeUntracked && flags.staged) throw new CliInputError("Cannot combine --include-untracked with --staged; the git index never holds untracked files.");
63799
63870
  if (flags.score && flags.json) throw new CliInputError("Cannot combine --score and --json; pick one output mode.");
63800
63871
  if (flags.score && flags.telemetry === false) throw new CliInputError("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
63801
63872
  if (flags.debug && (flags.score === false || flags.telemetry === false)) {
@@ -63804,6 +63875,39 @@ const validateModeFlags = (flags) => {
63804
63875
  }
63805
63876
  };
63806
63877
  //#endregion
63878
+ //#region src/cli/utils/parse-staged-snapshot-divergences.ts
63879
+ const SNAPSHOT_CONFIG_FILENAMES = new Set([...STAGED_FILES_PROJECT_CONFIG_FILENAMES, ...STAGED_SNAPSHOT_ADDITIONAL_CONFIG_FILENAMES]);
63880
+ const parseStagedSnapshotDivergences = (statusOutput) => {
63881
+ const divergentConfigFiles = /* @__PURE__ */ new Set();
63882
+ const statusEntries = statusOutput.split("\0").filter((entry) => entry.length > 0);
63883
+ for (let entryIndex = 0; entryIndex < statusEntries.length; entryIndex += 1) {
63884
+ const entry = statusEntries[entryIndex];
63885
+ const [indexStatus, worktreeStatus] = entry;
63886
+ const filePath = entry.slice(3);
63887
+ const hasRenameOrCopySource = indexStatus === "R" || indexStatus === "C" || worktreeStatus === "R" || worktreeStatus === "C";
63888
+ const sourcePath = hasRenameOrCopySource ? statusEntries[entryIndex + 1] : void 0;
63889
+ const worktreePaths = worktreeStatus === "R" || worktreeStatus === "C" ? [filePath, sourcePath] : [filePath];
63890
+ if (worktreeStatus !== " ") {
63891
+ for (const worktreePath of worktreePaths) if (worktreePath && SNAPSHOT_CONFIG_FILENAMES.has(Path.basename(worktreePath))) divergentConfigFiles.add(worktreePath);
63892
+ }
63893
+ if (hasRenameOrCopySource) entryIndex += 1;
63894
+ }
63895
+ return [...divergentConfigFiles].sort();
63896
+ };
63897
+ //#endregion
63898
+ //#region src/cli/utils/find-staged-snapshot-divergences.ts
63899
+ const findStagedSnapshotDivergences = (directory) => {
63900
+ const statusOutput = runGitRaw(directory, [
63901
+ "status",
63902
+ "--porcelain=v1",
63903
+ "-z",
63904
+ "--untracked-files=all",
63905
+ "--ignored=matching"
63906
+ ]);
63907
+ if (statusOutput === null) return null;
63908
+ return parseStagedSnapshotDivergences(statusOutput);
63909
+ };
63910
+ //#endregion
63807
63911
  //#region src/cli/commands/inspect.ts
63808
63912
  const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
63809
63913
  if (categoryFilters.size === 0) return [...completedScans];
@@ -63818,9 +63922,14 @@ const filterCompletedScansByCategories = (completedScans, categoryFilters) => {
63818
63922
  /**
63819
63923
  * Post-scan finalization shared by the staged-arm and project-loop
63820
63924
  * paths of `inspectAction`: emit the JSON report (when in JSON mode)
63821
- * and set `process.exitCode = 1` when a diagnostic at or above the
63822
- * `--blocking` threshold (default `"error"`) reaches the `ciFailure`
63823
- * surface. `--blocking none` keeps the scan advisory (always exits 0).
63925
+ * and set `process.exitCode = 1` when any scan's lint pass hard-failed
63926
+ * (an engine/plugin/binding failure destroys the findings, so success
63927
+ * would be a false clean) or a diagnostic at or above the `--blocking`
63928
+ * threshold (default `"error"`) reaches the `ciFailure` surface.
63929
+ * `--blocking none` keeps the scan advisory (always exits 0), and
63930
+ * fail-open degradations — `--no-lint`, `--max-duration` truncation,
63931
+ * supply-chain/security skips — stay advisory too, surfaced through
63932
+ * `complete: false` in the JSON report.
63824
63933
  */
63825
63934
  const finalizeScans = (input) => {
63826
63935
  const baselineDeltas = input.completedScans.flatMap((scan) => scan.result.baselineDelta ? [scan.result.baselineDelta] : []);
@@ -63850,8 +63959,13 @@ const finalizeScans = (input) => {
63850
63959
  baselineDegraded
63851
63960
  }));
63852
63961
  }
63962
+ const blockingLevel = resolveBlockingLevel(input.flags, input.userConfig);
63963
+ if (input.completedScans.some(({ result }) => hasLintHardFailure(result)) && blockingLevel !== "none") {
63964
+ process.exitCode = 1;
63965
+ return;
63966
+ }
63853
63967
  if (input.isScoreOnly || baselineDegraded) return;
63854
- if (shouldBlockCi(filterScansForSurface(input.completedScans, "ciFailure"), resolveBlockingLevel(input.flags, input.userConfig))) process.exitCode = 1;
63968
+ if (shouldBlockCi(filterScansForSurface(input.completedScans, "ciFailure"), blockingLevel)) process.exitCode = 1;
63855
63969
  };
63856
63970
  const buildChangedFilesDiffInfo = (changedFiles) => ({
63857
63971
  currentBranch: process.env.GITHUB_HEAD_REF?.trim() || null,
@@ -63889,6 +64003,7 @@ const inspectAction = async (directory, flags) => {
63889
64003
  recordCount(METRIC.cliInvoked, 1, { command: "inspect" });
63890
64004
  try {
63891
64005
  validateModeFlags(flags);
64006
+ if (flags.staged) setJsonReportMode("staged");
63892
64007
  await maybeMigrateLegacyConfig(requestedDirectory, {
63893
64008
  isQuiet,
63894
64009
  isStaged: Boolean(flags.staged)
@@ -63904,6 +64019,14 @@ const inspectAction = async (directory, flags) => {
63904
64019
  cliLogger.dim(`Redirected to ${highlighter.info(toRelativePath(resolvedDirectory, requestedDirectory))} via react-doctor config "rootDir".`);
63905
64020
  cliLogger.break();
63906
64021
  }
64022
+ if (flags.staged) {
64023
+ const divergentConfigFiles = findStagedSnapshotDivergences(resolvedDirectory);
64024
+ if (divergentConfigFiles === null) throw new CliInputError("Could not verify that staged configuration matches the worktree. Run the command from a Git worktree with Git available.");
64025
+ if (divergentConfigFiles.length > 0) {
64026
+ recordCount(METRIC.stagedSnapshotDivergence, 1, { divergentInputCount: divergentConfigFiles.length });
64027
+ throw new CliInputError(`Cannot scan staged files while configuration differs between the index and worktree: ${divergentConfigFiles.join(", ")}. Stage or restore those files, then rerun react-doctor --staged.`);
64028
+ }
64029
+ }
63907
64030
  const explainArgument = flags.explain;
63908
64031
  if (explainArgument !== void 0) {
63909
64032
  await runExplain(explainArgument, {
@@ -63924,7 +64047,6 @@ const inspectAction = async (directory, flags) => {
63924
64047
  json: flags.json
63925
64048
  });
63926
64049
  if (flags.staged) {
63927
- setJsonReportMode("staged");
63928
64050
  const stagedFiles = await getStagedSourceFiles(resolvedDirectory);
63929
64051
  if (stagedFiles.length === 0) {
63930
64052
  if (isJsonMode) writeJsonReport(buildJsonReport({
@@ -64005,13 +64127,15 @@ const inspectAction = async (directory, flags) => {
64005
64127
  const projectDirectories = await selectProjects(resolvedDirectory, flags.project, skipPrompts, userConfig?.projects);
64006
64128
  const changedFilesDiffInfo = flags.changedFilesFrom ? buildChangedFilesDiffInfo(readChangedFilesFrom(Path.resolve(flags.changedFilesFrom))) : null;
64007
64129
  const requestedScope = resolveScope(flags, userConfig);
64130
+ const includeUntracked = flags.includeUntracked ?? false;
64008
64131
  const scopeRequest = requestedScope.scope === void 0 && changedFilesDiffInfo !== null ? {
64009
64132
  ...requestedScope,
64010
64133
  scope: "changed"
64011
64134
  } : requestedScope;
64135
+ validateIncludeUntrackedScope(includeUntracked, scopeRequest.scope);
64012
64136
  const wantsDiffMode = scopeRequest.scope !== void 0 && scopeRequest.scope !== "full";
64013
64137
  const shouldDetectDiff = changedFilesDiffInfo === null && (wantsDiffMode || scopeRequest.scope === void 0 && !skipPrompts && !isQuiet);
64014
- const diffInfo = changedFilesDiffInfo ?? (shouldDetectDiff ? await getDiffInfo(resolvedDirectory, scopeRequest.base) : null);
64138
+ const diffInfo = changedFilesDiffInfo ?? (shouldDetectDiff ? await getDiffInfo(resolvedDirectory, scopeRequest.base, includeUntracked) : null);
64015
64139
  const scope = await finalizeScope({
64016
64140
  requested: scopeRequest,
64017
64141
  diffInfo,
@@ -64025,7 +64149,8 @@ const inspectAction = async (directory, flags) => {
64025
64149
  const changedLineRanges = scope === "lines" && diffInfo !== null && (diffInfo.isCurrentChanges || linesBaseRef !== null) && diffInfo !== null ? await getChangedLineRanges({
64026
64150
  directory: resolvedDirectory,
64027
64151
  baseRef: linesBaseRef ?? void 0,
64028
- files: [...diffInfo.changedFiles]
64152
+ files: [...diffInfo.changedFiles],
64153
+ includeUntracked
64029
64154
  }) : null;
64030
64155
  if (scope === "lines" && changedLineRanges === null && !isQuiet) {
64031
64156
  cliLogger.warn("Could not determine changed lines (no base ref or git diff failed); reporting all issues in changed files.");
@@ -64958,6 +65083,7 @@ const ROOT_FLAG_SPEC = {
64958
65083
  "--dead-code",
64959
65084
  "--debug",
64960
65085
  "--help",
65086
+ "--include-untracked",
64961
65087
  "--json",
64962
65088
  "--json-compact",
64963
65089
  "--lint",
@@ -65221,7 +65347,7 @@ ${highlighter.dim("Learn more:")}
65221
65347
  ${highlighter.info(CI_URL)}
65222
65348
  `;
65223
65349
  const collectCategoryOption = (value, previousValues) => [...previousValues ?? [], value];
65224
- 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("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").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 supported JS/TS source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch 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("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").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);
65350
+ 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("--supply-chain", "enable the dependency supply-chain scan (default)").option("--no-supply-chain", "skip the dependency supply-chain scan (Socket.dev dependency health checks)").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 supported JS/TS source to scan/report: full (default), files, changed (only new issues vs base), or lines (issues whose source spans touch changed lines)").option("--base <ref>", "base git ref for files/changed/lines scope (auto-detected when omitted)").option("--include-untracked", "with --scope files/changed/lines, also scan ordinary untracked files (respects .gitignore)").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("--max-duration <seconds>", "scan time budget for the whole run, shared across workspace projects: past it, remaining lint batches and dead-code are skipped and partial results are reported (skipped files are listed in the JSON report)").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);
65225
65351
  program.action(inspectAction);
65226
65352
  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));
65227
65353
  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);
@@ -65271,4 +65397,4 @@ Promise.resolve().then(() => assertNoRemovedFlags(process.argv)).then(() => prog
65271
65397
  export {};
65272
65398
 
65273
65399
  //# sourceMappingURL=cli.js.map
65274
- //# debugId=9a5c1a3c-7706-5a23-bca4-ddddd2f085af
65400
+ //# debugId=58e54b02-7ba2-5f13-a535-98c236467a86
package/dist/index.d.ts CHANGED
@@ -10256,7 +10256,7 @@ declare const buildJsonReport: (input: BuildJsonReportInput) => JsonReportV3$1;
10256
10256
  * render diff-base mistakes (`GitBaseBranchInvalid` /
10257
10257
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
10258
10258
  */
10259
- declare const getDiffInfo: (directory: string, explicitBaseBranch?: string) => Promise<DiffInfo | null>;
10259
+ declare const getDiffInfo: (directory: string, explicitBaseBranch?: string, includeUntracked?: boolean) => Promise<DiffInfo | null>;
10260
10260
  declare const filterSourceFiles: (filePaths: string[]) => string[];
10261
10261
  /**
10262
10262
  * Programmatic façade over `Git.changedLineRanges` (the `lines` scope). Diffs
package/dist/index.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]="11364a34-6c8b-5bd3-b9e8-aaa7c0ea264c")}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]="2aa6fc3c-96ab-5a4c-8a52-e74d27fb4b5a")}catch(e){}}();
3
3
  import { r as __toESM$1, t as __commonJSMin$1 } from "./chunk-N93fKeF6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import reactDoctorPlugin, { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor";
@@ -38579,6 +38579,7 @@ const parseGithubViewerPermission = (stdout) => {
38579
38579
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
38580
38580
  };
38581
38581
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
38582
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
38582
38583
  /**
38583
38584
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
38584
38585
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -38649,6 +38650,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38649
38650
  args,
38650
38651
  directory
38651
38652
  });
38653
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
38654
+ "ls-files",
38655
+ "-z",
38656
+ "--others",
38657
+ "--exclude-standard",
38658
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
38659
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
38660
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
38652
38661
  const currentBranch = (directory) => runGit(directory, [
38653
38662
  "rev-parse",
38654
38663
  "--abbrev-ref",
@@ -38771,7 +38780,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38771
38780
  githubViewerPermission,
38772
38781
  branchExists,
38773
38782
  mergeBase,
38774
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
38783
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
38775
38784
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
38776
38785
  if (explicitBaseBranch !== void 0) {
38777
38786
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -38800,7 +38809,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38800
38809
  "HEAD"
38801
38810
  ]);
38802
38811
  if (uncommitted.status !== 0) return null;
38803
- const files = splitNullSeparated(uncommitted.stdout);
38812
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
38804
38813
  if (files.length === 0) return null;
38805
38814
  return {
38806
38815
  currentBranch: resolvedCurrentBranch,
@@ -38830,7 +38839,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38830
38839
  currentBranch: resolvedCurrentBranch,
38831
38840
  baseBranch,
38832
38841
  diffBaseRef: mergeBaseRef,
38833
- changedFiles: splitNullSeparated(diff.stdout),
38842
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
38834
38843
  isCurrentChanges: false
38835
38844
  };
38836
38845
  }).pipe(withSpan("Git.diffSelection")),
@@ -38876,7 +38885,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38876
38885
  stdout: result.stdout
38877
38886
  };
38878
38887
  }).pipe(withSpan("Git.grep")),
38879
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
38888
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
38880
38889
  if (files.length === 0) return [];
38881
38890
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
38882
38891
  const result = yield* runGit(directory, [
@@ -38890,7 +38899,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38890
38899
  ...files
38891
38900
  ]);
38892
38901
  if (result.status !== 0) return null;
38893
- return parseChangedLineRanges(result.stdout);
38902
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
38903
+ if (cached || !includeUntracked) return changedLineRanges;
38904
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
38905
+ if (untrackedFilePaths === null) return changedLineRanges;
38906
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
38907
+ file,
38908
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
38909
+ }))];
38894
38910
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38895
38911
  });
38896
38912
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
@@ -40399,11 +40415,11 @@ const buildRelatedLocations = (labels, filePath) => {
40399
40415
  }
40400
40416
  return related;
40401
40417
  };
40402
- const isOxlintOutput = (value) => {
40403
- if (typeof value !== "object" || value === null) return false;
40404
- const candidate = value;
40405
- return Array.isArray(candidate.diagnostics);
40406
- };
40418
+ const isOxlintSpan = (value) => isRecord(value) && typeof value.offset === "number" && typeof value.length === "number" && typeof value.line === "number" && typeof value.column === "number";
40419
+ const isOxlintLabel = (value) => isRecord(value) && isOxlintSpan(value.span);
40420
+ const isMappableOxlintDiagnostic = (value) => isRecord(value) && typeof value.code === "string" && value.code.length > 0 && typeof value.filename === "string" && value.filename.length > 0 && (value.severity === "warning" || value.severity === "error") && Array.isArray(value.labels) && value.labels.every(isOxlintLabel);
40421
+ const isEngineFailureDiagnostic = (value) => !isRecord(value) || typeof value.filename !== "string" || value.filename.length === 0;
40422
+ const isOxlintOutput = (value) => isRecord(value) && Array.isArray(value.diagnostics);
40407
40423
  /**
40408
40424
  * Parses one oxlint subprocess's stdout into a flat `Diagnostic[]`.
40409
40425
  * Tolerates the leading-notice-line shape oxlint sometimes prints
@@ -40422,6 +40438,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40422
40438
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40423
40439
  }
40424
40440
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40441
+ const engineFailureDiagnostic = parsed.diagnostics.find(isEngineFailureDiagnostic);
40442
+ if (engineFailureDiagnostic !== void 0) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: JSON.stringify(engineFailureDiagnostic).slice(0, 600) }) });
40425
40443
  const minifiedFileCache = /* @__PURE__ */ new Map();
40426
40444
  const sourceBufferCache = /* @__PURE__ */ new Map();
40427
40445
  const sourceLinesCache = /* @__PURE__ */ new Map();
@@ -40456,7 +40474,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40456
40474
  minifiedFileCache.set(absolutePath, minified);
40457
40475
  return minified;
40458
40476
  };
40459
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40477
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40460
40478
  const { plugin, rule } = parseRuleCode(diagnostic.code);
40461
40479
  const primaryLabel = diagnostic.labels[0];
40462
40480
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -42948,10 +42966,11 @@ const buildSkippedChecks = (input) => {
42948
42966
  * render diff-base mistakes (`GitBaseBranchInvalid` /
42949
42967
  * `GitBaseBranchMissing`) as clean user errors instead of crashes.
42950
42968
  */
42951
- const getDiffInfo = (directory, explicitBaseBranch) => runPromise(gen(function* () {
42969
+ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runPromise(gen(function* () {
42952
42970
  const selection = yield* (yield* Git).diffSelection({
42953
42971
  directory,
42954
- explicitBaseBranch
42972
+ explicitBaseBranch,
42973
+ includeUntracked
42955
42974
  });
42956
42975
  if (selection === null) return null;
42957
42976
  return {
@@ -43241,4 +43260,4 @@ const toJsonReport = (result, options) => buildJsonReport({
43241
43260
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
43242
43261
 
43243
43262
  //# sourceMappingURL=index.js.map
43244
- //# debugId=11364a34-6c8b-5bd3-b9e8-aaa7c0ea264c
43263
+ //# debugId=2aa6fc3c-96ab-5a4c-8a52-e74d27fb4b5a
package/dist/lsp.js CHANGED
@@ -38557,6 +38557,7 @@ const parseGithubViewerPermission = (stdout) => {
38557
38557
  return /^[A-Z_]+$/.test(value) ? value.toLowerCase() : null;
38558
38558
  };
38559
38559
  const splitNullSeparated = (value) => value.split("\0").filter((entry) => entry.length > 0);
38560
+ const UNTRACKED_FILE_LAST_LINE = Number.MAX_SAFE_INTEGER;
38560
38561
  /**
38561
38562
  * `Git` wraps every `git`-via-subprocess call react-doctor makes
38562
38563
  * behind a `Context.Service`. The production layer (`layerNode`)
@@ -38627,6 +38628,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38627
38628
  args,
38628
38629
  directory
38629
38630
  });
38631
+ const listUntrackedFilePaths = (directory, includePaths = []) => runGit(directory, [
38632
+ "ls-files",
38633
+ "-z",
38634
+ "--others",
38635
+ "--exclude-standard",
38636
+ ...includePaths.length > 0 ? ["--", ...includePaths] : []
38637
+ ]).pipe(map$3((result) => result.status === 0 ? splitNullSeparated(result.stdout) : null));
38638
+ const mergeUntracked = (directory, trackedFilePaths, includeUntracked) => includeUntracked ? listUntrackedFilePaths(directory).pipe(map$3((untracked) => untracked === null ? trackedFilePaths : [...new Set([...trackedFilePaths, ...untracked])])) : succeed$2(trackedFilePaths);
38630
38639
  const currentBranch = (directory) => runGit(directory, [
38631
38640
  "rev-parse",
38632
38641
  "--abbrev-ref",
@@ -38749,7 +38758,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38749
38758
  githubViewerPermission,
38750
38759
  branchExists,
38751
38760
  mergeBase,
38752
- diffSelection: ({ directory, explicitBaseBranch }) => gen(function* () {
38761
+ diffSelection: ({ directory, explicitBaseBranch, includeUntracked = false }) => gen(function* () {
38753
38762
  if (explicitBaseBranch !== void 0 && explicitBaseBranch.trim().length === 0) return yield* fail$4(new ReactDoctorError({ reason: new GitBaseBranchInvalid({ detail: "Diff base branch cannot be empty." }) }));
38754
38763
  if (explicitBaseBranch !== void 0) {
38755
38764
  const range = parseGitDiffRange(explicitBaseBranch);
@@ -38778,7 +38787,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38778
38787
  "HEAD"
38779
38788
  ]);
38780
38789
  if (uncommitted.status !== 0) return null;
38781
- const files = splitNullSeparated(uncommitted.stdout);
38790
+ const files = yield* mergeUntracked(directory, splitNullSeparated(uncommitted.stdout), includeUntracked);
38782
38791
  if (files.length === 0) return null;
38783
38792
  return {
38784
38793
  currentBranch: resolvedCurrentBranch,
@@ -38808,7 +38817,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38808
38817
  currentBranch: resolvedCurrentBranch,
38809
38818
  baseBranch,
38810
38819
  diffBaseRef: mergeBaseRef,
38811
- changedFiles: splitNullSeparated(diff.stdout),
38820
+ changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
38812
38821
  isCurrentChanges: false
38813
38822
  };
38814
38823
  }).pipe(withSpan("Git.diffSelection")),
@@ -38854,7 +38863,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
38854
38863
  stdout: result.stdout
38855
38864
  };
38856
38865
  }).pipe(withSpan("Git.grep")),
38857
- changedLineRanges: ({ directory, baseRef, cached, files }) => gen(function* () {
38866
+ changedLineRanges: ({ directory, baseRef, cached, files, includeUntracked = false }) => gen(function* () {
38858
38867
  if (files.length === 0) return [];
38859
38868
  if (baseRef !== void 0 && !isSafeGitRevision(baseRef)) return null;
38860
38869
  const result = yield* runGit(directory, [
@@ -38868,7 +38877,14 @@ var Git = class Git extends Service()("react-doctor/Git") {
38868
38877
  ...files
38869
38878
  ]);
38870
38879
  if (result.status !== 0) return null;
38871
- return parseChangedLineRanges(result.stdout);
38880
+ const changedLineRanges = parseChangedLineRanges(result.stdout);
38881
+ if (cached || !includeUntracked) return changedLineRanges;
38882
+ const untrackedFilePaths = yield* listUntrackedFilePaths(directory, files);
38883
+ if (untrackedFilePaths === null) return changedLineRanges;
38884
+ return [...changedLineRanges, ...untrackedFilePaths.map((file) => ({
38885
+ file,
38886
+ ranges: [[1, UNTRACKED_FILE_LAST_LINE]]
38887
+ }))];
38872
38888
  }).pipe(catch_$1((error) => error.reason._tag === "GitInvocationFailed" ? succeed$2(null) : fail$4(error)), withSpan("Git.changedLineRanges"))
38873
38889
  });
38874
38890
  })).pipe(provide$2(layer$2.pipe(provide$2(mergeAll$1(layer$1, layer)))));
@@ -40377,11 +40393,11 @@ const buildRelatedLocations = (labels, filePath) => {
40377
40393
  }
40378
40394
  return related;
40379
40395
  };
40380
- const isOxlintOutput = (value) => {
40381
- if (typeof value !== "object" || value === null) return false;
40382
- const candidate = value;
40383
- return Array.isArray(candidate.diagnostics);
40384
- };
40396
+ const isOxlintSpan = (value) => isRecord(value) && typeof value.offset === "number" && typeof value.length === "number" && typeof value.line === "number" && typeof value.column === "number";
40397
+ const isOxlintLabel = (value) => isRecord(value) && isOxlintSpan(value.span);
40398
+ const isMappableOxlintDiagnostic = (value) => isRecord(value) && typeof value.code === "string" && value.code.length > 0 && typeof value.filename === "string" && value.filename.length > 0 && (value.severity === "warning" || value.severity === "error") && Array.isArray(value.labels) && value.labels.every(isOxlintLabel);
40399
+ const isEngineFailureDiagnostic = (value) => !isRecord(value) || typeof value.filename !== "string" || value.filename.length === 0;
40400
+ const isOxlintOutput = (value) => isRecord(value) && Array.isArray(value.diagnostics);
40385
40401
  /**
40386
40402
  * Parses one oxlint subprocess's stdout into a flat `Diagnostic[]`.
40387
40403
  * Tolerates the leading-notice-line shape oxlint sometimes prints
@@ -40400,6 +40416,8 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40400
40416
  throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40401
40417
  }
40402
40418
  if (!isOxlintOutput(parsed)) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: stdout.slice(0, 600) }) });
40419
+ const engineFailureDiagnostic = parsed.diagnostics.find(isEngineFailureDiagnostic);
40420
+ if (engineFailureDiagnostic !== void 0) throw new ReactDoctorError({ reason: new OxlintOutputUnparseable({ preview: JSON.stringify(engineFailureDiagnostic).slice(0, 600) }) });
40403
40421
  const minifiedFileCache = /* @__PURE__ */ new Map();
40404
40422
  const sourceBufferCache = /* @__PURE__ */ new Map();
40405
40423
  const sourceLinesCache = /* @__PURE__ */ new Map();
@@ -40434,7 +40452,7 @@ const parseOxlintOutput = (stdout, project, rootDirectory) => {
40434
40452
  minifiedFileCache.set(absolutePath, minified);
40435
40453
  return minified;
40436
40454
  };
40437
- return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => diagnostic.code && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40455
+ return suppressMemoizationInBailedOutFunctions(parsed.diagnostics.filter((diagnostic) => isMappableOxlintDiagnostic(diagnostic) && isLintableSourceFile(diagnostic.filename) && !isMinifiedDiagnosticFile(diagnostic.filename) && !shouldSuppressLocalUseHookDiagnostic(diagnostic, rootDirectory) && !shouldSuppressCompilerFindingInWorklet(diagnostic, project, rootDirectory)).map((diagnostic) => {
40438
40456
  const { plugin, rule } = parseRuleCode(diagnostic.code);
40439
40457
  const primaryLabel = diagnostic.labels[0];
40440
40458
  const cleaned = cleanDiagnosticMessage(diagnostic.message, diagnostic.help, plugin, rule, project);
@@ -44524,6 +44542,7 @@ const METRIC = {
44524
44542
  cliInvoked: "cli.invoked",
44525
44543
  cliError: "cli.error",
44526
44544
  cliEnvironmentError: "cli.env_error",
44545
+ stagedSnapshotDivergence: "staged.snapshot_divergence",
44527
44546
  projectDetected: "project.detected",
44528
44547
  projectPathSelected: "project.path_selected",
44529
44548
  projectConfigSelected: "project.config_selected",
@@ -44878,5 +44897,5 @@ const startLanguageServer = () => {
44878
44897
  };
44879
44898
  //#endregion
44880
44899
  export { startLanguageServer };
44881
- !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]="f9337714-b38f-5f28-ac00-c9d7d79ebaea")}catch(e){}}();
44882
- //# debugId=f9337714-b38f-5f28-ac00-c9d7d79ebaea
44900
+ !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]="8ed74459-2a69-556c-bb95-8cdebdb094cd")}catch(e){}}();
44901
+ //# debugId=8ed74459-2a69-556c-bb95-8cdebdb094cd
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.7.6-dev.a8cd24e",
3
+ "version": "0.7.6-dev.b36e439",
4
4
  "description": "Your agent writes bad React. This catches it",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -65,8 +65,8 @@
65
65
  "vscode-languageserver-textdocument": "^1.0.12",
66
66
  "vscode-uri": "^3.1.0",
67
67
  "yaml": "^2.9.0",
68
- "oxlint-plugin-react-doctor": "0.7.6-dev.a8cd24e",
69
- "deslop-js": "0.7.6"
68
+ "deslop-js": "0.7.6",
69
+ "oxlint-plugin-react-doctor": "0.7.6-dev.b36e439"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/babel__code-frame": "^7.27.0",
@@ -75,8 +75,8 @@
75
75
  "commander": "^14.0.3",
76
76
  "ora": "^9.4.0",
77
77
  "@react-doctor/api": "0.7.6",
78
- "@react-doctor/language-server": "0.7.6",
79
- "@react-doctor/core": "0.7.6"
78
+ "@react-doctor/core": "0.7.6",
79
+ "@react-doctor/language-server": "0.7.6"
80
80
  },
81
81
  "engines": {
82
82
  "node": "^20.19.0 || >=22.13.0"