Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions app/src/terminal/cli_agent_sessions/plugin_manager/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,16 @@ use crate::terminal::model::session::LocalCommandExecutor;
use crate::terminal::shell::ShellType;

const PLUGIN_KEY: &str = "warp@claude-code-warp";
const PLATFORM_PLUGIN_KEY: &str = "oz-harness-support@claude-code-warp";

const MARKETPLACE_REPO: &str = "warpdotdev/claude-code-warp";
const MARKETPLACE_NAME: &str = "claude-code-warp";

const PLATFORM_PLUGIN_KEY: &str = "oz-harness-support@claude-code-warp";
// Note: we will eventually publish this to the same marketplace repo, but are using the internal one as we build out multi-harness.
const PLATFORM_MARKETPLACE_REPO: &str = "warpdotdev/claude-code-warp-internal";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [CRITICAL] Deleting this constant leaves the unchanged update_platform_plugin method referencing PLATFORM_MARKETPLACE_REPO, so the crate will fail to compile; update that call site to MARKETPLACE_REPO too or keep the constant.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems important

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed


// Keep in sync with the plugin version in warpdotdev/claude-code-warp.
// (See the Versioning section of that repo's README.)
const MINIMUM_PLUGIN_VERSION: &str = "2.1.0";
// Keep in sync with the oz-harness-support plugin version in warpdotdev/claude-code-warp-internal.
const MINIMUM_PLATFORM_PLUGIN_VERSION: &str = "1.1.3";
// Keep in sync with the oz-harness-support plugin version in warpdotdev/claude-code-warp.
const MINIMUM_PLATFORM_PLUGIN_VERSION: &str = "1.1.2";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we should have some sort of automated test for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


