Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
646f844
feat(react-doctor): add ScanReporter event interface
May 8, 2026
b89fcc5
feat(react-doctor-tui): interactive Ink-based code-health TUI
May 8, 2026
6a1a6bc
feat(react-doctor): wire watch / review subcommands to lazy-loaded TUI
May 8, 2026
641a953
refactor(react-doctor): merge TUI into the main package + redesign th…
May 8, 2026
d60277e
refactor(react-doctor): replace console.error with logger for improve…
NisargIO May 8, 2026
2d98d6c
refactor(react-doctor-tui): simplify imports and enhance diagnostics …
NisargIO May 8, 2026
fb3de8a
refactor(tui): focus the dashboard on what users can act on
May 8, 2026
d96776f
fix(react-doctor): reverse sorted rule groups for improved diagnostic…
NisargIO May 8, 2026
239933b
Merge branch 'cursor/react-doctor-tui-50e9' of https://github.com/mil…
cursoragent May 8, 2026
ebc6722
Revert "refactor(react-doctor-tui): simplify imports and enhance diag…
cursoragent May 8, 2026
8a31e39
Revert "refactor(react-doctor): replace console.error with logger for…
cursoragent May 8, 2026
200c2cb
Merge branch 'cursor/react-doctor-tui-50e9' of https://github.com/mil…
cursoragent May 8, 2026
6caeccc
feat(tui): add an Ink-native project picker for monorepos
May 8, 2026
d2fc800
fix(tui): keep the review and dashboard readable at narrow terminal w…
May 8, 2026
ef19fa2
test(tui): add snapshots at 91 and 100 cols to lock in narrow-screen …
May 8, 2026
077c63f
fix(tui): refuse to start in agent / CI environments, name the trigger
May 8, 2026
f4816e9
feat(tui): add a 'By category' overview chart above the focused issue
May 8, 2026
abeee57
feat(tui): copy diagnostic to clipboard, esc closes help, nail down a…
May 8, 2026
c3746ee
feat(tui): surface react.doctor home URL, share URL, and diagnostics …
May 8, 2026
49819bb
fix(tui): drop the 'Last scan {elapsed}' prefix from the summary footer
May 8, 2026
56f108c
rename(tui): filter -> search across user-facing strings and internals
May 8, 2026
a61df72
fix(tui): render in alternate screen buffer to stop frame doubling
May 8, 2026
dc6cc91
feat(tui): always-on watch, copy-all-on-c, narrow-screen tightening, …
May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/react-doctor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ You'll get a score (75+ Great, 50 to 74 Needs work, under 50 Critical) and a lis

https://github.com/user-attachments/assets/07cc88d9-9589-44c3-aa73-5d603cb1c570

## Interactive TUI

Want a live, health-app-style dashboard with bordered tiles, an animated doctor face, score gauge, top-issues list, category breakdown, and a master/detail diagnostic browser?

```bash
react-doctor tui . # dashboard
react-doctor tui . --watch # rescan on save
react-doctor tui . --review # open straight into the diagnostic browser
```

The TUI lives in the same `react-doctor` package and only loads its UI deps (Ink, React, chokidar) when you invoke the `tui` subcommand, so non-TUI scans stay fast.

## Install for your coding agent

Teach your coding agent React best practices so it stops writing the bad code in the first place.
Expand Down
12 changes: 11 additions & 1 deletion packages/react-doctor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./tui": {
"types": "./dist/tui.d.ts",
"default": "./dist/tui.js"
},
"./oxlint-plugin": {
"types": "./dist/react-doctor-plugin.d.ts",
"default": "./dist/react-doctor-plugin.js"
Expand All @@ -65,17 +69,23 @@
},
"dependencies": {
"agent-install": "0.0.5",
"chokidar": "^5.0.0",
"commander": "^14.0.3",
"ink": "^7.0.2",
"ink-spinner": "^5.0.0",
"knip": "^6.10.0",
"ora": "^9.4.0",
"oxlint": "^1.63.0",
"picocolors": "^1.1.1",
"prompts": "^2.4.2",
"react": "^19.2.5",
"typescript": ">=5.0.4 <7"
},
"devDependencies": {
"@types/prompts": "^2.4.9",
"eslint-plugin-react-hooks": "^7.1.1"
"@types/react": "^19.2.0",
"eslint-plugin-react-hooks": "^7.1.1",
"ink-testing-library": "^4.0.0"
},
"peerDependencies": {
"eslint-plugin-react-hooks": "^6 || ^7"
Expand Down
71 changes: 43 additions & 28 deletions packages/react-doctor/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { loadConfig } from "./utils/load-config.js";
import { logger, setLoggerSilent } from "./utils/logger.js";
import { encodeAnnotationProperty, encodeAnnotationMessage } from "./utils/annotation-encoding.js";
import { findOwningProjectDirectory } from "./utils/find-owning-project.js";
import { isNonInteractiveEnvironment } from "./utils/is-non-interactive-environment.js";
import { parseFileLineArgument } from "./utils/parse-file-line-argument.js";
import { prompts } from "./utils/prompts.js";
import { selectProjects } from "./utils/select-projects.js";
Expand Down Expand Up @@ -121,41 +122,13 @@ const exitGracefully = () => {
process.on("SIGINT", exitGracefully);
process.on("SIGTERM", exitGracefully);

// HACK: env vars that mean "user is not at an interactive shell." We use this
// to skip prompts but NOT to auto-flip --offline, because dev shells often
// have JENKINS_URL / TF_BUILD set as ambient config without actually running
// in CI.
const NON_INTERACTIVE_ENVIRONMENT_VARIABLES = [
"CI",
"GITHUB_ACTIONS",
"GITLAB_CI",
"BUILDKITE",
"JENKINS_URL",
"TF_BUILD",
"CODEBUILD_BUILD_ID",
"TEAMCITY_VERSION",
"BITBUCKET_BUILD_NUMBER",
"CIRCLECI",
"TRAVIS",
"DRONE",
"CLAUDECODE",
"CLAUDE_CODE",
"CURSOR_AGENT",
"CODEX_CI",
"OPENCODE",
"AMP_HOME",
];

// HACK: only flip --offline by default for the narrowest set of CI signals
// where we're confident the run is automated and a share URL would be
// useless. Other tools that set non-interactive env vars (Jenkins agents,
// Azure DevOps tasks running interactively, agentic coding sessions) still
// get telemetry-on-by-default; users can pass --offline explicitly.
const CI_ENVIRONMENT_VARIABLES = ["GITHUB_ACTIONS", "GITLAB_CI", "CIRCLECI"];

const isNonInteractiveEnvironment = (): boolean =>
NON_INTERACTIVE_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable]));

