diff options
| author | akbash <akbash@herdr.dev> | 2026-09-16 23:22:37 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-16 22:22:37 +0200 |
| commit | 266d6f6d876ab3eb020b88ef6caee0f91776db37 (patch) | |
| tree | d5ca72f86c30fac4211209a1d2377511e1107fc2 /src/integration | |
| parent | 956f23ff6b72d16fe66169730b76bf565d12d449 (diff) | |
fix: respect opencode tui.json registrations (#4250)
refs #4240
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/integration')
| -rw-r--r-- | src/integration/actions.rs | 4 | ||||
| -rw-r--r-- | src/integration/opencode_config.rs | 72 | ||||
| -rw-r--r-- | src/integration/targets.rs | 14 | ||||
| -rw-r--r-- | src/integration/tests.rs | 118 | ||||
| -rw-r--r-- | src/integration/types.rs | 3 |
5 files changed, 186 insertions, 25 deletions
diff --git a/src/integration/actions.rs b/src/integration/actions.rs index d51682f8..e57fc0c2 100644 --- a/src/integration/actions.rs +++ b/src/integration/actions.rs @@ -556,10 +556,10 @@ pub(crate) fn uninstall_target( result.tui_plugin_path.display() ) }); - if result.updated_tui_config { + for path in result.updated_tui_configs { messages.push(format!( "removed herdr opencode plugin entry from {}", - result.tui_config_path.display() + path.display() )); } messages diff --git a/src/integration/opencode_config.rs b/src/integration/opencode_config.rs index b062365b..888efa13 100644 --- a/src/integration/opencode_config.rs +++ b/src/integration/opencode_config.rs @@ -10,12 +10,17 @@ use super::config_file::{check_config_target, write_config}; const TUI_CONFIG_NAME: &str = "tui.jsonc"; -pub(crate) fn tui_config_path(config_dir: &Path) -> PathBuf { - config_dir.join(TUI_CONFIG_NAME) +fn tui_config_paths(config_dir: &Path) -> [PathBuf; 2] { + [ + config_dir.join(TUI_CONFIG_NAME), + config_dir.join("tui.json"), + ] } pub(crate) fn validate_tui_plugin_config(config_dir: &Path) -> io::Result<()> { - validate_plugin_config(&tui_config_path(config_dir), "plugin")?; + for path in tui_config_paths(config_dir) { + validate_plugin_config(&path, "plugin")?; + } validate_plugin_config(&config_dir.join("cli.json"), "plugins") } @@ -37,7 +42,13 @@ fn validate_plugin_config(config_path: &Path, key: &str) -> io::Result<()> { } pub(crate) fn add_tui_plugin(config_dir: &Path, plugin_spec: &str) -> io::Result<PathBuf> { - add_plugin(tui_config_path(config_dir), "plugin", plugin_spec) + for path in tui_config_paths(config_dir) { + if plugin_is_configured(&path, "plugin", plugin_spec) { + return Ok(path); + } + } + // Keep tui.json absent on fresh installs so OpenCode can migrate its settings. + add_plugin(config_dir.join(TUI_CONFIG_NAME), "plugin", plugin_spec) } pub(crate) fn add_cli_plugin( @@ -98,8 +109,21 @@ fn add_plugin(config_path: PathBuf, key: &str, plugin_spec: &str) -> io::Result< Ok(config_path) } -pub(crate) fn remove_tui_plugin(config_dir: &Path, plugin_spec: &str) -> io::Result<bool> { - remove_plugin(&tui_config_path(config_dir), "plugin", plugin_spec) +pub(crate) fn remove_tui_plugin(config_dir: &Path, plugin_spec: &str) -> io::Result<Vec<PathBuf>> { + let mut updated = Vec::new(); + let mut errors = Vec::new(); + for path in tui_config_paths(config_dir) { + match remove_plugin(&path, "plugin", plugin_spec) { + Ok(true) => updated.push(path), + Ok(false) => {} + Err(err) => errors.push(err.to_string()), + } + } + if errors.is_empty() { + Ok(updated) + } else { + Err(io::Error::other(errors.join("; "))) + } } pub(crate) fn remove_cli_plugin(config_dir: &Path, plugin_spec: &str) -> io::Result<bool> { @@ -143,7 +167,9 @@ fn remove_plugin(config_path: &Path, key: &str, plugin_spec: &str) -> io::Result } pub(crate) fn tui_plugin_is_configured(config_dir: &Path, plugin_spec: &str) -> bool { - plugin_is_configured(&tui_config_path(config_dir), "plugin", plugin_spec) + tui_config_paths(config_dir) + .iter() + .any(|path| plugin_is_configured(path, "plugin", plugin_spec)) } pub(crate) fn cli_plugin_is_configured(config_dir: &Path, plugin_spec: &str) -> bool { @@ -323,7 +349,10 @@ mod tests { ]) ); - assert!(remove_tui_plugin(&dir, "./herdr-tui-state.js").unwrap()); + assert_eq!( + remove_tui_plugin(&dir, "./herdr-tui-state.js").unwrap(), + vec![config_path.clone()] + ); let removed_content = fs::read_to_string(&config_path).unwrap(); assert!(removed_content.contains("// Keep this comment.")); let removed = parse_config(&config_path); @@ -364,7 +393,10 @@ mod tests { let dir = unique_dir(); let config_path = add_tui_plugin(&dir, "./herdr-tui-state.js").unwrap(); - assert!(remove_tui_plugin(&dir, "./herdr-tui-state.js").unwrap()); + assert_eq!( + remove_tui_plugin(&dir, "./herdr-tui-state.js").unwrap(), + vec![config_path.clone()] + ); assert!(config_path.is_file()); assert_eq!(parse_config(&config_path), json!({})); @@ -372,6 +404,23 @@ mod tests { } #[test] + fn configured_tui_plugin_accepts_json_registration() { + let dir = unique_dir(); + fs::write( + dir.join("tui.json"), + r#"{"plugin":["./herdr-tui-session.js"]}"#, + ) + .unwrap(); + + let configured = tui_plugin_is_configured(&dir, "./herdr-tui-session.js"); + fs::remove_dir_all(dir).unwrap(); + assert!( + configured, + "OpenCode loads plugin registrations from tui.json" + ); + } + + #[test] fn configured_tui_plugin_accepts_option_tuple() { let dir = unique_dir(); fs::write( @@ -381,7 +430,10 @@ mod tests { .unwrap(); assert!(tui_plugin_is_configured(&dir, "./herdr-tui-state.js")); - assert!(remove_tui_plugin(&dir, "./herdr-tui-state.js").unwrap()); + assert_eq!( + remove_tui_plugin(&dir, "./herdr-tui-state.js").unwrap(), + vec![dir.join(TUI_CONFIG_NAME)] + ); fs::remove_dir_all(dir).unwrap(); } diff --git a/src/integration/targets.rs b/src/integration/targets.rs index 6cb90c18..ec605866 100644 --- a/src/integration/targets.rs +++ b/src/integration/targets.rs @@ -29,7 +29,7 @@ use super::file_ops::{ make_executable, remove_dir_all_if_exists, remove_file_if_exists, remove_legacy_bash_hook_file, }; use super::opencode_config::{ - add_cli_plugin, add_tui_plugin, remove_cli_plugin, remove_tui_plugin, tui_config_path, + add_cli_plugin, add_tui_plugin, remove_cli_plugin, remove_tui_plugin, validate_tui_plugin_config, }; use super::types::{ @@ -461,7 +461,7 @@ pub(crate) fn install_droid() -> io::Result<DroidInstallPaths> { pub(crate) fn install_opencode() -> io::Result<OpenCodeInstallPaths> { let dir = opencode_dir()?; - check_config_targets(&dir, &["tui.jsonc", "cli.json"])?; + check_config_targets(&dir, &["tui.jsonc", "tui.json", "cli.json"])?; if !dir.is_dir() { return Err(io::Error::other(format!( "opencode config directory not found at {}. install opencode first", @@ -844,8 +844,7 @@ pub(crate) fn uninstall_droid() -> io::Result<DroidUninstallResult> { pub(crate) fn uninstall_opencode() -> io::Result<OpenCodeUninstallResult> { let dir = opencode_dir()?; - check_config_targets(&dir, &["tui.jsonc", "cli.json"])?; - let tui_config_path = tui_config_path(&dir); + check_config_targets(&dir, &["tui.jsonc", "tui.json", "cli.json"])?; let plugin_path = dir.join("plugins").join(OPENCODE_PLUGIN_INSTALL_NAME); let tui_plugin_path = dir.join(OPENCODE_TUI_PLUGIN_INSTALL_NAME); let mut errors = Vec::new(); @@ -858,10 +857,10 @@ pub(crate) fn uninstall_opencode() -> io::Result<OpenCodeUninstallResult> { errors.push(format!("failed to remove {}: {err}", v2_dir.display())); false }); - let updated_tui_config = + let updated_tui_configs = remove_tui_plugin(&dir, OPENCODE_TUI_PLUGIN_SPEC).unwrap_or_else(|err| { errors.push(err.to_string()); - false + Vec::new() }); let removed_plugin = remove_file_if_exists(&plugin_path).unwrap_or_else(|err| { errors.push(format!("failed to remove {}: {err}", plugin_path.display())); @@ -881,10 +880,9 @@ pub(crate) fn uninstall_opencode() -> io::Result<OpenCodeUninstallResult> { Ok(OpenCodeUninstallResult { plugin_path, tui_plugin_path, - tui_config_path, removed_plugin, removed_tui_plugin, - updated_tui_config, + updated_tui_configs, }) } diff --git a/src/integration/tests.rs b/src/integration/tests.rs index 1a102992..f7e2154e 100644 --- a/src/integration/tests.rs +++ b/src/integration/tests.rs @@ -2326,6 +2326,69 @@ fn install_opencode_writes_server_and_tui_plugins() { let _ = fs::remove_dir_all(base); } +#[cfg(unix)] +#[test] +fn opencode_reuses_json_registration_in_symlinked_config_directory() { + let _lock = integration_env_lock(); + let base = unique_base(); + let home = base.join("home"); + let dotfiles = base.join("dotfiles"); + let dir = home.join(".config/opencode"); + fs::create_dir_all(home.join(".config")).unwrap(); + fs::create_dir_all(&dotfiles).unwrap(); + std::os::unix::fs::symlink(&dotfiles, &dir).unwrap(); + std::env::set_var("HOME", &home); + let json_path = dir.join("tui.json"); + let original = "{\n // User preferences\n \"theme\":\"system\",\n \"plugin\":[\"other\",[\"./herdr-tui-session.js\",{\"enabled\":true}]]\n}\n"; + fs::write(&json_path, original).unwrap(); + + for _ in 0..2 { + let installed = install_opencode().unwrap(); + assert_eq!(installed.tui_config_path, json_path); + assert!(installed.cli_config_path.is_none()); + assert!(!dir.join("tui.jsonc").exists()); + assert_eq!(fs::read_to_string(&json_path).unwrap(), original); + assert_eq!( + integration_status_at( + crate::api::schema::IntegrationTarget::Opencode, + installed.plugin_path, + OPENCODE_INTEGRATION_VERSION, + ) + .state, + IntegrationStatusKind::Current + ); + } + + // Older installs may have registered the same plugin in both files. + let jsonc_path = dir.join("tui.jsonc"); + fs::write( + &jsonc_path, + r#"{"plugin":["./herdr-tui-session.js","another"]}"#, + ) + .unwrap(); + assert_eq!(install_opencode().unwrap().tui_config_path, jsonc_path); + let result = uninstall_opencode().unwrap(); + assert_eq!( + result.updated_tui_configs, + vec![jsonc_path.clone(), json_path.clone()] + ); + let json = fs::read_to_string(&json_path).unwrap(); + assert!(json.contains("// User preferences")); + assert!(!json.contains("herdr-tui-session.js")); + assert!(json.contains("\"other\"")); + assert!(json.contains("\"system\"")); + assert_eq!( + serde_json::from_str::<Value>(&fs::read_to_string(jsonc_path).unwrap()).unwrap(), + json!({"plugin":["another"]}) + ); + assert!(uninstall_opencode().unwrap().updated_tui_configs.is_empty()); + assert_eq!(fs::read_link(&dir).unwrap(), dotfiles); + assert!(!result.plugin_path.exists()); + assert!(!result.tui_plugin_path.exists()); + std::env::remove_var("HOME"); + fs::remove_dir_all(base).unwrap(); +} + #[test] fn opencode_install_defers_v2_registration_while_migration_pending() { let _lock = integration_env_lock(); @@ -2427,6 +2490,42 @@ fn opencode_hard_link_rejection_precedes_install_and_uninstall_asset_changes() { fs::remove_dir_all(base).unwrap(); } +#[test] +fn opencode_json_config_validation_precedes_asset_changes() { + let _lock = integration_env_lock(); + let base = unique_base(); + let home = base.join("home"); + let dir = home.join(".config/opencode"); + fs::create_dir_all(dir.join("plugins")).unwrap(); + std::env::set_var("HOME", &home); + let plugin = dir.join("plugins").join(OPENCODE_PLUGIN_INSTALL_NAME); + fs::write(&plugin, "previous integration").unwrap(); + let config = dir.join("tui.json"); + fs::write(&config, r#"{"plugin":{}}"#).unwrap(); + assert!(install_opencode() + .unwrap_err() + .to_string() + .contains("plugin list")); + assert_eq!(fs::read_to_string(&plugin).unwrap(), "previous integration"); + let original = r#"{"plugin":["./herdr-tui-session.js"]}"#; + fs::write(&config, original).unwrap(); + let alias = base.join("linked-config"); + fs::hard_link(&config, &alias).unwrap(); + for error in [ + install_opencode().unwrap_err(), + uninstall_opencode().unwrap_err(), + ] { + assert!(error.to_string().contains("multiple hard links")); + assert!(error.to_string().contains("tui.json")); + } + assert_eq!(fs::read_to_string(&plugin).unwrap(), "previous integration"); + assert_eq!(fs::read_to_string(&alias).unwrap(), original); + assert!(!dir.join("tui.jsonc").exists()); + assert!(!dir.join(OPENCODE_TUI_PLUGIN_INSTALL_NAME).exists()); + std::env::remove_var("HOME"); + fs::remove_dir_all(base).unwrap(); +} + #[cfg(windows)] #[test] fn opencode_recovery_copy_blocks_retry_before_parsing_or_asset_changes() { @@ -2539,12 +2638,15 @@ fn uninstall_opencode_removes_plugins_and_managed_tui_config_entry() { assert!(result.removed_plugin); assert!(result.removed_tui_plugin); - assert!(result.updated_tui_config); + assert_eq!( + result.updated_tui_configs, + vec![installed.tui_config_path.clone()] + ); assert!(!result.plugin_path.exists()); assert!(!result.tui_plugin_path.exists()); - assert!(result.tui_config_path.exists()); + assert!(installed.tui_config_path.exists()); let tui_config: Value = - serde_json::from_str(&fs::read_to_string(&result.tui_config_path).unwrap()).unwrap(); + serde_json::from_str(&fs::read_to_string(&installed.tui_config_path).unwrap()).unwrap(); assert_eq!(tui_config, json!({})); assert_eq!(installed.plugin_path, result.plugin_path); @@ -2588,6 +2690,12 @@ fn uninstall_opencode_removes_plugins_when_tui_config_is_invalid() { fs::write(&plugin_path, OPENCODE_PLUGIN_ASSET).unwrap(); fs::write(&tui_plugin_path, OPENCODE_TUI_PLUGIN_ASSET).unwrap(); fs::write(opencode_dir.join("tui.jsonc"), "{\"plugin\":").unwrap(); + let json_path = opencode_dir.join("tui.json"); + fs::write( + &json_path, + r#"{"plugin":["./herdr-tui-session.js","other"]}"#, + ) + .unwrap(); std::env::set_var("HOME", &home); let err = uninstall_opencode().unwrap_err().to_string(); @@ -2595,6 +2703,10 @@ fn uninstall_opencode_removes_plugins_when_tui_config_is_invalid() { assert!(err.contains("failed to parse OpenCode TUI config")); assert!(!plugin_path.exists()); assert!(!tui_plugin_path.exists()); + assert_eq!( + serde_json::from_str::<Value>(&fs::read_to_string(json_path).unwrap()).unwrap(), + json!({"plugin":["other"]}) + ); std::env::remove_var("HOME"); let _ = fs::remove_dir_all(base); diff --git a/src/integration/types.rs b/src/integration/types.rs index 928675f6..8e42a5da 100644 --- a/src/integration/types.rs +++ b/src/integration/types.rs @@ -259,10 +259,9 @@ pub(crate) struct DroidUninstallResult { pub(crate) struct OpenCodeUninstallResult { pub plugin_path: PathBuf, pub tui_plugin_path: PathBuf, - pub tui_config_path: PathBuf, pub removed_plugin: bool, pub removed_tui_plugin: bool, - pub updated_tui_config: bool, + pub updated_tui_configs: Vec<PathBuf>, } #[derive(Debug)] |
