Skip to content
Open
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
12 changes: 12 additions & 0 deletions src-tauri/src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,18 @@ pub fn sync_global_config_from_db(db: &Database) -> Result<(), String> {
info!("[Config] Wrote global config to Gemini CLI");
}
}
editor_id if crate::utils::wsl::is_wsl_editor(editor_id) => {
if let Some(distro) = crate::utils::wsl::distro_from_editor_id(editor_id) {
crate::services::wsl_config::write_wsl_global_config(&distro, &mcps)
.map_err(|e| e.to_string())?;
info!("[Config] Wrote global config to WSL distro '{}'", distro);
} else {
warn!(
"[Config] WSL editor '{}' but could not resolve distro name. Skipping.",
editor_id
);
}
}
unknown => warn!("[Config] Unknown editor type '{}'. Skipping.", unknown),
}
}
Expand Down
44 changes: 43 additions & 1 deletion src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::db::{
AppSettings, CodexPaths, CopilotPaths, CursorPaths, Database, EditorInfo, GeminiPaths,
OpenCodePaths,
OpenCodePaths, WslClaudePaths,
};
use crate::utils::codex_paths::{get_codex_paths, is_codex_installed};
use crate::utils::copilot_paths::{get_copilot_paths, is_copilot_installed};
use crate::utils::cursor_paths::{get_cursor_paths, is_cursor_installed};
use crate::utils::gemini_paths::{get_gemini_paths, is_gemini_installed};
use crate::utils::opencode_paths::{get_opencode_paths, is_opencode_installed};
use crate::utils::paths::get_claude_paths;
use crate::utils::wsl;
use log::info;
use std::sync::{Arc, Mutex};
use tauri::State;
Expand Down Expand Up @@ -111,6 +112,25 @@ pub fn get_available_editors(
});
}

// WSL Claude Code installations (Windows only)
let wsl_installations = wsl::detect_wsl_claude_installations();
for wsl_info in &wsl_installations {
let editor_id = wsl::wsl_editor_id(&wsl_info.distro);
let config_path = wsl_info
.wsl_home
.as_ref()
.map(|h| format!("{}/.claude.json", h))
.unwrap_or_else(|| "~/.claude.json".to_string());

editors.push(EditorInfo {
id: editor_id.clone(),
name: format!("Claude Code (WSL: {})", wsl_info.distro),
is_installed: wsl_info.is_installed,
is_enabled: enabled.contains(&editor_id),
config_path: format!("WSL:{} {}", wsl_info.distro, config_path),
});
}

Ok(editors)
}

Expand Down Expand Up @@ -242,6 +262,28 @@ pub fn get_gemini_paths_cmd() -> Result<GeminiPaths, String> {
})
}

/// Get WSL Claude Code paths for all detected distros
#[tauri::command]
pub fn get_wsl_claude_paths_cmd() -> Result<Vec<WslClaudePaths>, String> {
info!("[Settings] Getting WSL Claude Code paths");

let installations = wsl::detect_wsl_claude_installations();

Ok(installations
.into_iter()
.filter(|info| info.is_installed)
.map(|info| {
let home = info.wsl_home.unwrap_or_else(|| "~".to_string());
WslClaudePaths {
distro: info.distro,
wsl_home: home.clone(),
claude_json: format!("{}/.claude.json", home),
claude_dir: format!("{}/.claude", home),
}
})
.collect())
}

// ============================================================================
// Testable helper functions (no Tauri State dependency)
// ============================================================================
Expand Down
14 changes: 12 additions & 2 deletions src-tauri/src/db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,12 +476,22 @@ pub struct GeminiPaths {
pub settings_file: String, // ~/.gemini/settings.json
}

// WSL Claude Code paths
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WslClaudePaths {
pub distro: String, // WSL distro name (e.g., "Ubuntu")
pub wsl_home: String, // Home directory inside WSL (e.g., "/home/user")
pub claude_json: String, // ~/.claude.json inside WSL
pub claude_dir: String, // ~/.claude/ inside WSL
}

