Skip to content

Commit 11c7e94

Browse files
committed
feat: add WSL shell environment support for Windows
Add shell environment detection and configuration system: Backend (Rust): - shell_environment.rs: Core WSL/Git Bash detection and path conversion - Detect available shells (Native, WSL, Git Bash) - List WSL distributions with version info - Check Claude installation in WSL - Convert Windows paths to WSL format (/mnt/c/...) - Create WSL-bridged commands for Claude execution - commands/shell.rs: Tauri commands for frontend integration - get_available_shells: Detect what's available - get_shell_config/save_shell_config: Persist preferences - check_wsl_claude: Verify Claude in WSL - auto_detect_wsl_claude: Auto-configure WSL setup Addresses issues winfunc#137 (Official WSL Support), winfunc#168 (Windows + WSL integration), and winfunc#186 (Windows shell support).
1 parent 0c3b94a commit 11c7e94

7 files changed

Lines changed: 677 additions & 2 deletions

File tree

bun.lock

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

launcher.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env bash
2+
#
3+
# installAppImageWSL2.sh
4+
#
5+
# Usage: ./installAppImageWSL2.sh /full/path/to/AppImageFile
6+
#
7+
set -e
8+
9+
if [ $# -ne 1 ]; then
10+
echo "Usage: $0 /full/path/to/AppImageFile"
11+
exit 1
12+
fi
13+
14+
APPIMAGE="$1"
15+
16+
# Check if appimage exists
17+
if [ ! -f "$APPIMAGE" ]; then
18+
echo "Error: file not found: $APPIMAGE"
19+
exit 1
20+
fi
21+
22+
APPIMAGE_OPT=$(basename -- "$APPIMAGE")
23+
APPIMAGE_DIR=~/.local/bin/"$APPIMAGE_OPT"
24+
# Create standard per-user directories if not already there
25+
mkdir -p $APPIMAGE_DIR
26+
mv "$APPIMAGE" "$APPIMAGE_DIR" && chmod a+x "$APPIMAGE_DIR/$APPIMAGE_OPT"
27+
"$APPIMAGE_DIR/$APPIMAGE_OPT" --appimage-extract
28+
29+
mv squashfs-root/* "$APPIMAGE_DIR/"
30+
rm -rf squashfs-root
31+
32+
# Create launcher
33+
TARGET="$APPIMAGE_DIR/AppRun"
34+
35+
# Create a wrapper that resolves the real path and runs from there
36+
cat > "$HOME/.local/bin/opcode" <<'WRAP'
37+
#!/usr/bin/env bash
38+
set -euo pipefail
39+
TARGET="__TARGET__"
40+
DIR="$(dirname "$(readlink -f "$TARGET")")"
41+
cd "$DIR"
42+
exec "$TARGET" "$@"
43+
WRAP
44+
45+
# Inject your real target path and make executable
46+
sed -i "s|__TARGET__|$TARGET|g" "$HOME/.local/bin/opcode"
47+
chmod +x "$HOME/.local/bin/opcode"
48+
49+
# Check PATH
50+
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
51+
echo ""
52+
echo "⚠️ Reminder: add this line to your ~/.bashrc or ~/.zshrc:"
53+
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
54+
fi
55+
echo "✅ Installation complete."

src-tauri/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod agents;
22
pub mod claude;
33
pub mod mcp;
44
pub mod proxy;
5+
pub mod shell;
56
pub mod slash_commands;
67
pub mod storage;
78
pub mod usage;

src-tauri/src/commands/shell.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
//! Shell environment Tauri commands
2+
//!
3+
//! These commands allow the frontend to:
4+
//! - Detect available shell environments (Native, WSL, Git Bash)
5+
//! - Get/set the preferred shell environment
6+
//! - Check if Claude is available in WSL
7+
8+
use crate::shell_environment::{
9+
check_claude_in_wsl, detect_available_shells, AvailableShells, ShellConfig, ShellEnvironment,
10+
};
11+
use log::{info, warn};
12+
use tauri::Manager;
13+
14+
/// Get available shell environments on the current system
15+
#[tauri::command]
16+
pub async fn get_available_shells() -> Result<AvailableShells, String> {
17+
info!("Getting available shell environments");
18+
Ok(detect_available_shells())
19+
}
20+
21+
/// Get the current shell configuration
22+
#[tauri::command]
23+
pub async fn get_shell_config(app: tauri::AppHandle) -> Result<ShellConfig, String> {
24+
info!("Getting shell configuration");
25+
26+
if let Ok(app_data_dir) = app.path().app_data_dir() {
27+
let db_path = app_data_dir.join("agents.db");
28+
if db_path.exists() {
29+
if let Ok(conn) = rusqlite::Connection::open(&db_path) {
30+
// Get shell environment preference
31+
let environment = conn
32+
.query_row(
33+
"SELECT value FROM app_settings WHERE key = 'shell_environment'",
34+
[],
35+
|row| row.get::<_, String>(0),
36+
)
37+
.ok()
38+
.and_then(|s| s.parse().ok())
39+
.unwrap_or_default();
40+
41+
// Get WSL distribution preference
42+
let wsl_distro = conn
43+
.query_row(
44+
"SELECT value FROM app_settings WHERE key = 'wsl_distro'",
45+
[],
46+
|row| row.get::<_, String>(0),
47+
)
48+
.ok();
49+
50+
// Get WSL Claude path
51+
let wsl_claude_path = conn
52+
.query_row(
53+
"SELECT value FROM app_settings WHERE key = 'wsl_claude_path'",
54+
[],
55+
|row| row.get::<_, String>(0),
56+
)
57+
.ok();
58+
59+
// Get Git Bash path
60+
let git_bash_path = conn
61+
.query_row(
62+
"SELECT value FROM app_settings WHERE key = 'git_bash_path'",
63+
[],
64+
|row| row.get::<_, String>(0),
65+
)
66+
.ok();
67+
68+
return Ok(ShellConfig {
69+
environment,
70+
wsl_distro,
71+
wsl_claude_path,
72+
git_bash_path,
73+
});
74+
}
75+
}
76+
}
77+
78+
// Return default config if no database or settings found
79+
Ok(ShellConfig::default())
80+
}
81+
82+
/// Save the shell configuration
83+
#[tauri::command]
84+
pub async fn save_shell_config(app: tauri::AppHandle, config: ShellConfig) -> Result<(), String> {
85+
info!("Saving shell configuration: {:?}", config);
86+
87+
let app_data_dir = app
88+
.path()
89+
.app_data_dir()
90+
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
91+
92+
let db_path = app_data_dir.join("agents.db");
93+
let conn = rusqlite::Connection::open(&db_path)
94+
.map_err(|e| format!("Failed to open database: {}", e))?;
95+
96+
// Ensure app_settings table exists
97+
conn.execute(
98+
"CREATE TABLE IF NOT EXISTS app_settings (
99+
key TEXT PRIMARY KEY,
100+
value TEXT NOT NULL
101+
)",
102+
[],
103+
)
104+
.map_err(|e| format!("Failed to create settings table: {}", e))?;
105+
106+
// Save shell environment
107+
conn.execute(
108+
"INSERT OR REPLACE INTO app_settings (key, value) VALUES ('shell_environment', ?)",
109+
[config.environment.to_string()],
110+
)
111+
.map_err(|e| format!("Failed to save shell_environment: {}", e))?;
112+
113+
// Save WSL distribution (if set)
114+
if let Some(ref distro) = config.wsl_distro {
115+
conn.execute(
116+
"INSERT OR REPLACE INTO app_settings (key, value) VALUES ('wsl_distro', ?)",
117+
[distro],
118+
)
119+
.map_err(|e| format!("Failed to save wsl_distro: {}", e))?;
120+
} else {
121+
conn.execute("DELETE FROM app_settings WHERE key = 'wsl_distro'", [])
122+
.ok();
123+
}
124+
125+
// Save WSL Claude path (if set)
126+
if let Some(ref path) = config.wsl_claude_path {
127+
conn.execute(
128+
"INSERT OR REPLACE INTO app_settings (key, value) VALUES ('wsl_claude_path', ?)",
129+
[path],
130+
)
131+
.map_err(|e| format!("Failed to save wsl_claude_path: {}", e))?;
132+
} else {
133+
conn.execute("DELETE FROM app_settings WHERE key = 'wsl_claude_path'", [])
134+
.ok();
135+
}
136+
137+
// Save Git Bash path (if set)
138+
if let Some(ref path) = config.git_bash_path {
139+
conn.execute(
140+
"INSERT OR REPLACE INTO app_settings (key, value) VALUES ('git_bash_path', ?)",
141+
[path],
142+
)
143+
.map_err(|e| format!("Failed to save git_bash_path: {}", e))?;
144+
} else {
145+
conn.execute("DELETE FROM app_settings WHERE key = 'git_bash_path'", [])
146+
.ok();
147+
}
148+
149+
info!("Shell configuration saved successfully");
150+
Ok(())
151+
}
152+
153+
/// Check if Claude is available in WSL and return the path
154+
#[tauri::command]
155+
pub async fn check_wsl_claude(distro: Option<String>) -> Result<Option<String>, String> {
156+
info!("Checking for Claude in WSL (distro: {:?})", distro);
157+
Ok(check_claude_in_wsl(distro.as_deref()))
158+
}
159+
160+
/// Detect Claude installation in WSL and auto-configure if found
161+
#[tauri::command]
162+
pub async fn auto_detect_wsl_claude(
163+
app: tauri::AppHandle,
164+
distro: Option<String>,
165+
) -> Result<Option<ShellConfig>, String> {
166+
info!("Auto-detecting Claude in WSL");
167+
168+
// First check available shells
169+
let shells = detect_available_shells();
170+
171+
// If no WSL distributions, return None
172+
if shells.wsl_distributions.is_empty() {
173+
info!("No WSL distributions found");
174+
return Ok(None);
175+
}
176+
177+
// Determine which distro to check
178+
let target_distro = distro.or_else(|| {
179+
shells
180+
.wsl_distributions
181+
.iter()
182+
.find(|d| d.is_default)
183+
.or(shells.wsl_distributions.first())
184+
.map(|d| d.name.clone())
185+
});
186+
187+
// Check for Claude in the target distro
188+
if let Some(ref distro_name) = target_distro {
189+
if let Some(claude_path) = check_claude_in_wsl(Some(distro_name)) {
190+
info!(
191+
"Found Claude at {} in WSL distro {}",
192+
claude_path, distro_name
193+
);
194+
195+
let config = ShellConfig {
196+
environment: ShellEnvironment::Wsl,
197+
wsl_distro: Some(distro_name.clone()),
198+
wsl_claude_path: Some(claude_path),
199+
git_bash_path: shells.git_bash_path,
200+
};
201+
202+
// Save the configuration
203+
save_shell_config(app, config.clone()).await?;
204+
205+
return Ok(Some(config));
206+
}
207+
}
208+
209+
warn!("Claude not found in any WSL distribution");
210+
Ok(None)
211+
}

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod checkpoint;
55
pub mod claude_binary;
66
pub mod commands;
77
pub mod process;
8+
pub mod shell_environment;
89
pub mod web_server;
910

1011
#[cfg_attr(mobile, tauri::mobile_entry_point)]

src-tauri/src/main.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod checkpoint;
55
mod claude_binary;
66
mod commands;
77
mod process;
8+
mod shell_environment;
89

910
use checkpoint::state::CheckpointState;
1011
use commands::agents::{
@@ -37,6 +38,10 @@ use commands::mcp::{
3738
};
3839

3940
use commands::proxy::{apply_proxy_settings, get_proxy_settings, save_proxy_settings};
41+
use commands::shell::{
42+
auto_detect_wsl_claude, check_wsl_claude, get_available_shells, get_shell_config,
43+
save_shell_config,
44+
};
4045
use commands::storage::{
4146
storage_delete_row, storage_execute_sql, storage_insert_row, storage_list_tables,
4247
storage_read_table, storage_reset_database, storage_update_row,
@@ -289,6 +294,12 @@ fn main() {
289294
// Proxy Settings
290295
get_proxy_settings,
291296
save_proxy_settings,
297+
// Shell Environment
298+
get_available_shells,
299+
get_shell_config,
300+
save_shell_config,
301+
check_wsl_claude,
302+
auto_detect_wsl_claude,
292303
])
293304
.run(tauri::generate_context!())
294305
.expect("error while running tauri application");

0 commit comments

Comments
 (0)