11import { execSync } from "node:child_process" ;
2- import { existsSync , mkdirSync , readFileSync , writeFileSync } from "node:fs" ;
2+ import { appendFileSync , existsSync , mkdirSync , readFileSync , writeFileSync } from "node:fs" ;
33import { homedir } from "node:os" ;
44import { join } from "node:path" ;
5- import { INSTALL_SKILL_URL } from "../constants.js" ;
65import { highlighter } from "./highlighter.js" ;
76import { logger } from "./logger.js" ;
87import { prompts } from "./prompts.js" ;
98
10- const CONFIG_DIRECTORY = join ( homedir ( ) , ".react-doctor" ) ;
9+ const HOME_DIRECTORY = homedir ( ) ;
10+ const CONFIG_DIRECTORY = join ( HOME_DIRECTORY , ".react-doctor" ) ;
1111const CONFIG_FILE = join ( CONFIG_DIRECTORY , "config.json" ) ;
1212
13+ const SKILL_NAME = "react-doctor" ;
14+ const WINDSURF_MARKER = "# React Doctor" ;
15+
16+ const SKILL_DESCRIPTION =
17+ "Run after making React changes to catch issues early. Use when reviewing code, finishing a feature, or fixing bugs in a React project." ;
18+
19+ const SKILL_BODY = `Scans your React codebase for security, performance, correctness, and architecture issues. Outputs a 0-100 score with actionable diagnostics.
20+
21+ ## Usage
22+
23+ \`\`\`bash
24+ npx -y react-doctor@latest . --verbose --diff
25+ \`\`\`
26+
27+ ## Workflow
28+
29+ Run after making changes to catch issues early. Fix errors first, then re-run to verify the score improved.` ;
30+
31+ const SKILL_CONTENT = `---
32+ name: ${ SKILL_NAME }
33+ description: ${ SKILL_DESCRIPTION }
34+ version: 1.0.0
35+ ---
36+
37+ # React Doctor
38+
39+ ${ SKILL_BODY }
40+ ` ;
41+
42+ const AGENTS_CONTENT = `# React Doctor
43+
44+ ${ SKILL_DESCRIPTION }
45+
46+ ${ SKILL_BODY }
47+ ` ;
48+
49+ const CODEX_AGENT_CONFIG = `interface:
50+ display_name: "${ SKILL_NAME } "
51+ short_description: "Diagnose and fix React codebase health issues"
52+ ` ;
53+
1354interface SkillPromptConfig {
1455 skillPromptDismissed ?: boolean ;
1556}
1657
58+ interface SkillTarget {
59+ name : string ;
60+ detect : ( ) => boolean ;
61+ install : ( ) => void ;
62+ }
63+
1764const readSkillPromptConfig = ( ) : SkillPromptConfig => {
1865 try {
1966 if ( ! existsSync ( CONFIG_FILE ) ) return { } ;
@@ -25,20 +72,127 @@ const readSkillPromptConfig = (): SkillPromptConfig => {
2572
2673const writeSkillPromptConfig = ( config : SkillPromptConfig ) : void => {
2774 try {
28- if ( ! existsSync ( CONFIG_DIRECTORY ) ) {
29- mkdirSync ( CONFIG_DIRECTORY , { recursive : true } ) ;
30- }
75+ mkdirSync ( CONFIG_DIRECTORY , { recursive : true } ) ;
3176 writeFileSync ( CONFIG_FILE , JSON . stringify ( config , null , 2 ) ) ;
3277 } catch { }
3378} ;
3479
80+ const writeSkillFiles = ( directory : string ) : void => {
81+ mkdirSync ( directory , { recursive : true } ) ;
82+ writeFileSync ( join ( directory , "SKILL.md" ) , SKILL_CONTENT ) ;
83+ writeFileSync ( join ( directory , "AGENTS.md" ) , AGENTS_CONTENT ) ;
84+ } ;
85+
86+ const isCommandAvailable = ( command : string ) : boolean => {
87+ try {
88+ const whichCommand = process . platform === "win32" ? "where" : "which" ;
89+ execSync ( `${ whichCommand } ${ command } ` , { stdio : "ignore" } ) ;
90+ return true ;
91+ } catch {
92+ return false ;
93+ }
94+ } ;
95+
96+ const SKILL_TARGETS : SkillTarget [ ] = [
97+ {
98+ name : "Claude Code" ,
99+ detect : ( ) => existsSync ( join ( HOME_DIRECTORY , ".claude" ) ) ,
100+ install : ( ) => writeSkillFiles ( join ( HOME_DIRECTORY , ".claude" , "skills" , SKILL_NAME ) ) ,
101+ } ,
102+ {
103+ name : "Amp Code" ,
104+ detect : ( ) => existsSync ( join ( HOME_DIRECTORY , ".amp" ) ) ,
105+ install : ( ) =>
106+ writeSkillFiles ( join ( HOME_DIRECTORY , ".config" , "amp" , "skills" , SKILL_NAME ) ) ,
107+ } ,
108+ {
109+ name : "Cursor" ,
110+ detect : ( ) => existsSync ( join ( HOME_DIRECTORY , ".cursor" ) ) ,
111+ install : ( ) => writeSkillFiles ( join ( HOME_DIRECTORY , ".cursor" , "skills" , SKILL_NAME ) ) ,
112+ } ,
113+ {
114+ name : "OpenCode" ,
115+ detect : ( ) =>
116+ isCommandAvailable ( "opencode" ) ||
117+ existsSync ( join ( HOME_DIRECTORY , ".config" , "opencode" ) ) ,
118+ install : ( ) =>
119+ writeSkillFiles ( join ( HOME_DIRECTORY , ".config" , "opencode" , "skills" , SKILL_NAME ) ) ,
120+ } ,
121+ {
122+ name : "Windsurf" ,
123+ detect : ( ) =>
124+ existsSync ( join ( HOME_DIRECTORY , ".codeium" ) ) ||
125+ existsSync ( join ( HOME_DIRECTORY , "Library" , "Application Support" , "Windsurf" ) ) ,
126+ install : ( ) => {
127+ const memoriesDirectory = join ( HOME_DIRECTORY , ".codeium" , "windsurf" , "memories" ) ;
128+ mkdirSync ( memoriesDirectory , { recursive : true } ) ;
129+ const rulesFile = join ( memoriesDirectory , "global_rules.md" ) ;
130+
131+ if ( existsSync ( rulesFile ) ) {
132+ const existingContent = readFileSync ( rulesFile , "utf-8" ) ;
133+ if ( existingContent . includes ( WINDSURF_MARKER ) ) return ;
134+ appendFileSync ( rulesFile , `\n${ WINDSURF_MARKER } \n\n${ SKILL_CONTENT } ` ) ;
135+ } else {
136+ writeFileSync ( rulesFile , `${ WINDSURF_MARKER } \n\n${ SKILL_CONTENT } ` ) ;
137+ }
138+ } ,
139+ } ,
140+ {
141+ name : "Antigravity" ,
142+ detect : ( ) =>
143+ isCommandAvailable ( "agy" ) ||
144+ existsSync ( join ( HOME_DIRECTORY , ".gemini" , "antigravity" ) ) ,
145+ install : ( ) =>
146+ writeSkillFiles ( join ( HOME_DIRECTORY , ".gemini" , "antigravity" , "skills" , SKILL_NAME ) ) ,
147+ } ,
148+ {
149+ name : "Gemini CLI" ,
150+ detect : ( ) =>
151+ isCommandAvailable ( "gemini" ) || existsSync ( join ( HOME_DIRECTORY , ".gemini" ) ) ,
152+ install : ( ) => writeSkillFiles ( join ( HOME_DIRECTORY , ".gemini" , "skills" , SKILL_NAME ) ) ,
153+ } ,
154+ {
155+ name : "Codex" ,
156+ detect : ( ) =>
157+ isCommandAvailable ( "codex" ) || existsSync ( join ( HOME_DIRECTORY , ".codex" ) ) ,
158+ install : ( ) => {
159+ const skillDirectory = join ( HOME_DIRECTORY , ".codex" , "skills" , SKILL_NAME ) ;
160+ writeSkillFiles ( skillDirectory ) ;
161+ const agentsDirectory = join ( skillDirectory , "agents" ) ;
162+ mkdirSync ( agentsDirectory , { recursive : true } ) ;
163+ writeFileSync ( join ( agentsDirectory , "openai.yaml" ) , CODEX_AGENT_CONFIG ) ;
164+ } ,
165+ } ,
166+ ] ;
167+
35168const installSkill = ( ) : void => {
169+ let installedCount = 0 ;
170+
171+ for ( const target of SKILL_TARGETS ) {
172+ if ( ! target . detect ( ) ) continue ;
173+ try {
174+ target . install ( ) ;
175+ logger . log ( ` ${ highlighter . success ( "✔" ) } ${ target . name } ` ) ;
176+ installedCount ++ ;
177+ } catch {
178+ logger . dim ( ` ✗ ${ target . name } (failed)` ) ;
179+ }
180+ }
181+
36182 try {
37- execSync ( `curl -fsSL ${ INSTALL_SKILL_URL } | bash` , { stdio : "inherit" } ) ;
183+ const projectSkillDirectory = join ( ".agents" , SKILL_NAME ) ;
184+ writeSkillFiles ( projectSkillDirectory ) ;
185+ logger . log ( ` ${ highlighter . success ( "✔" ) } .agents/` ) ;
186+ installedCount ++ ;
38187 } catch {
39- logger . break ( ) ;
40- logger . dim ( "Skill install failed. You can install manually:" ) ;
41- logger . dim ( ` curl -fsSL ${ INSTALL_SKILL_URL } | bash` ) ;
188+ logger . dim ( " ✗ .agents/ (failed)" ) ;
189+ }
190+
191+ logger . break ( ) ;
192+ if ( installedCount === 0 ) {
193+ logger . dim ( "No supported tools detected." ) ;
194+ } else {
195+ logger . success ( "Done! The skill will activate when working on React projects." ) ;
42196 }
43197} ;
44198
@@ -48,9 +202,7 @@ export const maybePromptSkillInstall = async (shouldSkipPrompts: boolean): Promi
48202 if ( shouldSkipPrompts ) return ;
49203
50204 logger . break ( ) ;
51- logger . log (
52- `${ highlighter . info ( "💡" ) } Have your coding agent fix these issues automatically?` ,
53- ) ;
205+ logger . log ( `${ highlighter . info ( "💡" ) } Have your coding agent fix these issues automatically?` ) ;
54206 logger . dim (
55207 ` Install the ${ highlighter . info ( "react-doctor" ) } skill to teach Cursor, Claude Code,` ,
56208 ) ;
0 commit comments