@@ -6,10 +6,11 @@ import pc from 'picocolors';
66import {
77 IMAGE_NAME , MAIN_REPO , DOCKERFILE , SCRIPTS_DIR ,
88 containerName , containerNameCandidates ,
9- worktreeDirCandidates , claudeConfigDirCandidates , codexConfigDirCandidates ,
9+ worktreeDirCandidates ,
1010 sanitizeBranchName , detectHostResources , assertValidBranchName ,
1111 parsePositiveIntegerOption ,
1212} from '../constants.js' ;
13+ import { AI_TOOLS , toolConfigDirCandidates , toolNpmPackagesArg } from '../tools.js' ;
1314import { run , runOk , runSafe } from '../shell.js' ;
1415
1516interface CreateOptions {
@@ -27,13 +28,15 @@ export async function create(branch: string, base: string | undefined, opts: Cre
2728 const safeName = sanitizeBranchName ( branch ) ;
2829 const container = containerName ( branch ) ;
2930 const worktreeCandidates = worktreeDirCandidates ( branch ) ;
30- const claudeDirCandidates = claudeConfigDirCandidates ( branch ) ;
31- const codexDirCandidates = codexConfigDirCandidates ( branch ) ;
3231 const worktree = worktreeCandidates . find ( ( dir ) => fs . existsSync ( dir ) ) ?? worktreeCandidates [ 0 ] ;
33- const claudeDir = claudeDirCandidates . find ( ( dir ) => fs . existsSync ( dir ) ) ?? claudeDirCandidates [ 0 ] ;
34- const codexDir = codexDirCandidates . find ( ( dir ) => fs . existsSync ( dir ) ) ?? codexDirCandidates [ 0 ] ;
3532 const baseBranch = base ?? runSafe ( 'git' , [ '-C' , MAIN_REPO , 'branch' , '--show-current' ] ) ;
3633
34+ // Resolve per-branch config directory for each AI tool
35+ const resolvedTools = AI_TOOLS . map ( ( tool ) => {
36+ const candidates = toolConfigDirCandidates ( tool , branch ) ;
37+ return { tool, dir : candidates . find ( ( d ) => fs . existsSync ( d ) ) ?? candidates [ 0 ] } ;
38+ } ) ;
39+
3740 p . intro ( pc . cyan ( 'AI Coding Sandbox (Colima)' ) ) ;
3841 p . log . info ( `Branch: ${ pc . bold ( branch ) } | Base: ${ pc . bold ( baseBranch ) } | VM: ${ vmCpu } CPU / ${ vmMemory } GB` ) ;
3942
@@ -81,6 +84,7 @@ export async function create(branch: string, base: string | undefined, opts: Cre
8184 'build' , '-t' , IMAGE_NAME ,
8285 '--build-arg' , `HOST_UID=${ hostUid } ` ,
8386 '--build-arg' , `HOST_GID=${ hostGid } ` ,
87+ '--build-arg' , `AI_TOOL_PACKAGES=${ toolNpmPackagesArg ( ) } ` ,
8488 '-f' , DOCKERFILE , '.' ,
8589 ] , { cwd : SCRIPTS_DIR } ) ;
8690 return 'Image built' ;
@@ -122,20 +126,25 @@ export async function create(branch: string, base: string | undefined, opts: Cre
122126 }
123127 }
124128
125- const envArgs : string [ ] = [ ] ;
126- envArgs . push ( '-e' , 'CLAUDE_CONFIG_DIR=/home/devuser/.claude' ) ;
127-
128- // Ensure config dirs exist
129- fs . mkdirSync ( claudeDir , { recursive : true } ) ;
130- fs . mkdirSync ( codexDir , { recursive : true } ) ;
131-
132- // Pre-seed Codex auth from host if available
133- const hostCodexAuth = path . join ( process . env . HOME ! , '.codex' , 'auth.json' ) ;
134- const sandboxCodexAuth = path . join ( codexDir , 'auth.json' ) ;
135- if ( fs . existsSync ( hostCodexAuth ) && ! fs . existsSync ( sandboxCodexAuth ) ) {
136- fs . copyFileSync ( hostCodexAuth , sandboxCodexAuth ) ;
129+ // Ensure config dirs exist + pre-seed auth from host
130+ for ( const { tool, dir } of resolvedTools ) {
131+ fs . mkdirSync ( dir , { recursive : true } ) ;
132+ if ( tool . hostAuthFile && tool . authFileName ) {
133+ const sandboxAuth = path . join ( dir , tool . authFileName ) ;
134+ if ( fs . existsSync ( tool . hostAuthFile ) && ! fs . existsSync ( sandboxAuth ) ) {
135+ fs . copyFileSync ( tool . hostAuthFile , sandboxAuth ) ;
136+ }
137+ }
137138 }
138139
140+ // Build env args and volume mounts from tool registry
141+ const envArgs = resolvedTools . flatMap ( ( { tool } ) =>
142+ Object . entries ( tool . envVars ?? { } ) . flatMap ( ( [ k , v ] ) => [ '-e' , `${ k } =${ v } ` ] )
143+ ) ;
144+ const toolVolumes = resolvedTools . flatMap ( ( { tool, dir } ) =>
145+ [ '-v' , `${ dir } :${ tool . containerMount } ` ]
146+ ) ;
147+
139148 run ( 'docker' , [
140149 'run' , '-d' ,
141150 '--name' , container ,
@@ -145,8 +154,7 @@ export async function create(branch: string, base: string | undefined, opts: Cre
145154 '-v' , `${ worktree } :/workspace` ,
146155 '-v' , `${ MAIN_REPO } /.git:${ MAIN_REPO } /.git` ,
147156 '-v' , `${ process . env . HOME } /.ssh:/home/devuser/.ssh:ro` ,
148- '-v' , `${ claudeDir } :/home/devuser/.claude` ,
149- '-v' , `${ codexDir } :/home/devuser/.codex` ,
157+ ...toolVolumes ,
150158 ...envArgs ,
151159 '-w' , '/workspace' ,
152160 IMAGE_NAME ,
@@ -168,12 +176,20 @@ export async function create(branch: string, base: string | undefined, opts: Cre
168176 { name : 'Container running' , ok : runningContainers . includes ( container ) } ,
169177 { name : 'Java' , ok : runOk ( 'docker' , [ 'exec' , container , 'java' , '-version' ] ) } ,
170178 { name : 'Maven' , ok : runOk ( 'docker' , [ 'exec' , container , 'mvn' , '--version' ] ) } ,
171- { name : 'Claude Code' , ok : runOk ( 'docker' , [ 'exec' , container , 'bash' , '-lc' , 'claude --version' ] ) } ,
172- { name : 'Codex' , ok : runOk ( 'docker' , [ 'exec' , container , 'bash' , '-lc' , 'codex --version' ] ) } ,
173179 ] ;
180+ const toolChecks = AI_TOOLS . map ( ( tool ) => ( {
181+ tool,
182+ ok : runOk ( 'docker' , [ 'exec' , container , 'bash' , '-lc' , tool . versionCmd ] ) ,
183+ } ) ) ;
174184 for ( const c of checks ) {
175185 p . log . info ( ` ${ c . ok ? pc . green ( '✓' ) : pc . yellow ( '?' ) } ${ c . name } ` ) ;
176186 }
187+ for ( const c of toolChecks ) {
188+ p . log . info ( ` ${ c . ok ? pc . green ( '✓' ) : pc . yellow ( '?' ) } ${ c . tool . name } ` ) ;
189+ if ( ! c . ok ) {
190+ p . log . warn ( ` ${ c . tool . name } 未安装或不可用(期望 npm 包:${ c . tool . npmPackage } ),可运行 sandbox rebuild` ) ;
191+ }
192+ }
177193
178194 // Result summary
179195 p . log . success ( pc . green ( 'Ready!' ) ) ;
@@ -187,6 +203,13 @@ export async function create(branch: string, base: string | undefined, opts: Cre
187203 const maxCmdLen = Math . max ( ...mgmtCmds . map ( ( [ c ] ) => c . length ) ) ;
188204 const mgmtLines = mgmtCmds . map ( ( [ cmd , comment ] ) => ` ${ cmd . padEnd ( maxCmdLen + 4 ) } ${ comment } ` ) . join ( '\n' ) ;
189205
206+ // Tool credential hints
207+ const toolHints = resolvedTools . map ( ( { tool, dir } ) => {
208+ const hasAuth = tool . authFileName && fs . existsSync ( path . join ( dir , tool . authFileName ) ) ;
209+ const hint = hasAuth ? '已从宿主机预植入认证凭据,可直接使用。' : tool . noAuthHint ;
210+ return `${ pc . cyan ( `${ tool . name } :` ) } \n ${ hint } \n 凭据持久化:${ dir } /` ;
211+ } ) . join ( '\n\n' ) ;
212+
190213 console . log ( `
191214${ pc . cyan ( '进入沙箱:' ) }
192215 docker exec -it ${ container } bash
@@ -200,13 +223,7 @@ ${pc.cyan('沙箱信息:')}
200223${ pc . cyan ( '管理命令:' ) }
201224${ mgmtLines }
202225
203- ${ pc . cyan ( 'Claude Code:' ) }
204- 首次使用需在容器内运行 claude 完成一次 OAuth 登录,之后免登录。
205- 凭据持久化:${ claudeDir } /
206-
207- ${ pc . cyan ( 'Codex:' ) }
208- ${ fs . existsSync ( path . join ( codexDir , 'auth.json' ) ) ? '已从宿主机预植入认证凭据,可直接使用。' : '首次使用需在容器内运行 codex,按 Esc 选择 Device Code 方式登录。' }
209- 凭据持久化:${ codexDir } /
226+ ${ toolHints }
210227` ) ;
211228}
212229
0 commit comments