11import * as fs from 'node:fs/promises' ;
22import * as path from 'node:path' ;
3+ import { execSync } from 'node:child_process' ;
34import chalk from 'chalk' ;
45
56/**
@@ -11,55 +12,272 @@ export interface AIToolConfig {
1112 description : string ; // Human-readable description for prompts
1213 default : boolean ; // Whether to include by default in quick start
1314 usesSymlink : boolean ; // Whether this tool uses a symlink (false for AGENTS.md itself)
15+ detection ?: { // Optional auto-detection configuration
16+ commands ?: string [ ] ; // CLI commands to check (e.g., ['claude', 'claude-code'])
17+ configDirs ?: string [ ] ; // Config directories to check (e.g., ['.claude'])
18+ envVars ?: string [ ] ; // Environment variables to check (e.g., ['ANTHROPIC_API_KEY'])
19+ extensions ?: string [ ] ; // VS Code extension IDs to check
20+ } ;
1421}
1522
16- export type AIToolKey = 'claude' | 'gemini ' | 'copilot' | 'cursor' | 'windsurf ' | 'cline ' | 'warp ' ;
23+ export type AIToolKey = 'aider' | ' claude' | 'codex ' | 'copilot' | 'cursor' | 'droid ' | 'gemini ' | 'opencode' | 'windsurf ';
1724
1825export const AI_TOOL_CONFIGS : Record < AIToolKey , AIToolConfig > = {
26+ aider : {
27+ file : 'AGENTS.md' ,
28+ description : 'Aider (uses AGENTS.md)' ,
29+ default : false ,
30+ usesSymlink : false ,
31+ detection : {
32+ commands : [ 'aider' ] ,
33+ configDirs : [ '.aider' ] ,
34+ } ,
35+ } ,
1936 claude : {
2037 file : 'CLAUDE.md' ,
21- description : 'Claude Code / Claude Desktop (CLAUDE.md)' ,
38+ description : 'Claude Code (CLAUDE.md)' ,
2239 default : true ,
2340 usesSymlink : true ,
41+ detection : {
42+ commands : [ 'claude' ] ,
43+ configDirs : [ '.claude' ] ,
44+ envVars : [ 'ANTHROPIC_API_KEY' ] ,
45+ } ,
2446 } ,
25- gemini : {
26- file : 'GEMINI .md' ,
27- description : 'Gemini CLI (GEMINI .md)' ,
47+ codex : {
48+ file : 'AGENTS .md' ,
49+ description : 'Codex CLI by OpenAI (uses AGENTS .md)' ,
2850 default : false ,
29- usesSymlink : true ,
51+ usesSymlink : false ,
52+ detection : {
53+ commands : [ 'codex' ] ,
54+ configDirs : [ '.codex' ] ,
55+ envVars : [ 'OPENAI_API_KEY' ] ,
56+ } ,
3057 } ,
3158 copilot : {
3259 file : 'AGENTS.md' ,
3360 description : 'GitHub Copilot (AGENTS.md - default)' ,
3461 default : true ,
3562 usesSymlink : false , // Primary file, no symlink needed
63+ detection : {
64+ commands : [ 'copilot' ] ,
65+ envVars : [ 'GITHUB_TOKEN' ] ,
66+ } ,
3667 } ,
3768 cursor : {
3869 file : 'AGENTS.md' ,
3970 description : 'Cursor (uses AGENTS.md)' ,
4071 default : false ,
4172 usesSymlink : false ,
73+ detection : {
74+ configDirs : [ '.cursor' , '.cursorules' ] ,
75+ commands : [ 'cursor' ] ,
76+ } ,
4277 } ,
43- windsurf : {
78+ droid : {
4479 file : 'AGENTS.md' ,
45- description : 'Windsurf (uses AGENTS.md)' ,
80+ description : 'Droid by Factory (uses AGENTS.md)' ,
4681 default : false ,
4782 usesSymlink : false ,
83+ detection : {
84+ commands : [ 'droid' ] ,
85+ } ,
4886 } ,
49- cline : {
87+ gemini : {
88+ file : 'GEMINI.md' ,
89+ description : 'Gemini CLI (GEMINI.md)' ,
90+ default : false ,
91+ usesSymlink : true ,
92+ detection : {
93+ commands : [ 'gemini' ] ,
94+ configDirs : [ '.gemini' ] ,
95+ envVars : [ 'GOOGLE_API_KEY' , 'GEMINI_API_KEY' ] ,
96+ } ,
97+ } ,
98+ opencode : {
5099 file : 'AGENTS.md' ,
51- description : 'Cline (uses AGENTS.md)' ,
100+ description : 'OpenCode (uses AGENTS.md)' ,
52101 default : false ,
53102 usesSymlink : false ,
103+ detection : {
104+ commands : [ 'opencode' ] ,
105+ configDirs : [ '.opencode' ] ,
106+ } ,
54107 } ,
55- warp : {
108+ windsurf : {
56109 file : 'AGENTS.md' ,
57- description : 'Warp Terminal (uses AGENTS.md)' ,
110+ description : 'Windsurf (uses AGENTS.md)' ,
58111 default : false ,
59112 usesSymlink : false ,
113+ detection : {
114+ configDirs : [ '.windsurf' , '.windsurfrules' ] ,
115+ commands : [ 'windsurf' ] ,
116+ } ,
60117 } ,
61118} ;
62119
120+ /**
121+ * Check if a command exists in PATH
122+ */
123+ function commandExists ( command : string ) : boolean {
124+ try {
125+ const which = process . platform === 'win32' ? 'where' : 'which' ;
126+ execSync ( `${ which } ${ command } ` , { stdio : 'ignore' } ) ;
127+ return true ;
128+ } catch {
129+ return false ;
130+ }
131+ }
132+
133+ /**
134+ * Check if a directory exists in the user's home directory
135+ */
136+ async function configDirExists ( dirName : string ) : Promise < boolean > {
137+ const homeDir = process . env . HOME || process . env . USERPROFILE || '' ;
138+ if ( ! homeDir ) return false ;
139+
140+ try {
141+ await fs . access ( path . join ( homeDir , dirName ) ) ;
142+ return true ;
143+ } catch {
144+ return false ;
145+ }
146+ }
147+
148+ /**
149+ * Check if an environment variable is set
150+ */
151+ function envVarExists ( varName : string ) : boolean {
152+ return ! ! process . env [ varName ] ;
153+ }
154+
155+ /**
156+ * Check if a VS Code extension is installed
157+ * Note: This is a best-effort check - may not work in all environments
158+ */
159+ async function extensionInstalled ( extensionId : string ) : Promise < boolean > {
160+ const homeDir = process . env . HOME || process . env . USERPROFILE || '' ;
161+ if ( ! homeDir ) return false ;
162+
163+ // Check common VS Code extension directories
164+ const extensionDirs = [
165+ path . join ( homeDir , '.vscode' , 'extensions' ) ,
166+ path . join ( homeDir , '.vscode-server' , 'extensions' ) ,
167+ path . join ( homeDir , '.cursor' , 'extensions' ) ,
168+ ] ;
169+
170+ for ( const extDir of extensionDirs ) {
171+ try {
172+ const entries = await fs . readdir ( extDir ) ;
173+ // Extension folders are named like 'github.copilot-1.234.567'
174+ if ( entries . some ( e => e . toLowerCase ( ) . startsWith ( extensionId . toLowerCase ( ) ) ) ) {
175+ return true ;
176+ }
177+ } catch {
178+ // Directory doesn't exist or not readable
179+ }
180+ }
181+
182+ return false ;
183+ }
184+
185+ export interface DetectionResult {
186+ tool : AIToolKey ;
187+ detected : boolean ;
188+ reasons : string [ ] ;
189+ }
190+
191+ /**
192+ * Auto-detect installed AI tools
193+ * Returns detection results with reasons for each tool
194+ */
195+ export async function detectInstalledAITools ( ) : Promise < DetectionResult [ ] > {
196+ const results : DetectionResult [ ] = [ ] ;
197+
198+ for ( const [ toolKey , config ] of Object . entries ( AI_TOOL_CONFIGS ) ) {
199+ const reasons : string [ ] = [ ] ;
200+ const detection = config . detection ;
201+
202+ if ( ! detection ) {
203+ results . push ( { tool : toolKey as AIToolKey , detected : false , reasons : [ ] } ) ;
204+ continue ;
205+ }
206+
207+ // Check commands
208+ if ( detection . commands ) {
209+ for ( const cmd of detection . commands ) {
210+ if ( commandExists ( cmd ) ) {
211+ reasons . push ( `'${ cmd } ' command found` ) ;
212+ }
213+ }
214+ }
215+
216+ // Check config directories
217+ if ( detection . configDirs ) {
218+ for ( const dir of detection . configDirs ) {
219+ if ( await configDirExists ( dir ) ) {
220+ reasons . push ( `~/${ dir } directory found` ) ;
221+ }
222+ }
223+ }
224+
225+ // Check environment variables
226+ if ( detection . envVars ) {
227+ for ( const envVar of detection . envVars ) {
228+ if ( envVarExists ( envVar ) ) {
229+ reasons . push ( `${ envVar } env var set` ) ;
230+ }
231+ }
232+ }
233+
234+ // Check VS Code extensions
235+ if ( detection . extensions ) {
236+ for ( const ext of detection . extensions ) {
237+ if ( await extensionInstalled ( ext ) ) {
238+ reasons . push ( `${ ext } extension installed` ) ;
239+ }
240+ }
241+ }
242+
243+ results . push ( {
244+ tool : toolKey as AIToolKey ,
245+ detected : reasons . length > 0 ,
246+ reasons,
247+ } ) ;
248+ }
249+
250+ return results ;
251+ }
252+
253+ /**
254+ * Get default selection for AI tools based on auto-detection
255+ * Falls back to copilot only (AGENTS.md) if nothing is detected
256+ */
257+ export async function getDefaultAIToolSelection ( ) : Promise < { defaults : AIToolKey [ ] ; detected : DetectionResult [ ] } > {
258+ const detectionResults = await detectInstalledAITools ( ) ;
259+ const detectedTools = detectionResults
260+ . filter ( r => r . detected )
261+ . map ( r => r . tool ) ;
262+
263+ // If any tools detected, use those as defaults
264+ if ( detectedTools . length > 0 ) {
265+ // Always include copilot if it's detected or nothing else is (AGENTS.md is primary)
266+ const copilotDetected = detectedTools . includes ( 'copilot' ) ;
267+ if ( ! copilotDetected ) {
268+ // Check if any detected tool uses AGENTS.md
269+ const usesAgentsMd = detectedTools . some ( t => ! AI_TOOL_CONFIGS [ t ] . usesSymlink ) ;
270+ if ( ! usesAgentsMd ) {
271+ detectedTools . push ( 'copilot' ) ;
272+ }
273+ }
274+ return { defaults : detectedTools , detected : detectionResults } ;
275+ }
276+
277+ // Fall back to copilot only (AGENTS.md is the primary file)
278+ return { defaults : [ 'copilot' ] , detected : detectionResults } ;
279+ }
280+
63281export interface SymlinkResult {
64282 file : string ;
65283 created ?: boolean ;
0 commit comments