use std::path::Path;
use std::sync::LazyLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Api {
V1,
V2,
}
const NAMES: [&str; 2] = ["opencode", "opencode-cli"];
static API: LazyLock<Api> = LazyLock::new(detect);
pub fn api() -> Api {
*API
}
fn detect() -> Api {
for name in NAMES {
let Ok(out) = std::process::Command::new(name).arg("--version").output() else {
continue;
};
if !out.status.success() {
continue;
}
let text = String::from_utf8_lossy(&out.stdout);
if let Some(major) = major(&text) {
return if major >= 2 { Api::V2 } else { Api::V1 };
}
}
Api::V1
}
fn major(text: &str) -> Option<u32> {
text.split(|c: char| !c.is_ascii_digit() && c != '.')
.find(|word| word.starts_with(|c: char| c.is_ascii_digit()))
.and_then(|word| word.split('.').next())
.and_then(|part| part.parse().ok())
}
const CLI_CONFIG_ENV: &str = "OPENCODE_CLI_CONFIG_CONTENT";
pub fn launch_env(argv: &[String]) -> Vec<(String, String)> {
launch_env_for(api(), argv)
}
fn launch_env_for(api: Api, argv: &[String]) -> Vec<(String, String)> {
if api != Api::V2 || !argv.first().is_some_and(|word| is_opencode(word)) {
return Vec::new();
}
vec![(CLI_CONFIG_ENV.to_string(), tabs_off())]
}
fn is_opencode(word: &str) -> bool {
let name = Path::new(word)
.file_name()
.map(|name| name.to_string_lossy())
.unwrap_or_default();
let name = name.strip_suffix(".exe").unwrap_or(&name);
NAMES.contains(&name)
}
fn tabs_off() -> String {
tabs_off_from(&|key| std::env::var(key).ok())
}
fn tabs_off_from(env: &dyn Fn(&str) -> Option<String>) -> String {
let mut root = env(CLI_CONFIG_ENV)
.as_deref()
.and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
.filter(serde_json::Value::is_object)
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
if let Some(object) = root.as_object_mut() {
let mut tabs = object
.remove("tabs")
.filter(|tabs| tabs.is_object())
.and_then(|tabs| tabs.as_object().cloned())
.unwrap_or_default();
tabs.remove("enabled");
tabs.insert("mode".into(), "off".into());
object.insert("tabs".into(), serde_json::Value::Object(tabs));
}
serde_json::to_string(&root).unwrap_or_else(|_| TABS_OFF.into())
}
const TABS_OFF: &str = r#"{"tabs":{"mode":"off"}}"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_version_is_read_out_of_whatever_shape_it_is_printed_in() {
assert_eq!(major("opencode v2.0.20"), Some(2));
assert_eq!(major("1.14.47"), Some(1));
assert_eq!(major("opencode/2.0.20-beta.3"), Some(2));
assert_eq!(major("2.1"), Some(2));
assert_eq!(major("command not found"), None);
assert_eq!(major(""), None);
}
#[test]
fn opencode_is_recognised_however_it_was_spelled() {
assert!(is_opencode("opencode"));
assert!(is_opencode("opencode-cli"));
assert!(is_opencode("/home/flo/.opencode/bin/opencode"));
assert!(!is_opencode("claude"));
assert!(!is_opencode("/usr/bin/opencode-helper"));
assert!(!is_opencode(""));
}
#[test]
fn opencode_two_is_launched_with_its_own_tabs_off() {
let env = launch_env_for(
Api::V2,
&[
"opencode".to_string(),
"--session".to_string(),
"ses_1".to_string(),
],
);
assert_eq!(
env,
vec![(CLI_CONFIG_ENV.to_string(), TABS_OFF.to_string())]
);
}
#[test]
fn no_other_agent_and_no_opencode_one_is_dressed() {
assert!(launch_env_for(Api::V2, &["claude".to_string()]).is_empty());
assert!(launch_env_for(Api::V1, &["opencode".to_string()]).is_empty());
assert!(launch_env_for(Api::V2, &[]).is_empty());
}
#[test]
fn an_inline_tab_setting_of_the_users_own_survives() {
let existing = r#"{"session":{"scrollbar":false},"tabs":{"layout":"vertical"}}"#;
let merged: serde_json::Value = serde_json::from_str(&tabs_off_from(&|key| {
(key == CLI_CONFIG_ENV).then(|| existing.to_string())
}))
.unwrap();
assert_eq!(merged["tabs"]["mode"], "off");
assert_eq!(merged["tabs"]["layout"], "vertical");
assert_eq!(merged["session"]["scrollbar"], false);
}
#[test]
fn the_legacy_boolean_gives_way_to_the_mode_it_became() {
let existing = r#"{"tabs":{"enabled":true}}"#;
let merged: serde_json::Value = serde_json::from_str(&tabs_off_from(&|key| {
(key == CLI_CONFIG_ENV).then(|| existing.to_string())
}))
.unwrap();
assert_eq!(merged["tabs"]["mode"], "off");
assert!(merged["tabs"].get("enabled").is_none());
}
#[test]
fn unreadable_inline_settings_are_replaced_rather_than_kept() {
let written =
tabs_off_from(&|key| (key == CLI_CONFIG_ENV).then(|| "{not json".to_string()));
assert_eq!(written, TABS_OFF);
}
}