react-doctor 0.0.35 → 0.0.36
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 +5 -3
- package/dist/cli.js +320 -153
- package/dist/cli.js.map +1 -1
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -1
- package/dist/react-doctor-plugin.d.ts.map +1 -1
- package/dist/react-doctor-plugin.js +549 -1
- package/dist/react-doctor-plugin.js.map +1 -1
- package/dist/skills/react-doctor/SKILL.md +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,13 +40,15 @@ npx -y react-doctor@latest . --verbose
|
|
|
40
40
|
|
|
41
41
|
## Install for your coding agent
|
|
42
42
|
|
|
43
|
-
Teach your coding agent all 47+ React best practice rules:
|
|
43
|
+
Teach your coding agent all 47+ React best practice rules. Run this at your project root:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
npx -y react-doctor@latest install
|
|
47
47
|
```
|
|
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
|
+
|
|
51
|
+
Supports Claude Code, Codex, GitHub Copilot, Gemini CLI, Cursor, OpenCode, Factory Droid, and Pi.
|
|
50
52
|
|
|
51
53
|
## GitHub Actions
|
|
52
54
|
|
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,281 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import fs, { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import fs, { accessSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import os, { tmpdir } from "node:os";
|
|
5
5
|
import path, { join } from "node:path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
import basePrompts from "prompts";
|
|
10
|
+
import ora from "ora";
|
|
7
11
|
import { randomUUID } from "node:crypto";
|
|
8
12
|
import { performance } from "node:perf_hooks";
|
|
9
13
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
10
|
-
import pc from "picocolors";
|
|
11
|
-
import basePrompts from "prompts";
|
|
12
14
|
import { main } from "knip";
|
|
13
15
|
import { createOptions } from "knip/session";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import ora from "ora";
|
|
16
16
|
|
|
17
|
+
//#region src/utils/detect-agents.ts
|
|
18
|
+
const SUPPORTED_AGENTS = {
|
|
19
|
+
claude: {
|
|
20
|
+
binaries: ["claude"],
|
|
21
|
+
displayName: "Claude Code",
|
|
22
|
+
skillDir: ".claude/skills"
|
|
23
|
+
},
|
|
24
|
+
codex: {
|
|
25
|
+
binaries: ["codex"],
|
|
26
|
+
displayName: "Codex",
|
|
27
|
+
skillDir: ".codex/skills"
|
|
28
|
+
},
|
|
29
|
+
copilot: {
|
|
30
|
+
binaries: ["copilot"],
|
|
31
|
+
displayName: "GitHub Copilot",
|
|
32
|
+
skillDir: ".github/copilot/skills"
|
|
33
|
+
},
|
|
34
|
+
gemini: {
|
|
35
|
+
binaries: ["gemini"],
|
|
36
|
+
displayName: "Gemini CLI",
|
|
37
|
+
skillDir: ".gemini/skills"
|
|
38
|
+
},
|
|
39
|
+
cursor: {
|
|
40
|
+
binaries: ["cursor", "agent"],
|
|
41
|
+
displayName: "Cursor",
|
|
42
|
+
skillDir: ".cursor/skills"
|
|
43
|
+
},
|
|
44
|
+
opencode: {
|
|
45
|
+
binaries: ["opencode"],
|
|
46
|
+
displayName: "OpenCode",
|
|
47
|
+
skillDir: ".opencode/skills"
|
|
48
|
+
},
|
|
49
|
+
droid: {
|
|
50
|
+
binaries: ["droid"],
|
|
51
|
+
displayName: "Factory Droid",
|
|
52
|
+
skillDir: ".droid/skills"
|
|
53
|
+
},
|
|
54
|
+
pi: {
|
|
55
|
+
binaries: ["pi", "omegon"],
|
|
56
|
+
displayName: "Pi",
|
|
57
|
+
skillDir: ".pi/skills"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const ALL_SUPPORTED_AGENTS = Object.keys(SUPPORTED_AGENTS);
|
|
61
|
+
const isCommandAvailable = (command) => {
|
|
62
|
+
const pathDirectories = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
63
|
+
for (const directory of pathDirectories) {
|
|
64
|
+
const binaryPath = path.join(directory, command);
|
|
65
|
+
try {
|
|
66
|
+
if (statSync(binaryPath).isFile()) {
|
|
67
|
+
accessSync(binaryPath, constants.X_OK);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
const detectAvailableAgents = () => ALL_SUPPORTED_AGENTS.filter((agent) => SUPPORTED_AGENTS[agent].binaries.some(isCommandAvailable));
|
|
75
|
+
const toDisplayName = (agent) => SUPPORTED_AGENTS[agent].displayName;
|
|
76
|
+
const toSkillDir = (agent) => SUPPORTED_AGENTS[agent].skillDir;
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/utils/highlighter.ts
|
|
80
|
+
const highlighter = {
|
|
81
|
+
error: pc.red,
|
|
82
|
+
warn: pc.yellow,
|
|
83
|
+
info: pc.cyan,
|
|
84
|
+
success: pc.green,
|
|
85
|
+
dim: pc.dim
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/utils/install-skill-for-agent.ts
|
|
90
|
+
const installSkillForAgent = (projectRoot, agent, skillSourceDirectory, skillName) => {
|
|
91
|
+
const installedSkillDirectory = path.join(projectRoot, toSkillDir(agent), skillName);
|
|
92
|
+
rmSync(installedSkillDirectory, {
|
|
93
|
+
recursive: true,
|
|
94
|
+
force: true
|
|
95
|
+
});
|
|
96
|
+
mkdirSync(path.dirname(installedSkillDirectory), { recursive: true });
|
|
97
|
+
cpSync(skillSourceDirectory, installedSkillDirectory, { recursive: true });
|
|
98
|
+
return installedSkillDirectory;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/utils/logger.ts
|
|
103
|
+
const logger = {
|
|
104
|
+
error(...args) {
|
|
105
|
+
console.log(highlighter.error(args.join(" ")));
|
|
106
|
+
},
|
|
107
|
+
warn(...args) {
|
|
108
|
+
console.log(highlighter.warn(args.join(" ")));
|
|
109
|
+
},
|
|
110
|
+
info(...args) {
|
|
111
|
+
console.log(highlighter.info(args.join(" ")));
|
|
112
|
+
},
|
|
113
|
+
success(...args) {
|
|
114
|
+
console.log(highlighter.success(args.join(" ")));
|
|
115
|
+
},
|
|
116
|
+
dim(...args) {
|
|
117
|
+
console.log(highlighter.dim(args.join(" ")));
|
|
118
|
+
},
|
|
119
|
+
log(...args) {
|
|
120
|
+
console.log(args.join(" "));
|
|
121
|
+
},
|
|
122
|
+
break() {
|
|
123
|
+
console.log("");
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/utils/should-auto-select-current-choice.ts
|
|
129
|
+
const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
|
|
130
|
+
if (choiceStates.some((choiceState) => choiceState.selected)) return false;
|
|
131
|
+
const currentChoice = choiceStates[cursor];
|
|
132
|
+
return Boolean(currentChoice) && !currentChoice.disabled;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/utils/should-select-all-choices.ts
|
|
137
|
+
const shouldSelectAllChoices = (choiceStates) => {
|
|
138
|
+
return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/utils/prompts.ts
|
|
143
|
+
const require = createRequire(import.meta.url);
|
|
144
|
+
const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
|
|
145
|
+
const PROMPTS_SELECT_MODULE_PATH = "prompts/lib/elements/select";
|
|
146
|
+
let didPatchMultiselectToggleAll = false;
|
|
147
|
+
let didPatchMultiselectSubmit = false;
|
|
148
|
+
let didPatchSelectBanner = false;
|
|
149
|
+
const selectBannerMap = /* @__PURE__ */ new Map();
|
|
150
|
+
const onCancel = () => {
|
|
151
|
+
logger.break();
|
|
152
|
+
logger.log("Cancelled.");
|
|
153
|
+
logger.dim("Run `npx react-doctor@latest --fix` to fix issues.");
|
|
154
|
+
logger.break();
|
|
155
|
+
process.exit(0);
|
|
156
|
+
};
|
|
157
|
+
const patchMultiselectToggleAll = () => {
|
|
158
|
+
if (didPatchMultiselectToggleAll) return;
|
|
159
|
+
didPatchMultiselectToggleAll = true;
|
|
160
|
+
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
161
|
+
multiselectPromptConstructor.prototype.toggleAll = function() {
|
|
162
|
+
const isCurrentChoiceDisabled = Boolean(this.value[this.cursor]?.disabled);
|
|
163
|
+
if (this.maxChoices !== void 0 || isCurrentChoiceDisabled) {
|
|
164
|
+
this.bell();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const shouldSelectAllEnabledChoices = shouldSelectAllChoices(this.value);
|
|
168
|
+
for (const choiceState of this.value) {
|
|
169
|
+
if (choiceState.disabled) continue;
|
|
170
|
+
choiceState.selected = shouldSelectAllEnabledChoices;
|
|
171
|
+
}
|
|
172
|
+
this.render();
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
const patchMultiselectSubmit = () => {
|
|
176
|
+
if (didPatchMultiselectSubmit) return;
|
|
177
|
+
didPatchMultiselectSubmit = true;
|
|
178
|
+
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
179
|
+
const originalSubmit = multiselectPromptConstructor.prototype.submit;
|
|
180
|
+
multiselectPromptConstructor.prototype.submit = function() {
|
|
181
|
+
if (shouldAutoSelectCurrentChoice(this.value, this.cursor)) this.value[this.cursor].selected = true;
|
|
182
|
+
originalSubmit.call(this);
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
const patchSelectBanner = () => {
|
|
186
|
+
if (didPatchSelectBanner) return;
|
|
187
|
+
didPatchSelectBanner = true;
|
|
188
|
+
const selectConstructor = require(PROMPTS_SELECT_MODULE_PATH);
|
|
189
|
+
const promptsClear = require("prompts/lib/util/clear");
|
|
190
|
+
const originalRender = selectConstructor.prototype.render;
|
|
191
|
+
selectConstructor.prototype.render = function() {
|
|
192
|
+
originalRender.call(this);
|
|
193
|
+
const banner = selectBannerMap.get(this.cursor);
|
|
194
|
+
if (!banner || this.closed || this.done) return;
|
|
195
|
+
this.out.write(promptsClear(this.outputText, this.out.columns));
|
|
196
|
+
this.outputText = `${banner}\n\n${this.outputText}`;
|
|
197
|
+
this.out.write(this.outputText);
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
const prompts = (questions) => {
|
|
201
|
+
patchMultiselectToggleAll();
|
|
202
|
+
patchMultiselectSubmit();
|
|
203
|
+
patchSelectBanner();
|
|
204
|
+
return basePrompts(questions, { onCancel });
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/utils/spinner.ts
|
|
209
|
+
let sharedInstance = null;
|
|
210
|
+
let activeCount = 0;
|
|
211
|
+
const pendingTexts = /* @__PURE__ */ new Set();
|
|
212
|
+
const finalize = (method, originalText, displayText) => {
|
|
213
|
+
pendingTexts.delete(originalText);
|
|
214
|
+
activeCount--;
|
|
215
|
+
if (activeCount <= 0 || !sharedInstance) {
|
|
216
|
+
sharedInstance?.[method](displayText);
|
|
217
|
+
sharedInstance = null;
|
|
218
|
+
activeCount = 0;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
sharedInstance.stop();
|
|
222
|
+
ora(displayText).start()[method](displayText);
|
|
223
|
+
const [remainingText] = pendingTexts;
|
|
224
|
+
if (remainingText) sharedInstance.text = remainingText;
|
|
225
|
+
sharedInstance.start();
|
|
226
|
+
};
|
|
227
|
+
const spinner = (text) => ({ start() {
|
|
228
|
+
activeCount++;
|
|
229
|
+
pendingTexts.add(text);
|
|
230
|
+
if (!sharedInstance) sharedInstance = ora({ text }).start();
|
|
231
|
+
else sharedInstance.text = text;
|
|
232
|
+
return {
|
|
233
|
+
succeed: (displayText) => finalize("succeed", text, displayText),
|
|
234
|
+
fail: (displayText) => finalize("fail", text, displayText)
|
|
235
|
+
};
|
|
236
|
+
} });
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/install-skill.ts
|
|
240
|
+
const SKILL_NAME = "react-doctor";
|
|
241
|
+
const getSkillSourceDirectory = () => {
|
|
242
|
+
const distDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
243
|
+
return path.join(distDirectory, "skills", SKILL_NAME);
|
|
244
|
+
};
|
|
245
|
+
const runInstallSkill = async (options = {}) => {
|
|
246
|
+
const projectRoot = process.cwd();
|
|
247
|
+
const sourceDir = getSkillSourceDirectory();
|
|
248
|
+
if (!existsSync(path.join(sourceDir, "SKILL.md"))) {
|
|
249
|
+
logger.error(`Could not locate the ${SKILL_NAME} skill bundled with this package.`);
|
|
250
|
+
process.exitCode = 1;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const detectedAgents = detectAvailableAgents();
|
|
254
|
+
if (detectedAgents.length === 0) {
|
|
255
|
+
logger.error("No supported coding agents detected on your PATH.");
|
|
256
|
+
logger.dim(" Supported: Claude Code, Codex, GitHub Copilot, Gemini CLI, Cursor, OpenCode, Factory Droid, Pi.");
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const selectedAgents = Boolean(options.yes) || !process.stdin.isTTY ? detectedAgents : (await prompts({
|
|
261
|
+
type: "multiselect",
|
|
262
|
+
name: "agents",
|
|
263
|
+
message: `Install the ${highlighter.info(SKILL_NAME)} skill for:`,
|
|
264
|
+
choices: detectedAgents.map((agent) => ({
|
|
265
|
+
title: toDisplayName(agent),
|
|
266
|
+
value: agent,
|
|
267
|
+
selected: true
|
|
268
|
+
})),
|
|
269
|
+
instructions: false,
|
|
270
|
+
min: 1
|
|
271
|
+
})).agents ?? [];
|
|
272
|
+
if (selectedAgents.length === 0) return;
|
|
273
|
+
const installSpinner = spinner(`Installing ${SKILL_NAME} skill...`).start();
|
|
274
|
+
for (const agent of selectedAgents) installSkillForAgent(projectRoot, agent, sourceDir, SKILL_NAME);
|
|
275
|
+
installSpinner.succeed(`${SKILL_NAME} skill installed for ${selectedAgents.map(toDisplayName).join(", ")}.`);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
17
279
|
//#region src/constants.ts
|
|
18
280
|
const SOURCE_FILE_PATTERN = /\.(tsx?|jsx?)$/;
|
|
19
281
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
@@ -140,16 +402,6 @@ const calculateScore = async (diagnostics) => {
|
|
|
140
402
|
}
|
|
141
403
|
};
|
|
142
404
|
|
|
143
|
-
//#endregion
|
|
144
|
-
//#region src/utils/highlighter.ts
|
|
145
|
-
const highlighter = {
|
|
146
|
-
error: pc.red,
|
|
147
|
-
warn: pc.yellow,
|
|
148
|
-
info: pc.cyan,
|
|
149
|
-
success: pc.green,
|
|
150
|
-
dim: pc.dim
|
|
151
|
-
};
|
|
152
|
-
|
|
153
405
|
//#endregion
|
|
154
406
|
//#region src/utils/colorize-by-score.ts
|
|
155
407
|
const colorizeByScore = (text, score) => {
|
|
@@ -803,32 +1055,6 @@ const discoverProject = (directory) => {
|
|
|
803
1055
|
};
|
|
804
1056
|
};
|
|
805
1057
|
|
|
806
|
-
//#endregion
|
|
807
|
-
//#region src/utils/logger.ts
|
|
808
|
-
const logger = {
|
|
809
|
-
error(...args) {
|
|
810
|
-
console.log(highlighter.error(args.join(" ")));
|
|
811
|
-
},
|
|
812
|
-
warn(...args) {
|
|
813
|
-
console.log(highlighter.warn(args.join(" ")));
|
|
814
|
-
},
|
|
815
|
-
info(...args) {
|
|
816
|
-
console.log(highlighter.info(args.join(" ")));
|
|
817
|
-
},
|
|
818
|
-
success(...args) {
|
|
819
|
-
console.log(highlighter.success(args.join(" ")));
|
|
820
|
-
},
|
|
821
|
-
dim(...args) {
|
|
822
|
-
console.log(highlighter.dim(args.join(" ")));
|
|
823
|
-
},
|
|
824
|
-
log(...args) {
|
|
825
|
-
console.log(args.join(" "));
|
|
826
|
-
},
|
|
827
|
-
break() {
|
|
828
|
-
console.log("");
|
|
829
|
-
}
|
|
830
|
-
};
|
|
831
|
-
|
|
832
1058
|
//#endregion
|
|
833
1059
|
//#region src/utils/framed-box.ts
|
|
834
1060
|
const createFramedLine = (plainText, renderedText = plainText) => ({
|
|
@@ -909,86 +1135,6 @@ const loadConfig = (rootDirectory) => {
|
|
|
909
1135
|
return null;
|
|
910
1136
|
};
|
|
911
1137
|
|
|
912
|
-
//#endregion
|
|
913
|
-
//#region src/utils/should-auto-select-current-choice.ts
|
|
914
|
-
const shouldAutoSelectCurrentChoice = (choiceStates, cursor) => {
|
|
915
|
-
if (choiceStates.some((choiceState) => choiceState.selected)) return false;
|
|
916
|
-
const currentChoice = choiceStates[cursor];
|
|
917
|
-
return Boolean(currentChoice) && !currentChoice.disabled;
|
|
918
|
-
};
|
|
919
|
-
|
|
920
|
-
//#endregion
|
|
921
|
-
//#region src/utils/should-select-all-choices.ts
|
|
922
|
-
const shouldSelectAllChoices = (choiceStates) => {
|
|
923
|
-
return choiceStates.filter((choiceState) => !choiceState.disabled).some((choiceState) => choiceState.selected !== true);
|
|
924
|
-
};
|
|
925
|
-
|
|
926
|
-
//#endregion
|
|
927
|
-
//#region src/utils/prompts.ts
|
|
928
|
-
const require = createRequire(import.meta.url);
|
|
929
|
-
const PROMPTS_MULTISELECT_MODULE_PATH = "prompts/lib/elements/multiselect";
|
|
930
|
-
const PROMPTS_SELECT_MODULE_PATH = "prompts/lib/elements/select";
|
|
931
|
-
let didPatchMultiselectToggleAll = false;
|
|
932
|
-
let didPatchMultiselectSubmit = false;
|
|
933
|
-
let didPatchSelectBanner = false;
|
|
934
|
-
const selectBannerMap = /* @__PURE__ */ new Map();
|
|
935
|
-
const onCancel = () => {
|
|
936
|
-
logger.break();
|
|
937
|
-
logger.log("Cancelled.");
|
|
938
|
-
logger.dim("Run `npx react-doctor@latest --fix` to fix issues.");
|
|
939
|
-
logger.break();
|
|
940
|
-
process.exit(0);
|
|
941
|
-
};
|
|
942
|
-
const patchMultiselectToggleAll = () => {
|
|
943
|
-
if (didPatchMultiselectToggleAll) return;
|
|
944
|
-
didPatchMultiselectToggleAll = true;
|
|
945
|
-
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
946
|
-
multiselectPromptConstructor.prototype.toggleAll = function() {
|
|
947
|
-
const isCurrentChoiceDisabled = Boolean(this.value[this.cursor]?.disabled);
|
|
948
|
-
if (this.maxChoices !== void 0 || isCurrentChoiceDisabled) {
|
|
949
|
-
this.bell();
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
const shouldSelectAllEnabledChoices = shouldSelectAllChoices(this.value);
|
|
953
|
-
for (const choiceState of this.value) {
|
|
954
|
-
if (choiceState.disabled) continue;
|
|
955
|
-
choiceState.selected = shouldSelectAllEnabledChoices;
|
|
956
|
-
}
|
|
957
|
-
this.render();
|
|
958
|
-
};
|
|
959
|
-
};
|
|
960
|
-
const patchMultiselectSubmit = () => {
|
|
961
|
-
if (didPatchMultiselectSubmit) return;
|
|
962
|
-
didPatchMultiselectSubmit = true;
|
|
963
|
-
const multiselectPromptConstructor = require(PROMPTS_MULTISELECT_MODULE_PATH);
|
|
964
|
-
const originalSubmit = multiselectPromptConstructor.prototype.submit;
|
|
965
|
-
multiselectPromptConstructor.prototype.submit = function() {
|
|
966
|
-
if (shouldAutoSelectCurrentChoice(this.value, this.cursor)) this.value[this.cursor].selected = true;
|
|
967
|
-
originalSubmit.call(this);
|
|
968
|
-
};
|
|
969
|
-
};
|
|
970
|
-
const patchSelectBanner = () => {
|
|
971
|
-
if (didPatchSelectBanner) return;
|
|
972
|
-
didPatchSelectBanner = true;
|
|
973
|
-
const selectConstructor = require(PROMPTS_SELECT_MODULE_PATH);
|
|
974
|
-
const promptsClear = require("prompts/lib/util/clear");
|
|
975
|
-
const originalRender = selectConstructor.prototype.render;
|
|
976
|
-
selectConstructor.prototype.render = function() {
|
|
977
|
-
originalRender.call(this);
|
|
978
|
-
const banner = selectBannerMap.get(this.cursor);
|
|
979
|
-
if (!banner || this.closed || this.done) return;
|
|
980
|
-
this.out.write(promptsClear(this.outputText, this.out.columns));
|
|
981
|
-
this.outputText = `${banner}\n\n${this.outputText}`;
|
|
982
|
-
this.out.write(this.outputText);
|
|
983
|
-
};
|
|
984
|
-
};
|
|
985
|
-
const prompts = (questions) => {
|
|
986
|
-
patchMultiselectToggleAll();
|
|
987
|
-
patchMultiselectSubmit();
|
|
988
|
-
patchSelectBanner();
|
|
989
|
-
return basePrompts(questions, { onCancel });
|
|
990
|
-
};
|
|
991
|
-
|
|
992
1138
|
//#endregion
|
|
993
1139
|
//#region src/utils/resolve-compatible-node.ts
|
|
994
1140
|
const parseNodeVersion = (versionString) => {
|
|
@@ -1386,6 +1532,21 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler, customRul
|
|
|
1386
1532
|
"react-doctor/query-no-query-in-effect": "warn",
|
|
1387
1533
|
"react-doctor/query-mutation-missing-invalidation": "warn",
|
|
1388
1534
|
"react-doctor/query-no-usequery-for-mutation": "warn",
|
|
1535
|
+
"react-doctor/no-inline-bounce-easing": "warn",
|
|
1536
|
+
"react-doctor/no-z-index-9999": "warn",
|
|
1537
|
+
"react-doctor/no-inline-exhaustive-style": "warn",
|
|
1538
|
+
"react-doctor/no-side-tab-border": "warn",
|
|
1539
|
+
"react-doctor/no-pure-black-background": "warn",
|
|
1540
|
+
"react-doctor/no-gradient-text": "warn",
|
|
1541
|
+
"react-doctor/no-dark-mode-glow": "warn",
|
|
1542
|
+
"react-doctor/no-justified-text": "warn",
|
|
1543
|
+
"react-doctor/no-tiny-text": "warn",
|
|
1544
|
+
"react-doctor/no-wide-letter-spacing": "warn",
|
|
1545
|
+
"react-doctor/no-gray-on-colored-background": "warn",
|
|
1546
|
+
"react-doctor/no-layout-transition-inline": "warn",
|
|
1547
|
+
"react-doctor/no-disabled-zoom": "error",
|
|
1548
|
+
"react-doctor/no-outline-none": "warn",
|
|
1549
|
+
"react-doctor/no-long-transition-duration": "warn",
|
|
1389
1550
|
"react-doctor/async-parallel": "warn",
|
|
1390
1551
|
...framework === "nextjs" ? NEXTJS_RULES : {},
|
|
1391
1552
|
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {},
|
|
@@ -1507,6 +1668,21 @@ const RULE_CATEGORY_MAP = {
|
|
|
1507
1668
|
"react-doctor/query-no-query-in-effect": "TanStack Query",
|
|
1508
1669
|
"react-doctor/query-mutation-missing-invalidation": "TanStack Query",
|
|
1509
1670
|
"react-doctor/query-no-usequery-for-mutation": "TanStack Query",
|
|
1671
|
+
"react-doctor/no-inline-bounce-easing": "Performance",
|
|
1672
|
+
"react-doctor/no-z-index-9999": "Architecture",
|
|
1673
|
+
"react-doctor/no-inline-exhaustive-style": "Architecture",
|
|
1674
|
+
"react-doctor/no-side-tab-border": "Architecture",
|
|
1675
|
+
"react-doctor/no-pure-black-background": "Architecture",
|
|
1676
|
+
"react-doctor/no-gradient-text": "Architecture",
|
|
1677
|
+
"react-doctor/no-dark-mode-glow": "Architecture",
|
|
1678
|
+
"react-doctor/no-justified-text": "Accessibility",
|
|
1679
|
+
"react-doctor/no-tiny-text": "Accessibility",
|
|
1680
|
+
"react-doctor/no-wide-letter-spacing": "Architecture",
|
|
1681
|
+
"react-doctor/no-gray-on-colored-background": "Accessibility",
|
|
1682
|
+
"react-doctor/no-layout-transition-inline": "Performance",
|
|
1683
|
+
"react-doctor/no-disabled-zoom": "Accessibility",
|
|
1684
|
+
"react-doctor/no-outline-none": "Accessibility",
|
|
1685
|
+
"react-doctor/no-long-transition-duration": "Performance",
|
|
1510
1686
|
"react-doctor/js-flatmap-filter": "Performance",
|
|
1511
1687
|
"react-doctor/async-parallel": "Performance",
|
|
1512
1688
|
"react-doctor/rn-no-raw-text": "React Native",
|
|
@@ -1565,6 +1741,21 @@ const RULE_HELP_MAP = {
|
|
|
1565
1741
|
"prefer-dynamic-import": "Use `const Component = dynamic(() => import('library'), { ssr: false })` from next/dynamic or React.lazy()",
|
|
1566
1742
|
"use-lazy-motion": "Use `import { LazyMotion, m } from \"framer-motion\"` with `domAnimation` features — saves ~30kb",
|
|
1567
1743
|
"no-undeferred-third-party": "Use `next/script` with `strategy=\"lazyOnload\"` or add the `defer` attribute",
|
|
1744
|
+
"no-inline-bounce-easing": "Use `cubic-bezier(0.16, 1, 0.3, 1)` (ease-out-expo) for natural deceleration — objects in the real world don't bounce",
|
|
1745
|
+
"no-z-index-9999": "Define a z-index scale in your design tokens (e.g. dropdown: 10, modal: 20, toast: 30). Create a new stacking context with `isolation: isolate` instead of escalating values",
|
|
1746
|
+
"no-inline-exhaustive-style": "Move styles to a CSS class, CSS module, Tailwind utilities, or a styled component — inline objects with many properties hurt readability and create new references every render",
|
|
1747
|
+
"no-side-tab-border": "Use a subtler accent (box-shadow inset, background gradient, or border-bottom) instead of a thick one-sided border",
|
|
1748
|
+
"no-pure-black-background": "Tint the background slightly toward your brand hue — e.g. `#0a0a0f` or Tailwind's `bg-gray-950`. Pure black looks harsh on modern displays",
|
|
1749
|
+
"no-gradient-text": "Use solid text colors for readability. If you need emphasis, use font weight, size, or a distinct color instead of gradients",
|
|
1750
|
+
"no-dark-mode-glow": "Use a subtle `box-shadow` with neutral colors for depth, or `border` with low opacity. Colored glows on dark backgrounds are the default AI-generated aesthetic",
|
|
1751
|
+
"no-justified-text": "Use `text-align: left` for body text, or add `hyphens: auto` and `overflow-wrap: break-word` if you must justify",
|
|
1752
|
+
"no-tiny-text": "Use at least 12px for body content, 16px is ideal. Small text is hard to read, especially on high-DPI mobile screens",
|
|
1753
|
+
"no-wide-letter-spacing": "Reserve wide tracking (letter-spacing > 0.05em) for short uppercase labels, navigation items, and buttons — not body text",
|
|
1754
|
+
"no-gray-on-colored-background": "Use a darker shade of the background color for text, or white/near-white for contrast. Gray text on colored backgrounds looks washed out",
|
|
1755
|
+
"no-layout-transition-inline": "Use `transform` and `opacity` for transitions — they run on the compositor thread. For height animations, use `grid-template-rows: 0fr → 1fr`",
|
|
1756
|
+
"no-disabled-zoom": "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If your layout breaks at 200% zoom, fix the layout — don't punish users with disabilities",
|
|
1757
|
+
"no-outline-none": "Use `:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px }` to show focus only for keyboard users while hiding it for mouse clicks",
|
|
1758
|
+
"no-long-transition-duration": "Keep UI transitions under 1s — 100-150ms for instant feedback, 200-300ms for state changes, 300-500ms for layout changes. Use longer durations only for page-load hero animations",
|
|
1568
1759
|
"no-array-index-as-key": "Use a stable unique identifier: `key={item.id}` or `key={item.slug}` — index keys break on reorder/filter",
|
|
1569
1760
|
"rendering-conditional-render": "Change to `{items.length > 0 && <List />}` or use a ternary: `{items.length ? <List /> : null}`",
|
|
1570
1761
|
"no-prevent-default": "Use `<form action={serverAction}>` (works without JS) or `<button>` instead of `<a>` with preventDefault",
|
|
@@ -1762,37 +1953,6 @@ const runOxlint = async (rootDirectory, hasTypeScript, framework, hasReactCompil
|
|
|
1762
1953
|
}
|
|
1763
1954
|
};
|
|
1764
1955
|
|
|
1765
|
-
//#endregion
|
|
1766
|
-
//#region src/utils/spinner.ts
|
|
1767
|
-
let sharedInstance = null;
|
|
1768
|
-
let activeCount = 0;
|
|
1769
|
-
const pendingTexts = /* @__PURE__ */ new Set();
|
|
1770
|
-
const finalize = (method, originalText, displayText) => {
|
|
1771
|
-
pendingTexts.delete(originalText);
|
|
1772
|
-
activeCount--;
|
|
1773
|
-
if (activeCount <= 0 || !sharedInstance) {
|
|
1774
|
-
sharedInstance?.[method](displayText);
|
|
1775
|
-
sharedInstance = null;
|
|
1776
|
-
activeCount = 0;
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
sharedInstance.stop();
|
|
1780
|
-
ora(displayText).start()[method](displayText);
|
|
1781
|
-
const [remainingText] = pendingTexts;
|
|
1782
|
-
if (remainingText) sharedInstance.text = remainingText;
|
|
1783
|
-
sharedInstance.start();
|
|
1784
|
-
};
|
|
1785
|
-
const spinner = (text) => ({ start() {
|
|
1786
|
-
activeCount++;
|
|
1787
|
-
pendingTexts.add(text);
|
|
1788
|
-
if (!sharedInstance) sharedInstance = ora({ text }).start();
|
|
1789
|
-
else sharedInstance.text = text;
|
|
1790
|
-
return {
|
|
1791
|
-
succeed: (displayText) => finalize("succeed", text, displayText),
|
|
1792
|
-
fail: (displayText) => finalize("fail", text, displayText)
|
|
1793
|
-
};
|
|
1794
|
-
} });
|
|
1795
|
-
|
|
1796
1956
|
//#endregion
|
|
1797
1957
|
//#region src/scan.ts
|
|
1798
1958
|
const SEVERITY_ORDER = {
|
|
@@ -2352,7 +2512,7 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
2352
2512
|
|
|
2353
2513
|
//#endregion
|
|
2354
2514
|
//#region src/cli.ts
|
|
2355
|
-
const VERSION = "0.0.
|
|
2515
|
+
const VERSION = "0.0.36";
|
|
2356
2516
|
const VALID_FAIL_ON_LEVELS = new Set([
|
|
2357
2517
|
"error",
|
|
2358
2518
|
"warning",
|
|
@@ -2510,6 +2670,13 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
2510
2670
|
${highlighter.dim("Learn more:")}
|
|
2511
2671
|
${highlighter.info("https://github.com/millionco/react-doctor")}
|
|
2512
2672
|
`);
|
|
2673
|
+
program.command("install").description("Install the react-doctor skill into your coding agents").option("-y, --yes", "skip prompts, install for all detected agents").action(async (options) => {
|
|
2674
|
+
try {
|
|
2675
|
+
await runInstallSkill({ yes: options.yes });
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
handleError(error);
|
|
2678
|
+
}
|
|
2679
|
+
});
|
|
2513
2680
|
const main$1 = async () => {
|
|
2514
2681
|
await program.parseAsync();
|
|
2515
2682
|
};
|