pub(super) struct ClaudeCodePluginManager {
executor: LocalCommandExecutor,
Expand Down Expand Up @@ -176,7 +174,7 @@ impl CliAgentPluginManager for ClaudeCodePluginManager {
async fn install_platform_plugin(&self) -> Result<(), PluginInstallError> {
let mut log = String::new();
self.run_logged(
&["plugin", "marketplace", "add", PLATFORM_MARKETPLACE_REPO],
&["plugin", "marketplace", "add", MARKETPLACE_REPO],
&mut log,
)
.await?;
Expand All @@ -188,7 +186,7 @@ impl CliAgentPluginManager for ClaudeCodePluginManager {
async fn update_platform_plugin(&self) -> Result<(), PluginInstallError> {
let mut log = String::new();
self.run_logged(
&["plugin", "marketplace", "add", PLATFORM_MARKETPLACE_REPO],
&["plugin", "marketplace", "add", MARKETPLACE_REPO],
&mut log,
)
.await?;
Expand Down Expand Up @@ -350,10 +348,16 @@ fn is_local_marketplace_path(source: &str) -> bool {
|| source.starts_with("file://")
}

/// Checks `CLAUDE_HOME` env var first, falls back to `~/.claude`.
/// Resolves the dir the Claude CLI reads/writes its state from.
///
/// Honors `CLAUDE_CONFIG_DIR` (respected by the Claude CLI, and set by the Oz
/// worker to a per-task dir), falling back to `~/.claude`. Must match where
/// `claude plugin install` writes, else install/verify checks read the wrong dir.
fn claude_home_dir() -> io::Result<PathBuf> {
if let Ok(claude_home) = env::var("CLAUDE_HOME") {
return Ok(PathBuf::from(claude_home));
if let Ok(dir) = env::var("CLAUDE_CONFIG_DIR") {
if !dir.is_empty() {
return Ok(PathBuf::from(dir));
}
}
dirs::home_dir()
.map(|home| home.join(".claude"))
Expand Down
61 changes: 39 additions & 22 deletions app/src/terminal/cli_agent_sessions/plugin_manager/claude_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,26 @@ use std::fs;
use super::{
check_installed, check_platform_plugin_installed, claude_code_marketplace_has_local_override,
installed_platform_plugin_version, installed_version, ClaudeCodePluginManager,
CliAgentPluginManager,
CliAgentPluginManager, MINIMUM_PLATFORM_PLUGIN_VERSION,
};

/// A version strictly below `version`, so below-minimum tests track the
/// constant instead of a hardcoded literal. Assumes `version` > "0.0.0".
fn version_below(version: &str) -> String {
let mut parts: Vec<u64> = version.split('.').map(|p| p.parse().unwrap_or(0)).collect();
for part in parts.iter_mut().rev() {
if *part > 0 {
*part -= 1;
break;
}
}
parts
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(".")
}

#[test]
fn installed_when_plugin_present() {
let dir = tempfile::tempdir().unwrap();
Expand Down Expand Up @@ -69,7 +86,7 @@ fn local_marketplace_override_ignores_repo_source() {

#[test]
#[serial_test::serial]
fn local_marketplace_override_via_trait_uses_claude_home() {
fn local_marketplace_override_via_trait_uses_claude_config_dir() {
let dir = tempfile::tempdir().unwrap();
let settings = serde_json::json!({
"extraKnownMarketplaces": {
Expand All @@ -87,9 +104,9 @@ fn local_marketplace_override_via_trait_uses_claude_home() {
)
.unwrap();

std::env::set_var("CLAUDE_HOME", dir.path());
std::env::set_var("CLAUDE_CONFIG_DIR", dir.path());
let result = ClaudeCodePluginManager::new(None, None, None).has_local_marketplace_override();
std::env::remove_var("CLAUDE_HOME");
std::env::remove_var("CLAUDE_CONFIG_DIR");

assert!(result);
}
Expand All @@ -102,7 +119,7 @@ fn installed_platform_plugin_version_returns_version_when_present() {

let json = serde_json::json!({
"plugins": {
"oz-harness-support@claude-code-warp": [{"version": "1.1.3"}]
"oz-harness-support@claude-code-warp": [{"version": MINIMUM_PLATFORM_PLUGIN_VERSION}]
}
});
fs::write(
Expand All @@ -113,7 +130,7 @@ fn installed_platform_plugin_version_returns_version_when_present() {

assert_eq!(
installed_platform_plugin_version(dir.path()).as_deref(),
Some("1.1.3")
Some(MINIMUM_PLATFORM_PLUGIN_VERSION)
);
}

Expand All @@ -125,7 +142,7 @@ fn platform_plugin_installed_when_platform_plugin_present() {

let json = serde_json::json!({
"plugins": {
"oz-harness-support@claude-code-warp": [{"version": "1.1.3"}]
"oz-harness-support@claude-code-warp": [{"version": MINIMUM_PLATFORM_PLUGIN_VERSION}]
}
});
fs::write(
Expand All @@ -146,7 +163,7 @@ fn platform_plugin_needs_update_via_trait_when_version_below_minimum() {

let json = serde_json::json!({
"plugins": {
"oz-harness-support@claude-code-warp": [{"version": "1.1.2"}]
"oz-harness-support@claude-code-warp": [{"version": version_below(MINIMUM_PLATFORM_PLUGIN_VERSION)}]
}
});
fs::write(
Expand All @@ -155,9 +172,9 @@ fn platform_plugin_needs_update_via_trait_when_version_below_minimum() {
)
.unwrap();

std::env::set_var("CLAUDE_HOME", dir.path());
std::env::set_var("CLAUDE_CONFIG_DIR", dir.path());
let result = ClaudeCodePluginManager::new(None, None, None).platform_plugin_needs_update();
std::env::remove_var("CLAUDE_HOME");
std::env::remove_var("CLAUDE_CONFIG_DIR");

assert!(result);
}
Expand All @@ -171,7 +188,7 @@ fn platform_plugin_does_not_need_update_via_trait_when_current() {

let json = serde_json::json!({
"plugins": {
"oz-harness-support@claude-code-warp": [{"version": "1.1.3"}]
"oz-harness-support@claude-code-warp": [{"version": MINIMUM_PLATFORM_PLUGIN_VERSION}]
}
});
fs::write(
Expand All @@ -180,9 +197,9 @@ fn platform_plugin_does_not_need_update_via_trait_when_current() {
)
.unwrap();

std::env::set_var("CLAUDE_HOME", dir.path());
std::env::set_var("CLAUDE_CONFIG_DIR", dir.path());
let result = ClaudeCodePluginManager::new(None, None, None).platform_plugin_needs_update();
std::env::remove_var("CLAUDE_HOME");
std::env::remove_var("CLAUDE_CONFIG_DIR");

assert!(!result);
}
Expand All @@ -205,9 +222,9 @@ fn platform_plugin_needs_update_via_trait_when_installed_without_version() {
)
.unwrap();

std::env::set_var("CLAUDE_HOME", dir.path());
std::env::set_var("CLAUDE_CONFIG_DIR", dir.path());
let result = ClaudeCodePluginManager::new(None, None, None).platform_plugin_needs_update();
std::env::remove_var("CLAUDE_HOME");
std::env::remove_var("CLAUDE_CONFIG_DIR");

assert!(result);
}
Expand Down Expand Up @@ -305,10 +322,10 @@ fn not_installed_when_plugins_key_missing() {
}

/// Tests `ClaudeCodePluginManager::is_installed` end-to-end by pointing
/// `CLAUDE_HOME` at a temp directory with a valid installed_plugins.json.
/// `CLAUDE_CONFIG_DIR` at a temp directory with a valid installed_plugins.json.
#[test]
#[serial_test::serial]
fn is_installed_via_trait_with_claude_home_env() {
fn is_installed_via_trait_with_claude_config_dir_env() {
let dir = tempfile::tempdir().unwrap();
let plugins_dir = dir.path().join("plugins");
fs::create_dir_all(&plugins_dir).unwrap();
Expand All @@ -324,21 +341,21 @@ fn is_installed_via_trait_with_claude_home_env() {
)
.unwrap();

std::env::set_var("CLAUDE_HOME", dir.path());
std::env::set_var("CLAUDE_CONFIG_DIR", dir.path());
let result = ClaudeCodePluginManager::new(None, None, None).is_installed();
std::env::remove_var("CLAUDE_HOME");
std::env::remove_var("CLAUDE_CONFIG_DIR");

assert!(result);
}

#[test]
#[serial_test::serial]
fn not_installed_via_trait_when_claude_home_empty() {
fn not_installed_via_trait_when_claude_config_dir_empty() {
let dir = tempfile::tempdir().unwrap();

std::env::set_var("CLAUDE_HOME", dir.path());
std::env::set_var("CLAUDE_CONFIG_DIR", dir.path());
let result = ClaudeCodePluginManager::new(None, None, None).is_installed();
std::env::remove_var("CLAUDE_HOME");
std::env::remove_var("CLAUDE_CONFIG_DIR");

assert!(!result);
}
Expand Down
Loading