Skip to content

refactor(cli): split src/cli.ts and CLI-only utils into src/cli/#220

Merged
aidenybai merged 1 commit into
mainfrom
refactor/cli-split-from-utils
May 13, 2026
Merged

refactor(cli): split src/cli.ts and CLI-only utils into src/cli/#220
aidenybai merged 1 commit into
mainfrom
refactor/cli-split-from-utils

Conversation

@aidenybai
Copy link
Copy Markdown
Member

@aidenybai aidenybai commented May 13, 2026

Same kind of organizational win as #218 (per-rule files), now for the CLI tier.

Summary

Carves the 743-line `src/cli.ts` orchestrator and its 6 CLI-only utility files out of `src/utils/` and into a dedicated `src/cli/` directory. Pure file moves + import-path rewrites — zero behavior change.

Before After
`src/cli.ts` `src/cli/index.ts`
`src/utils/annotation-encoding.ts` `src/cli/annotation-encoding.ts`
`src/utils/find-owning-project.ts` `src/cli/find-owning-project.ts`
`src/utils/get-staged-files.ts` `src/cli/get-staged-files.ts`
`src/utils/handle-error.ts` `src/cli/handle-error.ts`
`src/utils/parse-file-line-argument.ts` `src/cli/parse-file-line-argument.ts`
`src/utils/select-projects.ts` `src/cli/select-projects.ts`

All 7 are tracked as renames by git so log/blame history is preserved.

How I picked these 7

Wrote a classifier script that, for each `src/utils/*.ts`, found all non-test importers. The 6 utility files moved are the only ones whose sole non-test importer is `src/cli.ts`.

Utils still imported by `scan.ts`, `index.ts` (public API), `eslint-plugin.ts`, or `install-skill.ts` stay in `src/utils/` — they aren't CLI-only and moving them would couple the CLI tree to non-CLI consumers.

Import rewrites

  • Inside the 6 moved utils: relative imports to other moved utils stay as `./.js`; imports of shared utils that stayed in `src/utils/` become `../utils/.js`
  • Inside `cli/index.ts` (was `cli.ts`): `./utils/.js` -> `../utils/.js` for shared utils; `./utils/.js` -> `./.js` for the 6 moved utils; `./scan.js`, `./install-skill.js`, `./constants.js`, `./types.js` all gain a `../`
  • 4 test files updated to import the moved utils from `src/cli/` instead of `src/utils/`
  • `vite.config.ts`: cli build entry `"./src/cli.ts"` -> `"./src/cli/index.ts"`

What's NOT in this PR

Out of scope (would be a behavior change or pulls in unrelated work):

  • Not moving `prompts.ts`, `highlighter.ts`, `logger.ts`, `spinner.ts`, `format-error-chain.ts`, `colorize-by-score.ts`, `build-json-report.ts`, etc. — those are CLI-flavored but also used by `scan.ts` (which is exposed via `index.ts` as the programmatic `scan()` API)
  • Not renaming or splitting `scan.ts` (899 LOC, separate concern — v2 calls it `inspectReactProject`)
  • Not touching the SDK / errors / metadata / codebase-analyzer parts of v2

Verification

  • 12 files changed, +31/-31 (all import path rewrites)
  • `pnpm typecheck` — clean
  • `pnpm test` — 734/734 pass
  • `pnpm lint` — 0 warnings, 0 errors
  • `pnpm format:check` — clean
  • `pnpm build` — produces identical `dist/cli.js` output (and `dist/cli.d.ts`)

Test plan

  • typecheck / test / lint / format / build all clean
  • git mv preserves rename history (`git log --follow`)
  • No new dependencies, no API changes

Note

Low Risk
Pure module re-organization (file moves and import rewrites) around the CLI entrypoint; runtime risk is low but packaging/entrypoint paths could break the CLI build if misconfigured.

Overview
Moves the CLI entrypoint from src/cli.ts to src/cli/index.ts and relocates several CLI-only helpers (e.g. annotation-encoding, get-staged-files, handle-error, select-projects) under src/cli/, updating all internal import paths.

