| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- import { existsSync } from 'node:fs';
- import { createInterface } from 'node:readline/promises';
- import {
- detectBackgroundSubagentsTarget,
- expandHomePath,
- getBackgroundSubagentsBlock,
- isBackgroundSubagentsEnabled,
- manualBackgroundSubagentsInstructions,
- writeBackgroundSubagentsBlock,
- } from './background-subagents';
- import { installCompanion } from './companion';
- import {
- addPluginToOpenCodeConfig,
- addPluginToOpenCodeTuiConfig,
- detectCurrentConfig,
- disableDefaultAgents,
- enableLspByDefault,
- generateLiteConfig,
- getOpenCodePath,
- getOpenCodeVersion,
- isOpenCodeInstalled,
- warmOpenCodePluginCache,
- writeLiteConfig,
- } from './config-manager';
- import { CUSTOM_SKILLS, installCustomSkill } from './custom-skills';
- import { getExistingLiteConfigPath } from './paths';
- import type { ConfigMergeResult, 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}[ok]${RESET}`,
- cross: `${RED}[x]${RESET}`,
- arrow: `${BLUE}->${RESET}`,
- bullet: `${DIM}-${RESET}`,
- info: `${BLUE}[i]${RESET}`,
- warn: `${YELLOW}[!]${RESET}`,
- star: `${YELLOW}★${RESET}`,
- };
- const GITHUB_REPO = 'alvinunreal/oh-my-opencode-slim';
- const GITHUB_URL = `https://github.com/${GITHUB_REPO}`;
- 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}`);
- }
- async function confirm(message: string, defaultYes = true): Promise<boolean> {
- const suffix = defaultYes ? ' (Y/n) ' : ' (y/N) ';
- const rl = createInterface({ input: process.stdin, output: process.stdout });
- try {
- const answer = (await rl.question(`${message}${suffix}`))
- .trim()
- .toLowerCase();
- if (!answer) return defaultYes;
- return answer === 'y' || answer === 'yes';
- } finally {
- rl.close();
- }
- }
- async function askToStarRepo(config: InstallConfig): Promise<void> {
- if (!config.promptForStar || config.dryRun || !process.stdin.isTTY) return;
- console.log();
- const shouldStar = await confirm(
- `${SYMBOLS.star} Star the repo on GitHub?`,
- true,
- );
- if (!shouldStar) return;
- try {
- const { execFileSync } = await import('node:child_process');
- execFileSync(
- 'gh',
- ['api', '--silent', '--method', 'PUT', `/user/starred/${GITHUB_REPO}`],
- { stdio: 'ignore', timeout: 10_000 },
- );
- printSuccess('Thanks for starring! ★');
- } catch {
- printInfo(
- `Couldn't star automatically. You can star manually:\n ${BLUE}${GITHUB_URL}${RESET}`,
- );
- }
- }
- async function checkOpenCodeInstalled(): Promise<{
- ok: boolean;
- version?: string;
- path?: 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}`,
- );
- console.log();
- printInfo('Or if already installed, add it to your PATH:');
- console.log(` ${BLUE}export PATH="$HOME/.local/bin:$PATH"${RESET}`);
- console.log(` ${BLUE}export PATH="$HOME/.opencode/bin:$PATH"${RESET}`);
- return { ok: false };
- }
- const version = await getOpenCodeVersion();
- const path = getOpenCodePath();
- const detectedVersion = version ?? '';
- const pathInfo = path ? ` (${DIM}${path}${RESET})` : '';
- printSuccess(`OpenCode ${detectedVersion} detected${pathInfo}`);
- return { ok: true, version: version ?? undefined, path: path ?? undefined };
- }
- export async function configureBackgroundSubagents(
- config: InstallConfig,
- ): Promise<{ enabledNow: boolean; configuredTarget?: string }> {
- if (
- isBackgroundSubagentsEnabled(
- process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS,
- )
- ) {
- printSuccess(
- 'OpenCode background subagents already enabled in environment',
- );
- return { enabledNow: true };
- }
- const target =
- config.backgroundSubagentsTarget !== undefined
- ? expandHomePath(config.backgroundSubagentsTarget)
- : detectBackgroundSubagentsTarget();
- if (config.backgroundSubagents === 'no') {
- printInfo('OpenCode background subagents shell setup skipped.');
- console.log(manualBackgroundSubagentsInstructions({ targetPath: target }));
- return { enabledNow: false };
- }
- if (!target) {
- printInfo('No safe shell startup file detected.');
- console.log(manualBackgroundSubagentsInstructions());
- return { enabledNow: false };
- }
- const block = getBackgroundSubagentsBlock(target);
- if (config.dryRun) {
- printInfo(
- 'Dry run mode - background subagents block that would be written:',
- );
- console.log(`Target: ${target}`);
- console.log(`\n${block}\n`);
- return { enabledNow: false, configuredTarget: target };
- }
- if (config.backgroundSubagents === 'ask') {
- if (!process.stdin.isTTY) {
- printInfo('Skipped background subagents shell setup in non-TTY mode.');
- console.log(
- manualBackgroundSubagentsInstructions({ targetPath: target }),
- );
- return { enabledNow: false };
- }
- console.log();
- printInfo(
- 'V2 requires OpenCode background subagents for default orchestration.',
- );
- printInfo(
- `The installer can add the required environment export to ${target}.`,
- );
- const shouldWrite = await confirm(
- 'Add OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true now?',
- true,
- );
- if (!shouldWrite) {
- printInfo('Skipped background subagents shell setup.');
- console.log(
- manualBackgroundSubagentsInstructions({ targetPath: target }),
- );
- return { enabledNow: false };
- }
- }
- try {
- writeBackgroundSubagentsBlock(target);
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- printError(`Could not write background subagents shell config: ${message}`);
- printInfo('Add the setting manually instead:');
- console.log(manualBackgroundSubagentsInstructions({ targetPath: target }));
- return { enabledNow: false };
- }
- printSuccess(
- `Background subagents enabled ${SYMBOLS.arrow} ${DIM}${target}${RESET}`,
- );
- return { enabledNow: false, configuredTarget: target };
- }
- export async function shouldInstallCompanion(
- config: InstallConfig,
- ): Promise<boolean> {
- if (config.companion === 'yes') return true;
- if (config.companion === 'no') return false;
- if (config.dryRun) {
- printInfo(
- 'Dry run mode - would ask to install the desktop companion (default: yes).',
- );
- config.companion = 'yes';
- return true;
- }
- if (!process.stdin.isTTY) {
- printInfo(
- 'Skipped desktop companion prompt in non-TTY mode. Use --companion=yes to install it.',
- );
- config.companion = 'no';
- return false;
- }
- console.log();
- printInfo('The optional desktop companion shows live agent activity.');
- const shouldInstall = await confirm(
- 'Install and enable the desktop companion?',
- true,
- );
- config.companion = shouldInstall ? 'yes' : 'no';
- if (!shouldInstall) {
- printInfo('Desktop companion install skipped.');
- }
- return shouldInstall;
- }
- 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;
- }
- async function runInstall(config: InstallConfig): Promise<number> {
- const detected = detectCurrentConfig();
- const isUpdate = detected.isInstalled;
- printHeader(isUpdate);
- const companionInstall = await shouldInstallCompanion(config);
- let totalSteps = 7;
- if (config.installCustomSkills) totalSteps += 1;
- if (companionInstall) totalSteps += 1;
- totalSteps += 1;
- let step = 1;
- printStep(step++, totalSteps, 'Checking OpenCode installation...');
- if (config.dryRun) {
- printInfo('Dry run mode - skipping OpenCode check');
- } else {
- const { ok } = await checkOpenCodeInstalled();
- if (!ok) return 1;
- }
- printStep(step++, totalSteps, 'Adding oh-my-opencode-slim plugin...');
- if (config.dryRun) {
- printInfo('Dry run mode - skipping plugin installation');
- } else {
- const pluginResult = await addPluginToOpenCodeConfig();
- if (!handleStepResult(pluginResult, 'Plugin added')) return 1;
- }
- printStep(step++, totalSteps, 'Adding TUI version badge...');
- if (config.dryRun) {
- printInfo('Dry run mode - skipping TUI plugin installation');
- } else {
- const tuiResult = await addPluginToOpenCodeTuiConfig();
- if (!tuiResult.success) {
- printInfo(`Skipped TUI badge: ${tuiResult.error}`);
- } else {
- handleStepResult(tuiResult, 'TUI badge added');
- }
- }
- printStep(step++, totalSteps, 'Warming OpenCode plugin cache...');
- if (config.dryRun) {
- printInfo('Dry run mode - skipping cache warm-up');
- } else {
- const cacheResult = await warmOpenCodePluginCache();
- if (cacheResult === null) {
- printInfo('Local development install - cache warm-up not required');
- } else if (!cacheResult.success) {
- printInfo(`Skipped cache warm-up: ${cacheResult.error}`);
- } else {
- handleStepResult(cacheResult, 'OpenCode cache warmed');
- }
- }
- printStep(step++, totalSteps, 'Disabling OpenCode default agents...');
- if (config.dryRun) {
- printInfo('Dry run mode - skipping agent disabling');
- } else {
- const agentResult = disableDefaultAgents();
- if (!handleStepResult(agentResult, 'Default agents disabled')) return 1;
- }
- printStep(step++, totalSteps, 'Enabling OpenCode LSP integration...');
- if (config.dryRun) {
- printInfo('Dry run mode - skipping LSP configuration');
- } else {
- const lspResult = enableLspByDefault();
- if (!handleStepResult(lspResult, 'LSP enabled')) return 1;
- }
- printStep(step++, totalSteps, 'Configuring OpenCode background subagents...');
- const backgroundSubagents = await configureBackgroundSubagents(config);
- if (companionInstall) {
- printStep(step++, totalSteps, 'Installing desktop companion binary...');
- const companionResult = await installCompanion(config);
- if (!handleStepResult(companionResult, 'Companion installed')) return 1;
- }
- printStep(step++, totalSteps, 'Writing oh-my-opencode-slim configuration...');
- if (config.dryRun) {
- const liteConfig = generateLiteConfig(config);
- printInfo('Dry run mode - configuration that would be written:');
- console.log(`\n${JSON.stringify(liteConfig, null, 2)}\n`);
- } else {
- const configPath = getExistingLiteConfigPath();
- const configExists = existsSync(configPath);
- if (configExists && !config.reset) {
- printInfo(
- `Configuration already exists at ${configPath}. ` +
- 'Use --reset to overwrite.',
- );
- } else {
- const liteResult = writeLiteConfig(
- config,
- configExists ? configPath : undefined,
- );
- if (
- !handleStepResult(
- liteResult,
- configExists ? 'Config reset' : 'Config written',
- )
- )
- return 1;
- }
- }
- // Install custom skills if requested
- if (config.installCustomSkills) {
- printStep(step++, totalSteps, 'Installing custom skills...');
- if (config.dryRun) {
- printInfo('Dry run mode - would install custom skills:');
- for (const skill of CUSTOM_SKILLS) {
- printInfo(` - ${skill.name}`);
- }
- } else {
- let customSkillsInstalled = 0;
- for (const skill of CUSTOM_SKILLS) {
- printInfo(`Installing ${skill.name}...`);
- if (installCustomSkill(skill)) {
- printSuccess(`Installed: ${skill.name}`);
- customSkillsInstalled++;
- } else {
- printInfo(`Skipped: ${skill.name} (already installed)`);
- }
- }
- const totalCustom = CUSTOM_SKILLS.length;
- printSuccess(
- `${customSkillsInstalled}/${totalCustom} custom skills processed`,
- );
- }
- }
- const statusMsg = isUpdate
- ? 'Configuration updated!'
- : 'Installation complete!';
- console.log(`${SYMBOLS.star} ${BOLD}${GREEN}${statusMsg}${RESET}`);
- console.log();
- console.log(`${BOLD}Next steps:${RESET}`);
- console.log();
- const configPath = getExistingLiteConfigPath();
- console.log(' 1. Log in to the provider(s) you want to use:');
- console.log(` ${BLUE}$ opencode auth login${RESET}`);
- console.log();
- console.log(' 2. Refresh the models OpenCode can see:');
- console.log(` ${BLUE}$ opencode models --refresh${RESET}`);
- console.log();
- console.log(' 3. Review your generated config:');
- console.log(` ${BLUE}${configPath}${RESET}`);
- console.log();
- console.log(' 4. Start OpenCode:');
- if (backgroundSubagents.enabledNow) {
- console.log(` ${BLUE}$ opencode${RESET}`);
- } else if (backgroundSubagents.configuredTarget) {
- console.log(
- ` ${BLUE}$ source ${backgroundSubagents.configuredTarget}${RESET}`,
- );
- console.log(` ${BLUE}$ opencode${RESET}`);
- console.log(
- ` ${DIM}Or restart your terminal before running opencode.${RESET}`,
- );
- } else {
- console.log(
- ` ${BLUE}$ OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode${RESET}`,
- );
- }
- console.log();
- console.log(' 5. Verify the agents are responding:');
- console.log(` ${BLUE}> ping all agents${RESET}`);
- console.log();
- const modelsInfo =
- config.preset && config.preset !== 'openai'
- ? `Generated OpenAI and OpenCode Go presets; ${config.preset} is active.`
- : 'Generated OpenAI and OpenCode Go presets; OpenAI is active by default.';
- console.log(`${modelsInfo}`);
- const altProviders = 'For the full configuration reference, see:';
- console.log(altProviders);
- const docsUrl =
- 'https://github.com/alvinunreal/oh-my-opencode-slim/' +
- 'blob/master/docs/configuration.md';
- console.log(` ${BLUE}${docsUrl}${RESET}`);
- console.log();
- await askToStarRepo(config);
- return 0;
- }
- export async function install(args: InstallArgs): Promise<number> {
- const config: InstallConfig = {
- hasTmux: false,
- installCustomSkills: args.skills === 'yes',
- preset: args.preset,
- promptForStar: args.tui,
- dryRun: args.dryRun,
- reset: args.reset ?? false,
- backgroundSubagents: args.backgroundSubagents ?? 'ask',
- backgroundSubagentsTarget: args.backgroundSubagentsTarget,
- companion: args.companion,
- };
- return runInstall(config);
- }
|