| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- import * as readline from 'node:readline/promises';
- import {
- addPluginToOpenCodeConfig,
- addProviderConfig,
- detectCurrentConfig,
- disableDefaultAgents,
- generateLiteConfig,
- getOpenCodeVersion,
- isOpenCodeInstalled,
- writeLiteConfig,
- } from './config-manager';
- import { RECOMMENDED_SKILLS, installSkill } from './skills';
- import type {
- BooleanArg,
- ConfigMergeResult,
- DetectedConfig,
- InstallArgs,
- InstallConfig,
- } from './types';
- // Colors
- const GREEN = '\x1b[32m';
- const BLUE = '\x1b[34m';
- const YELLOW = '\x1b[33m';
- const RED = '\x1b[31m';
- const BOLD = '\x1b[1m';
- const DIM = '\x1b[2m';
- const RESET = '\x1b[0m';
- const SYMBOLS = {
- check: `${GREEN}✓${RESET}`,
- cross: `${RED}✗${RESET}`,
- arrow: `${BLUE}→${RESET}`,
- bullet: `${DIM}•${RESET}`,
- info: `${BLUE}ℹ${RESET}`,
- warn: `${YELLOW}⚠${RESET}`,
- star: `${YELLOW}★${RESET}`,
- };
- function printHeader(isUpdate: boolean): void {
- console.log();
- console.log(
- `${BOLD}oh-my-opencode-slim ${isUpdate ? 'Update' : 'Install'}${RESET}`,
- );
- console.log('='.repeat(30));
- console.log();
- }
- function printStep(step: number, total: number, message: string): void {
- console.log(`${DIM}[${step}/${total}]${RESET} ${message}`);
- }
- function printSuccess(message: string): void {
- console.log(`${SYMBOLS.check} ${message}`);
- }
- function printError(message: string): void {
- console.log(`${SYMBOLS.cross} ${RED}${message}${RESET}`);
- }
- function printInfo(message: string): void {
- console.log(`${SYMBOLS.info} ${message}`);
- }
- function printWarning(message: string): void {
- console.log(`${SYMBOLS.warn} ${YELLOW}${message}${RESET}`);
- }
- async function checkOpenCodeInstalled(): Promise<{
- ok: boolean;
- version?: string;
- }> {
- const installed = await isOpenCodeInstalled();
- if (!installed) {
- printError('OpenCode is not installed on this system.');
- printInfo('Install it with:');
- console.log(
- ` ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`,
- );
- return { ok: false };
- }
- const version = await getOpenCodeVersion();
- printSuccess(`OpenCode ${version ?? ''} detected`);
- return { ok: true, version: version ?? undefined };
- }
- function handleStepResult(
- result: ConfigMergeResult,
- successMsg: string,
- ): boolean {
- if (!result.success) {
- printError(`Failed: ${result.error}`);
- return false;
- }
- printSuccess(
- `${successMsg} ${SYMBOLS.arrow} ${DIM}${result.configPath}${RESET}`,
- );
- return true;
- }
- function formatConfigSummary(config: InstallConfig): string {
- const liteConfig = generateLiteConfig(config);
- const preset = (liteConfig.preset as string) || 'unknown';
- const lines: string[] = [];
- lines.push(`${BOLD}Configuration Summary${RESET}`);
- lines.push('');
- lines.push(` ${BOLD}Preset:${RESET} ${BLUE}${preset}${RESET}`);
- lines.push(
- ` ${config.hasAntigravity ? SYMBOLS.check : `${DIM}○${RESET}`} Antigravity`,
- );
- lines.push(
- ` ${config.hasOpenAI ? SYMBOLS.check : `${DIM}○${RESET}`} OpenAI`,
- );
- lines.push(` ${SYMBOLS.check} Opencode Zen (Big Pickle)`); // Always enabled
- lines.push(
- ` ${config.hasTmux ? SYMBOLS.check : `${DIM}○${RESET}`} Tmux Integration`,
- );
- return lines.join('\n');
- }
- function printAgentModels(config: InstallConfig): void {
- const liteConfig = generateLiteConfig(config);
- const presetName = (liteConfig.preset as string) || 'unknown';
- const presets = liteConfig.presets as Record<string, unknown>;
- const agents = presets?.[presetName] as Record<
- string,
- { model: string; skills: string[] }
- >;
- if (!agents || Object.keys(agents).length === 0) return;
- console.log(
- `${BOLD}Agent Configuration (Preset: ${BLUE}${presetName}${RESET}):${RESET}`,
- );
- console.log();
- const maxAgentLen = Math.max(...Object.keys(agents).map((a) => a.length));
- for (const [agent, info] of Object.entries(agents)) {
- const padding = ' '.repeat(maxAgentLen - agent.length);
- const skillsStr =
- info.skills.length > 0
- ? ` ${DIM}[${info.skills.join(', ')}]${RESET}`
- : '';
- console.log(
- ` ${DIM}${agent}${RESET}${padding} ${SYMBOLS.arrow} ${BLUE}${info.model}${RESET}${skillsStr}`,
- );
- }
- console.log();
- }
- function argsToConfig(args: InstallArgs): InstallConfig {
- return {
- hasAntigravity: args.antigravity === 'yes',
- hasOpenAI: args.openai === 'yes',
- hasOpencodeZen: true, // Always enabled - free models available to all users
- hasTmux: args.tmux === 'yes',
- installSkills: args.skills === 'yes',
- };
- }
- async function askYesNo(
- rl: readline.Interface,
- prompt: string,
- defaultValue: BooleanArg = 'no',
- ): Promise<BooleanArg> {
- const hint = defaultValue === 'yes' ? '[Y/n]' : '[y/N]';
- const answer = (await rl.question(`${BLUE}${prompt}${RESET} ${hint}: `))
- .trim()
- .toLowerCase();
- if (answer === '') return defaultValue;
- if (answer === 'y' || answer === 'yes') return 'yes';
- if (answer === 'n' || answer === 'no') return 'no';
- return defaultValue;
- }
- async function runInteractiveMode(
- detected: DetectedConfig,
- ): Promise<InstallConfig> {
- const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
- });
- // TODO: tmux has a bug, disabled for now
- // const tmuxInstalled = await isTmuxInstalled()
- // const totalQuestions = tmuxInstalled ? 3 : 2
- const totalQuestions = 2;
- try {
- console.log(`${BOLD}Question 1/${totalQuestions}:${RESET}`);
- const antigravity = await askYesNo(
- rl,
- 'Do you have an Antigravity subscription (via cliproxy)?',
- 'yes',
- );
- console.log();
- console.log(`${BOLD}Question 2/${totalQuestions}:${RESET}`);
- const openai = await askYesNo(
- rl,
- 'Do you have access to OpenAI API?',
- detected.hasOpenAI ? 'yes' : 'no',
- );
- console.log();
- // TODO: tmux has a bug, disabled for now
- // let tmux: BooleanArg = "no"
- // if (tmuxInstalled) {
- // console.log(`${BOLD}Question 3/3:${RESET}`)
- // printInfo(`${BOLD}Tmux detected!${RESET} We can enable tmux integration for you.`)
- // printInfo("This will spawn new panes for sub-agents, letting you watch them work in real-time.")
- // tmux = await askYesNo(rl, "Enable tmux integration?", detected.hasTmux ? "yes" : "no")
- // console.log()
- // }
- // Skills prompt
- console.log(`${BOLD}Recommended Skills:${RESET}`);
- for (const skill of RECOMMENDED_SKILLS) {
- console.log(` ${SYMBOLS.bullet} ${BOLD}${skill.name}${RESET}: ${skill.description}`);
- }
- console.log();
- const skills = await askYesNo(
- rl,
- 'Install recommended skills?',
- 'yes',
- );
- console.log();
- return {
- hasAntigravity: antigravity === 'yes',
- hasOpenAI: openai === 'yes',
- hasOpencodeZen: true,
- hasTmux: false,
- installSkills: skills === 'yes',
- };
- } finally {
- rl.close();
- }
- }
- async function runInstall(config: InstallConfig): Promise<number> {
- const detected = detectCurrentConfig();
- const isUpdate = detected.isInstalled;
- printHeader(isUpdate);
- // Calculate total steps dynamically
- let totalSteps = 4; // Base: check opencode, add plugin, disable default agents, write lite config
- if (config.hasAntigravity) totalSteps += 1; // provider config only (no auth plugin needed)
- if (config.installSkills) totalSteps += 1; // skills installation
- let step = 1;
- printStep(step++, totalSteps, 'Checking OpenCode installation...');
- const { ok } = await checkOpenCodeInstalled();
- if (!ok) return 1;
- printStep(step++, totalSteps, 'Adding oh-my-opencode-slim plugin...');
- const pluginResult = await addPluginToOpenCodeConfig();
- if (!handleStepResult(pluginResult, 'Plugin added')) return 1;
- printStep(step++, totalSteps, 'Disabling OpenCode default agents...');
- const agentResult = disableDefaultAgents();
- if (!handleStepResult(agentResult, 'Default agents disabled')) return 1;
- if (config.hasAntigravity) {
- printStep(step++, totalSteps, 'Adding cliproxy provider configuration...');
- const providerResult = addProviderConfig(config);
- if (!handleStepResult(providerResult, 'Cliproxy provider configured'))
- return 1;
- }
- printStep(step++, totalSteps, 'Writing oh-my-opencode-slim configuration...');
- const liteResult = writeLiteConfig(config);
- if (!handleStepResult(liteResult, 'Config written')) return 1;
- // Install skills if requested
- if (config.installSkills) {
- printStep(step++, totalSteps, 'Installing recommended skills...');
- let skillsInstalled = 0;
- for (const skill of RECOMMENDED_SKILLS) {
- printInfo(`Installing ${skill.name}...`);
- if (installSkill(skill)) {
- printSuccess(`Installed: ${skill.name}`);
- skillsInstalled++;
- } else {
- printWarning(`Failed to install: ${skill.name}`);
- }
- }
- printSuccess(`${skillsInstalled}/${RECOMMENDED_SKILLS.length} skills installed`);
- }
- // Summary
- console.log();
- console.log(formatConfigSummary(config));
- console.log();
- printAgentModels(config);
- if (!config.hasAntigravity && !config.hasOpenAI) {
- printWarning(
- 'No providers configured. Zen Big Pickle models will be used as fallback.',
- );
- }
- console.log(
- `${SYMBOLS.star} ${BOLD}${GREEN}${isUpdate ? 'Configuration updated!' : 'Installation complete!'}${RESET}`,
- );
- console.log();
- console.log(`${BOLD}Next steps:${RESET}`);
- console.log();
- let nextStep = 1;
- if (config.hasAntigravity) {
- console.log(` ${nextStep++}. Install cliproxy:`);
- console.log(` ${DIM}macOS:${RESET}`);
- console.log(` ${BLUE}$ brew install cliproxyapi${RESET}`);
- console.log(` ${BLUE}$ brew services start cliproxyapi${RESET}`);
- console.log(` ${DIM}Linux:${RESET}`);
- console.log(
- ` ${BLUE}$ curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash${RESET}`,
- );
- console.log();
- console.log(` ${nextStep++}. Authenticate with Antigravity via OAuth:`);
- console.log(` ${BLUE}$ ./cli-proxy-api --antigravity-login${RESET}`);
- console.log(
- ` ${DIM}(Add --no-browser to print login URL instead of opening browser)${RESET}`,
- );
- console.log();
- }
- if (config.hasOpenAI || !config.hasAntigravity) {
- console.log(` ${nextStep++}. Authenticate with your providers:`);
- console.log(` ${BLUE}$ opencode auth login${RESET}`);
- console.log();
- }
- // TODO: tmux has a bug, disabled for now
- // if (config.hasTmux) {
- // console.log(` ${nextStep++}. Run OpenCode inside tmux:`)
- // console.log(` ${BLUE}$ tmux${RESET}`)
- // console.log(` ${BLUE}$ opencode${RESET}`)
- // } else {
- console.log(` ${nextStep++}. Start OpenCode:`);
- console.log(` ${BLUE}$ opencode${RESET}`);
- // }
- console.log();
- return 0;
- }
- export async function install(args: InstallArgs): Promise<number> {
- // Non-interactive mode: all args must be provided
- if (!args.tui) {
- const requiredArgs = ['antigravity', 'openai', 'tmux'] as const;
- const errors = requiredArgs.filter((key) => {
- const value = args[key];
- return value === undefined || !['yes', 'no'].includes(value);
- });
- if (errors.length > 0) {
- printHeader(false);
- printError('Missing or invalid arguments:');
- for (const key of errors) {
- console.log(` ${SYMBOLS.bullet} --${key}=<yes|no>`);
- }
- console.log();
- printInfo(
- 'Usage: bunx oh-my-opencode-slim install --no-tui --antigravity=<yes|no> --openai=<yes|no> --tmux=<yes|no>',
- );
- console.log();
- return 1;
- }
- return runInstall(argsToConfig(args));
- }
- // Interactive mode
- const detected = detectCurrentConfig();
- printHeader(detected.isInstalled);
- printStep(1, 1, 'Checking OpenCode installation...');
- const { ok } = await checkOpenCodeInstalled();
- if (!ok) return 1;
- console.log();
- const config = await runInteractiveMode(detected);
- return runInstall(config);
- }
|