const isCiEnvironment = (): boolean =>
CI_ENVIRONMENT_VARIABLES.some((envVariable) => Boolean(process.env[envVariable])) ||
process.env.CI === "true";
Expand Down Expand Up @@ -693,6 +666,48 @@ program
}
});

interface TuiSubcommandOptions {
watch?: boolean;
review?: boolean;
project?: string;
}

const launchTui = async (directory: string, tuiOptions: TuiSubcommandOptions): Promise<void> => {
try {
// HACK: dynamic import keeps Ink + React + chokidar out of the
// default startup path. Non-TUI scans never load this entry, so
// `react-doctor .` stays as fast as before.
const tuiModule = await import("./tui.js");
await tuiModule.runTui({
directory: path.resolve(directory),
watch: tuiOptions.watch,
review: tuiOptions.review,
project: tuiOptions.project,
});
} catch (error) {
handleError(error);
}
};

program
.command("tui")
.description("Open the interactive React code-health TUI")
.argument("[directory]", "project directory to scan", ".")
.option("--watch", "rescan automatically when source files change", false)
.option("--review", "open straight into the diagnostic review screen", false)
.option("--project <name>", "preselect a workspace project (skips the picker)")
.addHelpText(
"after",
`\nThe TUI requires an interactive terminal. It refuses to start when stdin / stdout
isn't a TTY or when agent / CI env vars are set (CI, GITHUB_ACTIONS, CURSOR_AGENT,
CLAUDECODE, etc.); use \`react-doctor\` for those environments.

Color is auto-detected. Set NO_COLOR=1 to disable, FORCE_COLOR=1 to force on.\n`,
)
.action(async (directory: string, options: TuiSubcommandOptions) => {
await launchTui(directory, options);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented --watch flag missing from CLI command definition

High Severity

The README and PR description document react-doctor tui . --watch as a valid command, but the tui subcommand only registers --review and --project options. The TuiSubcommandOptions interface also lacks a watch field. Since Commander rejects unknown options by default, running react-doctor tui . --watch will produce a CLI error like "unknown option '--watch'". Watch mode is always-on in the app (per the comment in app.tsx), so either the --watch option needs to be registered (even if it's a no-op), or the documentation needs updating.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dc6cc91. Configure here.


// HACK: when stdout is piped into a process that closes early (e.g.
// `react-doctor . | head`), Node throws an uncaught EPIPE on the next
// write. Exit cleanly instead of dumping a stack trace.
Expand Down
2 changes: 2 additions & 0 deletions packages/react-doctor/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const SCORE_API_URL = "https://www.react.doctor/api/score";

export const SHARE_BASE_URL = "https://www.react.doctor/share";

export const REACT_DOCTOR_HOME_URL = "https://react.doctor";

export const FETCH_TIMEOUT_MS = 10_000;

export const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
Expand Down
25 changes: 25 additions & 0 deletions packages/react-doctor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import type {
JsonReportSummary,
ProjectInfo,
ReactDoctorConfig,
ScanCompleteEvent,
ScanEvent,
ScanFailedEvent,
ScanOptions,
ScanProjectDetectedEvent,
ScanReporter,
ScanResult,
ScanScoreResolvedEvent,
ScanStepFinishEvent,
ScanStepId,
ScanStepStartEvent,
ScanWarnEvent,
ScoreResult,
} from "./types.js";
import { buildJsonReport } from "./utils/build-json-report.js";
Expand Down Expand Up @@ -43,8 +55,21 @@ export type {
JsonReportSummary,
ProjectInfo,
ReactDoctorConfig,
ScanCompleteEvent,
ScanEvent,
ScanFailedEvent,
ScanOptions,
ScanProjectDetectedEvent,
ScanReporter,
ScanResult,
ScanScoreResolvedEvent,
ScanStepFinishEvent,
ScanStepId,
ScanStepStartEvent,
ScanWarnEvent,
ScoreResult,
};
export { scan } from "./scan.js";
export { getDiffInfo, filterSourceFiles } from "./utils/get-diff-files.js";
export { summarizeDiagnostics } from "./utils/summarize-diagnostics.js";
export { buildJsonReport, buildJsonReportError };
Expand Down
Loading