11import pc from "picocolors" ;
2- import { getApiKey , maskKey } from "../lib/config.js" ;
2+ import { existsSync } from "node:fs" ;
3+ import { copyFile , mkdir } from "node:fs/promises" ;
4+ import { basename , dirname , join } from "node:path" ;
5+ import { ask } from "../lib/prompt.js" ;
6+ import { getApiKey , getPreferences , maskKey , savePreferences } from "../lib/config.js" ;
37import { API_BASE , API_BASE_OPENAI } from "../lib/constants.js" ;
48import { SUPPORTED_TOOLS , getImplementedAdapter } from "../lib/tools.js" ;
9+ import type { ToolAdapter } from "../tools/types.js" ;
510
6- export async function setupCommand ( ) : Promise < void > {
11+ type SetupOptions = {
12+ interactive ?: boolean ;
13+ tools ?: string ;
14+ model ?: string ;
15+ dryRun ?: boolean ;
16+ backup ?: boolean ;
17+ } ;
18+
19+ const DEFAULT_MODEL = "claude-sonnet-4-5" ;
20+ const MODEL_CHOICES = [ "claude-sonnet-4-5" , "claude-opus-4-5" , "gpt-5.4" , "gpt-5" ] ;
21+
22+ function parseToolIds ( input : string , validIds : string [ ] ) : string [ ] {
23+ const raw = input
24+ . split ( "," )
25+ . map ( ( s ) => s . trim ( ) )
26+ . filter ( Boolean ) ;
27+ if ( raw . length === 0 ) return [ ] ;
28+ if ( raw . includes ( "all" ) ) return validIds ;
29+ const unknown = raw . filter ( ( id ) => ! validIds . includes ( id ) ) ;
30+ if ( unknown . length > 0 ) {
31+ throw new Error ( `无效工具 ID: ${ unknown . join ( ", " ) } ` ) ;
32+ }
33+ return [ ...new Set ( raw ) ] ;
34+ }
35+
36+ function getImplementedTools ( ) : Array < { id : string ; name : string ; adapter : ToolAdapter } > {
37+ return SUPPORTED_TOOLS . map ( ( tool ) => {
38+ const adapter = getImplementedAdapter ( tool . id ) ;
39+ return adapter ? { id : tool . id , name : tool . name , adapter } : null ;
40+ } ) . filter ( ( item ) : item is { id : string ; name : string ; adapter : ToolAdapter } => item !== null ) ;
41+ }
42+
43+ async function maybeBackupFiles ( files : string [ ] ) : Promise < string [ ] > {
44+ const backedUp : string [ ] = [ ] ;
45+ const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, "-" ) ;
46+ for ( const file of files ) {
47+ if ( ! existsSync ( file ) ) continue ;
48+ const backupDir = join ( dirname ( file ) , ".fishxcode-backups" ) ;
49+ await mkdir ( backupDir , { recursive : true } ) ;
50+ const backupFile = join ( backupDir , `${ basename ( file ) } .${ timestamp } .bak` ) ;
51+ await copyFile ( file , backupFile ) ;
52+ backedUp . push ( backupFile ) ;
53+ }
54+ return backedUp ;
55+ }
56+
57+ async function resolveInteractiveOptions (
58+ initial : SetupOptions ,
59+ validToolIds : string [ ] ,
60+ preferenceModel : string ,
61+ ) : Promise < Required < Pick < SetupOptions , "interactive" | "tools" | "model" | "dryRun" | "backup" > > > {
62+ const toolsHint = validToolIds . join ( "," ) ;
63+ const defaultTools = initial . tools ?? "all" ;
64+ const defaultModel = initial . model ?? preferenceModel ;
65+ const toolAnswer = (
66+ await ask ( `选择工具 ID(逗号分隔,默认 all,可选 ${ toolsHint } ): ` )
67+ ) . trim ( ) ;
68+ const modelAnswer = ( await ask ( `模型(默认 ${ defaultModel } ): ` ) ) . trim ( ) ;
69+ const dryRunAnswer = ( await ask ( "仅预览不写入?(y/N): " ) ) . trim ( ) . toLowerCase ( ) ;
70+ const backupAnswer = ( await ask ( "写入前备份配置?(y/N): " ) ) . trim ( ) . toLowerCase ( ) ;
71+
72+ return {
73+ interactive : true ,
74+ tools : toolAnswer || defaultTools ,
75+ model : modelAnswer || defaultModel ,
76+ dryRun : dryRunAnswer === "y" || dryRunAnswer === "yes" ,
77+ backup : backupAnswer === "y" || backupAnswer === "yes" ,
78+ } ;
79+ }
80+
81+ export async function setupCommand ( options : SetupOptions = { } ) : Promise < void > {
782 const key = await getApiKey ( ) ;
883 if ( ! key ) {
984 throw new Error ( "未检测到 API Key,请先执行 fishx login" ) ;
1085 }
1186
87+ const preferences = await getPreferences ( ) ;
88+ const implementedTools = getImplementedTools ( ) ;
89+ const validToolIds = implementedTools . map ( ( t ) => t . id ) ;
90+ const preferenceModel = preferences . defaultModel ?? DEFAULT_MODEL ;
91+
92+ let effectiveOptions : SetupOptions = {
93+ interactive : options . interactive ?? preferences . interactive ?? false ,
94+ tools : options . tools ?? preferences . defaultTools ?. join ( "," ) ?? "all" ,
95+ model : options . model ?? preferenceModel ,
96+ dryRun : options . dryRun ?? false ,
97+ backup : options . backup ?? preferences . backup ?? false ,
98+ } ;
99+
100+ if ( effectiveOptions . interactive ) {
101+ effectiveOptions = await resolveInteractiveOptions ( effectiveOptions , validToolIds , preferenceModel ) ;
102+ }
103+
104+ const selectedToolIds = parseToolIds ( effectiveOptions . tools ?? "all" , validToolIds ) ;
105+ const selectedModel = effectiveOptions . model ?. trim ( ) || DEFAULT_MODEL ;
106+ const dryRun = ! ! effectiveOptions . dryRun ;
107+ const backup = ! ! effectiveOptions . backup ;
108+
109+ await savePreferences ( {
110+ defaultTools : selectedToolIds ,
111+ defaultModel : selectedModel ,
112+ interactive : ! ! effectiveOptions . interactive ,
113+ backup,
114+ } ) ;
115+
12116 console . log ( pc . bold ( "开始配置 FishXCode 工具" ) ) ;
13117 console . log ( `- API Key: ${ maskKey ( key ) } ` ) ;
14118 console . log ( `- Anthropic Base URL: ${ API_BASE } ` ) ;
15119 console . log ( `- OpenAI Base URL: ${ API_BASE_OPENAI } ` ) ;
120+ console . log ( `- 模型: ${ selectedModel } ` ) ;
121+ console . log ( `- 模式: ${ dryRun ? "预览 (dry-run)" : "写入" } ` ) ;
122+ if ( backup ) {
123+ console . log ( "- 备份: 已启用" ) ;
124+ }
16125
17126 const applied : string [ ] = [ ] ;
18127 const skipped : string [ ] = [ ] ;
128+ const backups : string [ ] = [ ] ;
19129
20- for ( const tool of SUPPORTED_TOOLS ) {
130+ for ( const tool of SUPPORTED_TOOLS . filter ( ( t ) => selectedToolIds . includes ( t . id ) ) ) {
21131 const adapter = getImplementedAdapter ( tool . id ) ;
22132 if ( ! adapter ) {
23133 skipped . push ( tool . name ) ;
24134 continue ;
25135 }
26136
27137 try {
138+ const targetFiles = adapter . getTargetFiles ?.( ) ?? [ ] ;
139+ if ( dryRun ) {
140+ const previewFile = targetFiles [ 0 ] ?? "(未知路径)" ;
141+ applied . push ( `${ tool . name } -> ${ previewFile } ` ) ;
142+ console . log ( pc . cyan ( `~ ${ tool . name } ` ) , pc . gray ( previewFile ) , pc . gray ( "[dry-run]" ) ) ;
143+ continue ;
144+ }
145+
146+ if ( backup && targetFiles . length > 0 ) {
147+ const backupFiles = await maybeBackupFiles ( targetFiles ) ;
148+ backups . push ( ...backupFiles ) ;
149+ }
150+
28151 const result = await adapter . configure ( {
29152 apiKey : key ,
30153 baseAnthropic : API_BASE ,
31154 baseOpenAI : API_BASE_OPENAI ,
155+ model : selectedModel ,
32156 } ) ;
33157 applied . push ( `${ tool . name } -> ${ result . file } ` ) ;
34158 console . log ( pc . green ( `✓ ${ tool . name } ` ) , pc . gray ( result . file ) ) ;
@@ -45,4 +169,8 @@ export async function setupCommand(): Promise<void> {
45169
46170 console . log ( `- 未实装: ${ skipped . length } ` ) ;
47171 for ( const item of skipped ) console . log ( ` ${ pc . yellow ( "•" ) } ${ item } ` ) ;
172+ if ( backups . length > 0 ) {
173+ console . log ( `- 备份文件: ${ backups . length } ` ) ;
174+ for ( const file of backups ) console . log ( ` ${ pc . blue ( "•" ) } ${ file } ` ) ;
175+ }
48176}
0 commit comments