From d525958a9d097193ea58edad1c8a8b880c94f4df Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 16:25:12 +0900 Subject: [PATCH] refactor(cli): split install.ts and model-fallback.ts into focused modules Install pipeline: - cli-installer.ts, tui-installer.ts, tui-install-prompts.ts - install-validators.ts Model fallback: - model-fallback-types.ts, fallback-chain-resolution.ts - provider-availability.ts, provider-model-id-transform.ts --- src/cli/cli-installer.ts | 164 ++++++++ src/cli/fallback-chain-resolution.ts | 55 +++ src/cli/install-validators.ts | 189 +++++++++ src/cli/install.ts | 540 +------------------------ src/cli/model-fallback-types.ts | 29 ++ src/cli/model-fallback.ts | 130 +----- src/cli/provider-availability.ts | 30 ++ src/cli/provider-model-id-transform.ts | 12 + src/cli/tui-install-prompts.ts | 111 +++++ src/cli/tui-installer.ts | 135 +++++++ 10 files changed, 741 insertions(+), 654 deletions(-) create mode 100644 src/cli/cli-installer.ts create mode 100644 src/cli/fallback-chain-resolution.ts create mode 100644 src/cli/install-validators.ts create mode 100644 src/cli/model-fallback-types.ts create mode 100644 src/cli/provider-availability.ts create mode 100644 src/cli/provider-model-id-transform.ts create mode 100644 src/cli/tui-install-prompts.ts create mode 100644 src/cli/tui-installer.ts diff --git a/src/cli/cli-installer.ts b/src/cli/cli-installer.ts new file mode 100644 index 000000000..a38b2c803 --- /dev/null +++ b/src/cli/cli-installer.ts @@ -0,0 +1,164 @@ +import color from "picocolors" +import type { InstallArgs } from "./types" +import { + addAuthPlugins, + addPluginToOpenCodeConfig, + addProviderConfig, + detectCurrentConfig, + getOpenCodeVersion, + isOpenCodeInstalled, + writeOmoConfig, +} from "./config-manager" +import { + SYMBOLS, + argsToConfig, + detectedToInitialValues, + formatConfigSummary, + printBox, + printError, + printHeader, + printInfo, + printStep, + printSuccess, + printWarning, + validateNonTuiArgs, +} from "./install-validators" + +export async function runCliInstaller(args: InstallArgs, version: string): Promise { + const validation = validateNonTuiArgs(args) + if (!validation.valid) { + printHeader(false) + printError("Validation failed:") + for (const err of validation.errors) { + console.log(` ${SYMBOLS.bullet} ${err}`) + } + console.log() + printInfo( + "Usage: bunx oh-my-opencode install --no-tui --claude= --gemini= --copilot=", + ) + console.log() + return 1 + } + + const detected = detectCurrentConfig() + const isUpdate = detected.isInstalled + + printHeader(isUpdate) + + const totalSteps = 6 + let step = 1 + + printStep(step++, totalSteps, "Checking OpenCode installation...") + const installed = await isOpenCodeInstalled() + const openCodeVersion = await getOpenCodeVersion() + if (!installed) { + printWarning( + "OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.", + ) + printInfo("Visit https://opencode.ai/docs for installation instructions") + } else { + printSuccess(`OpenCode ${openCodeVersion ?? ""} detected`) + } + + if (isUpdate) { + const initial = detectedToInitialValues(detected) + printInfo(`Current config: Claude=${initial.claude}, Gemini=${initial.gemini}`) + } + + const config = argsToConfig(args) + + printStep(step++, totalSteps, "Adding oh-my-opencode plugin...") + const pluginResult = await addPluginToOpenCodeConfig(version) + if (!pluginResult.success) { + printError(`Failed: ${pluginResult.error}`) + return 1 + } + printSuccess( + `Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`, + ) + + if (config.hasGemini) { + printStep(step++, totalSteps, "Adding auth plugins...") + const authResult = await addAuthPlugins(config) + if (!authResult.success) { + printError(`Failed: ${authResult.error}`) + return 1 + } + printSuccess(`Auth plugins configured ${SYMBOLS.arrow} ${color.dim(authResult.configPath)}`) + + printStep(step++, totalSteps, "Adding provider configurations...") + const providerResult = addProviderConfig(config) + if (!providerResult.success) { + printError(`Failed: ${providerResult.error}`) + return 1 + } + printSuccess(`Providers configured ${SYMBOLS.arrow} ${color.dim(providerResult.configPath)}`) + } else { + step += 2 + } + + printStep(step++, totalSteps, "Writing oh-my-opencode configuration...") + const omoResult = writeOmoConfig(config) + if (!omoResult.success) { + printError(`Failed: ${omoResult.error}`) + return 1 + } + printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`) + + printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete") + + if (!config.hasClaude) { + console.log() + console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING ")))) + console.log() + console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5."))) + console.log(color.red(" Without Claude, you may experience significantly degraded performance:")) + console.log(color.dim(" • Reduced orchestration quality")) + console.log(color.dim(" • Weaker tool selection and delegation")) + console.log(color.dim(" • Less reliable task completion")) + console.log() + console.log(color.yellow(" Consider subscribing to Claude Pro/Max for the best experience.")) + console.log() + } + + if ( + !config.hasClaude && + !config.hasOpenAI && + !config.hasGemini && + !config.hasCopilot && + !config.hasOpencodeZen + ) { + printWarning("No model providers configured. Using opencode/glm-4.7-free as fallback.") + } + + console.log(`${SYMBOLS.star} ${color.bold(color.green(isUpdate ? "Configuration updated!" : "Installation complete!"))}`) + console.log(` Run ${color.cyan("opencode")} to start!`) + console.log() + + printBox( + `${color.bold("Pro Tip:")} Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` + + `All features work like magic—parallel agents, background tasks,\n` + + `deep exploration, and relentless execution until completion.`, + "The Magic Word", + ) + + console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`) + console.log( + ` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true")}`, + ) + console.log() + console.log(color.dim("oMoMoMoMo... Enjoy!")) + console.log() + + if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) { + printBox( + `Run ${color.cyan("opencode auth login")} and select your provider:\n` + + (config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") + + (config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") + + (config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""), + "Authenticate Your Providers", + ) + } + + return 0 +} diff --git a/src/cli/fallback-chain-resolution.ts b/src/cli/fallback-chain-resolution.ts new file mode 100644 index 000000000..528aef0e6 --- /dev/null +++ b/src/cli/fallback-chain-resolution.ts @@ -0,0 +1,55 @@ +import { + AGENT_MODEL_REQUIREMENTS, + type FallbackEntry, +} from "../shared/model-requirements" +import type { ProviderAvailability } from "./model-fallback-types" +import { isProviderAvailable } from "./provider-availability" +import { transformModelForProvider } from "./provider-model-id-transform" + +export function resolveModelFromChain( + fallbackChain: FallbackEntry[], + availability: ProviderAvailability +): { model: string; variant?: string } | null { + for (const entry of fallbackChain) { + for (const provider of entry.providers) { + if (isProviderAvailable(provider, availability)) { + const transformedModel = transformModelForProvider(provider, entry.model) + return { + model: `${provider}/${transformedModel}`, + variant: entry.variant, + } + } + } + } + return null +} + +export function getSisyphusFallbackChain(): FallbackEntry[] { + return AGENT_MODEL_REQUIREMENTS.sisyphus.fallbackChain +} + +export function isAnyFallbackEntryAvailable( + fallbackChain: FallbackEntry[], + availability: ProviderAvailability +): boolean { + return fallbackChain.some((entry) => + entry.providers.some((provider) => isProviderAvailable(provider, availability)) + ) +} + +export function isRequiredModelAvailable( + requiresModel: string, + fallbackChain: FallbackEntry[], + availability: ProviderAvailability +): boolean { + const matchingEntry = fallbackChain.find((entry) => entry.model === requiresModel) + if (!matchingEntry) return false + return matchingEntry.providers.some((provider) => isProviderAvailable(provider, availability)) +} + +export function isRequiredProviderAvailable( + requiredProviders: string[], + availability: ProviderAvailability +): boolean { + return requiredProviders.some((provider) => isProviderAvailable(provider, availability)) +} diff --git a/src/cli/install-validators.ts b/src/cli/install-validators.ts new file mode 100644 index 000000000..be601d681 --- /dev/null +++ b/src/cli/install-validators.ts @@ -0,0 +1,189 @@ +import color from "picocolors" +import type { + BooleanArg, + ClaudeSubscription, + DetectedConfig, + InstallArgs, + InstallConfig, +} from "./types" + +export const SYMBOLS = { + check: color.green("[OK]"), + cross: color.red("[X]"), + arrow: color.cyan("->"), + bullet: color.dim("*"), + info: color.blue("[i]"), + warn: color.yellow("[!]"), + star: color.yellow("*"), +} + +function formatProvider(name: string, enabled: boolean, detail?: string): string { + const status = enabled ? SYMBOLS.check : color.dim("○") + const label = enabled ? color.white(name) : color.dim(name) + const suffix = detail ? color.dim(` (${detail})`) : "" + return ` ${status} ${label}${suffix}` +} + +export function formatConfigSummary(config: InstallConfig): string { + const lines: string[] = [] + + lines.push(color.bold(color.white("Configuration Summary"))) + lines.push("") + + const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined + lines.push(formatProvider("Claude", config.hasClaude, claudeDetail)) + lines.push(formatProvider("OpenAI/ChatGPT", config.hasOpenAI, "GPT-5.2 for Oracle")) + lines.push(formatProvider("Gemini", config.hasGemini)) + lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback")) + lines.push(formatProvider("OpenCode Zen", config.hasOpencodeZen, "opencode/ models")) + lines.push(formatProvider("Z.ai Coding Plan", config.hasZaiCodingPlan, "Librarian/Multimodal")) + lines.push(formatProvider("Kimi For Coding", config.hasKimiForCoding, "Sisyphus/Prometheus fallback")) + + lines.push("") + lines.push(color.dim("─".repeat(40))) + lines.push("") + + lines.push(color.bold(color.white("Model Assignment"))) + lines.push("") + lines.push(` ${SYMBOLS.info} Models auto-configured based on provider priority`) + lines.push(` ${SYMBOLS.bullet} Priority: Native > Copilot > OpenCode Zen > Z.ai`) + + return lines.join("\n") +} + +export function printHeader(isUpdate: boolean): void { + const mode = isUpdate ? "Update" : "Install" + console.log() + console.log(color.bgMagenta(color.white(` oMoMoMoMo... ${mode} `))) + console.log() +} + +export function printStep(step: number, total: number, message: string): void { + const progress = color.dim(`[${step}/${total}]`) + console.log(`${progress} ${message}`) +} + +export function printSuccess(message: string): void { + console.log(`${SYMBOLS.check} ${message}`) +} + +export function printError(message: string): void { + console.log(`${SYMBOLS.cross} ${color.red(message)}`) +} + +export function printInfo(message: string): void { + console.log(`${SYMBOLS.info} ${message}`) +} + +export function printWarning(message: string): void { + console.log(`${SYMBOLS.warn} ${color.yellow(message)}`) +} + +export function printBox(content: string, title?: string): void { + const lines = content.split("\n") + const maxWidth = + Math.max( + ...lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "").length), + title?.length ?? 0, + ) + 4 + const border = color.dim("─".repeat(maxWidth)) + + console.log() + if (title) { + console.log( + color.dim("┌─") + + color.bold(` ${title} `) + + color.dim("─".repeat(maxWidth - title.length - 4)) + + color.dim("┐"), + ) + } else { + console.log(color.dim("┌") + border + color.dim("┐")) + } + + for (const line of lines) { + const stripped = line.replace(/\x1b\[[0-9;]*m/g, "") + const padding = maxWidth - stripped.length + console.log(color.dim("│") + ` ${line}${" ".repeat(padding - 1)}` + color.dim("│")) + } + + console.log(color.dim("└") + border + color.dim("┘")) + console.log() +} + +export function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string[] } { + const errors: string[] = [] + + if (args.claude === undefined) { + errors.push("--claude is required (values: no, yes, max20)") + } else if (!["no", "yes", "max20"].includes(args.claude)) { + errors.push(`Invalid --claude value: ${args.claude} (expected: no, yes, max20)`) + } + + if (args.gemini === undefined) { + errors.push("--gemini is required (values: no, yes)") + } else if (!["no", "yes"].includes(args.gemini)) { + errors.push(`Invalid --gemini value: ${args.gemini} (expected: no, yes)`) + } + + if (args.copilot === undefined) { + errors.push("--copilot is required (values: no, yes)") + } else if (!["no", "yes"].includes(args.copilot)) { + errors.push(`Invalid --copilot value: ${args.copilot} (expected: no, yes)`) + } + + if (args.openai !== undefined && !["no", "yes"].includes(args.openai)) { + errors.push(`Invalid --openai value: ${args.openai} (expected: no, yes)`) + } + + if (args.opencodeZen !== undefined && !["no", "yes"].includes(args.opencodeZen)) { + errors.push(`Invalid --opencode-zen value: ${args.opencodeZen} (expected: no, yes)`) + } + + if (args.zaiCodingPlan !== undefined && !["no", "yes"].includes(args.zaiCodingPlan)) { + errors.push(`Invalid --zai-coding-plan value: ${args.zaiCodingPlan} (expected: no, yes)`) + } + + if (args.kimiForCoding !== undefined && !["no", "yes"].includes(args.kimiForCoding)) { + errors.push(`Invalid --kimi-for-coding value: ${args.kimiForCoding} (expected: no, yes)`) + } + + return { valid: errors.length === 0, errors } +} + +export function argsToConfig(args: InstallArgs): InstallConfig { + return { + hasClaude: args.claude !== "no", + isMax20: args.claude === "max20", + hasOpenAI: args.openai === "yes", + hasGemini: args.gemini === "yes", + hasCopilot: args.copilot === "yes", + hasOpencodeZen: args.opencodeZen === "yes", + hasZaiCodingPlan: args.zaiCodingPlan === "yes", + hasKimiForCoding: args.kimiForCoding === "yes", + } +} + +export function detectedToInitialValues(detected: DetectedConfig): { + claude: ClaudeSubscription + openai: BooleanArg + gemini: BooleanArg + copilot: BooleanArg + opencodeZen: BooleanArg + zaiCodingPlan: BooleanArg + kimiForCoding: BooleanArg +} { + let claude: ClaudeSubscription = "no" + if (detected.hasClaude) { + claude = detected.isMax20 ? "max20" : "yes" + } + + return { + claude, + openai: detected.hasOpenAI ? "yes" : "no", + gemini: detected.hasGemini ? "yes" : "no", + copilot: detected.hasCopilot ? "yes" : "no", + opencodeZen: detected.hasOpencodeZen ? "yes" : "no", + zaiCodingPlan: detected.hasZaiCodingPlan ? "yes" : "no", + kimiForCoding: detected.hasKimiForCoding ? "yes" : "no", + } +} diff --git a/src/cli/install.ts b/src/cli/install.ts index a4143cd39..fe8722492 100644 --- a/src/cli/install.ts +++ b/src/cli/install.ts @@ -1,542 +1,10 @@ -import * as p from "@clack/prompts" -import color from "picocolors" -import type { InstallArgs, InstallConfig, ClaudeSubscription, BooleanArg, DetectedConfig } from "./types" -import { - addPluginToOpenCodeConfig, - writeOmoConfig, - isOpenCodeInstalled, - getOpenCodeVersion, - addAuthPlugins, - addProviderConfig, - detectCurrentConfig, -} from "./config-manager" -import { shouldShowChatGPTOnlyWarning } from "./model-fallback" import packageJson from "../../package.json" with { type: "json" } +import type { InstallArgs } from "./types" +import { runCliInstaller } from "./cli-installer" +import { runTuiInstaller } from "./tui-installer" const VERSION = packageJson.version -const SYMBOLS = { - check: color.green("[OK]"), - cross: color.red("[X]"), - arrow: color.cyan("->"), - bullet: color.dim("*"), - info: color.blue("[i]"), - warn: color.yellow("[!]"), - star: color.yellow("*"), -} - -function formatProvider(name: string, enabled: boolean, detail?: string): string { - const status = enabled ? SYMBOLS.check : color.dim("○") - const label = enabled ? color.white(name) : color.dim(name) - const suffix = detail ? color.dim(` (${detail})`) : "" - return ` ${status} ${label}${suffix}` -} - -function formatConfigSummary(config: InstallConfig): string { - const lines: string[] = [] - - lines.push(color.bold(color.white("Configuration Summary"))) - lines.push("") - - const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined - lines.push(formatProvider("Claude", config.hasClaude, claudeDetail)) - lines.push(formatProvider("OpenAI/ChatGPT", config.hasOpenAI, "GPT-5.2 for Oracle")) - lines.push(formatProvider("Gemini", config.hasGemini)) - lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback")) - lines.push(formatProvider("OpenCode Zen", config.hasOpencodeZen, "opencode/ models")) - lines.push(formatProvider("Z.ai Coding Plan", config.hasZaiCodingPlan, "Librarian/Multimodal")) - lines.push(formatProvider("Kimi For Coding", config.hasKimiForCoding, "Sisyphus/Prometheus fallback")) - - lines.push("") - lines.push(color.dim("─".repeat(40))) - lines.push("") - - lines.push(color.bold(color.white("Model Assignment"))) - lines.push("") - lines.push(` ${SYMBOLS.info} Models auto-configured based on provider priority`) - lines.push(` ${SYMBOLS.bullet} Priority: Native > Copilot > OpenCode Zen > Z.ai`) - - return lines.join("\n") -} - -function printHeader(isUpdate: boolean): void { - const mode = isUpdate ? "Update" : "Install" - console.log() - console.log(color.bgMagenta(color.white(` oMoMoMoMo... ${mode} `))) - console.log() -} - -function printStep(step: number, total: number, message: string): void { - const progress = color.dim(`[${step}/${total}]`) - console.log(`${progress} ${message}`) -} - -function printSuccess(message: string): void { - console.log(`${SYMBOLS.check} ${message}`) -} - -function printError(message: string): void { - console.log(`${SYMBOLS.cross} ${color.red(message)}`) -} - -function printInfo(message: string): void { - console.log(`${SYMBOLS.info} ${message}`) -} - -function printWarning(message: string): void { - console.log(`${SYMBOLS.warn} ${color.yellow(message)}`) -} - -function printBox(content: string, title?: string): void { - const lines = content.split("\n") - const maxWidth = Math.max(...lines.map(l => l.replace(/\x1b\[[0-9;]*m/g, "").length), title?.length ?? 0) + 4 - const border = color.dim("─".repeat(maxWidth)) - - console.log() - if (title) { - console.log(color.dim("┌─") + color.bold(` ${title} `) + color.dim("─".repeat(maxWidth - title.length - 4)) + color.dim("┐")) - } else { - console.log(color.dim("┌") + border + color.dim("┐")) - } - - for (const line of lines) { - const stripped = line.replace(/\x1b\[[0-9;]*m/g, "") - const padding = maxWidth - stripped.length - console.log(color.dim("│") + ` ${line}${" ".repeat(padding - 1)}` + color.dim("│")) - } - - console.log(color.dim("└") + border + color.dim("┘")) - console.log() -} - -function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string[] } { - const errors: string[] = [] - - if (args.claude === undefined) { - errors.push("--claude is required (values: no, yes, max20)") - } else if (!["no", "yes", "max20"].includes(args.claude)) { - errors.push(`Invalid --claude value: ${args.claude} (expected: no, yes, max20)`) - } - - if (args.gemini === undefined) { - errors.push("--gemini is required (values: no, yes)") - } else if (!["no", "yes"].includes(args.gemini)) { - errors.push(`Invalid --gemini value: ${args.gemini} (expected: no, yes)`) - } - - if (args.copilot === undefined) { - errors.push("--copilot is required (values: no, yes)") - } else if (!["no", "yes"].includes(args.copilot)) { - errors.push(`Invalid --copilot value: ${args.copilot} (expected: no, yes)`) - } - - if (args.openai !== undefined && !["no", "yes"].includes(args.openai)) { - errors.push(`Invalid --openai value: ${args.openai} (expected: no, yes)`) - } - - if (args.opencodeZen !== undefined && !["no", "yes"].includes(args.opencodeZen)) { - errors.push(`Invalid --opencode-zen value: ${args.opencodeZen} (expected: no, yes)`) - } - - if (args.zaiCodingPlan !== undefined && !["no", "yes"].includes(args.zaiCodingPlan)) { - errors.push(`Invalid --zai-coding-plan value: ${args.zaiCodingPlan} (expected: no, yes)`) - } - - if (args.kimiForCoding !== undefined && !["no", "yes"].includes(args.kimiForCoding)) { - errors.push(`Invalid --kimi-for-coding value: ${args.kimiForCoding} (expected: no, yes)`) - } - - return { valid: errors.length === 0, errors } -} - -function argsToConfig(args: InstallArgs): InstallConfig { - return { - hasClaude: args.claude !== "no", - isMax20: args.claude === "max20", - hasOpenAI: args.openai === "yes", - hasGemini: args.gemini === "yes", - hasCopilot: args.copilot === "yes", - hasOpencodeZen: args.opencodeZen === "yes", - hasZaiCodingPlan: args.zaiCodingPlan === "yes", - hasKimiForCoding: args.kimiForCoding === "yes", - } -} - -function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; openai: BooleanArg; gemini: BooleanArg; copilot: BooleanArg; opencodeZen: BooleanArg; zaiCodingPlan: BooleanArg; kimiForCoding: BooleanArg } { - let claude: ClaudeSubscription = "no" - if (detected.hasClaude) { - claude = detected.isMax20 ? "max20" : "yes" - } - - return { - claude, - openai: detected.hasOpenAI ? "yes" : "no", - gemini: detected.hasGemini ? "yes" : "no", - copilot: detected.hasCopilot ? "yes" : "no", - opencodeZen: detected.hasOpencodeZen ? "yes" : "no", - zaiCodingPlan: detected.hasZaiCodingPlan ? "yes" : "no", - kimiForCoding: detected.hasKimiForCoding ? "yes" : "no", - } -} - -async function runTuiMode(detected: DetectedConfig): Promise { - const initial = detectedToInitialValues(detected) - - const claude = await p.select({ - message: "Do you have a Claude Pro/Max subscription?", - options: [ - { value: "no" as const, label: "No", hint: "Will use opencode/glm-4.7-free as fallback" }, - { value: "yes" as const, label: "Yes (standard)", hint: "Claude Opus 4.5 for orchestration" }, - { value: "max20" as const, label: "Yes (max20 mode)", hint: "Full power with Claude Sonnet 4.5 for Librarian" }, - ], - initialValue: initial.claude, - }) - - if (p.isCancel(claude)) { - p.cancel("Installation cancelled.") - return null - } - - const openai = await p.select({ - message: "Do you have an OpenAI/ChatGPT Plus subscription?", - options: [ - { value: "no" as const, label: "No", hint: "Oracle will use fallback models" }, - { value: "yes" as const, label: "Yes", hint: "GPT-5.2 for Oracle (high-IQ debugging)" }, - ], - initialValue: initial.openai, - }) - - if (p.isCancel(openai)) { - p.cancel("Installation cancelled.") - return null - } - - const gemini = await p.select({ - message: "Will you integrate Google Gemini?", - options: [ - { value: "no" as const, label: "No", hint: "Frontend/docs agents will use fallback" }, - { value: "yes" as const, label: "Yes", hint: "Beautiful UI generation with Gemini 3 Pro" }, - ], - initialValue: initial.gemini, - }) - - if (p.isCancel(gemini)) { - p.cancel("Installation cancelled.") - return null - } - - const copilot = await p.select({ - message: "Do you have a GitHub Copilot subscription?", - options: [ - { value: "no" as const, label: "No", hint: "Only native providers will be used" }, - { value: "yes" as const, label: "Yes", hint: "Fallback option when native providers unavailable" }, - ], - initialValue: initial.copilot, - }) - - if (p.isCancel(copilot)) { - p.cancel("Installation cancelled.") - return null - } - - const opencodeZen = await p.select({ - message: "Do you have access to OpenCode Zen (opencode/ models)?", - options: [ - { value: "no" as const, label: "No", hint: "Will use other configured providers" }, - { value: "yes" as const, label: "Yes", hint: "opencode/claude-opus-4-6, opencode/gpt-5.2, etc." }, - ], - initialValue: initial.opencodeZen, - }) - - if (p.isCancel(opencodeZen)) { - p.cancel("Installation cancelled.") - return null - } - - const zaiCodingPlan = await p.select({ - message: "Do you have a Z.ai Coding Plan subscription?", - options: [ - { value: "no" as const, label: "No", hint: "Will use other configured providers" }, - { value: "yes" as const, label: "Yes", hint: "Fallback for Librarian and Multimodal Looker" }, - ], - initialValue: initial.zaiCodingPlan, - }) - - if (p.isCancel(zaiCodingPlan)) { - p.cancel("Installation cancelled.") - return null - } - - const kimiForCoding = await p.select({ - message: "Do you have a Kimi For Coding subscription?", - options: [ - { value: "no" as const, label: "No", hint: "Will use other configured providers" }, - { value: "yes" as const, label: "Yes", hint: "Kimi K2.5 for Sisyphus/Prometheus fallback" }, - ], - initialValue: initial.kimiForCoding, - }) - - if (p.isCancel(kimiForCoding)) { - p.cancel("Installation cancelled.") - return null - } - - return { - hasClaude: claude !== "no", - isMax20: claude === "max20", - hasOpenAI: openai === "yes", - hasGemini: gemini === "yes", - hasCopilot: copilot === "yes", - hasOpencodeZen: opencodeZen === "yes", - hasZaiCodingPlan: zaiCodingPlan === "yes", - hasKimiForCoding: kimiForCoding === "yes", - } -} - -async function runNonTuiInstall(args: InstallArgs): Promise { - const validation = validateNonTuiArgs(args) - if (!validation.valid) { - printHeader(false) - printError("Validation failed:") - for (const err of validation.errors) { - console.log(` ${SYMBOLS.bullet} ${err}`) - } - console.log() - printInfo("Usage: bunx oh-my-opencode install --no-tui --claude= --gemini= --copilot=") - console.log() - return 1 - } - - const detected = detectCurrentConfig() - const isUpdate = detected.isInstalled - - printHeader(isUpdate) - - const totalSteps = 6 - let step = 1 - - printStep(step++, totalSteps, "Checking OpenCode installation...") - const installed = await isOpenCodeInstalled() - const version = await getOpenCodeVersion() - if (!installed) { - printWarning("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.") - printInfo("Visit https://opencode.ai/docs for installation instructions") - } else { - printSuccess(`OpenCode ${version ?? ""} detected`) - } - - if (isUpdate) { - const initial = detectedToInitialValues(detected) - printInfo(`Current config: Claude=${initial.claude}, Gemini=${initial.gemini}`) - } - - const config = argsToConfig(args) - - printStep(step++, totalSteps, "Adding oh-my-opencode plugin...") - const pluginResult = await addPluginToOpenCodeConfig(VERSION) - if (!pluginResult.success) { - printError(`Failed: ${pluginResult.error}`) - return 1 - } - printSuccess(`Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`) - - if (config.hasGemini) { - printStep(step++, totalSteps, "Adding auth plugins...") - const authResult = await addAuthPlugins(config) - if (!authResult.success) { - printError(`Failed: ${authResult.error}`) - return 1 - } - printSuccess(`Auth plugins configured ${SYMBOLS.arrow} ${color.dim(authResult.configPath)}`) - - printStep(step++, totalSteps, "Adding provider configurations...") - const providerResult = addProviderConfig(config) - if (!providerResult.success) { - printError(`Failed: ${providerResult.error}`) - return 1 - } - printSuccess(`Providers configured ${SYMBOLS.arrow} ${color.dim(providerResult.configPath)}`) - } else { - step += 2 - } - - printStep(step++, totalSteps, "Writing oh-my-opencode configuration...") - const omoResult = writeOmoConfig(config) - if (!omoResult.success) { - printError(`Failed: ${omoResult.error}`) - return 1 - } - printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`) - - printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete") - - if (!config.hasClaude) { - console.log() - console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING ")))) - console.log() - console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5."))) - console.log(color.red(" Without Claude, you may experience significantly degraded performance:")) - console.log(color.dim(" • Reduced orchestration quality")) - console.log(color.dim(" • Weaker tool selection and delegation")) - console.log(color.dim(" • Less reliable task completion")) - console.log() - console.log(color.yellow(" Consider subscribing to Claude Pro/Max for the best experience.")) - console.log() - } - - if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) { - printWarning("No model providers configured. Using opencode/glm-4.7-free as fallback.") - } - - console.log(`${SYMBOLS.star} ${color.bold(color.green(isUpdate ? "Configuration updated!" : "Installation complete!"))}`) - console.log(` Run ${color.cyan("opencode")} to start!`) - console.log() - - printBox( - `${color.bold("Pro Tip:")} Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` + - `All features work like magic—parallel agents, background tasks,\n` + - `deep exploration, and relentless execution until completion.`, - "The Magic Word" - ) - - console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`) - console.log(` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true")}`) - console.log() - console.log(color.dim("oMoMoMoMo... Enjoy!")) - console.log() - - if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) { - printBox( - `Run ${color.cyan("opencode auth login")} and select your provider:\n` + - (config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") + - (config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") + - (config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""), - "Authenticate Your Providers" - ) - } - - return 0 -} - export async function install(args: InstallArgs): Promise { - if (!args.tui) { - return runNonTuiInstall(args) - } - - const detected = detectCurrentConfig() - const isUpdate = detected.isInstalled - - p.intro(color.bgMagenta(color.white(isUpdate ? " oMoMoMoMo... Update " : " oMoMoMoMo... "))) - - if (isUpdate) { - const initial = detectedToInitialValues(detected) - p.log.info(`Existing configuration detected: Claude=${initial.claude}, Gemini=${initial.gemini}`) - } - - const s = p.spinner() - s.start("Checking OpenCode installation") - - const installed = await isOpenCodeInstalled() - const version = await getOpenCodeVersion() - if (!installed) { - s.stop(`OpenCode binary not found ${color.yellow("[!]")}`) - p.log.warn("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.") - p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide") - } else { - s.stop(`OpenCode ${version ?? "installed"} ${color.green("[OK]")}`) - } - - const config = await runTuiMode(detected) - if (!config) return 1 - - s.start("Adding oh-my-opencode to OpenCode config") - const pluginResult = await addPluginToOpenCodeConfig(VERSION) - if (!pluginResult.success) { - s.stop(`Failed to add plugin: ${pluginResult.error}`) - p.outro(color.red("Installation failed.")) - return 1 - } - s.stop(`Plugin added to ${color.cyan(pluginResult.configPath)}`) - - if (config.hasGemini) { - s.start("Adding auth plugins (fetching latest versions)") - const authResult = await addAuthPlugins(config) - if (!authResult.success) { - s.stop(`Failed to add auth plugins: ${authResult.error}`) - p.outro(color.red("Installation failed.")) - return 1 - } - s.stop(`Auth plugins added to ${color.cyan(authResult.configPath)}`) - - s.start("Adding provider configurations") - const providerResult = addProviderConfig(config) - if (!providerResult.success) { - s.stop(`Failed to add provider config: ${providerResult.error}`) - p.outro(color.red("Installation failed.")) - return 1 - } - s.stop(`Provider config added to ${color.cyan(providerResult.configPath)}`) - } - - s.start("Writing oh-my-opencode configuration") - const omoResult = writeOmoConfig(config) - if (!omoResult.success) { - s.stop(`Failed to write config: ${omoResult.error}`) - p.outro(color.red("Installation failed.")) - return 1 - } - s.stop(`Config written to ${color.cyan(omoResult.configPath)}`) - - if (!config.hasClaude) { - console.log() - console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING ")))) - console.log() - console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5."))) - console.log(color.red(" Without Claude, you may experience significantly degraded performance:")) - console.log(color.dim(" • Reduced orchestration quality")) - console.log(color.dim(" • Weaker tool selection and delegation")) - console.log(color.dim(" • Less reliable task completion")) - console.log() - console.log(color.yellow(" Consider subscribing to Claude Pro/Max for the best experience.")) - console.log() - } - - if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) { - p.log.warn("No model providers configured. Using opencode/glm-4.7-free as fallback.") - } - - p.note(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete") - - p.log.success(color.bold(isUpdate ? "Configuration updated!" : "Installation complete!")) - p.log.message(`Run ${color.cyan("opencode")} to start!`) - - p.note( - `Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` + - `All features work like magic—parallel agents, background tasks,\n` + - `deep exploration, and relentless execution until completion.`, - "The Magic Word" - ) - - p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`) - p.log.message(` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true")}`) - - p.outro(color.green("oMoMoMoMo... Enjoy!")) - - if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) { - const providers: string[] = [] - if (config.hasClaude) providers.push(`Anthropic ${color.gray("→ Claude Pro/Max")}`) - if (config.hasGemini) providers.push(`Google ${color.gray("→ OAuth with Antigravity")}`) - if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`) - - console.log() - console.log(color.bold("Authenticate Your Providers")) - console.log() - console.log(` Run ${color.cyan("opencode auth login")} and select:`) - for (const provider of providers) { - console.log(` ${SYMBOLS.bullet} ${provider}`) - } - console.log() - } - - return 0 + return args.tui ? runTuiInstaller(args, VERSION) : runCliInstaller(args, VERSION) } diff --git a/src/cli/model-fallback-types.ts b/src/cli/model-fallback-types.ts new file mode 100644 index 000000000..98dcab86e --- /dev/null +++ b/src/cli/model-fallback-types.ts @@ -0,0 +1,29 @@ +export interface ProviderAvailability { + native: { + claude: boolean + openai: boolean + gemini: boolean + } + opencodeZen: boolean + copilot: boolean + zai: boolean + kimiForCoding: boolean + isMaxPlan: boolean +} + +export interface AgentConfig { + model: string + variant?: string +} + +export interface CategoryConfig { + model: string + variant?: string +} + +export interface GeneratedOmoConfig { + $schema: string + agents?: Record + categories?: Record + [key: string]: unknown +} diff --git a/src/cli/model-fallback.ts b/src/cli/model-fallback.ts index 531b8ee40..bbc8e02c1 100644 --- a/src/cli/model-fallback.ts +++ b/src/cli/model-fallback.ts @@ -1,133 +1,27 @@ import { - AGENT_MODEL_REQUIREMENTS, - CATEGORY_MODEL_REQUIREMENTS, - type FallbackEntry, + AGENT_MODEL_REQUIREMENTS, + CATEGORY_MODEL_REQUIREMENTS, } from "../shared/model-requirements" import type { InstallConfig } from "./types" -interface ProviderAvailability { - native: { - claude: boolean - openai: boolean - gemini: boolean - } - opencodeZen: boolean - copilot: boolean - zai: boolean - kimiForCoding: boolean - isMaxPlan: boolean -} +import type { AgentConfig, CategoryConfig, GeneratedOmoConfig } from "./model-fallback-types" +import { toProviderAvailability } from "./provider-availability" +import { + getSisyphusFallbackChain, + isAnyFallbackEntryAvailable, + isRequiredModelAvailable, + isRequiredProviderAvailable, + resolveModelFromChain, +} from "./fallback-chain-resolution" -interface AgentConfig { - model: string - variant?: string -} - -interface CategoryConfig { - model: string - variant?: string -} - -export interface GeneratedOmoConfig { - $schema: string - agents?: Record - categories?: Record - [key: string]: unknown -} +export type { GeneratedOmoConfig } from "./model-fallback-types" const ZAI_MODEL = "zai-coding-plan/glm-4.7" const ULTIMATE_FALLBACK = "opencode/glm-4.7-free" const SCHEMA_URL = "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json" -function toProviderAvailability(config: InstallConfig): ProviderAvailability { - return { - native: { - claude: config.hasClaude, - openai: config.hasOpenAI, - gemini: config.hasGemini, - }, - opencodeZen: config.hasOpencodeZen, - copilot: config.hasCopilot, - zai: config.hasZaiCodingPlan, - kimiForCoding: config.hasKimiForCoding, - isMaxPlan: config.isMax20, - } -} -function isProviderAvailable(provider: string, avail: ProviderAvailability): boolean { - const mapping: Record = { - anthropic: avail.native.claude, - openai: avail.native.openai, - google: avail.native.gemini, - "github-copilot": avail.copilot, - opencode: avail.opencodeZen, - "zai-coding-plan": avail.zai, - "kimi-for-coding": avail.kimiForCoding, - } - return mapping[provider] ?? false -} - -function transformModelForProvider(provider: string, model: string): string { - if (provider === "github-copilot") { - return model - .replace("claude-opus-4-6", "claude-opus-4.6") - .replace("claude-sonnet-4-5", "claude-sonnet-4.5") - .replace("claude-haiku-4-5", "claude-haiku-4.5") - .replace("claude-sonnet-4", "claude-sonnet-4") - .replace("gemini-3-pro", "gemini-3-pro-preview") - .replace("gemini-3-flash", "gemini-3-flash-preview") - } - return model -} - -function resolveModelFromChain( - fallbackChain: FallbackEntry[], - avail: ProviderAvailability -): { model: string; variant?: string } | null { - for (const entry of fallbackChain) { - for (const provider of entry.providers) { - if (isProviderAvailable(provider, avail)) { - const transformedModel = transformModelForProvider(provider, entry.model) - return { - model: `${provider}/${transformedModel}`, - variant: entry.variant, - } - } - } - } - return null -} - -function getSisyphusFallbackChain(): FallbackEntry[] { - return AGENT_MODEL_REQUIREMENTS.sisyphus.fallbackChain -} - -function isAnyFallbackEntryAvailable( - fallbackChain: FallbackEntry[], - avail: ProviderAvailability -): boolean { - return fallbackChain.some((entry) => - entry.providers.some((provider) => isProviderAvailable(provider, avail)) - ) -} - -function isRequiredModelAvailable( - requiresModel: string, - fallbackChain: FallbackEntry[], - avail: ProviderAvailability -): boolean { - const matchingEntry = fallbackChain.find((entry) => entry.model === requiresModel) - if (!matchingEntry) return false - return matchingEntry.providers.some((provider) => isProviderAvailable(provider, avail)) -} - -function isRequiredProviderAvailable( - requiredProviders: string[], - avail: ProviderAvailability -): boolean { - return requiredProviders.some((provider) => isProviderAvailable(provider, avail)) -} export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { const avail = toProviderAvailability(config) diff --git a/src/cli/provider-availability.ts b/src/cli/provider-availability.ts new file mode 100644 index 000000000..d0c76e45d --- /dev/null +++ b/src/cli/provider-availability.ts @@ -0,0 +1,30 @@ +import type { InstallConfig } from "./types" +import type { ProviderAvailability } from "./model-fallback-types" + +export function toProviderAvailability(config: InstallConfig): ProviderAvailability { + return { + native: { + claude: config.hasClaude, + openai: config.hasOpenAI, + gemini: config.hasGemini, + }, + opencodeZen: config.hasOpencodeZen, + copilot: config.hasCopilot, + zai: config.hasZaiCodingPlan, + kimiForCoding: config.hasKimiForCoding, + isMaxPlan: config.isMax20, + } +} + +export function isProviderAvailable(provider: string, availability: ProviderAvailability): boolean { + const mapping: Record = { + anthropic: availability.native.claude, + openai: availability.native.openai, + google: availability.native.gemini, + "github-copilot": availability.copilot, + opencode: availability.opencodeZen, + "zai-coding-plan": availability.zai, + "kimi-for-coding": availability.kimiForCoding, + } + return mapping[provider] ?? false +} diff --git a/src/cli/provider-model-id-transform.ts b/src/cli/provider-model-id-transform.ts new file mode 100644 index 000000000..5834247ee --- /dev/null +++ b/src/cli/provider-model-id-transform.ts @@ -0,0 +1,12 @@ +export function transformModelForProvider(provider: string, model: string): string { + if (provider === "github-copilot") { + return model + .replace("claude-opus-4-6", "claude-opus-4.6") + .replace("claude-sonnet-4-5", "claude-sonnet-4.5") + .replace("claude-haiku-4-5", "claude-haiku-4.5") + .replace("claude-sonnet-4", "claude-sonnet-4") + .replace("gemini-3-pro", "gemini-3-pro-preview") + .replace("gemini-3-flash", "gemini-3-flash-preview") + } + return model +} diff --git a/src/cli/tui-install-prompts.ts b/src/cli/tui-install-prompts.ts new file mode 100644 index 000000000..e817427cd --- /dev/null +++ b/src/cli/tui-install-prompts.ts @@ -0,0 +1,111 @@ +import * as p from "@clack/prompts" +import type { Option } from "@clack/prompts" +import type { + ClaudeSubscription, + DetectedConfig, + InstallConfig, +} from "./types" +import { detectedToInitialValues } from "./install-validators" + +async function selectOrCancel>(params: { + message: string + options: Option[] + initialValue: TValue +}): Promise { + const value = await p.select({ + message: params.message, + options: params.options, + initialValue: params.initialValue, + }) + if (p.isCancel(value)) { + p.cancel("Installation cancelled.") + return null + } + return value as TValue +} + +export async function promptInstallConfig(detected: DetectedConfig): Promise { + const initial = detectedToInitialValues(detected) + + const claude = await selectOrCancel({ + message: "Do you have a Claude Pro/Max subscription?", + options: [ + { value: "no", label: "No", hint: "Will use opencode/glm-4.7-free as fallback" }, + { value: "yes", label: "Yes (standard)", hint: "Claude Opus 4.5 for orchestration" }, + { value: "max20", label: "Yes (max20 mode)", hint: "Full power with Claude Sonnet 4.5 for Librarian" }, + ], + initialValue: initial.claude, + }) + if (!claude) return null + + const openai = await selectOrCancel({ + message: "Do you have an OpenAI/ChatGPT Plus subscription?", + options: [ + { value: "no", label: "No", hint: "Oracle will use fallback models" }, + { value: "yes", label: "Yes", hint: "GPT-5.2 for Oracle (high-IQ debugging)" }, + ], + initialValue: initial.openai, + }) + if (!openai) return null + + const gemini = await selectOrCancel({ + message: "Will you integrate Google Gemini?", + options: [ + { value: "no", label: "No", hint: "Frontend/docs agents will use fallback" }, + { value: "yes", label: "Yes", hint: "Beautiful UI generation with Gemini 3 Pro" }, + ], + initialValue: initial.gemini, + }) + if (!gemini) return null + + const copilot = await selectOrCancel({ + message: "Do you have a GitHub Copilot subscription?", + options: [ + { value: "no", label: "No", hint: "Only native providers will be used" }, + { value: "yes", label: "Yes", hint: "Fallback option when native providers unavailable" }, + ], + initialValue: initial.copilot, + }) + if (!copilot) return null + + const opencodeZen = await selectOrCancel({ + message: "Do you have access to OpenCode Zen (opencode/ models)?", + options: [ + { value: "no", label: "No", hint: "Will use other configured providers" }, + { value: "yes", label: "Yes", hint: "opencode/claude-opus-4-6, opencode/gpt-5.2, etc." }, + ], + initialValue: initial.opencodeZen, + }) + if (!opencodeZen) return null + + const zaiCodingPlan = await selectOrCancel({ + message: "Do you have a Z.ai Coding Plan subscription?", + options: [ + { value: "no", label: "No", hint: "Will use other configured providers" }, + { value: "yes", label: "Yes", hint: "Fallback for Librarian and Multimodal Looker" }, + ], + initialValue: initial.zaiCodingPlan, + }) + if (!zaiCodingPlan) return null + + const kimiForCoding = await selectOrCancel({ + message: "Do you have a Kimi For Coding subscription?", + options: [ + { value: "no", label: "No", hint: "Will use other configured providers" }, + { value: "yes", label: "Yes", hint: "Kimi K2.5 for Sisyphus/Prometheus fallback" }, + ], + initialValue: initial.kimiForCoding, + }) + if (!kimiForCoding) return null + + return { + hasClaude: claude !== "no", + isMax20: claude === "max20", + hasOpenAI: openai === "yes", + hasGemini: gemini === "yes", + hasCopilot: copilot === "yes", + hasOpencodeZen: opencodeZen === "yes", + hasZaiCodingPlan: zaiCodingPlan === "yes", + hasKimiForCoding: kimiForCoding === "yes", + } +} diff --git a/src/cli/tui-installer.ts b/src/cli/tui-installer.ts new file mode 100644 index 000000000..d960769c2 --- /dev/null +++ b/src/cli/tui-installer.ts @@ -0,0 +1,135 @@ +import * as p from "@clack/prompts" +import color from "picocolors" +import type { InstallArgs } from "./types" +import { + addAuthPlugins, + addPluginToOpenCodeConfig, + addProviderConfig, + detectCurrentConfig, + getOpenCodeVersion, + isOpenCodeInstalled, + writeOmoConfig, +} from "./config-manager" +import { detectedToInitialValues, formatConfigSummary, SYMBOLS } from "./install-validators" +import { promptInstallConfig } from "./tui-install-prompts" + +export async function runTuiInstaller(args: InstallArgs, version: string): Promise { + const detected = detectCurrentConfig() + const isUpdate = detected.isInstalled + + p.intro(color.bgMagenta(color.white(isUpdate ? " oMoMoMoMo... Update " : " oMoMoMoMo... "))) + + if (isUpdate) { + const initial = detectedToInitialValues(detected) + p.log.info(`Existing configuration detected: Claude=${initial.claude}, Gemini=${initial.gemini}`) + } + + const spinner = p.spinner() + spinner.start("Checking OpenCode installation") + + const installed = await isOpenCodeInstalled() + const openCodeVersion = await getOpenCodeVersion() + if (!installed) { + spinner.stop(`OpenCode binary not found ${color.yellow("[!]")}`) + p.log.warn("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.") + p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide") + } else { + spinner.stop(`OpenCode ${openCodeVersion ?? "installed"} ${color.green("[OK]")}`) + } + + const config = await promptInstallConfig(detected) + if (!config) return 1 + + spinner.start("Adding oh-my-opencode to OpenCode config") + const pluginResult = await addPluginToOpenCodeConfig(version) + if (!pluginResult.success) { + spinner.stop(`Failed to add plugin: ${pluginResult.error}`) + p.outro(color.red("Installation failed.")) + return 1 + } + spinner.stop(`Plugin added to ${color.cyan(pluginResult.configPath)}`) + + if (config.hasGemini) { + spinner.start("Adding auth plugins (fetching latest versions)") + const authResult = await addAuthPlugins(config) + if (!authResult.success) { + spinner.stop(`Failed to add auth plugins: ${authResult.error}`) + p.outro(color.red("Installation failed.")) + return 1 + } + spinner.stop(`Auth plugins added to ${color.cyan(authResult.configPath)}`) + + spinner.start("Adding provider configurations") + const providerResult = addProviderConfig(config) + if (!providerResult.success) { + spinner.stop(`Failed to add provider config: ${providerResult.error}`) + p.outro(color.red("Installation failed.")) + return 1 + } + spinner.stop(`Provider config added to ${color.cyan(providerResult.configPath)}`) + } + + spinner.start("Writing oh-my-opencode configuration") + const omoResult = writeOmoConfig(config) + if (!omoResult.success) { + spinner.stop(`Failed to write config: ${omoResult.error}`) + p.outro(color.red("Installation failed.")) + return 1 + } + spinner.stop(`Config written to ${color.cyan(omoResult.configPath)}`) + + if (!config.hasClaude) { + console.log() + console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING ")))) + console.log() + console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5."))) + console.log(color.red(" Without Claude, you may experience significantly degraded performance:")) + console.log(color.dim(" • Reduced orchestration quality")) + console.log(color.dim(" • Weaker tool selection and delegation")) + console.log(color.dim(" • Less reliable task completion")) + console.log() + console.log(color.yellow(" Consider subscribing to Claude Pro/Max for the best experience.")) + console.log() + } + + if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) { + p.log.warn("No model providers configured. Using opencode/glm-4.7-free as fallback.") + } + + p.note(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete") + + p.log.success(color.bold(isUpdate ? "Configuration updated!" : "Installation complete!")) + p.log.message(`Run ${color.cyan("opencode")} to start!`) + + p.note( + `Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` + + `All features work like magic—parallel agents, background tasks,\n` + + `deep exploration, and relentless execution until completion.`, + "The Magic Word", + ) + + p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`) + p.log.message( + ` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true")}`, + ) + + p.outro(color.green("oMoMoMoMo... Enjoy!")) + + if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) { + const providers: string[] = [] + if (config.hasClaude) providers.push(`Anthropic ${color.gray("→ Claude Pro/Max")}`) + if (config.hasGemini) providers.push(`Google ${color.gray("→ OAuth with Antigravity")}`) + if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`) + + console.log() + console.log(color.bold("Authenticate Your Providers")) + console.log() + console.log(` Run ${color.cyan("opencode auth login")} and select:`) + for (const provider of providers) { + console.log(` ${SYMBOLS.bullet} ${provider}`) + } + console.log() + } + + return 0 +}