11import chalk from "chalk" ;
2- import { spawnSync } from "node:child_process" ;
2+ import { spawnSync , execSync } from "node:child_process" ;
33import { createInterface } from "node:readline" ;
4- import type { MexConfig , SyncTarget , DriftIssue } from "../types.js" ;
4+ import type { MexConfig , SyncTarget , DriftIssue , AiTool } from "../types.js" ;
5+ import { AI_TOOLS } from "../types.js" ;
56import { runDriftCheck } from "../drift/index.js" ;
67import { buildSyncBrief , buildCombinedBrief } from "./brief-builder.js" ;
78
@@ -15,15 +16,52 @@ function askUser(question: string): Promise<string> {
1516 } ) ;
1617}
1718
18- function runClaudeInteractive ( brief : string , cwd : string ) : boolean {
19- const result = spawnSync ( "claude" , [ brief ] , {
19+ function hasCliTool ( cmd : string ) : boolean {
20+ try {
21+ execSync ( `which ${ cmd } ` , { stdio : "ignore" } ) ;
22+ return true ;
23+ } catch {
24+ return false ;
25+ }
26+ }
27+
28+ function runToolInteractive ( tool : AiTool , brief : string , cwd : string ) : boolean {
29+ const meta = AI_TOOLS [ tool ] ;
30+ if ( ! meta . cli ) return false ;
31+
32+ const args = [ ...meta . promptFlag , brief ] ;
33+ const result = spawnSync ( meta . cli , args , {
2034 cwd,
2135 stdio : "inherit" ,
2236 timeout : 300_000 ,
2337 } ) ;
2438 return result . status === 0 || result . status === null ;
2539}
2640
41+ /** Pick which AI tool to use for interactive sync */
42+ async function pickSyncTool ( configuredTools : AiTool [ ] ) : Promise < AiTool | null > {
43+ // Filter to tools that have a CLI and are installed
44+ const available = configuredTools . filter ( ( t ) => {
45+ const meta = AI_TOOLS [ t ] ;
46+ return meta . cli && hasCliTool ( meta . cli ) ;
47+ } ) ;
48+
49+ if ( available . length === 0 ) return null ;
50+ if ( available . length === 1 ) return available [ 0 ] ;
51+
52+ // Multiple CLI tools available — ask user
53+ console . log ( chalk . bold ( "\nWhich tool should fix these?" ) ) ;
54+ console . log ( ) ;
55+ available . forEach ( ( t , i ) => {
56+ console . log ( ` ${ i + 1 } ) ${ AI_TOOLS [ t ] . name } ` ) ;
57+ } ) ;
58+ console . log ( ) ;
59+
60+ const choice = await askUser ( `Choice [1-${ available . length } ] (default: 1): ` ) ;
61+ const idx = parseInt ( choice || "1" , 10 ) - 1 ;
62+ return available [ idx ] ?? available [ 0 ] ;
63+ }
64+
2765type SyncMode = "interactive" | "prompts" ;
2866
2967/** Run targeted sync: detect → brief → AI → verify → ask → loop */
@@ -33,6 +71,7 @@ export async function runSync(
3371) : Promise < void > {
3472 let cycle = 0 ;
3573 let mode : SyncMode | null = null ;
74+ let activeTool : AiTool | null = null ;
3675
3776 while ( true ) {
3877 cycle ++ ;
@@ -107,9 +146,17 @@ export async function runSync(
107146
108147 // Ask user for mode (only on first cycle)
109148 if ( mode === null ) {
149+ // Determine if any configured tool has a usable CLI
150+ const syncTool = await pickSyncTool ( config . aiTools ) ;
151+ const toolName = syncTool ? AI_TOOLS [ syncTool ] . name : null ;
152+
110153 console . log ( chalk . bold ( "\nHow should we fix these?" ) ) ;
111154 console . log ( ) ;
112- console . log ( " 1) Interactive — Claude fixes with you watching (default)" ) ;
155+ if ( toolName ) {
156+ console . log ( ` 1) Interactive — ${ toolName } fixes with you watching (default)` ) ;
157+ } else {
158+ console . log ( " 1) Interactive — AI fixes with you watching (default)" ) ;
159+ }
113160 console . log ( " 2) Show prompts — I'll paste manually" ) ;
114161 console . log ( " 3) Exit" ) ;
115162 console . log ( ) ;
@@ -119,7 +166,15 @@ export async function runSync(
119166
120167 switch ( picked ) {
121168 case "1" :
122- mode = "interactive" ;
169+ if ( ! syncTool ) {
170+ console . log ( chalk . yellow ( "No supported AI CLI detected. Falling back to prompts mode." ) ) ;
171+ console . log ( chalk . dim ( "Supported CLIs: claude, opencode, codex" ) ) ;
172+ console . log ( ) ;
173+ mode = "prompts" ;
174+ } else {
175+ activeTool = syncTool ;
176+ mode = "interactive" ;
177+ }
123178 break ;
124179 case "2" :
125180 mode = "prompts" ;
@@ -143,13 +198,14 @@ export async function runSync(
143198
144199 // Step 3: Fix all files in one interactive session
145200 console . log ( ) ;
146- console . log ( chalk . bold ( `\nSending all ${ targets . length } file(s) to Claude in one session...\n` ) ) ;
201+ const toolLabel = activeTool ? AI_TOOLS [ activeTool ] . name : "AI" ;
202+ console . log ( chalk . bold ( `\nSending all ${ targets . length } file(s) to ${ toolLabel } in one session...\n` ) ) ;
147203
148204 const brief = await buildCombinedBrief ( targets , config . projectRoot ) ;
149- const ok = runClaudeInteractive ( brief , config . projectRoot ) ;
205+ const ok = runToolInteractive ( activeTool ! , brief , config . projectRoot ) ;
150206
151207 if ( ! ok ) {
152- console . log ( chalk . red ( " ✗ Claude session failed" ) ) ;
208+ console . log ( chalk . red ( ` ✗ ${ toolLabel } session failed` ) ) ;
153209 }
154210
155211 // Step 4: Verify
0 commit comments