Updates tests to import these helpers from src/cli/ instead of src/utils/, and adjusts the Vite pack config to build the CLI from the new ./src/cli/index.ts entry.

Reviewed by Cursor Bugbot for commit 42ed2f9. Bugbot is set up for automated code reviews on this repo. Configure here.

Same shape as the plugin/ split (#218) but for the CLI tier: carves the
743-line src/cli.ts orchestrator and its 6 CLI-only utility files out of
src/utils/ and into a dedicated src/cli/ directory.

Files moved (via `git mv` so history is preserved):
  src/cli.ts                            -> src/cli/index.ts
  src/utils/annotation-encoding.ts      -> src/cli/annotation-encoding.ts
  src/utils/find-owning-project.ts      -> src/cli/find-owning-project.ts
  src/utils/get-staged-files.ts         -> src/cli/get-staged-files.ts
  src/utils/handle-error.ts             -> src/cli/handle-error.ts
  src/utils/parse-file-line-argument.ts -> src/cli/parse-file-line-argument.ts
  src/utils/select-projects.ts          -> src/cli/select-projects.ts

Picked these specifically because they are the only utils with cli.ts as
their sole non-test importer (per a classifier audit). Utils still used by
scan.ts, index.ts (public API), eslint-plugin.ts, or install-skill.ts
stay in src/utils/ since they aren't CLI-only.

Import path rewrites:
  - Inside the moved files: ./<other-cli-util>.js stays (same dir now),
    ./<shared-util>.js becomes ../utils/<util>.js
  - Inside cli/index.ts: ./utils/<x>.js -> ../utils/<x>.js for shared utils,
    -> ./<x>.js for the 6 moved CLI utils; ./scan.js, ./install-skill.js,
    ./constants.js, ./types.js -> ../<file>.js
  - 4 test files updated to import the moved utils from src/cli/ instead
    of src/utils/
  - vite.config.ts: cli entry "./src/cli.ts" -> "./src/cli/index.ts"

No behavior change. 734 tests pass, lint clean, typecheck clean, build
produces identical dist/cli.js output.
@reactreview
Copy link
Copy Markdown

reactreview Bot commented May 13, 2026

🔴 React Review0/100 (unchanged) · 0 ❌ errors · 2 ⚠️ warnings

Copy prompt for agent
Check if these React Review issues are valid. If so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.

Run this before and after your changes to verify the result:
npx react-doctor@latest --verbose --diff

Do not modify the react-doctor configuration unless explicitly asked.
Fix the underlying code issues instead of changing or suppressing the rules.

React Review found 0 errors and 2 warnings. This PR leaves the React health score unchanged.

<file name="packages/react-doctor/src/cli/select-projects.ts">

<violation number="1" location="packages/react-doctor/src/cli/select-projects.ts:44">
Severity: Warning

array.find() in a loop is O(n*m) — build a Map for O(1) lookups

Build an index `Map` once outside the loop instead of `array.find(...)` inside it

Rule: `js-index-maps`
</violation>

</file>

<file name="packages/react-doctor/src/cli/index.ts">

<violation number="1" location="packages/react-doctor/src/cli/index.ts:628">
Severity: Warning

await inside a for…of loop runs the calls sequentially — for independent operations, collect them and use `await Promise.all(items.map(...))` to run them concurrently

Collect the items and use `await Promise.all(items.map(...))` to run independent operations concurrently

Rule: `async-await-in-loop`
</violation>

</file>

Reviewed by react-review for commit 42ed2f9. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-doctor-website Ready Ready Preview, Comment May 13, 2026 4:48pm

@aidenybai aidenybai merged commit 6ca7c1e into main May 13, 2026
7 checks passed
aidenybai added a commit that referenced this pull request May 13, 2026
Audit after the refactor wave (#218 plugin/, #220 cli/, #221 scan/,
#222 drop browser-poc) found 16 zero-reference exports across src/. 15
of them were genuinely dead; 1 was kept (clearCaches is public API).

Removals:
  src/utils/is-rule-suppressed-at.ts                ENTIRELY ORPHANED:
                                                    no file imports it
                                                    (it just wraps
                                                    evaluateSuppression)

  src/constants.ts:
    REACT_19_DEPRECATION_MIN_MAJOR                  Defined for rule
                                                    version-gating that
                                                    inlined the numbers
                                                    in oxlint-config.ts
                                                    instead.
    REACT_DOM_LEGACY_API_MIN_MAJOR                  Same -- never wired up
    TAILWIND_SIZE_SHORTHAND_MIN_MAJOR               Same
    TAILWIND_SIZE_SHORTHAND_MIN_MINOR               Same
    (plus the orphaned HACK comment that described them)

Un-exported (interface/helpers only used inside own file):
  src/cli/parse-file-line-argument.ts ParsedFileLineArgument
  src/oxlint-config.ts                RuleMetadataEntry
                                      buildCapabilities
                                      shouldEnableRule
  src/utils/apply-ignore-overrides.ts CompiledIgnoreOverride
  src/utils/build-category-breakdown.ts CategoryBreakdownEntry
  src/utils/calculate-score-locally.ts ScoreBreakdown
  src/utils/evaluate-suppression.ts   SuppressionEvaluation
  src/utils/load-config.ts            LoadedReactDoctorConfig
  src/utils/parse-tailwind-major-minor.ts TailwindMajorMinor

Kept:
  src/index.ts clearCaches            Public-API entry, intentionally
                                      exposed even if no internal
                                      caller uses it.

10 files changed, +10 / -36. typecheck/lint/format/test all clean,
734/734 tests still pass.
aidenybai added a commit that referenced this pull request May 13, 2026
…s,scoring,runners}/ + src/cli/ (#224)

* refactor: split src/utils/ (68 flat files) into src/core/{config,detection,diagnostics,scoring,runners}/ + src/cli/

Final big organizational refactor in the series (#218 plugin/, #220 cli/,
#221 scan/, #222 drop browser-poc, #223 dead-code). Carves the 68-file
src/utils/ dumping ground into purpose-built directories.

New layout:

  src/cli/             presentation (26 files)
    + prompts.ts, highlighter.ts, spinner.ts, logger.ts
    + colorize-by-score.ts, format-error-chain.ts
    + indent-multiline-text.ts, wrap-indented-text.ts
    + build-hidden-diagnostics-summary.ts
    + should-auto-select-current-choice.ts, should-select-all-choices.ts
    + detect-agents.ts, to-display-name.ts
    + (existing cli/ files: index.ts, render-*.ts, etc.)

  src/core/            core logic (7 files at root + 5 subdirs)
    build-json-report.ts                shared JSON reporting (CLI + public API)
    build-json-report-error.ts          shared JSON error envelope
    group-by.ts, is-file.ts             generic primitives
    is-plain-object.ts, to-relative-path.ts
    read-file-lines-node.ts

    config/ (11)       config loading, root-dir resolution, ignore patterns,
                       glob matching, gitattributes parsing, validation
    detection/ (6)     project discovery, react/tailwind major-version parsing,
                       package.json reading, monorepo root detection
    diagnostics/ (12)  combine/filter/suppress/merge, jsx-opener-span
                       lookup, disable-directive walks
    scoring/ (6)       local + remote score calc, score breakdown,
                       reduced-motion check, proxy fetch
    runners/ (13)      oxlint + knip + their config builders, include-path
                       computation, Node-version compat, diff-files

  src/plugin/          (unchanged from #218)
  scan.ts, index.ts, errors.ts, types.ts, constants.ts,
  oxlint-config.ts, eslint-plugin.ts, install-skill.ts    top-level

Driven by a one-shot codemod that performed the moves via `git mv` (so
all 68 file renames preserve blame/log history) and rewrote every
relative import in the package + tests (98 importer files touched) to
the new paths.

One non-mechanical fix needed after the move:
  src/core/runners/run-oxlint.ts resolved its built plugin via
  `../../dist/react-doctor-plugin.js` -- with 1 extra level of nesting
  that needs to be `../../../dist/react-doctor-plugin.js`.

One test fixed:
  tests/regressions/scan-resilience.test.ts read a source file by
  hardcoded path (`src/utils/calculate-score-locally.ts`) to assert no
  `fetch(` reference -- updated to the new location
  (`src/core/scoring/calculate-score-locally.ts`).

Verification:
  - 128 files changed, +237 / -222 (almost entirely import-path edits)
  - typecheck/lint/format clean
  - 734/734 tests pass
  - build produces identical dist/cli.js, dist/index.js, etc.
  - No public API change
  - src/utils/ no longer exists

* refactor: move orchestrators into tiers + rename scan -> inspect (#225)

* refactor: move install-skill, scan, oxlint-config out of src/ root into their tiers

Top-level src/ was still holding three orchestrator-grade files that
logically belong inside the existing tier directories. Move them so the
src/ root only contains public-API entry points and cross-cutting types.

Moves (via git mv, history preserved):
  src/install-skill.ts   -> src/cli/install-skill.ts          CLI subcommand
                                                              (already imported
                                                              6 things from cli/)
  src/scan.ts            -> src/core/scan.ts                  Orchestrator
                                                              (lives with the
                                                              logic it calls)
  src/oxlint-config.ts   -> src/core/runners/oxlint-config.ts Sibling of
                                                              run-oxlint.ts
                                                              (config + runner)

After: top-level src/ contains only the entry points and shared types:
  src/
  ├── cli/                  presentation
  ├── core/                 logic
  ├── plugin/               rules
  ├── constants.ts
  ├── errors.ts
  ├── eslint-plugin.ts      public-API entry
  ├── index.ts              public-API entry
  ├── knip.d.ts             type declaration
  └── types.ts

Imports rewritten via a small codemod (11 files):
  - 3 moved files' internal imports recomputed for new depth
  - 4 src/ consumers of the moved files updated
  - 4 test files updated

Verification:
  - 11 files changed, +39/-39 (all import path edits)
  - typecheck/lint/format clean
  - 734/734 tests pass
  - build produces identical dist/* output
  - run-oxlint.ts's relative path to dist/react-doctor-plugin.js
    (../../../) is unchanged -- run-oxlint.ts itself didn't move

Stacks on top of #224 (utils->core reorg). Once #224 merges, this PR's
base auto-rebases to main.

* refactor: rename scan -> inspect (function, file, types, test)

"scan" was a misnomer -- the function does much more than scan files: it
runs lint via oxlint, runs dead-code via knip, calculates a score,
merges/filters diagnostics, and renders CLI output. "inspect" describes
the role accurately and matches v2's chosen name.

Renames:
  src/core/scan.ts            -> src/core/inspect.ts             (git mv)
  tests/scan.test.ts          -> tests/inspect.test.ts           (git mv)
  scan() function             -> inspect() function
  runScan()                   -> runInspect()
  mergeScanOptions()          -> mergeInspectOptions()
  ResolvedScanOptions         -> ResolvedInspectOptions
  ScanResult                  -> InspectResult                   (src/types.ts)
  ScanOptions                 -> InspectOptions                  (src/types.ts)
  describe("scan", ...)       -> describe("inspect", ...)        (test)

Importers updated:
  src/cli/index.ts                                4 sites
  src/core/build-json-report.ts                   3 sites
  tests/inspect.test.ts                           5 sites
  tests/regressions/cli-and-output.test.ts        4 sites
  tests/build-json-report.test.ts                 2 sites

Kept as English prose (not identifiers):
  - "full scan", "scan output", "scan only files", "scan banner" in
    comments, CLI option descriptions, and prompt text -- these describe
    the action of scanning the codebase, not references to the function.
  - constants.ts JSX_OPENER_SCAN_MAX_LINES (about lexical JSX scanning,
    unrelated to inspect()).

Verification:
  - 7 files changed, +54/-54 (all renames)
  - typecheck/lint/format clean
  - 734/734 tests pass
  - No public API change (scan/ScanResult/ScanOptions weren't exported
    from src/index.ts -- the public API still calls the underlying
    function diagnose() which is unchanged)

Stacks on #225 (which moved scan.ts to src/core/). Once #225 merges,
this PR's base auto-rebases to main.

* fix: restore help-text example paths corrupted by the import codemod

The utils->core import-path codemod's regex matched `from '...'`
substrings inside string literals -- specifically the EXAMPLE code
shown to users in oxlint diagnostic help text. Three messages in
HELP_TEXT_MAP had their example paths mangled into the codemod's
target form (`../../utils/...`) even though they were supposed to
illustrate paths in the USER'S code, not paths internal to this
package.

Restored to the original example paths:

  no-barrel-import:
    `import { Button } from '../../utils/components/Button.js'`
    -> `import { Button } from './components/Button'`

  no-dynamic-import-path:
    `import('../../utils/feature/heavy.js')`
    -> `import('./feature/heavy.js')`

  nextjs-no-css-link:
    `import styles from '../../utils/Button.module.css.js'`
    -> `import styles from './Button.module.css'`

Verified no other string literals were corrupted (grepped for
`["'`]../../(utils|core|cli)/` across src/ -- all remaining hits are
legitimate plugin/utils imports from the per-rule split in #218, which
is its own unrelated `utils/` directory).

Closes the Bugbot finding on PR #224.

* refactor: move format-error-chain / logger / highlighter to core/ (fix layering)

Addresses Bugbot's "Core modules depend backwards on CLI layer" finding
on PR #224. Several core/ files were importing from cli/, undermining
the layered structure the refactor was introducing:

  core/build-json-report-error.ts        -> cli/format-error-chain
  core/runners/extract-failed-plugin-name -> cli/format-error-chain
  core/config/load-config                -> cli/logger
  core/config/read-ignore-file           -> cli/logger
  core/config/resolve-config-root-dir    -> cli/logger

The 3 utilities in question are all pure abstractions (no CLI-specific
behavior beyond using console.* for output), so they belong in core/
where both core AND cli consumers can import them without inversion.

Moves (git mv, history preserved):
  cli/format-error-chain.ts -> core/format-error-chain.ts  (Error.cause walker)
  cli/logger.ts             -> core/logger.ts              (silent-mode logger)
  cli/highlighter.ts        -> core/highlighter.ts         (picocolors wrapper)

22 files changed, +29/-29 -- all relative-import path rewrites driven
by a small codemod with a strict `^import ... from "..."` regex
(line-anchored to avoid the help-text-literal corruption bug from the
previous codemod, which triggered the bigger Bugbot follow-up).

After this:
  - core/ no longer imports from cli/ for utilities -- only
    core/inspect.ts still does (legitimately: it's the CLI orchestrator
    that calls render-*.ts + resolve-oxlint-node prompt + spinner)
  - cli/ files import the moved utilities via `../core/<name>.js`

Verification:
  - typecheck/lint/format clean
  - 734/734 tests pass
  - `rg "from .+cli/" src/core` shows only inspect.ts's legitimate
    imports of render-*, spinner, resolve-oxlint-node (all of which
    are CLI presentation/interaction, not utilities)
cursor Bot pushed a commit that referenced this pull request May 13, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
#221, #223, #224, #226, #227) split the monolithic src/utils +
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 13, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request May 14, 2026
Rebased PR #164 onto main after a chain of refactors (#218, #220,
oxlint-config.ts into per-feature modules. Re-applied the HIR
additions to the new locations:

- packages/react-doctor/src/plugin/hir/{types,lower,infer-types,
  runner,validators,index}.ts — unchanged from prior revision
- Rule registration moved from src/oxlint-config.ts to
  src/core/runners/oxlint/rule-maps.ts (GLOBAL_REACT_DOCTOR_RULES)
- Help / category metadata moved to src/core/runners/run-oxlint.ts
- Plugin index import + rules map updated for the new export
  paths
- Tests now import from src/core/runners/run-oxlint.js

All 744 tests pass; lint, typecheck, format clean.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant