Skip to content

Commit ddcb5bc

Browse files
tylergraydevTyler Gray
authored andcommitted
Add WSL2 Claude Code detection and sync support
Rebased onto current main. The original commit also included fixes for preexisting type errors across usageStore/skill/invokeMock/etc., but main has since landed canonical fixes for all of them (#221, #226, #227, #236), so those parts of the original commit are dropped. Net change: WSL2-specific Rust code (utils/wsl.rs, services/wsl_config.rs) plus the settings/config plumbing to expose WSL editors in the UI. See PR #149 for full description.
1 parent 55281a5 commit ddcb5bc

10 files changed

Lines changed: 618 additions & 9 deletions

File tree

src-tauri/src/commands/config.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,18 @@ pub(crate) fn sync_global_config_from_db(db: &Database) -> Result<(), String> {
160160
info!("[Config] Wrote global config to Gemini CLI");
161161
}
162162
}
163+
editor_id if crate::utils::wsl::is_wsl_editor(editor_id) => {
164+
if let Some(distro) = crate::utils::wsl::distro_from_editor_id(editor_id) {
165+
crate::services::wsl_config::write_wsl_global_config(&distro, &mcps)
166+
.map_err(|e| e.to_string())?;
167+
info!("[Config] Wrote global config to WSL distro '{}'", distro);
168+
} else {
169+
warn!(
170+
"[Config] WSL editor '{}' but could not resolve distro name. Skipping.",
171+
editor_id
172+
);
173+
}
174+
}
163175
unknown => warn!("[Config] Unknown editor type '{}'. Skipping.", unknown),
164176
}
165177
}

src-tauri/src/commands/settings.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use crate::db::{
22
AppSettings, CodexPaths, CopilotPaths, CursorPaths, Database, EditorInfo, GeminiPaths,
3-
OpenCodePaths,
3+
OpenCodePaths, WslClaudePaths,
44
};
55
use crate::utils::codex_paths::{get_codex_paths, is_codex_installed};
66
use crate::utils::copilot_paths::{get_copilot_paths, is_copilot_installed};
77
use crate::utils::cursor_paths::{get_cursor_paths, is_cursor_installed};
88
use crate::utils::gemini_paths::{get_gemini_paths, is_gemini_installed};
99
use crate::utils::opencode_paths::{get_opencode_paths, is_opencode_installed};
1010
use crate::utils::paths::get_claude_paths;
11+
use crate::utils::wsl;
1112
use log::info;
1213
use std::sync::{Arc, Mutex};
1314
use tauri::State;
@@ -111,6 +112,25 @@ pub fn get_available_editors(
111112
});
112113
}
113114

115+
// WSL Claude Code installations (Windows only)
116+
let wsl_installations = wsl::detect_wsl_claude_installations();
117+
for wsl_info in &wsl_installations {
118+
let editor_id = wsl::wsl_editor_id(&wsl_info.distro);
119+
let config_path = wsl_info
120+
.wsl_home
121+
.as_ref()
122+
.map(|h| format!("{}/.claude.json", h))
123+
.unwrap_or_else(|| "~/.claude.json".to_string());
124+
125+
editors.push(EditorInfo {
126+
id: editor_id.clone(),
127+
name: format!("Claude Code (WSL: {})", wsl_info.distro),
128+
is_installed: wsl_info.is_installed,
129+
is_enabled: enabled.contains(&editor_id),
130+
config_path: format!("WSL:{} {}", wsl_info.distro, config_path),
131+
});
132+
}
133+
114134
Ok(editors)
115135
}
116136

@@ -224,6 +244,28 @@ pub fn get_gemini_paths_cmd() -> Result<GeminiPaths, String> {
224244
})
225245
}
226246

