react-doctor 0.2.14-dev.b3c3aa9 → 0.2.14-dev.b9e9bcb

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.
Files changed (2) hide show
  1. package/dist/cli.js +333 -30
  2. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -30,6 +30,7 @@ import * as ChildProcess from "effect/unstable/process/ChildProcess";
30
30
  import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
31
31
  import * as ts from "typescript";
32
32
  import { gzipSync } from "node:zlib";
33
+ import * as Sentry from "@sentry/node";
33
34
  import { performance } from "node:perf_hooks";
34
35
  import { stripVTControlCharacters } from "node:util";
35
36
  import tty from "node:tty";
@@ -6313,6 +6314,7 @@ const STAGED_FILES_PROJECT_CONFIG_FILENAMES = [
6313
6314
  ".oxlintrc.json"
6314
6315
  ];
6315
6316
  const CANONICAL_GITHUB_URL = "https://github.com/millionco/react-doctor";
6317
+ const CANONICAL_DISCORD_URL = "https://react.doctor/discord";
6316
6318
  const SKILL_NAME = "react-doctor";
6317
6319
  const OXLINT_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
6318
6320
  const OXLINT_SPAWN_TIMEOUT_MS = 6e4;
@@ -11417,6 +11419,25 @@ const highlighter = {
11417
11419
  bold: import_picocolors.default.bold
11418
11420
  };
11419
11421
  /**
11422
+ * Override picocolors' automatic color detection. picocolors decides
11423
+ * once, at import time, from `NO_COLOR` / `FORCE_COLOR` / `TERM` / TTY.
11424
+ * This lets the CLI honor an explicit `--color` / `--no-color` flag
11425
+ * (clig.dev, Output: "Disable color … if the user requested it") by
11426
+ * swapping in a fresh set of formatters. Call it before any colored
11427
+ * output is produced. Every call site reads `highlighter.<method>` at
11428
+ * call time, so reassigning the properties propagates everywhere.
11429
+ */
11430
+ const setColorEnabled = (enabled) => {
11431
+ const colors = import_picocolors.default.createColors(enabled);
11432
+ highlighter.error = colors.red;
11433
+ highlighter.warn = colors.yellow;
11434
+ highlighter.info = colors.cyan;
11435
+ highlighter.success = colors.green;
11436
+ highlighter.dim = colors.dim;
11437
+ highlighter.gray = colors.gray;
11438
+ highlighter.bold = colors.bold;
11439
+ };
11440
+ /**
11420
11441
  * Canonical URL for a rule's reviewer-tested fix recipe, served at
11421
11442
  * `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md`. The
11422
11443
  * `/doctor` playbook fetches it on demand so each fix follows the
@@ -11448,6 +11469,46 @@ const groupBy = (items, keyFn) => {
11448
11469
  */
11449
11470
  const hasPublishedFixRecipe = (diagnostic) => diagnostic.plugin === "react-doctor" && Object.hasOwn(reactDoctorPlugin.rules, diagnostic.rule);
11450
11471
  //#endregion
11472
+ //#region src/cli/utils/constants.ts
11473
+ const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
11474
+ const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
11475
+ const SENTRY_DSN = "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920";
11476
+ //#endregion
11477
+ //#region src/cli/utils/version.ts
11478
+ const VERSION = "0.2.14-dev.b9e9bcb";
11479
+ //#endregion
11480
+ //#region src/instrument.ts
11481
+ let isInitialized = false;
11482
+ const shouldEnableSentry = () => {
11483
+ if (process.argv.includes("--no-score") || process.argv.includes("--no-telemetry")) return false;
11484
+ if (process.env.VITEST || process.env.NODE_ENV === "test") return false;
11485
+ return true;
11486
+ };
11487
+ /**
11488
+ * Initializes the Sentry Node SDK for CLI crash reporting. Invoked as
11489
+ * the first statement of the CLI entry (`cli/index.ts`) so the SDK's
11490
+ * global `uncaughtException` / `unhandledRejection` handlers are armed
11491
+ * before any command runs.
11492
+ *
11493
+ * Exported as a function rather than a bare side-effecting import
11494
+ * because the package declares `"sideEffects": false`, which lets the
11495
+ * bundler tree-shake side-effect-only modules. An explicit call keeps
11496
+ * the initialization in the published `dist/cli.js`.
11497
+ *
11498
+ * Scoped to the CLI application only — the programmatic
11499
+ * `@react-doctor/api` library never initializes Sentry, so importing
11500
+ * `diagnose()` into a consumer app can't hijack their telemetry.
11501
+ */
11502
+ const initializeSentry = () => {
11503
+ if (isInitialized || !shouldEnableSentry()) return;
11504
+ isInitialized = true;
11505
+ Sentry.init({
11506
+ dsn: SENTRY_DSN,
11507
+ sendDefaultPii: true,
11508
+ release: VERSION
11509
+ });
11510
+ };
11511
+ //#endregion
11451
11512
  //#region ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
