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 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, GitHub Copilot, Gemini CLI, Cursor, OpenCode, Factory Droid, and Pi.
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, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
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 AGENTS_SKILL_DIR = ".agents/skills";
51
- const SUPPORTED_AGENTS = {
52
- claude: {
53
- binaries: ["claude"],
54
- displayName: "Claude Code",
55
- skillDir: ".claude/skills"
56
- },
57
- codex: {
58
- binaries: ["codex"],
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 detectAvailableAgents = () => ALL_SUPPORTED_AGENTS.filter((agent) => SUPPORTED_AGENTS[agent].binaries.some(isCommandAvailable));
108
- const toDisplayName = (agent) => SUPPORTED_AGENTS[agent].displayName;
109
- const toSkillDir = (agent) => SUPPORTED_AGENTS[agent].skillDir;
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, "SKILL.md"))) {
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 on your PATH.");
294
- logger.dim(" Supported: Claude Code, Codex, GitHub Copilot, Gemini CLI, Cursor, OpenCode, Factory Droid, Pi.");
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(SKILL_NAME)} skill for:`,
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
- const installedDirectories = /* @__PURE__ */ new Set();
319
- for (const agent of selectedAgents) {
320
- const installedDirectory = installSkillForAgent(projectRoot, agent, sourceDir, SKILL_NAME, installedDirectories);
321
- installedDirectories.add(installedDirectory);
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
- categories: {
1948
- correctness: "off",
1949
- suspicious: "off",
1950
- pedantic: "off",
1951
- perf: "off",
1952
- restriction: "off",
1953
- style: "off",
1954
- nursery: "off"
1955
- },
1956
- plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
1957
- jsPlugins: [...resolveReactHooksJsPlugin(hasReactCompiler, customRulesOnly), pluginPath],
1958
- rules: {
1959
- ...customRulesOnly ? {} : BUILTIN_REACT_RULES,
1960
- ...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
1961
- ...hasReactCompiler && !customRulesOnly ? REACT_COMPILER_RULES : {},
1962
- ...GLOBAL_REACT_DOCTOR_RULES,
1963
- ...framework === "nextjs" ? NEXTJS_RULES : {},
1964
- ...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
1965
- ...framework === "tanstack-start" ? TANSTACK_START_RULES : {},
1966
- ...hasTanStackQuery ? TANSTACK_QUERY_RULES : {}
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.44";
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
- categories: {
1672
- correctness: "off",
1673
- suspicious: "off",
1674
- pedantic: "off",
1675
- perf: "off",
1676
- restriction: "off",
1677
- style: "off",
1678
- nursery: "off"
1679
- },
1680
- plugins: customRulesOnly ? [] : ["react", "jsx-a11y"],
1681
- jsPlugins: [...resolveReactHooksJsPlugin(hasReactCompiler, customRulesOnly), pluginPath],
1682
- rules: {
1683
- ...customRulesOnly ? {} : BUILTIN_REACT_RULES,
1684
- ...customRulesOnly ? {} : BUILTIN_A11Y_RULES,
1685
- ...hasReactCompiler && !customRulesOnly ? REACT_COMPILER_RULES : {},
1686
- ...GLOBAL_REACT_DOCTOR_RULES,
1687
- ...framework === "nextjs" ? NEXTJS_RULES : {},
1688
- ...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
1689
- ...framework === "tanstack-start" ? TANSTACK_START_RULES : {},
1690
- ...hasTanStackQuery ? TANSTACK_QUERY_RULES : {}
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.44",
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",