// Editor info for frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorInfo {
pub id: String, // "claude_code" or "opencode"
pub name: String, // "Claude Code" or "OpenCode"
pub id: String, // "claude_code", "opencode", "wsl_ubuntu", etc.
pub name: String, // "Claude Code", "Claude Code (WSL: Ubuntu)", etc.
pub is_installed: bool, // Whether config directory exists
pub is_enabled: bool, // Whether syncing to this editor is enabled
pub config_path: String, // Path to main config file
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ pub fn run() {
commands::settings::get_copilot_paths_cmd,
commands::settings::get_cursor_paths_cmd,
commands::settings::get_gemini_paths_cmd,
commands::settings::get_wsl_claude_paths_cmd,
commands::settings::toggle_editor,
commands::settings::set_github_token,
commands::settings::clear_github_token,
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/services/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ pub mod stats_cache;
pub mod statusline_gallery;
pub mod statusline_writer;
pub mod subagent_writer;
pub mod wsl_config;
146 changes: 146 additions & 0 deletions src-tauri/src/services/wsl_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use crate::utils::wsl;
use anyhow::Result;
use log::info;
use serde_json::{json, Map, Value};

/// MCP tuple type (same as used by other config writers)
pub type McpTuple = (
String, // name
String, // type (stdio, sse, http)
Option<String>, // command
Option<String>, // args (JSON)
Option<String>, // url
Option<String>, // headers (JSON)
Option<String>, // env (JSON)
);

/// Write global MCP config to a WSL distro's ~/.claude.json
pub fn write_wsl_global_config(distro: &str, mcps: &[McpTuple]) -> Result<()> {
let config_path = "$HOME/.claude.json";

// Read existing config or start fresh
let mut claude_json: Value = match wsl::read_wsl_file(distro, config_path) {
Ok(content) => serde_json::from_str(&content).map_err(|e| {
anyhow::anyhow!(
"Failed to parse existing Claude config in WSL distro '{}': {}. \
Refusing to overwrite to prevent data loss.",
distro,
e
)
})?,
Err(_) => json!({}),
};

// Build mcpServers object
let mut servers = Map::new();
for mcp in mcps {
let (name, mcp_type, command, args, url, headers, env) = mcp;

let config = match mcp_type.as_str() {
"stdio" => {
let mut obj = Map::new();
if let Some(cmd) = command {
obj.insert("command".to_string(), json!(cmd));
}
if let Some(args_json) = args {
if let Ok(args_val) = serde_json::from_str::<Vec<String>>(args_json) {
obj.insert("args".to_string(), json!(args_val));
}
}
if let Some(env_json) = env {
if let Ok(env_val) = serde_json::from_str::<Map<String, Value>>(env_json) {
obj.insert("env".to_string(), Value::Object(env_val));
}
}
Some(Value::Object(obj))
}
"sse" => {
let mut obj = Map::new();
obj.insert("type".to_string(), json!("sse"));
if let Some(u) = url {
obj.insert("url".to_string(), json!(u));
}
if let Some(headers_json) = headers {
if let Ok(headers_val) =
serde_json::from_str::<Map<String, Value>>(headers_json)
{
obj.insert("headers".to_string(), Value::Object(headers_val));
}
}
Some(Value::Object(obj))
}
"http" => {
let mut obj = Map::new();
obj.insert("type".to_string(), json!("http"));
if let Some(u) = url {
obj.insert("url".to_string(), json!(u));
}
if let Some(headers_json) = headers {
if let Ok(headers_val) =
serde_json::from_str::<Map<String, Value>>(headers_json)
{
obj.insert("headers".to_string(), Value::Object(headers_val));
}
}
Some(Value::Object(obj))
}
_ => None,
};

if let Some(cfg) = config {
servers.insert(name.clone(), cfg);
}
}

claude_json["mcpServers"] = Value::Object(servers);

// Backup existing file
let _ = wsl::backup_wsl_file(distro, config_path);

// Write the config
let content = serde_json::to_string_pretty(&claude_json)?;
wsl::write_wsl_file(distro, config_path, &content)?;

info!(
"[WSL] Wrote global config to distro '{}' with {} MCPs",
distro,
mcps.len()
);

Ok(())
}

/// Write a command/skill markdown file to a WSL distro
pub fn write_wsl_command_file(
distro: &str,
dir: &str,
filename: &str,
content: &str,
) -> Result<()> {
wsl::mkdir_wsl(distro, dir)?;
let path = format!("{}/{}", dir, filename);
wsl::write_wsl_file(distro, &path, content)?;
info!("[WSL] Wrote command file '{}' to distro '{}'", path, distro);
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_mcp_tuple_type_alias() {
// Verify the type alias works correctly
let mcp: McpTuple = (
"test".to_string(),
"stdio".to_string(),
Some("npx".to_string()),
None,
None,
None,
None,
);
assert_eq!(mcp.0, "test");
assert_eq!(mcp.1, "stdio");
}
}
1 change: 1 addition & 0 deletions src-tauri/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod cursor_paths;
pub mod gemini_paths;
pub mod opencode_paths;
pub mod paths;
pub mod wsl;
Loading
Loading