react-doctor 0.0.6 → 0.0.8

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/dist/cli.js CHANGED
@@ -5,8 +5,8 @@ import path, { join } from "node:path";
5
5
  import { Command } from "commander";
6
6
  import pc from "picocolors";
7
7
  import { randomUUID } from "node:crypto";
8
- import fs, { mkdirSync, writeFileSync } from "node:fs";
9
- import os, { tmpdir } from "node:os";
8
+ import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import os, { homedir, tmpdir } from "node:os";
10
10
  import { performance } from "node:perf_hooks";
11
11
  import { main } from "knip";
12
12
  import { createOptions } from "knip/session";
@@ -73,6 +73,7 @@ const SCORE_GOOD_THRESHOLD = 75;
73
73
  const SCORE_OK_THRESHOLD = 50;
74
74
  const SCORE_BAR_WIDTH_CHARS = 50;
75
75
  const SCORE_API_URL = "https://www.react.doctor/api/score";
76
+ const SHARE_BASE_URL = "https://www.react.doctor/share";
76
77
  const OFFLINE_MESSAGE = "You are offline, could not calculate score. Reconnect to calculate.";
77
78
 
78
79
  //#endregion
@@ -493,7 +494,8 @@ const NEXTJS_RULES = {
493
494
  "react-doctor/nextjs-no-font-link": "warn",
494
495
  "react-doctor/nextjs-no-css-link": "warn",
495
496
  "react-doctor/nextjs-no-polyfill-script": "warn",
496
- "react-doctor/nextjs-no-head-import": "error"
497
+ "react-doctor/nextjs-no-head-import": "error",
498
+ "react-doctor/nextjs-no-side-effect-in-get-handler": "error"
497
499
  };
