Improve cmux omo error when opencode is not installed (#2230)

* Improve cmux omo error when opencode is not installed

On a fresh computer without opencode, cmux omo would fail with the
unhelpful "Failed to launch opencode: No such file or directory".
Now checks for opencode before doing setup work and shows install
instructions: npm install -g opencode-ai

* Show npm install progress and improve first-run experience

On first run, npm install oh-my-opencode can take 30+ seconds.
Previously piped to /dev/null, making it look like cmux omo hung.
Now pipes npm output directly to stderr so users see progress.
Also improves the message: "this may take a minute on first run".

* Move opencode check before plugin setup to fail fast

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
Lawrence Chen 2026-03-26 19:28:22 -07:00 committed by GitHub
parent 30cb3718fc
commit 6e5903e4e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -9779,30 +9779,14 @@ struct CMUXCLI {
} else {
throw CLIError(message: "Neither bun nor npm found in PATH. Install oh-my-opencode manually: bunx oh-my-opencode install")
}
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
FileHandle.standardError.write("Installing oh-my-opencode plugin...\n".data(using: .utf8)!)
// Show install output directly so the user sees progress (npm can take 30s+)
process.standardOutput = FileHandle.standardError
process.standardError = FileHandle.standardError
FileHandle.standardError.write("Installing oh-my-opencode plugin (this may take a minute on first run)...\n".data(using: .utf8)!)
try process.run()
// Drain pipes concurrently to prevent deadlock from full buffers
var stderrData = Data()
let drainGroup = DispatchGroup()
drainGroup.enter()
DispatchQueue.global().async {
stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
drainGroup.leave()
}
drainGroup.enter()
DispatchQueue.global().async {
_ = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
drainGroup.leave()
}
drainGroup.wait()
process.waitUntilExit()
if process.terminationStatus != 0 {
let errText = String(data: stderrData, encoding: .utf8) ?? ""
throw CLIError(message: "Failed to install oh-my-opencode: \(errText)")
throw CLIError(message: "Failed to install oh-my-opencode. Try manually: npm install -g oh-my-opencode")
}
FileHandle.standardError.write("oh-my-opencode plugin installed\n".data(using: .utf8)!)
// Re-create symlink if we installed into user dir
@ -9899,9 +9883,6 @@ struct CMUXCLI {
socketPath: String,
explicitPassword: String?
) throws {
// Ensure oh-my-opencode plugin is registered and installed
try omoEnsurePlugin()
let processEnvironment = ProcessInfo.processInfo.environment
var launcherEnvironment = processEnvironment
launcherEnvironment["CMUX_SOCKET_PATH"] = socketPath
@ -9910,13 +9891,31 @@ struct CMUXCLI {
!explicitPassword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
launcherEnvironment["CMUX_SOCKET_PASSWORD"] = explicitPassword
}
// Check for opencode before doing expensive plugin setup
let openCodeExecutablePath = resolveOpenCodeExecutable(searchPath: launcherEnvironment["PATH"])
if openCodeExecutablePath == nil {
let checkProcess = Process()
checkProcess.executableURL = URL(fileURLWithPath: "/usr/bin/which")
checkProcess.arguments = ["opencode"]
checkProcess.standardOutput = Pipe()
checkProcess.standardError = Pipe()
try? checkProcess.run()
checkProcess.waitUntilExit()
if checkProcess.terminationStatus != 0 {
throw CLIError(message: "opencode is not installed. Install it first:\n npm install -g opencode-ai\n # or\n bun install -g opencode-ai\n\nThen run: cmux omo")
}
}
// Ensure oh-my-opencode plugin is registered and installed
try omoEnsurePlugin()
let shimDirectory = try createOMOShimDirectory()
let executablePath = resolvedExecutableURL()?.path ?? (args.first ?? "cmux")
let focusedContext = tmuxCompatFocusedContext(
processEnvironment: launcherEnvironment,
explicitPassword: explicitPassword
)
let openCodeExecutablePath = resolveOpenCodeExecutable(searchPath: launcherEnvironment["PATH"])
configureOMOEnvironment(
processEnvironment: launcherEnvironment,
shimDirectory: shimDirectory,
@ -9949,7 +9948,7 @@ struct CMUXCLI {
execvp("opencode", &argv)
}
let code = errno
throw CLIError(message: "Failed to launch opencode: \(String(cString: strerror(code)))")
throw CLIError(message: "Failed to launch opencode: \(String(cString: strerror(code)))\n\nIs opencode installed? Install with:\n npm install -g opencode-ai")
}
private func runClaudeTeamsTmuxCompat(