11452
11513
  const ANSI_BACKGROUND_OFFSET = 10;
11453
11514
  const wrapAnsi16 = (offset = 0) => (code) => `\u001B[${code + offset}m`;
@@ -14401,29 +14462,60 @@ const CI_ENVIRONMENT_VARIABLES = [
14401
14462
  "GITLAB_CI",
14402
14463
  "CIRCLECI"
14403
14464
  ];
14404
- const CODING_AGENT_ENVIRONMENT_VARIABLES = [
14405
- "CLAUDECODE",
14406
- "CLAUDE_CODE",
14407
- "CURSOR_AGENT",
14408
- "CODEX_CI",
14409
- "CODEX_SANDBOX",
14410
- "CODEX_SANDBOX_NETWORK_DISABLED",
14411
- "OPENCODE",
14412
- "GOOSE_TERMINAL",
14413
- "AGENT_SESSION_ID",
14414
- "AMP_THREAD_ID",
14415
- "AGENT_THREAD_ID"
14465
+ const CI_PROVIDER_BY_ENVIRONMENT_VARIABLE = [
14466
+ ["GITHUB_ACTIONS", "github-actions"],
14467
+ ["GITLAB_CI", "gitlab-ci"],
14468
+ ["CIRCLECI", "circleci"],
14469
+ ["BUILDKITE", "buildkite"],
14470
+ ["JENKINS_URL", "jenkins"],
14471
+ ["TF_BUILD", "azure-pipelines"],
14472
+ ["CODEBUILD_BUILD_ID", "aws-codebuild"],
14473
+ ["TEAMCITY_VERSION", "teamcity"],
14474
+ ["BITBUCKET_BUILD_NUMBER", "bitbucket"],
14475
+ ["TRAVIS", "travis"],
14476
+ ["DRONE", "drone"]
14416
14477
  ];
14478
+ const CODING_AGENT_BY_ENVIRONMENT_VARIABLE = [
14479
+ ["CLAUDECODE", "claude-code"],
14480
+ ["CLAUDE_CODE", "claude-code"],
14481
+ ["CURSOR_AGENT", "cursor"],
14482
+ ["CODEX_CI", "codex"],
14483
+ ["CODEX_SANDBOX", "codex"],
14484
+ ["CODEX_SANDBOX_NETWORK_DISABLED", "codex"],
14485
+ ["OPENCODE", "opencode"],
14486
+ ["GOOSE_TERMINAL", "goose"],
14487
+ ["AMP_THREAD_ID", "amp"]
14488
+ ];
14489
+ const GENERIC_CODING_AGENT_ENVIRONMENT_VARIABLES = ["AGENT_SESSION_ID", "AGENT_THREAD_ID"];
14417
14490
  const CODING_AGENT_ENVIRONMENT_VALUE_VARIABLES = ["AGENT"];
14418
14491
  const CODING_AGENT_ENVIRONMENT_VALUES = { AGENT: ["amp", "goose"] };
14492
+ [...CODING_AGENT_BY_ENVIRONMENT_VARIABLE.map(([environmentVariable]) => environmentVariable), ...GENERIC_CODING_AGENT_ENVIRONMENT_VARIABLES];
14419
14493
  const FALSY_CI_FLAG_VALUES = new Set([
14420
14494
  "",
14421
14495
  "0",
14422
14496
  "false"
14423
14497
  ]);
14424
14498
  const isCiFlagSet = (value) => value !== void 0 && !FALSY_CI_FLAG_VALUES.has(value.toLowerCase());
14425
- const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || isCiFlagSet(process.env.CI);
14426
- const isCodingAgentEnvironment = () => CODING_AGENT_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) || CODING_AGENT_ENVIRONMENT_VALUE_VARIABLES.some((envVariable) => CODING_AGENT_ENVIRONMENT_VALUES[envVariable].some((value) => process.env[envVariable]?.toLowerCase() === value));
14499
+ const isCiEnvironment = () => CI_ENVIRONMENT_VARIABLES.some((environmentVariable) => Boolean(process.env[environmentVariable])) || isCiFlagSet(process.env.CI);
14500
+ const detectCiProvider = () => {
14501
+ for (const [environmentVariable, provider] of CI_PROVIDER_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return provider;
14502
+ return isCiFlagSet(process.env.CI) ? "unknown" : null;
14503
+ };
14504
+ const detectCodingAgentFromValue = () => {
14505
+ for (const environmentVariable of CODING_AGENT_ENVIRONMENT_VALUE_VARIABLES) {
14506
+ const value = process.env[environmentVariable]?.toLowerCase();
14507
+ if (value && CODING_AGENT_ENVIRONMENT_VALUES[environmentVariable].includes(value)) return value;
14508
+ }
14509
+ return null;
14510
+ };
14511
+ const detectCodingAgent = () => {
14512
+ for (const [environmentVariable, agent] of CODING_AGENT_BY_ENVIRONMENT_VARIABLE) if (process.env[environmentVariable]) return agent;
14513
+ const agentFromValue = detectCodingAgentFromValue();
14514
+ if (agentFromValue) return agentFromValue;
14515
+ if (GENERIC_CODING_AGENT_ENVIRONMENT_VARIABLES.some((environmentVariable) => process.env[environmentVariable])) return "unknown";
14516
+ return null;
14517
+ };
14518
+ const isCodingAgentEnvironment = () => detectCodingAgent() !== null;
14427
14519
  const isCiOrCodingAgentEnvironment = () => isCiEnvironment() || isCodingAgentEnvironment();
14428
14520
  //#endregion
14429
14521
  //#region src/cli/utils/is-non-interactive-environment.ts
@@ -14956,10 +15048,6 @@ const colorizeByScore = (text, score) => {
14956
15048
  return highlighter.error(text);
14957
15049
  };
14958
15050
  //#endregion
14959
- //#region src/cli/utils/constants.ts
14960
- const STAGED_FILES_TEMP_DIR_PREFIX = "react-doctor-staged-";
14961
- const INTERNAL_ERROR_JSON_FALLBACK = "{\"schemaVersion\":1,\"ok\":false,\"error\":{\"message\":\"Internal error\",\"name\":\"Error\",\"chain\":[]}}\n";
14962
- //#endregion
14963
15051
  //#region src/cli/utils/render-score-header.ts
14964
15052
  const RAINBOW_HUE_SHIFT_PER_FRAME = 9;
14965
15053
  const RAINBOW_GRADIENT_WIDTH = 80;
@@ -15344,9 +15432,6 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
15344
15432
  });
