Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .agents/skills/product-thinking/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Run the pass whenever a diff touches a surface a user — a developer running Re
| GitHub Action input/output | `action.yml` | Versioned independently (`vN`); workflows in other repos break when an input or output changes. |
| Terminal output / UX | `cli/utils/` renderers | The first impression and the daily experience; noise or confusion here is what makes people stop running it. |

**Not here:** lint rules go through the `rule-research` → `rule-writing` → `rule-validate` pipeline, and rule configuration through `doctor-explain` — this pass is for the surface _around_ the rules, not the rules themselves. Internal-only changes (the engine, private `core` types, tests, tooling) skip the pass entirely; note in one line why the change is internal and move on.
**Not here:** lint rules go through the `rule-research` → `rule-writing` → `rule-validate` pipeline, and rule configuration through `react-doctor/references/explain.md` — this pass is for the surface _around_ the rules, not the rules themselves. Internal-only changes (the engine, private `core` types, tests, tooling) skip the pass entirely; note in one line why the change is internal and move on.

## Steps

Expand Down Expand Up @@ -87,7 +87,7 @@ A surface nobody can discover is wasted, and a stale doc is a trust bug. Documen

- The `--help` / usage text next to the new flag or command, so it's discoverable from the CLI itself.
- The website page and the canonical prompt under `react.doctor/prompts/...`, which is what agents fetch at runtime.
- The distributed skills (`skills/react-doctor`, `skills/doctor-explain`) when the change alters the user-facing workflow.
- The distributed skills (`skills/react-doctor`, `skills/react-doctor/references/explain.md`) when the change alters the user-facing workflow.

