2025-11-18 23:44:44 +08:00
|
|
|
use serde::{Deserialize, Serialize};
|
2025-11-19 10:06:52 +08:00
|
|
|
#[cfg(not(target_os = "windows"))]
|
2025-11-18 23:44:44 +08:00
|
|
|
use std::fs;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
|
pub struct EnvConflict {
|
|
|
|
|
pub var_name: String,
|
|
|
|
|
pub var_value: String,
|
|
|
|
|
pub source_type: String, // "system" | "file"
|
|
|
|
|
pub source_path: String, // Registry path or file path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
use winreg::enums::*;
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
use winreg::RegKey;
|
|
|
|
|
|
|
|
|
|
/// Check environment variables for conflicts
|
|
|
|
|
pub fn check_env_conflicts(app: &str) -> Result<Vec<EnvConflict>, String> {
|
|
|
|
|
let keywords = get_keywords_for_app(app);
|
|
|
|
|
let mut conflicts = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Check system environment variables
|
|
|
|
|
conflicts.extend(check_system_env(&keywords)?);
|
|
|
|
|
|
|
|
|
|
// Check shell configuration files (Unix only)
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
|
|
|
conflicts.extend(check_shell_configs(&keywords)?);
|
|
|
|
|
|
|
|
|
|
Ok(conflicts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get relevant keywords for each app
|
|
|
|
|
fn get_keywords_for_app(app: &str) -> Vec<&str> {
|
|
|
|
|
match app.to_lowercase().as_str() {
|
|
|
|
|
"claude" => vec!["ANTHROPIC"],
|
|
|
|
|
"codex" => vec!["OPENAI"],
|
2025-11-19 08:33:02 +08:00
|
|
|
"gemini" => vec!["GEMINI", "GOOGLE_GEMINI"],
|
2025-11-18 23:44:44 +08:00
|
|
|
_ => vec![],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check system environment variables (Windows Registry or Unix env)
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
|
|
|
|
let mut conflicts = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Check HKEY_CURRENT_USER\Environment
|
|
|
|
|
if let Ok(hkcu) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") {
|
|
|
|
|
for (name, value) in hkcu.enum_values().filter_map(Result::ok) {
|
|
|
|
|
if keywords.iter().any(|k| name.to_uppercase().contains(k)) {
|
2025-11-19 10:06:52 +08:00
|
|
|
conflicts.push(EnvConflict {
|
|
|
|
|
var_name: name.clone(),
|
|
|
|
|
var_value: value.to_string(),
|
|
|
|
|
source_type: "system".to_string(),
|
|
|
|
|
source_path: "HKEY_CURRENT_USER\\Environment".to_string(),
|
|
|
|
|
});
|
2025-11-18 23:44:44 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
|
|
|
|
|
if let Ok(hklm) = RegKey::predef(HKEY_LOCAL_MACHINE)
|
|
|
|
|
.open_subkey("SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment")
|
|
|
|
|
{
|
|
|
|
|
for (name, value) in hklm.enum_values().filter_map(Result::ok) {
|
|
|
|
|
if keywords.iter().any(|k| name.to_uppercase().contains(k)) {
|
2025-11-19 10:06:52 +08:00
|
|
|
conflicts.push(EnvConflict {
|
|
|
|
|
var_name: name.clone(),
|
|
|
|
|
var_value: value.to_string(),
|
|
|
|
|
source_type: "system".to_string(),
|
|
|
|
|
source_path: "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment".to_string(),
|
|
|
|
|
});
|
2025-11-18 23:44:44 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(conflicts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
|
|
|
fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
|
|
|
|
let mut conflicts = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Check current process environment
|
|
|
|
|
for (key, value) in std::env::vars() {
|
|
|
|
|
if keywords.iter().any(|k| key.to_uppercase().contains(k)) {
|
|
|
|
|
conflicts.push(EnvConflict {
|
|
|
|
|
var_name: key,
|
|
|
|
|
var_value: value,
|
|
|
|
|
source_type: "system".to_string(),
|
|
|
|
|
source_path: "Process Environment".to_string(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(conflicts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check shell configuration files for environment variable exports (Unix only)
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
|
|
|
fn check_shell_configs(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
|
|
|
|
let mut conflicts = Vec::new();
|
|
|
|
|
|
|
|
|
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
|
|
|
|
let config_files = vec![
|
|
|
|
|
format!("{}/.bashrc", home),
|
|
|
|
|
format!("{}/.bash_profile", home),
|
|
|
|
|
format!("{}/.zshrc", home),
|
|
|
|
|
format!("{}/.zprofile", home),
|
|
|
|
|
format!("{}/.profile", home),
|
|
|
|
|
"/etc/profile".to_string(),
|
|
|
|
|
"/etc/bashrc".to_string(),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for file_path in config_files {
|
|
|
|
|
if let Ok(content) = fs::read_to_string(&file_path) {
|
|
|
|
|
// Parse lines for export statements
|
|
|
|
|
for (line_num, line) in content.lines().enumerate() {
|
|
|
|
|
let trimmed = line.trim();
|
|
|
|
|
|
|
|
|
|
// Match patterns like: export VAR=value or VAR=value
|
feat: add model configuration support and fix Gemini deeplink bug (#251)
* feat(providers): add notes field for provider management
- Add notes field to Provider model (backend and frontend)
- Display notes with higher priority than URL in provider card
- Style notes as non-clickable text to differentiate from URLs
- Add notes input field in provider form
- Add i18n support (zh/en) for notes field
* chore: format code and clean up unused props
- Run cargo fmt on Rust backend code
- Format TypeScript imports and code style
- Remove unused appId prop from ProviderPresetSelector
- Clean up unused variables in tests
- Integrate notes field handling in provider dialogs
* feat(deeplink): implement ccswitch:// protocol for provider import
Add deep link support to enable one-click provider configuration import via ccswitch:// URLs.
Backend:
- Implement URL parsing and validation (src-tauri/src/deeplink.rs)
- Add Tauri commands for parse and import (src-tauri/src/commands/deeplink.rs)
- Register ccswitch:// protocol in macOS Info.plist
- Add comprehensive unit tests (src-tauri/tests/deeplink_import.rs)
Frontend:
- Create confirmation dialog with security review UI (src/components/DeepLinkImportDialog.tsx)
- Add API wrapper (src/lib/api/deeplink.ts)
- Integrate event listeners in App.tsx
Configuration:
- Update Tauri config for deep link handling
- Add i18n support for Chinese and English
- Include test page for deep link validation (deeplink-test.html)
Files: 15 changed, 1312 insertions(+)
* chore(deeplink): integrate deep link handling into app lifecycle
Wire up deep link infrastructure with app initialization and event handling.
Backend Integration:
- Register deep link module and commands in mod.rs
- Add URL handling in app setup (src-tauri/src/lib.rs:handle_deeplink_url)
- Handle deep links from single instance callback (Windows/Linux CLI)
- Handle deep links from macOS system events
- Add tauri-plugin-deep-link dependency (Cargo.toml)
Frontend Integration:
- Listen for deeplink-import/deeplink-error events in App.tsx
- Update DeepLinkImportDialog component imports
Configuration:
- Enable deep link plugin in tauri.conf.json
- Update Cargo.lock for new dependencies
Localization:
- Add Chinese translations for deep link UI (zh.json)
- Add English translations for deep link UI (en.json)
Files: 9 changed, 359 insertions(+), 18 deletions(-)
* refactor(deeplink): enhance Codex provider template generation
Align deep link import with UI preset generation logic by:
- Adding complete config.toml template matching frontend defaults
- Generating safe provider name from sanitized input
- Including model_provider, reasoning_effort, and wire_api settings
- Removing minimal template that only contained base_url
- Cleaning up deprecated test file deeplink-test.html
* style: fix clippy uninlined_format_args warnings
Apply clippy --fix to use inline format arguments in:
- src/mcp.rs (8 fixes)
- src/services/env_manager.rs (10 fixes)
* style: apply code formatting and cleanup
- Format TypeScript files with Prettier (App.tsx, EnvWarningBanner.tsx, formatters.ts)
- Organize Rust imports and module order alphabetically
- Add newline at end of JSON files (en.json, zh.json)
- Update Cargo.lock for dependency changes
* feat: add model name configuration support for Codex and fix Gemini model handling
- Add visual model name input field for Codex providers
- Add model name extraction and update utilities in providerConfigUtils
- Implement model name state management in useCodexConfigState hook
- Add conditional model field rendering in CodexFormFields (non-official only)
- Integrate model name sync with TOML config in ProviderForm
- Fix Gemini deeplink model injection bug
- Correct environment variable name from GOOGLE_GEMINI_MODEL to GEMINI_MODEL
- Add test cases for Gemini model injection (with/without model)
- All tests passing (9/9)
- Fix Gemini model field binding in edit mode
- Add geminiModel state to useGeminiConfigState hook
- Extract model value during initialization and reset
- Sync model field with geminiEnv state to prevent data loss on submit
- Fix missing model value display when editing Gemini providers
Changes:
- 6 files changed, 245 insertions(+), 13 deletions(-)
2025-11-19 09:03:18 +08:00
|
|
|
if trimmed.starts_with("export ")
|
|
|
|
|
|| (!trimmed.starts_with('#') && trimmed.contains('='))
|
|
|
|
|
{
|
2025-11-18 23:44:44 +08:00
|
|
|
let export_line = trimmed.strip_prefix("export ").unwrap_or(trimmed);
|
|
|
|
|
|
|
|
|
|
if let Some(eq_pos) = export_line.find('=') {
|
|
|
|
|
let var_name = export_line[..eq_pos].trim();
|
|
|
|
|
let var_value = export_line[eq_pos + 1..].trim();
|
|
|
|
|
|
|
|
|
|
// Check if variable name contains any keyword
|
|
|
|
|
if keywords.iter().any(|k| var_name.to_uppercase().contains(k)) {
|
|
|
|
|
conflicts.push(EnvConflict {
|
|
|
|
|
var_name: var_name.to_string(),
|
feat: add model configuration support and fix Gemini deeplink bug (#251)
* feat(providers): add notes field for provider management
- Add notes field to Provider model (backend and frontend)
- Display notes with higher priority than URL in provider card
- Style notes as non-clickable text to differentiate from URLs
- Add notes input field in provider form
- Add i18n support (zh/en) for notes field
* chore: format code and clean up unused props
- Run cargo fmt on Rust backend code
- Format TypeScript imports and code style
- Remove unused appId prop from ProviderPresetSelector
- Clean up unused variables in tests
- Integrate notes field handling in provider dialogs
* feat(deeplink): implement ccswitch:// protocol for provider import
Add deep link support to enable one-click provider configuration import via ccswitch:// URLs.
Backend:
- Implement URL parsing and validation (src-tauri/src/deeplink.rs)
- Add Tauri commands for parse and import (src-tauri/src/commands/deeplink.rs)
- Register ccswitch:// protocol in macOS Info.plist
- Add comprehensive unit tests (src-tauri/tests/deeplink_import.rs)
Frontend:
- Create confirmation dialog with security review UI (src/components/DeepLinkImportDialog.tsx)
- Add API wrapper (src/lib/api/deeplink.ts)
- Integrate event listeners in App.tsx
Configuration:
- Update Tauri config for deep link handling
- Add i18n support for Chinese and English
- Include test page for deep link validation (deeplink-test.html)
Files: 15 changed, 1312 insertions(+)
* chore(deeplink): integrate deep link handling into app lifecycle
Wire up deep link infrastructure with app initialization and event handling.
Backend Integration:
- Register deep link module and commands in mod.rs
- Add URL handling in app setup (src-tauri/src/lib.rs:handle_deeplink_url)
- Handle deep links from single instance callback (Windows/Linux CLI)
- Handle deep links from macOS system events
- Add tauri-plugin-deep-link dependency (Cargo.toml)
Frontend Integration:
- Listen for deeplink-import/deeplink-error events in App.tsx
- Update DeepLinkImportDialog component imports
Configuration:
- Enable deep link plugin in tauri.conf.json
- Update Cargo.lock for new dependencies
Localization:
- Add Chinese translations for deep link UI (zh.json)
- Add English translations for deep link UI (en.json)
Files: 9 changed, 359 insertions(+), 18 deletions(-)
* refactor(deeplink): enhance Codex provider template generation
Align deep link import with UI preset generation logic by:
- Adding complete config.toml template matching frontend defaults
- Generating safe provider name from sanitized input
- Including model_provider, reasoning_effort, and wire_api settings
- Removing minimal template that only contained base_url
- Cleaning up deprecated test file deeplink-test.html
* style: fix clippy uninlined_format_args warnings
Apply clippy --fix to use inline format arguments in:
- src/mcp.rs (8 fixes)
- src/services/env_manager.rs (10 fixes)
* style: apply code formatting and cleanup
- Format TypeScript files with Prettier (App.tsx, EnvWarningBanner.tsx, formatters.ts)
- Organize Rust imports and module order alphabetically
- Add newline at end of JSON files (en.json, zh.json)
- Update Cargo.lock for dependency changes
* feat: add model name configuration support for Codex and fix Gemini model handling
- Add visual model name input field for Codex providers
- Add model name extraction and update utilities in providerConfigUtils
- Implement model name state management in useCodexConfigState hook
- Add conditional model field rendering in CodexFormFields (non-official only)
- Integrate model name sync with TOML config in ProviderForm
- Fix Gemini deeplink model injection bug
- Correct environment variable name from GOOGLE_GEMINI_MODEL to GEMINI_MODEL
- Add test cases for Gemini model injection (with/without model)
- All tests passing (9/9)
- Fix Gemini model field binding in edit mode
- Add geminiModel state to useGeminiConfigState hook
- Extract model value during initialization and reset
- Sync model field with geminiEnv state to prevent data loss on submit
- Fix missing model value display when editing Gemini providers
Changes:
- 6 files changed, 245 insertions(+), 13 deletions(-)
2025-11-19 09:03:18 +08:00
|
|
|
var_value: var_value
|
|
|
|
|
.trim_matches('"')
|
|
|
|
|
.trim_matches('\'')
|
|
|
|
|
.to_string(),
|
2025-11-18 23:44:44 +08:00
|
|
|
source_type: "file".to_string(),
|
|
|
|
|
source_path: format!("{}:{}", file_path, line_num + 1),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(conflicts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_get_keywords() {
|
|
|
|
|
assert_eq!(get_keywords_for_app("claude"), vec!["ANTHROPIC"]);
|
|
|
|
|
assert_eq!(get_keywords_for_app("codex"), vec!["OPENAI"]);
|
2025-11-19 11:26:31 +08:00
|
|
|
assert_eq!(
|
|
|
|
|
get_keywords_for_app("gemini"),
|
|
|
|
|
vec!["GEMINI", "GOOGLE_GEMINI"]
|
|
|
|
|
);
|
2025-11-18 23:44:44 +08:00
|
|
|
assert_eq!(get_keywords_for_app("unknown"), Vec::<&str>::new());
|
|
|
|
|
}
|
|
|
|
|
}
|