15345
15433
  const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
15346
15434
  //#endregion
15347
- //#region src/cli/utils/version.ts
15348
- const VERSION = "0.2.14-dev.b3c3aa9";
15349
- //#endregion
15350
15435
  //#region src/inspect.ts
15351
15436
  const silentConsole = makeNoopConsole();
15352
15437
  const runConsole = (effect) => {
@@ -15635,6 +15720,7 @@ const handleErrorEffect = (error) => Effect.gen(function* () {
15635
15720
  yield* Console.error("");
15636
15721
  yield* Console.error(highlighter.error("Something went wrong. Please check the error below for more details."));
15637
15722
  yield* Console.error(highlighter.error(`If the problem persists, please open this prefilled issue: ${buildErrorIssueUrl(error)}`));
15723
+ yield* Console.error(highlighter.error(`You can also ask for help in Discord: ${CANONICAL_DISCORD_URL}`));
15638
15724
  yield* Console.error("");
15639
15725
  yield* Console.error(highlighter.error(formatErrorForReport(error)));
15640
15726
  yield* Console.error("");
@@ -17084,6 +17170,78 @@ const printBrandedHeader = Effect.gen(function* () {
17084
17170
  yield* Console.log("");
17085
17171
  });
17086
17172
  //#endregion
17173
+ //#region src/cli/utils/build-run-context.ts
17174
+ const ROOT_SUBCOMMANDS = new Set(["install", "setup"]);
17175
+ const detectOrigin = () => {
17176
+ if (process.env.GIT_DIR) return "git-hook";
17177
+ if (isCodingAgentEnvironment()) return "agent";
17178
+ if (isCiEnvironment()) return "ci";
17179
+ return "cli";
17180
+ };
17181
+ const detectCommand = (userArguments) => {
17182
+ for (const argument of userArguments) {
17183
+ if (argument === "--") break;
17184
+ if (argument.startsWith("-")) continue;
17185
+ return ROOT_SUBCOMMANDS.has(argument) ? argument : "inspect";
17186
+ }
17187
+ return "inspect";
17188
+ };
17189
+ /**
17190
+ * Snapshot of the current invocation, attached to Sentry events as the
17191
+ * `run` context to make crashes triage-able (which version, platform,
17192
+ * CI/agent, how it was invoked). Every field is cheap, synchronous, and
17193
+ * safe to read at any point — cwd reads fall back, env reads are
17194
+ * booleans — so it's rebuilt lazily at capture time when runtime-only
17195
+ * signals like `jsonMode` are finally known.
17196
+ */
17197
+ const buildRunContext = () => {
17198
+ const userArguments = process.argv.slice(2);
17199
+ return {
17200
+ version: VERSION,
17201
+ origin: detectOrigin(),
17202
+ command: detectCommand(userArguments),
17203
+ argv: userArguments.join(" "),
17204
+ cwd: process.cwd(),
17205
+ node: process.version,
17206
+ platform: process.platform,
17207
+ arch: process.arch,
17208
+ ci: isCiEnvironment(),
17209
+ ciProvider: detectCiProvider(),
17210
+ codingAgent: detectCodingAgent(),
17211
+ interactive: !isNonInteractiveEnvironment(),
17212
+ jsonMode: isJsonModeActive()
17213
+ };
17214
+ };
17215
+ //#endregion
17216
+ //#region src/cli/utils/report-error.ts
17217
+ /**
17218
+ * Sends an error to Sentry, enriched with a snapshot of the current run
17219
+ * (version, platform, CI/agent, invocation), and waits for delivery
17220
+ * before the caller exits. The CLI tears down the process synchronously
17221
+ * after rendering an error, so the awaited `flush` is what actually gets
17222
+ * the event off the machine (see the Sentry CLI/serverless flush
17223
+ * contract).
17224
+ *
17225
+ * Returns early when Sentry was never initialized (`--no-score`, tests,
17226
+ * or a missing DSN), and swallows any transport failure so telemetry can
17227
+ * never mask the user's original error.
17228
+ */
17229
+ const reportErrorToSentry = async (error) => {
17230
+ if (!Sentry.isInitialized()) return;
17231
+ try {
17232
+ const runContext = buildRunContext();
17233
+ Sentry.setContext("run", { ...runContext });
17234
+ Sentry.setTags({
17235
+ origin: runContext.origin,
17236
+ command: runContext.command,
17237
+ ciProvider: runContext.ciProvider,
17238
+ codingAgent: runContext.codingAgent
17239
+ });
17240
+ Sentry.captureException(error);
17241
+ await Sentry.flush(2e3);
17242
+ } catch {}
17243
+ };
17244
+ //#endregion
17087
17245
  //#region src/cli/utils/path-format.ts
17088
17246
  const toForwardSlashes = (filePath) => filePath.replaceAll("\\", "/");
17089
17247
  //#endregion
@@ -17278,7 +17436,7 @@ const resolveCliInspectOptions = (flags, userConfig) => {
17278
17436
  respectInlineDisables: flags.respectInlineDisables,
17279
17437
  warnings: flags.warnings ?? (wantsWarningGate ? true : void 0),
17280
17438
  scoreOnly: flags.score === true,
17281
- noScore: flags.score === false || (userConfig?.noScore ?? false),
17439
+ noScore: flags.score === false || flags.telemetry === false || (userConfig?.noScore ?? false),
17282
17440
  isCi: isCiEnvironment(),
17283
17441
  silent: Boolean(flags.json),
17284
17442
  outputSurface: flags.prComment ? "prComment" : "cli",
@@ -17547,6 +17705,7 @@ const validateModeFlags = (flags) => {
17547
17705
  if (exclusiveModes.length > 1) throw new Error(`Cannot combine ${exclusiveModes.join(" and ")}; pick one mode.`);
17548
17706
  if (flags.yes && flags.full) throw new Error("Cannot combine --yes and --full; pick one.");
17549
17707
  if (flags.score && flags.json) throw new Error("Cannot combine --score and --json; pick one output mode.");
17708
+ if (flags.score && flags.telemetry === false) throw new Error("Cannot combine --score with --no-telemetry; --score prints the score that --no-telemetry disables.");
17550
17709
  if (flags.prComment && (flags.json || flags.score)) throw new Error("--pr-comment cannot be combined with --json or --score.");
17551
17710
  if (flags.annotations && flags.score) throw new Error("--annotations cannot be combined with --score.");
17552
17711
  if (flags.explain !== void 0 && flags.why !== void 0) throw new Error("Use --explain or --why, not both — they're aliases of the same flag.");
@@ -17766,6 +17925,7 @@ const inspectAction = async (directory, flags) => {
17766
17925
  })) printAgentInstallHint();
17767
17926
  }
17768
17927
  } catch (error) {
17928
+ await reportErrorToSentry(error);
17769
17929
  if (isJsonMode) {
17770
17930
  writeJsonErrorReport(error);
17771
17931
  process.exitCode = 1;
@@ -17787,10 +17947,61 @@ const installAction = async (options, command) => {
17787
17947
  projectRoot: options.cwd ?? process.cwd()
17788
17948
  });
17789
17949
  } catch (error) {
17950
+ await reportErrorToSentry(error);
17790
17951
  handleError(error);
17791
17952
  }
17792
17953
  };
