Browse Source

Adding tmux in install (#5)

Alvin 2 months ago
parent
commit
69cde58522
5 changed files with 189 additions and 150 deletions
  1. 1 1
      package.json
  2. 75 19
      src/cli/config-manager.ts
  3. 4 1
      src/cli/index.ts
  4. 106 129
      src/cli/install.ts
  5. 3 0
      src/cli/types.ts

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "oh-my-opencode-slim",
-  "version": "0.2.2",
+  "version": "0.3.1",
   "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",

+ 75 - 19
src/cli/config-manager.ts

@@ -58,6 +58,19 @@ export async function isOpenCodeInstalled(): Promise<boolean> {
   }
 }
 
+export async function isTmuxInstalled(): Promise<boolean> {
+  try {
+    const proc = Bun.spawn(["tmux", "-V"], {
+      stdout: "pipe",
+      stderr: "pipe",
+    })
+    await proc.exited
+    return proc.exitCode === 0
+  } catch {
+    return false
+  }
+}
+
 export async function getOpenCodeVersion(): Promise<string | null> {
   try {
     const proc = Bun.spawn(["opencode", "--version"], {
@@ -211,6 +224,36 @@ export function addProviderConfig(installConfig: InstallConfig): ConfigMergeResu
   }
 }
 
+/**
+ * Add server configuration to opencode.json for tmux integration
+ */
+export function addServerConfig(installConfig: InstallConfig): ConfigMergeResult {
+  const configPath = getConfigJson()
+
+  try {
+    ensureConfigDir()
+    let config = parseConfig(configPath) ?? {}
+
+    if (installConfig.hasTmux) {
+      const server = (config.server ?? {}) as Record<string, unknown>
+      // Only set port if not already configured
+      if (server.port === undefined) {
+        server.port = 4096
+      }
+      config.server = server
+    }
+
+    writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
+    return { success: true, configPath }
+  } catch (err) {
+    return {
+      success: false,
+      configPath,
+      error: `Failed to add server config: ${err}`,
+    }
+  }
+}
+
 // Model mappings by provider priority
 const MODEL_MAPPINGS = {
   antigravity: {
@@ -255,28 +298,37 @@ export function generateLiteConfig(installConfig: InstallConfig): Record<string,
         ? "cerebras"
         : null;
 
-  if (!baseProvider) {
-    return { agents: {} };
-  }
+  const config: Record<string, unknown> = { agents: {} };
 
-  // Start with base provider models
-  const agents: Record<string, { model: string }> = Object.fromEntries(
-    Object.entries(MODEL_MAPPINGS[baseProvider]).map(([k, v]) => [k, { model: v }])
-  );
+  if (baseProvider) {
+    // Start with base provider models
+    const agents: Record<string, { model: string }> = Object.fromEntries(
+      Object.entries(MODEL_MAPPINGS[baseProvider]).map(([k, v]) => [k, { model: v }])
+    );
 
-  // Apply provider-specific overrides for mixed configurations
-  if (installConfig.hasAntigravity) {
-    if (installConfig.hasOpenAI) {
-      agents["oracle"] = { model: "openai/gpt-5.2-codex" };
-    }
-    if (installConfig.hasCerebras) {
+    // Apply provider-specific overrides for mixed configurations
+    if (installConfig.hasAntigravity) {
+      if (installConfig.hasOpenAI) {
+        agents["oracle"] = { model: "openai/gpt-5.2-codex" };
+      }
+      if (installConfig.hasCerebras) {
+        agents["explore"] = { model: "cerebras/zai-glm-4.6" };
+      }
+    } else if (installConfig.hasOpenAI && installConfig.hasCerebras) {
       agents["explore"] = { model: "cerebras/zai-glm-4.6" };
     }
-  } else if (installConfig.hasOpenAI && installConfig.hasCerebras) {
-    agents["explore"] = { model: "cerebras/zai-glm-4.6" };
+    config.agents = agents;
+  }
+
+  if (installConfig.hasTmux) {
+    config.tmux = {
+      enabled: true,
+      layout: "main-vertical",
+      main_pane_size: 60,
+    };
   }
 
-  return { agents };
+  return config;
 }
 
 export function writeLiteConfig(installConfig: InstallConfig): ConfigMergeResult {
@@ -302,6 +354,7 @@ export function detectCurrentConfig(): DetectedConfig {
     hasAntigravity: false,
     hasOpenAI: false,
     hasCerebras: false,
+    hasTmux: false,
   }
 
   const config = parseConfig(getConfigJson())
@@ -314,9 +367,8 @@ export function detectCurrentConfig(): DetectedConfig {
   // Try to detect from lite config
   const liteConfig = parseConfig(getLiteConfig())
   if (liteConfig && typeof liteConfig === "object") {
-    const agents = (liteConfig as Record<string, unknown>).agents as
-      | Record<string, { model?: string }>
-      | undefined
+    const configObj = liteConfig as Record<string, any>
+    const agents = configObj.agents as Record<string, { model?: string }> | undefined
 
     if (agents) {
       const models = Object.values(agents)
@@ -325,6 +377,10 @@ export function detectCurrentConfig(): DetectedConfig {
       result.hasOpenAI = models.some((m) => m?.startsWith("openai/"))
       result.hasCerebras = models.some((m) => m?.startsWith("cerebras/"))
     }
+
+    if (configObj.tmux && typeof configObj.tmux === "object") {
+      result.hasTmux = configObj.tmux.enabled === true
+    }
   }
 
   return result

+ 4 - 1
src/cli/index.ts

@@ -18,6 +18,8 @@ function parseArgs(args: string[]): InstallArgs {
       result.openai = arg.split("=")[1] as BooleanArg
     } else if (arg.startsWith("--cerebras=")) {
       result.cerebras = arg.split("=")[1] as BooleanArg
+    } else if (arg.startsWith("--tmux=")) {
+      result.tmux = arg.split("=")[1] as BooleanArg
     } else if (arg === "-h" || arg === "--help") {
       printHelp()
       process.exit(0)
@@ -37,13 +39,14 @@ Options:
   --antigravity=yes|no   Antigravity subscription (yes/no)
   --openai=yes|no        OpenAI API access (yes/no)
   --cerebras=yes|no      Cerebras API access (yes/no)
+  --tmux=yes|no          Enable tmux integration (yes/no)
   --no-tui               Non-interactive mode (requires all flags)
   --skip-auth            Skip authentication reminder
   -h, --help             Show this help message
 
 Examples:
   bunx oh-my-opencode-slim install
-  bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --cerebras=no
+  bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --cerebras=no --tmux=yes
 `)
 }
 

+ 106 - 129
src/cli/install.ts

@@ -1,5 +1,5 @@
 import type { InstallArgs, InstallConfig, BooleanArg, DetectedConfig } from "./types"
-import * as readline from "readline"
+import * as readline from "readline/promises"
 import {
   addPluginToOpenCodeConfig,
   writeLiteConfig,
@@ -7,62 +7,13 @@ import {
   getOpenCodeVersion,
   addAuthPlugins,
   addProviderConfig,
+  addServerConfig,
   detectCurrentConfig,
+  isTmuxInstalled,
+  generateLiteConfig,
 } from "./config-manager"
 
-// Line reader for TUI mode that handles both TTY and piped input
-let lineReader: readline.Interface | null = null
-let lineBuffer: string[] = []
-let lineResolvers: ((line: string) => void)[] = []
-
-function initLineReader(): void {
-  if (lineReader) return
-
-  lineReader = readline.createInterface({
-    input: process.stdin,
-    output: process.stdout,
-    terminal: process.stdin.isTTY ?? false,
-  })
-
-  lineReader.on("line", (line) => {
-    if (lineResolvers.length > 0) {
-      const resolve = lineResolvers.shift()!
-      resolve(line)
-    } else {
-      lineBuffer.push(line)
-    }
-  })
-
-  lineReader.on("close", () => {
-    // Resolve any pending readers with empty string
-    while (lineResolvers.length > 0) {
-      const resolve = lineResolvers.shift()!
-      resolve("")
-    }
-  })
-}
-
-async function readLine(): Promise<string> {
-  initLineReader()
-
-  if (lineBuffer.length > 0) {
-    return lineBuffer.shift()!
-  }
-
-  return new Promise((resolve) => {
-    lineResolvers.push(resolve)
-  })
-}
-
-function closeLineReader(): void {
-  if (lineReader) {
-    lineReader.close()
-    lineReader = null
-    lineBuffer = []
-    lineResolvers = []
-  }
-}
-
+// Colors
 const GREEN = "\x1b[32m"
 const BLUE = "\x1b[34m"
 const YELLOW = "\x1b[33m"
@@ -82,9 +33,8 @@ const SYMBOLS = {
 }
 
 function printHeader(isUpdate: boolean): void {
-  const mode = isUpdate ? "Update" : "Install"
   console.log()
-  console.log(`${BOLD}oh-my-opencode-slim ${mode}${RESET}`)
+  console.log(`${BOLD}oh-my-opencode-slim ${isUpdate ? "Update" : "Install"}${RESET}`)
   console.log("=".repeat(30))
   console.log()
 }
@@ -140,18 +90,26 @@ function formatConfigSummary(config: InstallConfig): string {
   lines.push(`  ${config.hasAntigravity ? SYMBOLS.check : DIM + "○" + RESET} Antigravity`)
   lines.push(`  ${config.hasOpenAI ? SYMBOLS.check : DIM + "○" + RESET} OpenAI`)
   lines.push(`  ${config.hasCerebras ? SYMBOLS.check : DIM + "○" + RESET} Cerebras`)
+  lines.push(`  ${config.hasTmux ? SYMBOLS.check : DIM + "○" + RESET} Tmux Integration`)
   return lines.join("\n")
 }
 
-function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string[] } {
-  const requiredArgs = ["antigravity", "openai", "cerebras"] as const
-  const errors = requiredArgs.flatMap((key) => {
-    const value = args[key]
-    if (value === undefined) return [`--${key} is required (values: yes, no)`]
-    if (!["yes", "no"].includes(value)) return [`Invalid --${key} value: ${value} (expected: yes, no)`]
-    return []
-  })
-  return { valid: errors.length === 0, errors }
+function printAgentModels(config: InstallConfig): void {
+  const liteConfig = generateLiteConfig(config)
+  const agents = liteConfig.agents as Record<string, { model: string }>
+
+  if (!agents || Object.keys(agents).length === 0) return
+
+  console.log(`${BOLD}Agent Model Configuration:${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)
+    console.log(`  ${DIM}${agent}${RESET}${padding} ${SYMBOLS.arrow} ${BLUE}${info.model}${RESET}`)
+  }
+  console.log()
 }
 
 function argsToConfig(args: InstallArgs): InstallConfig {
@@ -159,27 +117,17 @@ function argsToConfig(args: InstallArgs): InstallConfig {
     hasAntigravity: args.antigravity === "yes",
     hasOpenAI: args.openai === "yes",
     hasCerebras: args.cerebras === "yes",
+    hasTmux: args.tmux === "yes",
   }
 }
 
-function detectedToInitialValues(detected: DetectedConfig): {
-  antigravity: BooleanArg
-  openai: BooleanArg
-  cerebras: BooleanArg
-} {
-  return {
-    antigravity: detected.hasAntigravity ? "yes" : "no",
-    openai: detected.hasOpenAI ? "yes" : "no",
-    cerebras: detected.hasCerebras ? "yes" : "no",
-  }
-}
-
-async function askYesNo(promptText: string, defaultValue: BooleanArg = "no"): Promise<BooleanArg> {
-  const defaultHint = defaultValue === "yes" ? "[Y/n]" : "[y/N]"
-  const fullPrompt = `${BLUE}${promptText}${RESET} ${defaultHint}: `
-
-  process.stdout.write(fullPrompt)
-  const answer = (await readLine()).trim().toLowerCase()
+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"
@@ -187,30 +135,42 @@ async function askYesNo(promptText: string, defaultValue: BooleanArg = "no"): Pr
   return defaultValue
 }
 
-async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | null> {
-  const initial = detectedToInitialValues(detected)
-
-  console.log(`${BOLD}Question 1/3:${RESET}`)
-  const antigravity = await askYesNo(
-    "Do you have an Antigravity subscription?",
-    initial.antigravity
-  )
-  console.log()
-
-  console.log(`${BOLD}Question 2/3:${RESET}`)
-  const openai = await askYesNo("Do you have access to OpenAI API?", initial.openai)
-  console.log()
-
-  console.log(`${BOLD}Question 3/3:${RESET}`)
-  const cerebras = await askYesNo("Do you have access to Cerebras API?", initial.cerebras)
-  console.log()
-
-  closeLineReader()
+async function runInteractiveMode(detected: DetectedConfig): Promise<InstallConfig> {
+  const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
+  const tmuxInstalled = await isTmuxInstalled()
+  const totalQuestions = tmuxInstalled ? 4 : 3
+
+  try {
+    console.log(`${BOLD}Question 1/${totalQuestions}:${RESET}`)
+    printInfo("The Pantheon is tuned for Antigravity's model routing. Other models work, but results may vary.")
+    const antigravity = await askYesNo(rl, "Do you have an Antigravity subscription?", "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()
+
+    console.log(`${BOLD}Question 3/${totalQuestions}:${RESET}`)
+    const cerebras = await askYesNo(rl, "Do you have access to Cerebras API?", detected.hasCerebras ? "yes" : "no")
+    console.log()
+
+    let tmux: BooleanArg = "no"
+    if (tmuxInstalled) {
+      console.log(`${BOLD}Question 4/4:${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()
+    }
 
-  return {
-    hasAntigravity: antigravity === "yes",
-    hasOpenAI: openai === "yes",
-    hasCerebras: cerebras === "yes",
+    return {
+      hasAntigravity: antigravity === "yes",
+      hasOpenAI: openai === "yes",
+      hasCerebras: cerebras === "yes",
+      hasTmux: tmux === "yes",
+    }
+  } finally {
+    rl.close()
   }
 }
 
@@ -220,20 +180,21 @@ async function runInstall(config: InstallConfig): Promise<number> {
 
   printHeader(isUpdate)
 
-  const totalSteps = config.hasAntigravity ? 5 : 3
+  // Calculate total steps dynamically
+  let totalSteps = 3 // Base: check opencode, add plugin, write lite config
+  if (config.hasAntigravity) totalSteps += 2 // auth plugins + provider config
+  if (config.hasTmux) totalSteps += 1 // server config
+
   let step = 1
 
-  // Step 1: Check OpenCode
   printStep(step++, totalSteps, "Checking OpenCode installation...")
   const { ok } = await checkOpenCodeInstalled()
   if (!ok) return 1
 
-  // Step 2: Add plugin
   printStep(step++, totalSteps, "Adding oh-my-opencode-slim plugin...")
   const pluginResult = await addPluginToOpenCodeConfig()
   if (!handleStepResult(pluginResult, "Plugin added")) return 1
 
-  // Step 3-4: Auth plugins and provider config (if Antigravity)
   if (config.hasAntigravity) {
     printStep(step++, totalSteps, "Adding auth plugins...")
     const authResult = await addAuthPlugins(config)
@@ -244,7 +205,12 @@ async function runInstall(config: InstallConfig): Promise<number> {
     if (!handleStepResult(providerResult, "Providers configured")) return 1
   }
 
-  // Step 5: Write lite config
+  if (config.hasTmux) {
+    printStep(step++, totalSteps, "Configuring OpenCode HTTP server for tmux...")
+    const serverResult = addServerConfig(config)
+    if (!handleStepResult(serverResult, "Server configured")) return 1
+  }
+
   printStep(step++, totalSteps, "Writing oh-my-opencode-slim configuration...")
   const liteResult = writeLiteConfig(config)
   if (!handleStepResult(liteResult, "Config written")) return 1
@@ -254,6 +220,8 @@ async function runInstall(config: InstallConfig): Promise<number> {
   console.log(formatConfigSummary(config))
   console.log()
 
+  printAgentModels(config)
+
   if (!config.hasAntigravity && !config.hasOpenAI && !config.hasCerebras) {
     printWarning("No providers configured. At least one provider is required.")
     return 1
@@ -263,39 +231,50 @@ async function runInstall(config: InstallConfig): Promise<number> {
   console.log()
   console.log(`${BOLD}Next steps:${RESET}`)
   console.log()
-  console.log(`  1. Authenticate with your providers:`)
+
+  let nextStep = 1
+  console.log(`  ${nextStep++}. Authenticate with your providers:`)
   console.log(`     ${BLUE}$ opencode auth login${RESET}`)
   console.log()
-  console.log(`  2. Start OpenCode:`)
-  console.log(`     ${BLUE}$ opencode${RESET}`)
+
+  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) {
-    // Non-TUI mode: validate args
-    const validation = validateNonTuiArgs(args)
-    if (!validation.valid) {
+    const requiredArgs = ["antigravity", "openai", "cerebras", "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("Validation failed:")
-      for (const err of validation.errors) {
-        console.log(`  ${SYMBOLS.bullet} ${err}`)
+      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> --cerebras=<yes|no>"
-      )
+      printInfo("Usage: bunx oh-my-opencode-slim install --no-tui --antigravity=<yes|no> --openai=<yes|no> --cerebras=<yes|no> --tmux=<yes|no>")
       console.log()
       return 1
     }
 
-    const config = argsToConfig(args)
-    return runInstall(config)
+    return runInstall(argsToConfig(args))
   }
 
-  // TUI mode
+  // Interactive mode
   const detected = detectCurrentConfig()
 
   printHeader(detected.isInstalled)
@@ -305,8 +284,6 @@ export async function install(args: InstallArgs): Promise<number> {
   if (!ok) return 1
   console.log()
 
-  const config = await runTuiMode(detected)
-  if (!config) return 1
-
+  const config = await runInteractiveMode(detected)
   return runInstall(config)
 }

+ 3 - 0
src/cli/types.ts

@@ -5,6 +5,7 @@ export interface InstallArgs {
   antigravity?: BooleanArg
   openai?: BooleanArg
   cerebras?: BooleanArg
+  tmux?: BooleanArg
   skipAuth?: boolean
 }
 
@@ -12,6 +13,7 @@ export interface InstallConfig {
   hasAntigravity: boolean
   hasOpenAI: boolean
   hasCerebras: boolean
+  hasTmux: boolean
 }
 
 export interface ConfigMergeResult {
@@ -25,4 +27,5 @@ export interface DetectedConfig {
   hasAntigravity: boolean
   hasOpenAI: boolean
   hasCerebras: boolean
+  hasTmux: boolean
 }