react-doctor 0.0.44 → 0.0.46
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/README.md +1 -1
- package/dist/cli.js +76 -101
- package/dist/index.js +30 -27
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ npx -y react-doctor@latest install
|
|
|
48
48
|
|
|
49
49
|
You'll be prompted to pick which detected agents to install for. Pass `--yes` to skip prompts and install for every detected agent.
|
|
50
50
|
|
|
51
|
-
Supports Claude Code, Codex,
|
|
51
|
+
Supports 50+ coding agents via [`agent-install`](https://www.npmjs.com/package/agent-install), including Claude Code, Codex, Cursor, Factory Droid, Gemini CLI, GitHub Copilot, Goose, OpenCode, Pi, Windsurf, Roo Code, Cline, Kilo Code, Warp, Replit, OpenHands, Continue, and many more. Detection is the union of CLI binaries on `$PATH` and config dirs in `$HOME` (`~/.claude`, `~/.cursor`, `~/.codex`, `~/.factory`, `~/.pi`, etc.).
|
|
52
52
|
|
|
53
53
|
## GitHub Actions
|
|
54
54
|
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import fs, { accessSync, constants,
|
|
2
|
+
import fs, { accessSync, constants, existsSync, mkdirSync, mkdtempSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os, { tmpdir } from "node:os";
|
|
4
4
|
import path, { join } from "node:path";
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { SKILL_MANIFEST_FILE, detectInstalledSkillAgents, getSkillAgentConfig, getSkillAgentTypes, installSkillsFromSource } from "agent-install";
|
|
8
9
|
import pc from "picocolors";
|
|
9
10
|
import basePrompts from "prompts";
|
|
10
11
|
import ora from "ora";
|
|
@@ -43,54 +44,21 @@ const IGNORED_DIRECTORIES = new Set([
|
|
|
43
44
|
"coverage"
|
|
44
45
|
]);
|
|
45
46
|
const CANONICAL_GITHUB_URL = "https://github.com/millionco/react-doctor";
|
|
47
|
+
const SKILL_NAME = "react-doctor";
|
|
46
48
|
const PROXY_OUTPUT_MAX_BYTES = 50 * 1024 * 1024;
|
|
47
49
|
const buildNoReactDependencyError = (directory) => `No React dependency found in ${directory}/package.json. Add "react" to dependencies (or peerDependencies) and re-run.`;
|
|
48
50
|
//#endregion
|
|
49
51
|
//#region src/utils/detect-agents.ts
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
displayName: "Codex",
|
|
60
|
-
skillDir: AGENTS_SKILL_DIR
|
|
61
|
-
},
|
|
62
|
-
copilot: {
|
|
63
|
-
binaries: ["copilot"],
|
|
64
|
-
displayName: "GitHub Copilot",
|
|
65
|
-
skillDir: AGENTS_SKILL_DIR
|
|
66
|
-
},
|
|
67
|
-
gemini: {
|
|
68
|
-
binaries: ["gemini"],
|
|
69
|
-
displayName: "Gemini CLI",
|
|
70
|
-
skillDir: AGENTS_SKILL_DIR
|
|
71
|
-
},
|
|
72
|
-
cursor: {
|
|
73
|
-
binaries: ["cursor", "agent"],
|
|
74
|
-
displayName: "Cursor",
|
|
75
|
-
skillDir: AGENTS_SKILL_DIR
|
|
76
|
-
},
|
|
77
|
-
opencode: {
|
|
78
|
-
binaries: ["opencode"],
|
|
79
|
-
displayName: "OpenCode",
|
|
80
|
-
skillDir: AGENTS_SKILL_DIR
|
|
81
|
-
},
|
|
82
|
-
droid: {
|
|
83
|
-
binaries: ["droid"],
|
|
84
|
-
displayName: "Factory Droid",
|
|
85
|
-
skillDir: ".factory/skills"
|
|
86
|
-
},
|
|
87
|
-
pi: {
|
|
88
|
-
binaries: ["pi", "omegon"],
|
|
89
|
-
displayName: "Pi",
|
|
90
|
-
skillDir: AGENTS_SKILL_DIR
|
|
91
|
-
}
|
|
52
|
+
const PATH_BINARIES = {
|
|
53
|
+
"claude-code": ["claude"],
|
|
54
|
+
codex: ["codex"],
|
|
55
|
+
cursor: ["cursor", "agent"],
|
|
56
|
+
droid: ["droid"],
|
|
57
|
+
"gemini-cli": ["gemini"],
|
|
58
|
+
"github-copilot": ["copilot"],
|
|
59
|
+
opencode: ["opencode"],
|
|
60
|
+
pi: ["pi", "omegon"]
|
|
92
61
|
};
|
|
93
|
-
const ALL_SUPPORTED_AGENTS = Object.keys(SUPPORTED_AGENTS);
|
|
94
62
|
const isCommandAvailable = (command) => {
|
|
95
63
|
const pathDirectories = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
96
64
|
for (const directory of pathDirectories) {
|
|
@@ -104,9 +72,15 @@ const isCommandAvailable = (command) => {
|
|
|
104
72
|
}
|
|
105
73
|
return false;
|
|
106
74
|
};
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
75
|
+
const detectPathAvailableAgents = () => {
|
|
76
|
+
const detected = [];
|
|
77
|
+
for (const [agent, binaries] of Object.entries(PATH_BINARIES)) if (binaries.some(isCommandAvailable)) detected.push(agent);
|
|
78
|
+
return detected;
|
|
79
|
+
};
|
|
80
|
+
const detectAvailableAgents = async () => {
|
|
81
|
+
const detected = new Set([...detectPathAvailableAgents(), ...await detectInstalledSkillAgents()]);
|
|
82
|
+
return getSkillAgentTypes().filter((agent) => agent !== "universal" && detected.has(agent));
|
|
83
|
+
};
|
|
110
84
|
//#endregion
|
|
111
85
|
//#region src/utils/highlighter.ts
|
|
112
86
|
const highlighter = {
|
|
@@ -117,19 +91,6 @@ const highlighter = {
|
|
|
117
91
|
dim: pc.dim
|
|
118
92
|
};
|
|
119
93
|
//#endregion
|
|
120
|
-
//#region src/utils/install-skill-for-agent.ts
|
|
121
|
-
const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillName, alreadyInstalledDirectories) => {
|
|
122
|
-
const installedSkillDirectory = path.join(projectRoot, toSkillDir(agent), skillName);
|
|
123
|
-
if (alreadyInstalledDirectories?.has(installedSkillDirectory)) return installedSkillDirectory;
|
|
124
|
-
rmSync(installedSkillDirectory, {
|
|
125
|
-
recursive: true,
|
|
126
|
-
force: true
|
|
127
|
-
});
|
|
128
|
-
mkdirSync(path.dirname(installedSkillDirectory), { recursive: true });
|
|
129
|
-
cpSync(skillSourceDirectory, installedSkillDirectory, { recursive: true });
|
|
130
|
-
return installedSkillDirectory;
|
|
131
|
-
};
|
|
132
|
-
//#endregion
|
|
133
94
|
//#region src/utils/logger.ts
|
|
134
95
|
let isSilent$1 = false;
|
|
135
96
|
const setLoggerSilent = (silent) => {
|
|
@@ -274,31 +235,34 @@ const spinner = (text) => ({ start() {
|
|
|
274
235
|
return handle;
|
|
275
236
|
} });
|
|
276
237
|
//#endregion
|
|
238
|
+
//#region src/utils/to-display-name.ts
|
|
239
|
+
const toDisplayName = (agent) => getSkillAgentConfig(agent).displayName;
|
|
240
|
+
//#endregion
|
|
277
241
|
//#region src/install-skill.ts
|
|
278
|
-
const SKILL_NAME = "react-doctor";
|
|
279
242
|
const getSkillSourceDirectory = () => {
|
|
280
243
|
const distDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
281
244
|
return path.join(distDirectory, "skills", SKILL_NAME);
|
|
282
245
|
};
|
|
283
246
|
const runInstallSkill = async (options = {}) => {
|
|
284
|
-
const projectRoot = process.cwd();
|
|
285
|
-
const sourceDir = getSkillSourceDirectory();
|
|
286
|
-
if (!existsSync(path.join(sourceDir,
|
|
247
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
248
|
+
const sourceDir = options.sourceDir ?? getSkillSourceDirectory();
|
|
249
|
+
if (!existsSync(path.join(sourceDir, SKILL_MANIFEST_FILE))) {
|
|
287
250
|
logger.error(`Could not locate the ${SKILL_NAME} skill bundled with this package.`);
|
|
288
251
|
process.exitCode = 1;
|
|
289
252
|
return;
|
|
290
253
|
}
|
|
291
|
-
const detectedAgents = detectAvailableAgents();
|
|
254
|
+
const detectedAgents = options.detectedAgents ?? await detectAvailableAgents();
|
|
292
255
|
if (detectedAgents.length === 0) {
|
|
293
|
-
logger.error("No supported coding agents detected
|
|
294
|
-
logger.dim("
|
|
256
|
+
logger.error("No supported coding agents detected.");
|
|
257
|
+
logger.dim(" Looked for binaries on PATH (claude, codex, cursor, droid, gemini, copilot, opencode, pi)");
|
|
258
|
+
logger.dim(" and config dirs in $HOME (~/.claude, ~/.cursor, ~/.codex, ~/.gemini, ...).");
|
|
295
259
|
process.exitCode = 1;
|
|
296
260
|
return;
|
|
297
261
|
}
|
|
298
262
|
const selectedAgents = Boolean(options.yes) || !process.stdin.isTTY ? detectedAgents : (await prompts({
|
|
299
263
|
type: "multiselect",
|
|
300
264
|
name: "agents",
|
|
301
|
-
message: `Install the ${highlighter.info(
|
|
265
|
+
message: `Install the ${highlighter.info("react-doctor")} skill for:`,
|
|
302
266
|
choices: detectedAgents.map((agent) => ({
|
|
303
267
|
title: toDisplayName(agent),
|
|
304
268
|
value: agent,
|
|
@@ -315,12 +279,20 @@ const runInstallSkill = async (options = {}) => {
|
|
|
315
279
|
return;
|
|
316
280
|
}
|
|
317
281
|
const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
282
|
+
try {
|
|
283
|
+
const installResult = await installSkillsFromSource({
|
|
284
|
+
source: sourceDir,
|
|
285
|
+
agents: selectedAgents,
|
|
286
|
+
cwd: projectRoot,
|
|
287
|
+
mode: "copy"
|
|
288
|
+
});
|
|
289
|
+
if (installResult.skills.length === 0) throw new Error(`Could not parse ${SKILL_MANIFEST_FILE} for ${SKILL_NAME} (missing or invalid frontmatter).`);
|
|
290
|
+
if (installResult.failed.length > 0) throw new Error(installResult.failed.map((failure) => `${toDisplayName(failure.agent)}: ${failure.error}`).join("\n"));
|
|
291
|
+
installSpinner.succeed(`${SKILL_NAME} skill installed for ${selectedAgents.map(toDisplayName).join(", ")}.`);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
installSpinner.fail(`Failed to install ${SKILL_NAME} skill.`);
|
|
294
|
+
throw error;
|
|
322
295
|
}
|
|
323
|
-
installSpinner.succeed(`${SKILL_NAME} skill installed for ${selectedAgents.map(toDisplayName).join(", ")}.`);
|
|
324
296
|
};
|
|
325
297
|
//#endregion
|
|
326
298
|
//#region src/core/calculate-score-locally.ts
|
|
@@ -1789,14 +1761,14 @@ const REACT_COMPILER_RULES = {
|
|
|
1789
1761
|
"react-hooks-js/todo": "warn"
|
|
1790
1762
|
};
|
|
1791
1763
|
const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
|
|
1792
|
-
if (!hasReactCompiler || customRulesOnly) return
|
|
1764
|
+
if (!hasReactCompiler || customRulesOnly) return null;
|
|
1793
1765
|
try {
|
|
1794
|
-
return
|
|
1766
|
+
return {
|
|
1795
1767
|
name: "react-hooks-js",
|
|
1796
1768
|
specifier: esmRequire$1.resolve("eslint-plugin-react-hooks")
|
|
1797
|
-
}
|
|
1769
|
+
};
|
|
1798
1770
|
} catch {
|
|
1799
|
-
return
|
|
1771
|
+
return null;
|
|
1800
1772
|
}
|
|
1801
1773
|
};
|
|
1802
1774
|
const TANSTACK_QUERY_RULES = {
|
|
@@ -1943,29 +1915,32 @@ const ALL_REACT_DOCTOR_RULE_KEYS = new Set([
|
|
|
1943
1915
|
...Object.keys(TANSTACK_START_RULES),
|
|
1944
1916
|
...Object.keys(TANSTACK_QUERY_RULES)
|
|
1945
1917
|
]);
|
|
1946
|
-
const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, hasTanStackQuery, customRulesOnly = false }) =>
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
}
|
|
1918
|
+
const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, hasTanStackQuery, customRulesOnly = false }) => {
|
|
1919
|
+
const reactHooksJsPlugin = resolveReactHooksJsPlugin(hasReactCompiler, customRulesOnly);
|
|
1920
|
+
return {
|
|
1921
|
+
categories: {
|
|
1922
|
+
correctness: "off",
|
|
1923
|
+
suspicious: "off",
|
|
1924
|
+
pedantic: "off",
|
|
1925
|
+
perf: "off",
|
|
1926
|
+
restriction: "off",
|
|
1927
|
+
style: "off",
|
|
1928
|
+
nursery: "off"
|
|
1929
|
+
},
|
|
1930
|
+
plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
|
|
1931
|
+
jsPlugins: reactHooksJsPlugin ? [reactHooksJsPlugin, pluginPath] : [pluginPath],
|
|
1932
|
+
rules: {
|
|
1933
|
+
...customRulesOnly ? {} : BUILTIN_REACT_RULES,
|
|
1934
|
+
...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
|
|
1935
|
+
...reactHooksJsPlugin ? REACT_COMPILER_RULES : {},
|
|
1936
|
+
...GLOBAL_REACT_DOCTOR_RULES,
|
|
1937
|
+
...framework === "nextjs" ? NEXTJS_RULES : {},
|
|
1938
|
+
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
|
|
1939
|
+
...framework === "tanstack-start" ? TANSTACK_START_RULES : {},
|
|
1940
|
+
...hasTanStackQuery ? TANSTACK_QUERY_RULES : {}
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
};
|
|
1969
1944
|
//#endregion
|
|
1970
1945
|
//#region src/utils/neutralize-disable-directives.ts
|
|
1971
1946
|
const DISABLE_DIRECTIVE_PATTERN = /(eslint|oxlint)-disable/;
|
|
@@ -3358,7 +3333,7 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
3358
3333
|
};
|
|
3359
3334
|
//#endregion
|
|
3360
3335
|
//#region src/cli.ts
|
|
3361
|
-
const VERSION = "0.0.
|
|
3336
|
+
const VERSION = "0.0.46";
|
|
3362
3337
|
const VALID_FAIL_ON_LEVELS = new Set([
|
|
3363
3338
|
"error",
|
|
3364
3339
|
"warning",
|
package/dist/index.js
CHANGED
|
@@ -1513,14 +1513,14 @@ const REACT_COMPILER_RULES = {
|
|
|
1513
1513
|
"react-hooks-js/todo": "warn"
|
|
1514
1514
|
};
|
|
1515
1515
|
const resolveReactHooksJsPlugin = (hasReactCompiler, customRulesOnly) => {
|
|
1516
|
-
if (!hasReactCompiler || customRulesOnly) return
|
|
1516
|
+
if (!hasReactCompiler || customRulesOnly) return null;
|
|
1517
1517
|
try {
|
|
1518
|
-
return
|
|
1518
|
+
return {
|
|
1519
1519
|
name: "react-hooks-js",
|
|
1520
1520
|
specifier: esmRequire$1.resolve("eslint-plugin-react-hooks")
|
|
1521
|
-
}
|
|
1521
|
+
};
|
|
1522
1522
|
} catch {
|
|
1523
|
-
return
|
|
1523
|
+
return null;
|
|
1524
1524
|
}
|
|
1525
1525
|
};
|
|
1526
1526
|
const TANSTACK_QUERY_RULES = {
|
|
@@ -1667,29 +1667,32 @@ const ALL_REACT_DOCTOR_RULE_KEYS = new Set([
|
|
|
1667
1667
|
...Object.keys(TANSTACK_START_RULES),
|
|
1668
1668
|
...Object.keys(TANSTACK_QUERY_RULES)
|
|
1669
1669
|
]);
|
|
1670
|
-
const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, hasTanStackQuery, customRulesOnly = false }) =>
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
}
|
|
1670
|
+
const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, hasTanStackQuery, customRulesOnly = false }) => {
|
|
1671
|
+
const reactHooksJsPlugin = resolveReactHooksJsPlugin(hasReactCompiler, customRulesOnly);
|
|
1672
|
+
return {
|
|
1673
|
+
categories: {
|
|
1674
|
+
correctness: "off",
|
|
1675
|
+
suspicious: "off",
|
|
1676
|
+
pedantic: "off",
|
|
1677
|
+
perf: "off",
|
|
1678
|
+
restriction: "off",
|
|
1679
|
+
style: "off",
|
|
1680
|
+
nursery: "off"
|
|
1681
|
+
},
|
|
1682
|
+
plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
|
|
1683
|
+
jsPlugins: reactHooksJsPlugin ? [reactHooksJsPlugin, pluginPath] : [pluginPath],
|
|
1684
|
+
rules: {
|
|
1685
|
+
...customRulesOnly ? {} : BUILTIN_REACT_RULES,
|
|
1686
|
+
...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
|
|
1687
|
+
...reactHooksJsPlugin ? REACT_COMPILER_RULES : {},
|
|
1688
|
+
...GLOBAL_REACT_DOCTOR_RULES,
|
|
1689
|
+
...framework === "nextjs" ? NEXTJS_RULES : {},
|
|
1690
|
+
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
|
|
1691
|
+
...framework === "tanstack-start" ? TANSTACK_START_RULES : {},
|
|
1692
|
+
...hasTanStackQuery ? TANSTACK_QUERY_RULES : {}
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
};
|
|
1693
1696
|
//#endregion
|
|
1694
1697
|
//#region src/utils/neutralize-disable-directives.ts
|
|
1695
1698
|
const DISABLE_DIRECTIVE_PATTERN = /(eslint|oxlint)-disable/;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.46",
|
|
4
4
|
"description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
+
"agent-install": "0.0.4",
|
|
65
66
|
"commander": "^14.0.3",
|
|
66
67
|
"knip": "^6.3.1",
|
|
67
68
|
"ora": "^9.3.0",
|