17793
17954
  //#endregion
17955
+ //#region src/cli/commands/version.ts
17956
+ /**
17957
+ * oclif-style version line. 12-factor CLI Apps (#3, "What version am I
17958
+ * on?"): the `version` command is the primary place users grab debugging
17959
+ * info, so it carries the Node runtime and platform alongside the CLI
17960
+ * version. The `-v` / `-V` / `--version` flags stay terse (just the
17961
+ * number) so scripts can parse them.
17962
+ */
17963
+ const buildVersionString = () => `react-doctor/${VERSION} ${process.platform}-${process.arch} node-${process.version}`;
17964
+ const versionAction = () => {
17965
+ process.stdout.write(`${buildVersionString()}\n`);
17966
+ };
17967
+ //#endregion
17968
+ //#region src/cli/utils/apply-color-preference.ts
17969
+ /**
17970
+ * Resolve an explicit color preference from `--color` / `--no-color` or the
17971
+ * app-specific `REACT_DOCTOR_NO_COLOR` / `REACT_DOCTOR_FORCE_COLOR` env vars
17972
+ * (clig.dev Output; 12-factor #6), overriding picocolors' own
17973
+ * `NO_COLOR` / `FORCE_COLOR` / `TERM` / TTY detection. Flags win over env
17974
+ * vars; with neither set, picocolors' detection stands.
17975
+ *
17976
+ * A resolved preference is mirrored onto the standard `NO_COLOR` /
17977
+ * `FORCE_COLOR` env vars in addition to our picocolors highlighter, so
17978
+ * libraries with their own color stacks (the `ora` spinner, `prompts`)
17979
+ * honor it too rather than only the scan report. Scanning argv directly
17980
+ * (not Commander's parsed options) applies the preference before Commander
17981
+ * parses, so it reaches every later path. The scan stops at `--`.
17982
+ */
17983
+ const applyColorPreference = (argv, env = process.env) => {
17984
+ let enabled;
17985
+ for (const argument of argv) {
17986
+ if (argument === "--") break;
17987
+ if (argument === "--no-color") enabled = false;
17988
+ else if (argument === "--color") enabled = true;
17989
+ }
17990
+ if (enabled === void 0) {
17991
+ if (env.REACT_DOCTOR_NO_COLOR) enabled = false;
17992
+ else if (env.REACT_DOCTOR_FORCE_COLOR) enabled = true;
17993
+ }
17994
+ if (enabled === void 0) return;
17995
+ if (enabled) {
17996
+ env.FORCE_COLOR = "1";
17997
+ delete env.NO_COLOR;
17998
+ } else {
17999
+ env.NO_COLOR = "1";
18000
+ delete env.FORCE_COLOR;
18001
+ }
18002
+ setColorEnabled(enabled);
18003
+ };
18004
+ //#endregion
17794
18005
  //#region src/cli/utils/exit-gracefully.ts