### 6. Record the kill metric

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/project-info/count-source-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const countSourceFilesViaGit = (rootDirectory: string): number | null => {
// filesystem walk in countSourceFilesViaFilesystem.
const result = spawnSync(
"git",
["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."],
{
cwd: rootDirectory,
encoding: "utf-8",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/list-source-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const listSourceFilesViaGit = (rootDirectory: string): string[] | null => {
// skipping submodule files entirely.
const result = spawnSync(
"git",
["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."],
{
cwd: rootDirectory,
encoding: "utf-8",
Expand Down
21 changes: 21 additions & 0 deletions packages/core/tests/list-source-files-with-size.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { spawnSync } from "node:child_process";
import * as fs from "node:fs";
import os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test";
import { MINIFIED_MIN_SIZE_BYTES } from "../src/project-info/constants.js";
import { countSourceFiles } from "../src/project-info/count-source-files.js";
import { listSourceFiles, listSourceFilesWithSize } from "../src/utils/list-source-files.js";

describe("listSourceFilesWithSize", () => {
Expand Down Expand Up @@ -56,4 +58,23 @@ describe("listSourceFilesWithSize", () => {
listSourceFilesWithSize(temporaryDirectory).map((entry) => entry.path),
);
});

it("scopes git listings to the requested subdirectory", () => {
const workspaceDirectory = path.join(temporaryDirectory, "workspace");
const appDirectory = path.join(workspaceDirectory, "apps", "web");
const packageDirectory = path.join(workspaceDirectory, "packages", "ui");
fs.mkdirSync(appDirectory, { recursive: true });
fs.mkdirSync(packageDirectory, { recursive: true });
fs.writeFileSync(path.join(appDirectory, "App.tsx"), "export const App = () => null;\n");
fs.writeFileSync(
path.join(packageDirectory, "Button.tsx"),
"export const Button = () => null;\n",
);

spawnSync("git", ["init"], { cwd: workspaceDirectory });
spawnSync("git", ["add", "."], { cwd: workspaceDirectory });

expect(listSourceFiles(appDirectory)).toEqual(["App.tsx"]);
expect(countSourceFiles(appDirectory)).toBe(1);
});
});
12 changes: 1 addition & 11 deletions packages/core/tests/services/dead-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Stream from "effect/Stream";
import { describe, expect, it } from "vite-plus/test";
import type { Diagnostic, ProjectInfo } from "@react-doctor/core";
import type { Diagnostic } from "@react-doctor/core";
import { DeadCode } from "../../src/services/dead-code.js";

const sampleDiagnostic: Diagnostic = {
Expand All @@ -17,14 +17,6 @@ const sampleDiagnostic: Diagnostic = {
category: "Maintainability",
};

const sampleInput = {
rootDirectory: "/repo",
userConfig: null,
} satisfies {
rootDirectory: string;
userConfig: ProjectInfo["framework"] extends string ? null : null;
};

describe("DeadCode.layerOf", () => {
it("emits the supplied diagnostics as a stream", async () => {
const collected = await Effect.runPromise(
Expand Down Expand Up @@ -70,5 +62,3 @@ describe("DeadCode.layerNode", () => {
}
});
});

void sampleInput;
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ interface ProjectScanEntry {

const getScoreLabel = (score: number): string => {
if (score >= SCORE_GOOD_THRESHOLD) return "Great";
if (score >= SCORE_OK_THRESHOLD) return "OK";
return "Needs work";
if (score >= SCORE_OK_THRESHOLD) return "Needs work";
return "Critical";
};

const buildSummaryLine = (entry: ProjectScanEntry, longestProjectNameLength: number): string => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const resolveCliInspectOptions = (
noScore: flags.score === false || flags.telemetry === false || (userConfig?.noScore ?? false),
isCi: isCiEnvironment(),
silent: Boolean(flags.json),
suppressRendering: Boolean(flags.json),
concurrency: resolveParallelFlag(flags.parallel),
categoryFilters: resolveCliCategories(flags.category),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ describe("resolveCliInspectOptions: --no-telemetry alias", () => {
it("keeps scoring on by default", () => {
expect(resolveCliInspectOptions({}, null).noScore).toBe(false);
});

it("suppresses rendering when JSON owns stdout", () => {
expect(resolveCliInspectOptions({ json: true }, null)).toMatchObject({
silent: true,
suppressRendering: true,
});
});
});

describe("resolveCliInspectOptions: category filtering", () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/website/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Diagnose React codebase health. Scans for security, performance, correctness, and architecture issues, then outputs a 0–100 score with actionable diagnostics.

Note: dead-code detection was removed in v0.2 (previously powered by knip). For dead-code analysis, run `npx knip` directly.
Dead-code detection is included by default and powered by deslop-js. Use `--no-dead-code` when you only want lint diagnostics.

## Usage

Expand Down
17 changes: 8 additions & 9 deletions packages/website/src/components/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ const DIAGNOSTICS: RuleDiagnostic[] = [
location: "src/components/Dashboard.tsx:42",
},
{
ruleKey: "react-doctor/no-server-action-auth",
ruleKey: "react-doctor/server-auth-actions",
severity: "error",
message: 'Server action "deleteUser" has no auth check, so any caller could run it',
help: "Add an authentication check at the top of every server action.",
count: 2,
location: "src/app/actions/users.ts:18",
},
{
ruleKey: "react/no-array-index-key",
ruleKey: "react-doctor/no-array-index-key",
severity: "error",
message: "Array index is used as a key, so reordered items can keep the wrong state",
help: "Use a unique, stable identifier from each item as the key prop.",
Expand Down Expand Up @@ -171,6 +171,7 @@ const DiagnosticItem = ({ diagnostic }: { diagnostic: RuleDiagnostic }) => {
return (
<div className="mb-1">
<button
type="button"
onClick={() => setIsOpen((previous) => !previous)}
className="inline-flex items-start gap-1 text-left"
>
Expand Down Expand Up @@ -220,7 +221,7 @@ const CopyCommand = () => {
return (
<div className="group flex items-center gap-4 border border-white/20 px-3 py-1.5 transition-colors hover:bg-white/5">
<span className="select-all whitespace-nowrap text-white">{RUN_COMMAND}</span>
<button onClick={handleCopy}>
<button type="button" onClick={handleCopy}>
<IconComponent size={16} className={iconClass} />
</button>
</div>
Expand Down Expand Up @@ -272,14 +273,11 @@ const markAnimationCompleted = () => {
};

const Terminal = () => {
const [state, setState] = useState<AnimationState>(INITIAL_STATE);
const [state, setState] = useState<AnimationState>(() =>
didAnimationComplete() ? COMPLETED_STATE : INITIAL_STATE,
);

useEffect(() => {
if (didAnimationComplete()) {
setState(COMPLETED_STATE);
return;
}

const abortController = new AbortController();
const { signal } = abortController;

Expand Down Expand Up @@ -408,6 +406,7 @@ const Terminal = () => {
{state.showCta && (
<div className="mt-8">
<button
type="button"
onClick={() => {
try {
localStorage.removeItem(ANIMATION_COMPLETED_KEY);
Expand Down
131 changes: 0 additions & 131 deletions scripts/convert-node-imports.mjs

This file was deleted.

2 changes: 1 addition & 1 deletion skills/react-doctor/references/explain.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ npx react-doctor@latest rules explain react-doctor/no-array-index-as-key
5. Validate the change did what they wanted:

```bash
npx react-doctor@latest --verbose --diff
npx react-doctor@latest --verbose --scope changed
```

## Commands
Expand Down
Loading