247+
/// Get WSL Claude Code paths for all detected distros
248+
#[tauri::command]
249+
pub fn get_wsl_claude_paths_cmd() -> Result<Vec<WslClaudePaths>, String> {
250+
info!("[Settings] Getting WSL Claude Code paths");
251+
252+
let installations = wsl::detect_wsl_claude_installations();
253+
254+
Ok(installations
255+
.into_iter()
256+
.filter(|info| info.is_installed)
257+
.map(|info| {
258+
let home = info.wsl_home.unwrap_or_else(|| "~".to_string());
259+
WslClaudePaths {
260+
distro: info.distro,
261+
wsl_home: home.clone(),
262+
claude_json: format!("{}/.claude.json", home),
263+
claude_dir: format!("{}/.claude", home),
264+
}
265+
})
266+
.collect())
267+
}
268+
227269
// ============================================================================
228270
// Claude Code container settings
229271
// ============================================================================

src-tauri/src/db/models.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,12 +571,22 @@ pub struct GeminiPaths {
571571
pub settings_file: String, // ~/.gemini/settings.json
572572
}
573573

574+
// WSL Claude Code paths
575+
#[derive(Debug, Clone, Serialize, Deserialize)]
576+
#[serde(rename_all = "camelCase")]
577+
pub struct WslClaudePaths {
578+
pub distro: String, // WSL distro name (e.g., "Ubuntu")
579+
pub wsl_home: String, // Home directory inside WSL (e.g., "/home/user")
580+
pub claude_json: String, // ~/.claude.json inside WSL
581+
pub claude_dir: String, // ~/.claude/ inside WSL
582+
}
583+
574584
// Editor info for frontend
575585
#[derive(Debug, Clone, Serialize, Deserialize)]
576586
#[serde(rename_all = "camelCase")]
577587
pub struct EditorInfo {
578-
pub id: String, // "claude_code" or "opencode"
579-
pub name: String, // "Claude Code" or "OpenCode"
588+
pub id: String, // "claude_code", "opencode", "wsl_ubuntu", etc.
589+
pub name: String, // "Claude Code", "Claude Code (WSL: Ubuntu)", etc.
580590
pub is_installed: bool, // Whether config directory exists
581591
pub is_enabled: bool, // Whether syncing to this editor is enabled
582592
pub config_path: String, // Path to main config file

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ pub fn run() {
360360
commands::settings::get_copilot_paths_cmd,
361361
commands::settings::get_cursor_paths_cmd,
362362
commands::settings::get_gemini_paths_cmd,
363+
commands::settings::get_wsl_claude_paths_cmd,
363364
commands::settings::toggle_editor,
364365
commands::settings::set_github_token,
365366
commands::settings::clear_github_token,

src-tauri/src/services/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ pub mod stats_cache;
3434
pub mod statusline_gallery;
3535
pub mod statusline_writer;
3636
pub mod subagent_writer;
37+
pub mod wsl_config;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
use crate::utils::wsl;
2+
use anyhow::Result;
3+
use log::info;
4+
use serde_json::{json, Map, Value};
5+
6+
/// MCP tuple type (same as used by other config writers)
7+
pub type McpTuple = (
8+
String, // name
9+
String, // type (stdio, sse, http)
10+
Option<String>, // command
11+
Option<String>, // args (JSON)
12+
Option<String>, // url
13+
Option<String>, // headers (JSON)
14+
Option<String>, // env (JSON)
15+
);
16+
17+
/// Write global MCP config to a WSL distro's ~/.claude.json
18+
pub fn write_wsl_global_config(distro: &str, mcps: &[McpTuple]) -> Result<()> {
19+
let config_path = "$HOME/.claude.json";
20+
21+
// Read existing config or start fresh
22+
let mut claude_json: Value = match wsl::read_wsl_file(distro, config_path) {
23+
Ok(content) => serde_json::from_str(&content).map_err(|e| {
24+
anyhow::anyhow!(
25+
"Failed to parse existing Claude config in WSL distro '{}': {}. \
26+
Refusing to overwrite to prevent data loss.",
27+
distro,
28+
e
29+
)
30+
})?,
31+
Err(_) => json!({}),
32+
};
33+
34+
// Build mcpServers object
35+
let mut servers = Map::new();
36+
for mcp in mcps {
37+
let (name, mcp_type, command, args, url, headers, env) = mcp;
38+
39+
let config = match mcp_type.as_str() {
40+
"stdio" => {
41+
let mut obj = Map::new();
42+
if let Some(cmd) = command {
43+
obj.insert("command".to_string(), json!(cmd));
44+
}
45+
if let Some(args_json) = args {
46+
if let Ok(args_val) = serde_json::from_str::<Vec<String>>(args_json) {
47+
obj.insert("args".to_string(), json!(args_val));
48+
}
49+
}
50+
if let Some(env_json) = env {
51+
if let Ok(env_val) = serde_json::from_str::<Map<String, Value>>(env_json) {
52+
obj.insert("env".to_string(), Value::Object(env_val));
53+
}
54+
}
55+
Some(Value::Object(obj))
56+
}
57+
"sse" => {
58+
let mut obj = Map::new();
59+
obj.insert("type".to_string(), json!("sse"));
60+
if let Some(u) = url {
61+
obj.insert("url".to_string(), json!(u));
62+
}
63+
if let Some(headers_json) = headers {
64+
if let Ok(headers_val) =
65+
serde_json::from_str::<Map<String, Value>>(headers_json)
66+
{
67+
obj.insert("headers".to_string(), Value::Object(headers_val));
68+
}
69+
}
70+
Some(Value::Object(obj))
71+
}
72+
"http" => {
73+
let mut obj = Map::new();
74+
obj.insert("type".to_string(), json!("http"));
75+
if let Some(u) = url {
76+
obj.insert("url".to_string(), json!(u));
77+
}
78+
if let Some(headers_json) = headers {
79+
if let Ok(headers_val) =
80+
serde_json::from_str::<Map<String, Value>>(headers_json)
81+
{
82+
obj.insert("headers".to_string(), Value::Object(headers_val));
83+
}
84+
}
85+
Some(Value::Object(obj))
86+
}
87+
_ => None,
88+
};
89+
90+
if let Some(cfg) = config {
91+
servers.insert(name.clone(), cfg);
92+
}
93+
}
94+
95+
claude_json["mcpServers"] = Value::Object(servers);
96+
97+
// Backup existing file
98+
let _ = wsl::backup_wsl_file(distro, config_path);
99+
100+
// Write the config
101+
let content = serde_json::to_string_pretty(&claude_json)?;
102+
wsl::write_wsl_file(distro, config_path, &content)?;
103+
104+
info!(
105+
"[WSL] Wrote global config to distro '{}' with {} MCPs",
106+
distro,
107+
mcps.len()
108+
);
109+
110+
Ok(())
111+
}
112+
113+
/// Write a command/skill markdown file to a WSL distro
114+
pub fn write_wsl_command_file(
115+
distro: &str,
116+
dir: &str,
117+
filename: &str,
118+
content: &str,
119+
) -> Result<()> {
120+
wsl::mkdir_wsl(distro, dir)?;
121+
let path = format!("{}/{}", dir, filename);
122+
wsl::write_wsl_file(distro, &path, content)?;
123+
info!("[WSL] Wrote command file '{}' to distro '{}'", path, distro);
124+
Ok(())
125+
}
126+
127+
#[cfg(test)]
128+
mod tests {
129+
use super::*;
130+
131+
#[test]
132+
fn test_mcp_tuple_type_alias() {
133+
// Verify the type alias works correctly
134+
let mcp: McpTuple = (
135+
"test".to_string(),
136+
"stdio".to_string(),
137+
Some("npx".to_string()),
138+
None,
139+
None,
140+
None,
141+
None,
142+
);
143+
assert_eq!(mcp.0, "test");
144+
assert_eq!(mcp.1, "stdio");
145+
}
146+
}

src-tauri/src/utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ pub mod cursor_paths;
55
pub mod gemini_paths;
66
pub mod opencode_paths;
77
pub mod paths;
8+
pub mod wsl;

0 commit comments

Comments
 (0)