Skip to content

Commit d2cc385

Browse files
committed
feat(cli): add Gemini CLI, Droid (Factory.ai), and Junie (JetBrains) to devtools
Expands the developer tools installer with 3 additional AI coding assistants: - Gemini CLI (@google/gemini-cli) — npm-based, Google AI - Droid CLI (Factory.ai) — curl installer - Junie CLI (JetBrains) — curl installer All tools use the same selection/detection pattern: auto-detect existing installs, show in multi-select picker, best-effort install.
1 parent cac87c7 commit d2cc385

1 file changed

Lines changed: 75 additions & 6 deletions

File tree

dream-server/cli-installer/src/phases/devtools.ts

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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

56
import { type InstallContext, TIER_MAP } from '../lib/config.ts';
67
import { 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

Comments
 (0)