17795
18006
  const exitGracefully = () => {
17796
18007
  try {
@@ -17800,21 +18011,54 @@ const exitGracefully = () => {
17800
18011
  process.exit(130);
17801
18012
  };
17802
18013
  //#endregion
18014
+ //#region src/cli/utils/normalize-help-command.ts
18015
+ /**
18016
+ * 12-factor CLI Apps (#1, "Great help is essential"): `mycli help` and
18017
+ * `mycli help <command>` must display help. Commander doesn't wire this
18018
+ * up once the root command has its own default action plus a positional
18019
+ * argument — it treats a leading `help` as the `[directory]` to scan,
18020
+ * which then errors with "No React project found in ./help".
18021
+ *
18022
+ * We rewrite the argv up front so the existing `--help` paths handle it:
18023
+ * `react-doctor help` -> `react-doctor --help`
18024
+ * `react-doctor help install` -> `react-doctor install --help`
18025
+ *
18026
+ * Only a *leading* `help` token is rewritten, so a flag value such as
18027
+ * `--project help` is never mistaken for the help command. The target is
18028
+ * the first non-flag token after `help`, so intervening flags like
18029
+ * `help --no-color install` still resolve to `install`. An unknown target
18030
+ * (`help bogus`) falls back to root help rather than erroring.
18031
+ */
18032
+ const normalizeHelpInvocation = (argv, knownCommands) => {
18033
+ const nodeArguments = argv.slice(0, 2);
18034
+ const userArguments = argv.slice(2);
18035
+ if (userArguments[0] !== "help") return [...argv];
18036
+ const target = userArguments.slice(1).find((argument) => !argument.startsWith("-"));
18037
+ if (target !== void 0 && knownCommands.includes(target)) return [
18038
+ ...nodeArguments,
18039
+ target,
18040
+ "--help"
18041
+ ];
18042
+ return [...nodeArguments, "--help"];
18043
+ };
18044
+ //#endregion
17803
18045
  //#region src/cli/utils/strip-unknown-cli-flags.ts
17804
- const NODE_ARGUMENT_COUNT = 2;
17805
18046
  const ROOT_FLAG_SPEC = {
17806
18047
  longOptionsWithoutValues: new Set([
17807
18048
  "--annotations",
18049
+ "--color",
17808
18050
  "--dead-code",
17809
18051
  "--full",
17810
18052
  "--help",
17811
18053
  "--json",
17812
18054
  "--json-compact",
17813
18055
  "--lint",
18056
+ "--no-color",
17814
18057
  "--no-dead-code",
17815
18058
  "--no-lint",
17816
18059
  "--no-respect-inline-disables",
17817
18060
  "--no-score",
18061
+ "--no-telemetry",
17818
18062
  "--no-warnings",
17819
18063
  "--pr-comment",
17820
18064
  "--respect-inline-disables",
@@ -17843,8 +18087,10 @@ const ROOT_FLAG_SPEC = {
17843
18087
  const INSTALL_FLAG_SPEC = {
17844
18088
  longOptionsWithoutValues: new Set([
17845
18089
  "--agent-hooks",
18090
+ "--color",
17846
18091
  "--dry-run",
17847
18092
  "--help",
18093
+ "--no-color",
17848
18094
  "--yes"
17849
18095
  ]),
17850
18096
  longOptionsWithRequiredValues: new Set(["--cwd"]),
@@ -17852,7 +18098,21 @@ const INSTALL_FLAG_SPEC = {
17852
18098
  shortOptionsWithoutValues: new Set(["-h", "-y"]),
17853
18099
  shortOptionsWithRequiredValues: new Set(["-c"])
17854
18100
  };
17855
- const COMMAND_FLAG_SPECS = new Map([["install", INSTALL_FLAG_SPEC], ["setup", INSTALL_FLAG_SPEC]]);
18101
+ const COMMAND_FLAG_SPECS = new Map([
18102
+ ["install", INSTALL_FLAG_SPEC],
18103
+ ["setup", INSTALL_FLAG_SPEC],
18104
+ ["version", {
18105
+ longOptionsWithoutValues: new Set([
18106
+ "--color",
18107
+ "--help",
18108
+ "--no-color"
18109
+ ]),
18110
+ longOptionsWithRequiredValues: /* @__PURE__ */ new Set(),
18111
+ longOptionsWithOptionalValues: /* @__PURE__ */ new Set(),
18112
+ shortOptionsWithoutValues: new Set(["-h"]),
18113
+ shortOptionsWithRequiredValues: /* @__PURE__ */ new Set()
18114
+ }]
18115
+ ]);
17856
18116
  const isFlagLike = (argument) => argument.startsWith("-") && argument !== "-";
17857
18117
  const getLongOptionName = (argument) => {
17858
18118
  const equalsIndex = argument.indexOf("=");
@@ -17906,8 +18166,8 @@ const stripUnknownFlags = (userArguments, flagSpec) => {
17906
18166
  return sanitizedArguments;
17907
18167
  };
17908
18168
  const stripUnknownCliFlags = (argv) => {
17909
- const nodeArguments = argv.slice(0, NODE_ARGUMENT_COUNT);
17910
- const userArguments = argv.slice(NODE_ARGUMENT_COUNT);
18169
+ const nodeArguments = argv.slice(0, 2);
18170
+ const userArguments = argv.slice(2);
17911
18171
  const commandIndex = findCommandIndex(userArguments);
17912
18172
  if (commandIndex === null) return [...nodeArguments, ...stripUnknownFlags(userArguments, ROOT_FLAG_SPEC)];
17913
18173
  const commandName = userArguments[commandIndex];
@@ -17921,23 +18181,66 @@ const stripUnknownCliFlags = (argv) => {
17921
18181
  };
17922
18182
  //#endregion
17923
18183
  //#region src/cli/index.ts
18184
+ initializeSentry();
17924
18185
  process.on("SIGINT", exitGracefully);
17925
18186
  process.on("SIGTERM", exitGracefully);
17926
18187
  unrefStdin();
17927
- const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--experimental-parallel [workers]", "experimental: lint with N parallel workers (default: auto-detect CPU cores) — speeds up large repos").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--changed-files-from <file>", "internal: scan source files listed in a newline-delimited changed-files file").option("--no-score", "skip the score API and the share URL").option("--staged", "scan only staged (git index) files for pre-commit hooks").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none (default: none)").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--pr-comment", "tune CLI output for sticky PR comments (drops weak-signal rule families like `design` from the printed list and the fail-on gate; configure via config.surfaces)").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").option("--why <file:line>", "alias for --explain").option("--respect-inline-disables", "respect inline `// eslint-disable*` / `// oxlint-disable*` comments (default)").option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (errors always show)").option("--no-warnings", "hide warning-severity diagnostics (default)").addHelpText("after", `
18188
+ const formatExampleLines = (examples) => {
18189
+ const width = Math.max(...examples.map(([command]) => command.length));
18190
+ return examples.map(([command, description]) => ` $ ${command.padEnd(width)} ${highlighter.dim(`# ${description}`)}`).join("\n");
18191
+ };
18192
+ const renderRootHelpEpilog = () => `
18193
+ ${highlighter.dim("Examples:")}
18194
+ ${formatExampleLines([
18195
+ ["react-doctor", "scan the current project"],
18196
+ ["react-doctor ./apps/web", "scan a specific directory"],
18197
+ ["react-doctor --diff main", "scan only files changed vs. main"],
18198
+ ["react-doctor --staged", "scan staged files (pre-commit hook)"],
18199
+ ["react-doctor --fail-on warning", "exit non-zero on warnings (CI gate)"],
18200
+ ["react-doctor --json > report.json", "write a machine-readable report"],
18201
+ ["react-doctor --explain src/App.tsx:42", "explain why a rule fired there"],
18202
+ ["react-doctor install", "set up the agent skill and git hook"]
18203
+ ])}
18204
+
17928
18205
  ${highlighter.dim("Configuration:")}
17929
18206
  Place a ${highlighter.info("react-doctor.config.json")} (or ${highlighter.info("\"reactDoctor\"")} key in your package.json) in the project root.
17930
18207
  CLI flags always override config values. See the README for the full schema.
17931
18208
 
18209
+ ${highlighter.dim("Feedback & bug reports:")}
18210
+ ${highlighter.info(`${CANONICAL_GITHUB_URL}/issues`)}
18211
+
17932
18212
  ${highlighter.dim("Learn more:")}
17933
18213
  ${highlighter.info(CANONICAL_GITHUB_URL)}
17934
- `);
18214
+ `;
18215
+ const renderInstallHelpEpilog = () => `
18216
+ ${highlighter.dim("Examples:")}
18217
+ ${formatExampleLines([
18218
+ ["react-doctor install", "interactive setup"],
18219
+ ["react-doctor install --yes", "non-interactive; all detected agents"],
18220
+ ["react-doctor install --dry-run", "preview without writing files"],
18221
+ ["react-doctor install --agent-hooks", "also install native agent hooks"]
18222
+ ])}
18223
+
18224
+ ${highlighter.dim("Learn more:")}
18225
+ ${highlighter.info(CANONICAL_GITHUB_URL)}
18226
+ `;
18227
+ const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead-code analysis (default)").option("--no-dead-code", "skip dead-code analysis (unused files / exports / dependencies, circular imports)").option("--verbose", "show every rule and per-file details (default shows top 3 rules)").option("--score", "output only the score").option("--json", "output a single structured JSON report (suppresses other output)").option("--json-compact", "with --json, emit compact JSON (no indentation)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--full", "force a full scan (overrides any `diff` value in config or `--diff`)").option("--experimental-parallel [workers]", "experimental: lint with N parallel workers (default: auto-detect CPU cores) — speeds up large repos").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch (pass `false` to disable; overridden by --full)").option("--changed-files-from <file>", "internal: scan source files listed in a newline-delimited changed-files file").option("--no-score", "skip the score API, the share URL, and crash reporting").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("--fail-on <level>", "exit with error code on diagnostics: error, warning, none (default: none)").option("--annotations", "output diagnostics as GitHub Actions annotations").option("--pr-comment", "tune CLI output for sticky PR comments (drops weak-signal rule families like `design` from the printed list and the fail-on gate; configure via config.surfaces)").option("--explain <file:line>", "diagnose why a rule fired or why a suppression didn't apply at a specific location").option("--why <file:line>", "alias for --explain").option("--respect-inline-disables", "respect inline `// eslint-disable*` / `// oxlint-disable*` comments (default)").option("--no-respect-inline-disables", "audit mode: neutralize inline lint suppressions before scanning").option("--warnings", "show warning-severity diagnostics (errors always show)").option("--no-warnings", "hide warning-severity diagnostics (default)").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").addHelpText("after", renderRootHelpEpilog);
17935
18228
  program.action(inspectAction);
17936
- 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()).action(installAction);
18229
+ 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);
18230
+ program.command("version").description("show the version with Node and platform info").option("--color", "force colored output").option("--no-color", "disable colored output (also honors NO_COLOR)").action(versionAction);
17937
18231
  process.stdout.on("error", (error) => {
17938
18232
  if (error.code === "EPIPE") process.exit(0);
17939
18233
  });
17940
- program.parseAsync(stripUnknownCliFlags(process.argv)).catch((error) => {
18234
+ const knownCommands = program.commands.flatMap((command) => [command.name(), ...command.aliases()]);
18235
+ const strippedArgv = stripUnknownCliFlags(process.argv);
18236
+ if (process.argv.includes("-V") && !strippedArgv.includes("-V")) {
18237
+ process.stdout.write(`${VERSION}\n`);
18238
+ process.exit(0);
18239
+ }
18240
+ applyColorPreference(strippedArgv);
18241
+ const argv = normalizeHelpInvocation(strippedArgv, knownCommands);
18242
+ program.parseAsync(argv).catch(async (error) => {
18243
+ await reportErrorToSentry(error);
17941
18244
  if (isJsonModeActive()) {
17942
18245
  writeJsonErrorReport(error);
17943
18246
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-doctor",
3
- "version": "0.2.14-dev.b3c3aa9",
3
+ "version": "0.2.14-dev.b9e9bcb",
4
4
  "description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -51,6 +51,7 @@
51
51
  "dependencies": {
52
52
  "@babel/code-frame": "^7.29.0",
53
53
  "@effect/platform-node-shared": "4.0.0-beta.70",
54
+ "@sentry/node": "^10.54.0",
54
55
  "agent-install": "0.0.5",
55
56
  "conf": "^15.1.0",
56
57
  "deslop-js": "^0.0.14",
@@ -59,7 +60,7 @@
59
60
  "oxlint": "^1.66.0",
60
61
  "prompts": "^2.4.2",
61
62
  "typescript": ">=5.0.4 <7",
62
- "oxlint-plugin-react-doctor": "0.2.14-dev.b3c3aa9"
63
+ "oxlint-plugin-react-doctor": "0.2.14-dev.b9e9bcb"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@types/babel__code-frame": "^7.27.0",