498
500
  const REACT_COMPILER_RULES = {
499
501
  "react-hooks-js/set-state-in-render": "error",
@@ -670,6 +672,7 @@ const RULE_CATEGORY_MAP = {
670
672
  "react-doctor/nextjs-no-css-link": "Next.js",
671
673
  "react-doctor/nextjs-no-polyfill-script": "Next.js",
672
674
  "react-doctor/nextjs-no-head-import": "Next.js",
675
+ "react-doctor/nextjs-no-side-effect-in-get-handler": "Security",
673
676
  "react-doctor/server-auth-actions": "Server",
674
677
  "react-doctor/server-after-nonblocking": "Server",
675
678
  "react-doctor/client-passive-event-listeners": "Performance",
@@ -725,6 +728,7 @@ const RULE_HELP_MAP = {
725
728
  "nextjs-no-css-link": "Import CSS directly: `import './styles.css'` or use CSS Modules: `import styles from './Button.module.css'`",
726
729
  "nextjs-no-polyfill-script": "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
727
730
  "nextjs-no-head-import": "Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
731
+ "nextjs-no-side-effect-in-get-handler": "Move the side effect to a POST handler and use a <form> or fetch with method POST — GET requests can be triggered by prefetching and are vulnerable to CSRF",
728
732
  "server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
729
733
  "server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
730
734
  "client-passive-event-listeners": "Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`",
@@ -971,6 +975,17 @@ const printBranding = (score) => {
971
975
  logger.log(` React Doctor ${highlighter.dim("(www.react.doctor)")}`);
972
976
  logger.break();
973
977
  };
978
+ const buildShareUrl = (diagnostics, scoreResult) => {
979
+ const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
980
+ const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
981
+ const affectedFileCount = collectAffectedFiles(diagnostics).size;
982
+ const params = new URLSearchParams();
983
+ if (scoreResult) params.set("s", String(scoreResult.score));
984
+ if (errorCount > 0) params.set("e", String(errorCount));
985
+ if (warningCount > 0) params.set("w", String(warningCount));
986
+ if (affectedFileCount > 0) params.set("f", String(affectedFileCount));
987
+ return `${SHARE_BASE_URL}?${params.toString()}`;
988
+ };
974
989
  const printSummary = (diagnostics, elapsedMilliseconds, scoreResult) => {
975
990
  const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
976
991
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
@@ -997,6 +1012,9 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult) => {
997
1012
  } catch {
998
1013
  logger.break();
999
1014
  }
1015
+ const shareUrl = buildShareUrl(diagnostics, scoreResult);
1016
+ logger.break();
1017
+ logger.dim(` Share your results: ${highlighter.info(shareUrl)}`);
1000
1018
  };
1001
1019
  const scan = async (directory, options) => {
1002
1020
  const startTime = performance.now();
@@ -1125,9 +1143,68 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
1125
1143
  return selectedDirectories;
1126
1144
  };
1127
1145
 
1146
+ //#endregion
1147
+ //#region src/utils/skill-prompt.ts
1148
+ const CONFIG_DIRECTORY = join(homedir(), ".react-doctor");
1149
+ const CONFIG_FILE = join(CONFIG_DIRECTORY, "config.json");
1150
+ const SKILL_REPO = "aidenybai/react-doctor";
1151
+ const readConfig = () => {
1152
+ try {
1153
+ if (!existsSync(CONFIG_FILE)) return {};
1154
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
1155
+ } catch {
1156
+ return {};
1157
+ }
1158
+ };
1159
+ const writeConfig = (config) => {
1160
+ try {
1161
+ if (!existsSync(CONFIG_DIRECTORY)) mkdirSync(CONFIG_DIRECTORY, { recursive: true });
1162
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
1163
+ } catch {}
1164
+ };
1165
+ const installSkill = () => {
1166
+ try {
1167
+ execSync(`npx -y skills add ${SKILL_REPO}`, { stdio: "inherit" });
1168
+ return true;
1169
+ } catch {
1170
+ return false;
1171
+ }
1172
+ };
1173
+ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
1174
+ const config = readConfig();
1175
+ if (config.skillPromptDismissed) return;
1176
+ if (shouldSkipPrompts) return;
1177
+ logger.break();
1178
+ logger.log(`${highlighter.info("💡")} Have your coding agent fix these issues automatically?`);
1179
+ logger.dim(` Install the ${highlighter.info("react-doctor")} skill to teach Cursor, Claude Code, Copilot,`);
1180
+ logger.dim(" Ami, and other AI agents how to diagnose and fix these React issues.");
1181
+ logger.break();
1182
+ const { shouldInstall } = await prompts({
1183
+ type: "confirm",
1184
+ name: "shouldInstall",
1185
+ message: "Install skill?",
1186
+ initial: true
1187
+ });
1188
+ if (shouldInstall) {
1189
+ logger.break();
1190
+ if (installSkill()) {
1191
+ logger.break();
1192
+ logger.success("Skill installed!");
1193
+ } else {
1194
+ logger.break();
1195
+ logger.dim("Skill install failed. You can install manually:");
1196
+ logger.dim(` npx skills add ${SKILL_REPO}`);
1197
+ }
1198
+ }
1199
+ writeConfig({
1200
+ ...config,
1201
+ skillPromptDismissed: true
1202
+ });
1203
+ };
1204
+
1128
1205
  //#endregion
1129
1206
  //#region src/cli.ts
1130
- const VERSION = "0.0.6";
1207
+ const VERSION = "0.0.8";
1131
1208
  process.on("SIGINT", () => process.exit(0));
1132
1209
  process.on("SIGTERM", () => process.exit(0));
1133
1210
  const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--fix", "open Ami to auto-fix all issues").action(async (directory, flags) => {
@@ -1144,7 +1221,17 @@ const program = new Command().name("react-doctor").description("Diagnose React c
1144
1221
  verbose: Boolean(flags.verbose),
1145
1222
  scoreOnly: isScoreOnly
1146
1223
  };
1147
- const shouldSkipPrompts = flags.yes || Boolean(process.env.CI) || Boolean(process.env.CLAUDECODE) || Boolean(process.env.AMI) || !process.stdin.isTTY;
1224
+ const isAutomatedEnvironment = [
1225
+ process.env.CI,
1226
+ process.env.CLAUDECODE,
1227
+ process.env.CURSOR_TRACE_ID,
1228
+ process.env.CURSOR_AGENT,
1229
+ process.env.CODEX_CI,
1230
+ process.env.OPENCODE,
1231
+ process.env.AMP_HOME,
1232
+ process.env.AMI
1233
+ ].some(Boolean);
1234
+ const shouldSkipPrompts = flags.yes || isAutomatedEnvironment || !process.stdin.isTTY;
1148
1235
  const projectDirectories = await selectProjects(resolvedDirectory, flags.project, shouldSkipPrompts);
1149
1236
  for (const projectDirectory of projectDirectories) {
1150
1237
  if (!isScoreOnly) {
@@ -1155,6 +1242,10 @@ const program = new Command().name("react-doctor").description("Diagnose React c
1155
1242
  if (!isScoreOnly) logger.break();
1156
1243
  }
1157
1244
  if (flags.fix) openAmiToFix(resolvedDirectory);
1245
+ if (!isScoreOnly) {
1246
+ await maybePromptSkillInstall(shouldSkipPrompts);
1247
+ if (!shouldSkipPrompts && !flags.fix) await maybePromptAmiFix(resolvedDirectory);
1248
+ }
1158
1249
  } catch (error) {
1159
1250
  handleError(error);
1160
1251
  }
@@ -1164,9 +1255,6 @@ ${highlighter.dim("Learn more:")}
1164
1255
  `);
1165
1256
  const AMI_INSTALL_URL = "https://ami.dev/install.sh";
1166
1257
  const AMI_FIX_PROMPT = "Run npx -y react-doctor@latest . --verbose, read every diagnostic, then fix all issues one by one. After fixing, re-run react-doctor to verify the score improved.";
1167
- const buildAmiDeeplink = (projectDirectory) => {
1168
- return `ami://new-chat?cwd=${encodeURIComponent(projectDirectory)}&prompt=${encodeURIComponent(AMI_FIX_PROMPT)}&mode=agent`;
1169
- };
1170
1258
  const isAmiInstalled = () => {
1171
1259
  try {
1172
1260
  execSync("ls /Applications/Ami.app", { stdio: "ignore" });
@@ -1190,7 +1278,7 @@ const openAmiToFix = (directory) => {
1190
1278
  const resolvedDirectory = path.resolve(directory);
1191
1279
  if (!isAmiInstalled()) installAmi();
1192
1280
  logger.log("Opening Ami to fix react-doctor issues...");
1193
- const deeplink = buildAmiDeeplink(resolvedDirectory);
1281
+ const deeplink = `ami://open-project?cwd=${encodeURIComponent(resolvedDirectory)}&prompt=${encodeURIComponent(AMI_FIX_PROMPT)}&mode=agent`;
1194
1282
  try {
1195
1283
  execSync(`open "${deeplink}"`, { stdio: "ignore" });
1196
1284
  logger.success("Opened Ami with react-doctor fix prompt.");
@@ -1200,6 +1288,20 @@ const openAmiToFix = (directory) => {
1200
1288
  logger.info(deeplink);
1201
1289
  }
1202
1290
  };
1291
+ const maybePromptAmiFix = async (directory) => {
1292
+ logger.break();
1293
+ logger.log(`Fix these issues with ${highlighter.info("Ami")}?`);
1294
+ logger.dim(" Ami is a coding agent built to understand your codebase and fix issues");
1295
+ logger.dim(` automatically. Learn more at ${highlighter.info("https://ami.dev")}`);
1296
+ logger.break();
1297
+ const { shouldFix } = await prompts({
1298
+ type: "confirm",
1299
+ name: "shouldFix",
1300
+ message: "Open Ami to fix?",
1301
+ initial: true
1302
+ });
1303
+ if (shouldFix) openAmiToFix(directory);
1304
+ };
1203
1305
  const fixAction = (directory) => {
1204
1306
  try {
1205
1307
  openAmiToFix(directory);
@@ -1208,7 +1310,7 @@ const fixAction = (directory) => {
1208
1310
  }
1209
1311
  };
1210
1312
  const fixCommand = new Command("fix").description("Open Ami to auto-fix react-doctor issues").argument("[directory]", "project directory", ".").action(fixAction);
1211
- const installAmiCommand = new Command("install-ami").description("Open Ami to auto-fix react-doctor issues").argument("[directory]", "project directory", ".").action(fixAction);
1313
+ const installAmiCommand = new Command("install-ami").description("Install Ami and open it to auto-fix issues").argument("[directory]", "project directory", ".").action(fixAction);
1212
1314
  program.addCommand(fixCommand);
1213
1315
  program.addCommand(installAmiCommand);
1214
1316
  const main$1 = async () => {