程序员阿江(Relakkes) 562523aea6 fix: keep portable desktop CLI installs in the selected config dir
Desktop portable mode depends on CLI-driven installs using the same filesystem root as the app. The installed terminal launcher now carries CLAUDE_CONFIG_DIR into macOS/Linux shell invocations and uses a Windows cmd wrapper so plugin, skill, and MCP operations do not fall back to the native home config. Moved portable bundles also rebase cached plugin install paths when the cache exists under the current config root.

Constraint: Desktop portable bundles are file-system based and must survive zip/unzip relocation.
Rejected: Copy the sidecar binary directly into user bin | it cannot inject the selected portable config directory for future CLI installs.
Rejected: Rewrite plugin install paths unconditionally | missing cache directories should remain visible as broken state instead of being silently retargeted.
Confidence: high
Scope-risk: moderate
Directive: Keep desktop launcher wrappers as env-carrying sh/cmd files; do not revert to raw sidecar copies without portable install verification.
Tested: bun test src/server/__tests__/desktop-cli-launcher.test.ts src/utils/plugins/installedPluginsManager.test.ts
Tested: cargo test portable --manifest-path desktop/src-tauri/Cargo.toml
Tested: bun run check:server
Tested: bun run check:native
Tested: macOS launcher fixture plus Computer Use Finder verification of portable marker output.
Not-tested: Windows physical desktop execution; Windows wrapper behavior is covered by unit test only.
2026-05-25 16:09:31 +08:00

141 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Prevents additional console window on Windows in release
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::fs;
use std::path::PathBuf;
fn main() {
// Determine if we should start in portable mode and set CLAUDE_CONFIG_DIR
// before any Tauri/WebView2 initialization.
//
// Mode resolution order:
// 1. External CLAUDE_CONFIG_DIR env var (batch script etc.) — always respected
// 2. Persisted app-mode.json saying "portable"
// 3. Auto-detect: default portable dir already has data files
//
// In "default" mode the app does NOT set CLAUDE_CONFIG_DIR itself.
// It relies on the env var (if set externally) or falls back to system dirs.
// All existing std::env::var("CLAUDE_CONFIG_DIR") checks in lib.rs handle this.
if let Some(portable_dir) = determine_startup_portable_dir() {
std::env::set_var(
"CLAUDE_CONFIG_DIR",
portable_dir.to_string_lossy().to_string(),
);
std::env::set_var("CC_HAHA_APP_PORTABLE_DIR", "1");
}
// If CLAUDE_CONFIG_DIR is set (either from env or from our startup logic above),
// redirect WebView2 user data folder so EBWebView cache lives alongside it.
if let Ok(config_dir) = std::env::var("CLAUDE_CONFIG_DIR") {
let webview_data = PathBuf::from(&config_dir).join("EBWebView");
if let Err(e) = fs::create_dir_all(&webview_data) {
eprintln!("[desktop] failed to create EBWebView dir: {e}");
}
std::env::set_var("WEBVIEW2_USER_DATA_FOLDER", &webview_data);
}
claude_code_desktop_lib::run()
}
/// Determine if we should start in portable mode.
/// Returns the portable config directory path if yes, None for default mode.
fn determine_startup_portable_dir() -> Option<PathBuf> {
// 1. 如果外部已经设置了 CLAUDE_CONFIG_DIR 环境变量,我们不应该覆盖它,直接返回 None 让 main 保持原样
if std::env::var("CLAUDE_CONFIG_DIR").is_ok() {
return None;
}
let exe = std::env::current_exe().ok()?;
let exe_dir = exe.parent()?;
let mut default_portable = exe_dir.to_path_buf();
default_portable.push("CLAUDE_CONFIG_DIR");
// 辅助函数:读取 app-mode.json 获取模式和自定义便携路径
fn get_mode_from_config(dir: &std::path::Path) -> Option<(String, Option<PathBuf>)> {
let path = dir.join("app-mode.json");
let data = std::fs::read_to_string(&path).ok()?;
let parsed: serde_json::Value = serde_json::from_str(&data).ok()?;
let mode = parsed
.get("mode")
.and_then(|m| m.as_str())
.unwrap_or("default")
.to_ascii_lowercase();
let portable_dir = parsed
.get("portable_dir")
.and_then(|v| v.as_str())
.map(PathBuf::from);
Some((mode, portable_dir))
}
// 2. 优先检查便携目录本地的配置文件(保证移动便携版到新电脑依然生效,并能正确处理切回默认模式)
if let Some((mode, portable_dir)) = get_mode_from_config(&default_portable) {
if mode == "portable" {
if dir_has_portable_data(&default_portable) {
return Some(default_portable.clone());
}
return Some(portable_dir.unwrap_or(default_portable.clone()));
} else {
return None; // 明确设置了 default直接使用系统默认
}
}
// 3. 检查系统全局配置
#[cfg(target_os = "windows")]
let system_config: Option<PathBuf> = std::env::var("APPDATA").ok().map(PathBuf::from);
#[cfg(target_os = "macos")]
let system_config: Option<PathBuf> = std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join("Library").join("Application Support"));
#[cfg(target_os = "linux")]
let system_config: Option<PathBuf> = std::env::var("XDG_CONFIG_HOME")
.ok()
.map(PathBuf::from)
.or_else(|| {
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".config"))
});
if let Some(ref sys_cfg) = system_config {
// 修复:必须使用 Tauri 默认的 bundle identifier
let app_subdir = sys_cfg.join("com.claude-code-haha.desktop");
if let Some((mode, portable_dir)) = get_mode_from_config(&app_subdir) {
if mode == "portable" {
return Some(portable_dir.unwrap_or(default_portable.clone()));
} else {
return None; // 明确设置了 default
}
}
}
// 4. 自动检测:如果默认便携目录中已经存在数据文件,则自动进入便携模式
fn dir_has_portable_data(dir: &std::path::Path) -> bool {
if !dir.is_dir() {
return false;
}
[
"settings.json",
".claude.json",
".mcp.json",
"window-state.json",
"terminal-config.json",
]
.iter()
.any(|f| dir.join(f).is_file())
|| dir.join("Cache").is_dir()
|| dir.join("EBWebView").is_dir()
|| dir.join("projects").is_dir()
|| dir.join("skills").is_dir()
|| dir.join("plugins").is_dir()
|| dir.join("cowork_plugins").is_dir()
|| dir.join("cc-haha").is_dir()
}
if dir_has_portable_data(&default_portable) {
return Some(default_portable);
}
None
}