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.
- package/dist/cli.js +333 -30
- 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
|
|
14405
|
-
"
|
|
14406
|
-
"
|
|
14407
|
-
"
|
|
14408
|
-
"
|
|
14409
|
-
"
|
|
14410
|
-
"
|
|
14411
|
-
"
|
|
14412
|
-
"
|
|
14413
|
-
"
|
|
14414
|
-
"
|
|
14415
|
-
"
|
|
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((
|
|
14426
|
-
const
|
|
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([
|
|
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,
|
|
17910
|
-
const userArguments = argv.slice(
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
63
|
+
"oxlint-plugin-react-doctor": "0.2.14-dev.b9e9bcb"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@types/babel__code-frame": "^7.27.0",
|