11// ── Phase 07: Developer Tools ────────────────────────────────────────────────
22// Port of installers/phases/07-devtools.sh
3- // Installs Claude Code, Codex CLI, and OpenCode. All installs are best-effort.
3+ // Installs Claude Code, Codex CLI, Gemini CLI, OpenCode, Droid (Factory.ai), and Junie (JetBrains).
4+ // All installs are best-effort.
45
56import { type InstallContext , TIER_MAP } from '../lib/config.ts' ;
67import { exec , commandExists } from '../lib/shell.ts' ;
@@ -15,22 +16,29 @@ export async function devtools(ctx: InstallContext): Promise<void> {
1516 ui . phase ( 0 , 0 , 'Developer Tools' ) ;
1617
1718 if ( ctx . dryRun ) {
18- ui . info ( '[DRY RUN] Would prompt for AI developer tools (Claude Code, Codex CLI, OpenCode)' ) ;
19+ ui . info ( '[DRY RUN] Would prompt for AI developer tools (Claude Code, Codex CLI, Gemini CLI, OpenCode, Droid, Junie )' ) ;
1920 return ;
2021 }
2122
2223 // ── Detect what's already installed ──
23- const [ hasClaude , hasCodex , hasOpenCode ] = await Promise . all ( [
24+ const [ hasClaude , hasCodex , hasGemini , hasOpenCode , hasDroid , hasJunie ] = await Promise . all ( [
2425 commandExists ( 'claude' ) ,
2526 commandExists ( 'codex' ) ,
27+ commandExists ( 'gemini' ) ,
2628 commandExists ( 'opencode' ) . then ( found =>
2729 found || existsSync ( join ( homedir ( ) , '.opencode' , 'bin' , 'opencode' ) ) ) ,
30+ commandExists ( 'droid' ) ,
31+ commandExists ( 'junie' ) . then ( found =>
32+ found || existsSync ( join ( homedir ( ) , '.local' , 'bin' , 'junie' ) ) ) ,
2833 ] ) ;
2934
3035 const alreadyInstalled : string [ ] = [ ] ;
3136 if ( hasClaude ) alreadyInstalled . push ( 'Claude Code' ) ;
3237 if ( hasCodex ) alreadyInstalled . push ( 'Codex CLI' ) ;
38+ if ( hasGemini ) alreadyInstalled . push ( 'Gemini CLI' ) ;
3339 if ( hasOpenCode ) alreadyInstalled . push ( 'OpenCode' ) ;
40+ if ( hasDroid ) alreadyInstalled . push ( 'Droid' ) ;
41+ if ( hasJunie ) alreadyInstalled . push ( 'Junie' ) ;
3442
3543 if ( alreadyInstalled . length > 0 ) {
3644 ui . ok ( `Already installed: ${ alreadyInstalled . join ( ', ' ) } ` ) ;
@@ -41,7 +49,10 @@ export async function devtools(ctx: InstallContext): Promise<void> {
4149 const available : DevTool [ ] = [ ] ;
4250 if ( ! hasClaude ) available . push ( { name : 'claude' , label : 'Claude Code' , desc : 'Anthropic AI coding assistant' } ) ;
4351 if ( ! hasCodex ) available . push ( { name : 'codex' , label : 'Codex CLI' , desc : 'OpenAI coding assistant' } ) ;
52+ if ( ! hasGemini ) available . push ( { name : 'gemini' , label : 'Gemini CLI' , desc : 'Google AI coding assistant' } ) ;
4453 if ( ! hasOpenCode ) available . push ( { name : 'opencode' , label : 'OpenCode' , desc : 'Open-source AI coding (local LLM)' } ) ;
54+ if ( ! hasDroid ) available . push ( { name : 'droid' , label : 'Droid CLI' , desc : 'Factory.ai AI coding assistant' } ) ;
55+ if ( ! hasJunie ) available . push ( { name : 'junie' , label : 'Junie CLI' , desc : 'JetBrains AI coding assistant' } ) ;
4556
4657 if ( available . length === 0 ) {
4758 ui . ok ( 'All developer tools already installed' ) ;
@@ -73,24 +84,31 @@ export async function devtools(ctx: InstallContext): Promise<void> {
7384 && selected [ available . findIndex ( t => t . name === 'claude' ) ] ;
7485 const installCodex = available . findIndex ( t => t . name === 'codex' ) >= 0
7586 && selected [ available . findIndex ( t => t . name === 'codex' ) ] ;
87+ const installGemini = available . findIndex ( t => t . name === 'gemini' ) >= 0
88+ && selected [ available . findIndex ( t => t . name === 'gemini' ) ] ;
7689 const installOpenCode = available . findIndex ( t => t . name === 'opencode' ) >= 0
7790 && selected [ available . findIndex ( t => t . name === 'opencode' ) ] ;
91+ const installDroid = available . findIndex ( t => t . name === 'droid' ) >= 0
92+ && selected [ available . findIndex ( t => t . name === 'droid' ) ] ;
93+ const installJunie = available . findIndex ( t => t . name === 'junie' ) >= 0
94+ && selected [ available . findIndex ( t => t . name === 'junie' ) ] ;
7895
79- if ( ! installClaude && ! installCodex && ! installOpenCode ) {
96+ if ( ! installClaude && ! installCodex && ! installGemini && ! installOpenCode && ! installDroid && ! installJunie ) {
8097 ui . info ( 'No developer tools selected — skipping' ) ;
8198 return ;
8299 }
83100
84101 const home = homedir ( ) ;
85102 const npmGlobalDir = join ( home , '.npm-global' ) ;
86103
87- // ── npm-based tools (Claude Code, Codex CLI) ──
88- if ( installClaude || installCodex ) {
104+ // ── npm-based tools (Claude Code, Codex CLI, Gemini CLI ) ──
105+ if ( installClaude || installCodex || installGemini ) {
89106 const hasNpm = await commandExists ( 'npm' ) ;
90107 if ( ! hasNpm ) {
91108 ui . warn ( 'npm not available — skipping CLI tool installs' ) ;
92109 if ( installClaude ) ui . info ( ' Install later: npm i -g @anthropic-ai/claude-code' ) ;
93110 if ( installCodex ) ui . info ( ' Install later: npm i -g @openai/codex' ) ;
111+ if ( installGemini ) ui . info ( ' Install later: npm i -g @google/gemini-cli' ) ;
94112 } else {
95113 // Set up user-level npm global prefix (no sudo needed)
96114 if ( ! existsSync ( npmGlobalDir ) ) {
@@ -121,6 +139,15 @@ export async function devtools(ctx: InstallContext): Promise<void> {
121139 }
122140 }
123141
142+ if ( installGemini ) {
143+ try {
144+ await exec ( [ 'npm' , 'install' , '-g' , '@google/gemini-cli' ] , { timeout : 120_000 , throwOnError : false } ) ;
145+ ui . ok ( "Gemini CLI installed (run 'gemini' to start)" ) ;
146+ } catch {
147+ ui . warn ( 'Gemini CLI install failed — install later with: npm i -g @google/gemini-cli' ) ;
148+ }
149+ }
150+
124151 // Ensure ~/.npm-global/bin is on PATH permanently
125152 const bashrc = join ( home , '.bashrc' ) ;
126153 if ( existsSync ( join ( npmGlobalDir , 'bin' ) ) ) {
@@ -163,6 +190,48 @@ export async function devtools(ctx: InstallContext): Promise<void> {
163190 }
164191 }
165192
193+ // ── Droid CLI (Factory.ai) ──
194+ if ( installDroid ) {
195+ ui . info ( 'Installing Droid CLI...' ) ;
196+ try {
197+ const result = await exec (
198+ [ 'sh' , '-c' , 'curl -fsSL https://app.factory.ai/cli | sh' ] ,
199+ { throwOnError : false , timeout : 120_000 } ,
200+ ) ;
201+ if ( result . exitCode === 0 ) {
202+ ui . ok ( "Droid CLI installed (run 'droid' to start)" ) ;
203+ } else {
204+ ui . warn ( 'Droid CLI install failed — install later with: curl -fsSL https://app.factory.ai/cli | sh' ) ;
205+ }
206+ } catch {
207+ ui . warn ( 'Droid CLI install failed — install later with: curl -fsSL https://app.factory.ai/cli | sh' ) ;
208+ }
209+ }
210+
211+ // ── Junie CLI (JetBrains) ──
212+ if ( installJunie ) {
213+ ui . info ( 'Installing Junie CLI...' ) ;
214+ try {
215+ const result = await exec (
216+ [ 'bash' , '-c' , 'curl -fsSL https://junie.jetbrains.com/install.sh | bash' ] ,
217+ { throwOnError : false , timeout : 120_000 } ,
218+ ) ;
219+ if ( result . exitCode === 0 ) {
220+ ui . ok ( "Junie CLI installed (run 'junie' to start)" ) ;
221+ // Ensure ~/.local/bin is on PATH
222+ const home = homedir ( ) ;
223+ const localBin = join ( home , '.local' , 'bin' ) ;
224+ if ( existsSync ( localBin ) && ! process . env . PATH ?. includes ( localBin ) ) {
225+ process . env . PATH = `${ localBin } :${ process . env . PATH } ` ;
226+ }
227+ } else {
228+ ui . warn ( 'Junie CLI install failed — install later with: curl -fsSL https://junie.jetbrains.com/install.sh | bash' ) ;
229+ }
230+ } catch {
231+ ui . warn ( 'Junie CLI install failed — install later with: curl -fsSL https://junie.jetbrains.com/install.sh | bash' ) ;
232+ }
233+ }
234+
166235 // ── Configure OpenCode for local llama-server ──
167236 await configureOpenCode ( ctx ) ;
168237}
0 commit comments