├── .gitignore ├── .claude └── settings.local.json ├── Cargo.toml ├── LICENSE ├── src ├── main.rs ├── cli.rs ├── constants.rs ├── config.rs ├── provider.rs └── tui.rs ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── CHANGELOG.md ├── readme_zh.md ├── CLAUDE.md ├── install.ps1 ├── install.sh ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | /target/ 3 | **/*.rs.bk 4 | 5 | # IDE 6 | .vscode/ 7 | .idea/ 8 | *.swp 9 | *.swo 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Logs 16 | *.log 17 | 18 | # Local env files 19 | .env 20 | .env.local 21 | .env.*.local 22 | 23 | # Build artifacts 24 | *.exe 25 | *.pdb 26 | 27 | # Test files 28 | test_* 29 | demo_* 30 | -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(cargo check:*)", 5 | "Bash(cargo run:*)", 6 | "Bash(cargo install:*)", 7 | "Bash(git add:*)", 8 | "Bash(git commit:*)", 9 | "Bash(git tag:*)", 10 | "Bash(git push:*)" 11 | ], 12 | "deny": [], 13 | "ask": [] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cce" 3 | version = "0.2.7" 4 | edition = "2021" 5 | authors = ["Your Name "] 6 | description = "Claude Config Environment - A tool for switching Claude environment variables" 7 | 8 | [dependencies] 9 | clap = { version = "4.4", features = ["derive"] } 10 | serde = { version = "1.0", features = ["derive"] } 11 | toml = "0.8" 12 | dirs = "5.0" 13 | anyhow = "1.0" 14 | colored = "2.0" 15 | ratatui = "0.26" 16 | crossterm = "0.27" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Zhao Peng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod config; 3 | mod constants; 4 | mod provider; 5 | mod tui; 6 | 7 | use anyhow::Result; 8 | use cli::{Cli, Commands}; 9 | use config::Config; 10 | use provider::ProviderManager; 11 | 12 | fn main() -> Result<()> { 13 | let cli = Cli::parse_args(); 14 | let mut config = Config::load()?; 15 | 16 | match cli.command { 17 | Commands::List => { 18 | ProviderManager::list_providers(&config)?; 19 | } 20 | 21 | Commands::Add { 22 | name, 23 | api_url, 24 | token, 25 | model, 26 | } => { 27 | ProviderManager::add_provider(&mut config, name, api_url, token, model)?; 28 | } 29 | 30 | Commands::Delete { name } => { 31 | ProviderManager::remove_provider(&mut config, &name)?; 32 | } 33 | 34 | Commands::Use { name } => { 35 | ProviderManager::use_provider(&mut config, &name)?; 36 | } 37 | 38 | Commands::Check => { 39 | ProviderManager::check_environment(&config)?; 40 | } 41 | 42 | Commands::Shellenv => { 43 | ProviderManager::output_shellenv()?; 44 | } 45 | 46 | Commands::Clear => { 47 | ProviderManager::clear_provider(&mut config)?; 48 | } 49 | 50 | Commands::Install { force } => { 51 | ProviderManager::install_shell_integration(force)?; 52 | } 53 | 54 | Commands::Tui => { 55 | tui::run_tui(config)?; 56 | } 57 | } 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser)] 4 | #[command( 5 | name = "cce", 6 | about = "Claude Config Environment - A tool for switching Claude environment variables", 7 | version = "0.2.7" 8 | )] 9 | pub struct Cli { 10 | #[command(subcommand)] 11 | pub command: Commands, 12 | } 13 | 14 | #[derive(Subcommand)] 15 | pub enum Commands { 16 | /// List all service providers 17 | #[command(alias = "ls")] 18 | List, 19 | 20 | /// Add a service provider 21 | Add { 22 | /// Provider name 23 | name: String, 24 | /// API URL 25 | api_url: String, 26 | /// API Token 27 | token: String, 28 | /// Model name (optional) 29 | #[arg(short, long)] 30 | model: Option, 31 | }, 32 | 33 | /// Delete the specified service provider 34 | #[command(alias = "del")] 35 | Delete { 36 | /// Name of provider to delete 37 | name: String, 38 | }, 39 | 40 | /// Use the specified service provider 41 | Use { 42 | /// Name of provider to use 43 | name: String, 44 | }, 45 | 46 | /// Check current environment variable status 47 | Check, 48 | 49 | /// Output shell integration function 50 | Shellenv, 51 | 52 | /// Clear environment variables to use official Claude client 53 | Clear, 54 | 55 | /// Install shell integration for immediate environment variable effects 56 | Install { 57 | /// Force reinstall even if already installed 58 | #[arg(long)] 59 | force: bool, 60 | }, 61 | 62 | /// Launch interactive TUI (Text User Interface) 63 | Tui, 64 | } 65 | 66 | impl Cli { 67 | pub fn parse_args() -> Self { 68 | Self::parse() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build-and-release: 16 | name: Build and Release 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | include: 21 | - os: ubuntu-latest 22 | target: x86_64-unknown-linux-gnu 23 | artifact_name: cce 24 | asset_name: cce-linux-x86_64 25 | - os: windows-latest 26 | target: x86_64-pc-windows-msvc 27 | artifact_name: cce.exe 28 | asset_name: cce-windows-x86_64.exe 29 | - os: macos-latest 30 | target: x86_64-apple-darwin 31 | artifact_name: cce 32 | asset_name: cce-macos-x86_64 33 | - os: macos-latest 34 | target: aarch64-apple-darwin 35 | artifact_name: cce 36 | asset_name: cce-macos-aarch64 37 | 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v4 41 | 42 | - name: Setup Rust 43 | uses: dtolnay/rust-toolchain@v1 44 | with: 45 | toolchain: stable 46 | targets: ${{ matrix.target }} 47 | 48 | - name: Build binary 49 | run: cargo build --release --target ${{ matrix.target }} 50 | 51 | - name: Compress binary (Unix) 52 | if: matrix.os != 'windows-latest' 53 | run: | 54 | cd target/${{ matrix.target }}/release 55 | tar -czf ${{ matrix.asset_name }}.tar.gz ${{ matrix.artifact_name }} 56 | 57 | - name: Compress binary (Windows) 58 | if: matrix.os == 'windows-latest' 59 | run: | 60 | cd target/${{ matrix.target }}/release 61 | 7z a ${{ matrix.asset_name }}.zip ${{ matrix.artifact_name }} 62 | 63 | - name: Upload Release Asset 64 | uses: softprops/action-gh-release@v1 65 | with: 66 | files: | 67 | target/${{ matrix.target }}/release/${{ matrix.asset_name }}.tar.gz 68 | target/${{ matrix.target }}/release/${{ matrix.asset_name }}.zip 69 | make_latest: true 70 | generate_release_notes: true 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.2.0] - 2025-10-18 9 | 10 | ### Added 11 | - `cce add` now accepts an optional `--model` flag and exports both `ANTHROPIC_MODEL` and `ANTHROPIC_DEFAULT_HAIKU_MODEL` when selected. 12 | - Introduced `cce clear` to unset the active provider and remove Claude-specific environment variables. 13 | - Added `cce install` for one-command shell integration setup across bash, zsh, and fish, plus an enriched `cce shellenv` helper. 14 | 15 | ### Improved 16 | - `cce use` highlights the currently active provider, skips redundant switches, and guides users to apply environment changes immediately. 17 | - `cce check` now validates whether the live environment matches the stored configuration and suggests corrective actions. 18 | 19 | ## [0.1.0] - 2024-01-XX 20 | 21 | ### Added 22 | - Initial release of CCE (Claude Config Environment) 23 | - Service provider management (`add`, `delete`, `list`) 24 | - Environment variable switching with `use` command 25 | - Shell integration with `shellenv` command for immediate effect 26 | - Configuration checking with `check` command 27 | - Support for `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL` environment variables 28 | - Local TOML configuration storage in `~/.cce/config.toml` 29 | - Colorful CLI output with user-friendly messages 30 | - Built with Rust for high performance and reliability 31 | 32 | ### Features 33 | - **Easy Switching**: Quickly switch between different Claude API service providers 34 | - **Shell Integration**: `eval "$(cce shellenv)"` for immediate environment variable effects 35 | - **Configuration Management**: Secure local storage of multiple service provider configurations 36 | - **User-Friendly Interface**: Intuitive command-line interface with colored output 37 | - **Cross-Platform**: Works on macOS, Linux, and Windows 38 | - **No Confirmation Prompts**: Streamlined workflow without unnecessary interruptions 39 | 40 | ### Commands 41 | - `cce list` - List all configured service providers 42 | - `cce add ` - Add a new service provider 43 | - `cce delete ` - Remove a service provider 44 | - `cce use ` - Switch to a service provider (with shell integration) 45 | - `CCE_SHELL_INTEGRATION=1 cce use ` - Emit environment variable exports for integration 46 | - `CCE_SHELL_INTEGRATION=1 cce clear` - Emit unset commands for integration 47 | - `cce check` - Verify current environment variable status 48 | - `cce shellenv` - Output shell integration function 49 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | /// Environment variable names used by CCE 2 | pub const ENV_AUTH_TOKEN: &str = "ANTHROPIC_AUTH_TOKEN"; 3 | pub const ENV_BASE_URL: &str = "ANTHROPIC_BASE_URL"; 4 | pub const ENV_MODEL: &str = "ANTHROPIC_MODEL"; 5 | pub const ENV_DEFAULT_OPUS_MODEL: &str = "ANTHROPIC_DEFAULT_OPUS_MODEL"; 6 | pub const ENV_DEFAULT_SONNET_MODEL: &str = "ANTHROPIC_DEFAULT_SONNET_MODEL"; 7 | pub const ENV_DEFAULT_HAIKU_MODEL: &str = "ANTHROPIC_DEFAULT_HAIKU_MODEL"; 8 | 9 | /// Control variable for shell integration 10 | pub const ENV_SHELL_INTEGRATION: &str = "CCE_SHELL_INTEGRATION"; 11 | 12 | /// Helper functions for environment variable management 13 | use crate::config::Provider; 14 | 15 | /// Set all environment variables for a provider 16 | pub fn set_provider_env_vars(provider: &Provider) { 17 | std::env::set_var(ENV_AUTH_TOKEN, &provider.token); 18 | std::env::set_var(ENV_BASE_URL, &provider.api_url); 19 | 20 | if let Some(ref model) = provider.model { 21 | std::env::set_var(ENV_MODEL, model); 22 | std::env::set_var(ENV_DEFAULT_OPUS_MODEL, model); 23 | std::env::set_var(ENV_DEFAULT_SONNET_MODEL, model); 24 | std::env::set_var(ENV_DEFAULT_HAIKU_MODEL, model); 25 | } 26 | } 27 | 28 | /// Clear all environment variables managed by CCE 29 | pub fn clear_all_env_vars() { 30 | std::env::remove_var(ENV_AUTH_TOKEN); 31 | std::env::remove_var(ENV_BASE_URL); 32 | std::env::remove_var(ENV_MODEL); 33 | std::env::remove_var(ENV_DEFAULT_OPUS_MODEL); 34 | std::env::remove_var(ENV_DEFAULT_SONNET_MODEL); 35 | std::env::remove_var(ENV_DEFAULT_HAIKU_MODEL); 36 | } 37 | 38 | /// Generate export commands for shell integration 39 | pub fn generate_export_commands(provider: &Provider) -> String { 40 | let mut commands = Vec::new(); 41 | commands.push(format!("export {}=\"{}\"", ENV_AUTH_TOKEN, provider.token)); 42 | commands.push(format!("export {}=\"{}\"", ENV_BASE_URL, provider.api_url)); 43 | 44 | if let Some(ref model) = provider.model { 45 | commands.push(format!("export {}=\"{}\"", ENV_MODEL, model)); 46 | commands.push(format!("export {}=\"{}\"", ENV_DEFAULT_OPUS_MODEL, model)); 47 | commands.push(format!("export {}=\"{}\"", ENV_DEFAULT_SONNET_MODEL, model)); 48 | commands.push(format!("export {}=\"{}\"", ENV_DEFAULT_HAIKU_MODEL, model)); 49 | } 50 | 51 | commands.join("\n") 52 | } 53 | 54 | /// Generate unset commands for shell integration 55 | pub fn generate_unset_commands() -> String { 56 | let env_vars = [ 57 | ENV_AUTH_TOKEN, 58 | ENV_BASE_URL, 59 | ENV_MODEL, 60 | ENV_DEFAULT_OPUS_MODEL, 61 | ENV_DEFAULT_SONNET_MODEL, 62 | ENV_DEFAULT_HAIKU_MODEL, 63 | ]; 64 | 65 | env_vars 66 | .iter() 67 | .map(|var| format!("unset {}", var)) 68 | .collect::>() 69 | .join("\n") 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, develop] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macos-latest] 19 | rust: [stable, beta] 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Rust 26 | uses: dtolnay/rust-toolchain@v1 27 | with: 28 | toolchain: ${{ matrix.rust }} 29 | 30 | - name: Cache dependencies 31 | uses: actions/cache@v4 32 | with: 33 | path: | 34 | ~/.cargo/registry 35 | ~/.cargo/git 36 | target 37 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 38 | 39 | - name: Run tests 40 | run: cargo test --verbose 41 | 42 | - name: Install components 43 | run: | 44 | rustup component add clippy 45 | rustup component add rustfmt 46 | 47 | - name: Run clippy 48 | run: cargo clippy -- -D warnings 49 | 50 | - name: Check formatting 51 | run: cargo fmt -- --check 52 | 53 | build: 54 | name: Build 55 | runs-on: ${{ matrix.os }} 56 | strategy: 57 | matrix: 58 | include: 59 | - os: ubuntu-latest 60 | target: x86_64-unknown-linux-gnu 61 | artifact_name: cce 62 | asset_name: cce-linux-x86_64 63 | - os: windows-latest 64 | target: x86_64-pc-windows-msvc 65 | artifact_name: cce.exe 66 | asset_name: cce-windows-x86_64.exe 67 | - os: macos-latest 68 | target: x86_64-apple-darwin 69 | artifact_name: cce 70 | asset_name: cce-macos-x86_64 71 | - os: macos-latest 72 | target: aarch64-apple-darwin 73 | artifact_name: cce 74 | asset_name: cce-macos-aarch64 75 | 76 | steps: 77 | - name: Checkout code 78 | uses: actions/checkout@v4 79 | 80 | - name: Setup Rust 81 | uses: dtolnay/rust-toolchain@v1 82 | with: 83 | toolchain: stable 84 | targets: ${{ matrix.target }} 85 | 86 | - name: Cache dependencies 87 | uses: actions/cache@v4 88 | with: 89 | path: | 90 | ~/.cargo/registry 91 | ~/.cargo/git 92 | target 93 | key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} 94 | 95 | - name: Build binary 96 | run: cargo build --release --target ${{ matrix.target }} 97 | 98 | - name: Upload artifact 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: ${{ matrix.asset_name }} 102 | path: target/${{ matrix.target }}/release/${{ matrix.artifact_name }} 103 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | use std::fs; 5 | use std::path::PathBuf; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct Provider { 9 | pub name: String, 10 | pub api_url: String, 11 | pub token: String, 12 | pub model: Option, 13 | } 14 | 15 | #[derive(Debug, Serialize, Deserialize, Default)] 16 | pub struct Config { 17 | pub providers: HashMap, 18 | pub current_provider: Option, 19 | } 20 | 21 | impl Config { 22 | pub fn load() -> Result { 23 | let config_path = Self::get_config_path()?; 24 | 25 | if !config_path.exists() { 26 | return Ok(Self::default()); 27 | } 28 | 29 | let content = fs::read_to_string(&config_path) 30 | .with_context(|| format!("Failed to read config file: {:?}", config_path))?; 31 | 32 | let config: Config = 33 | toml::from_str(&content).with_context(|| "Invalid config file format")?; 34 | 35 | Ok(config) 36 | } 37 | 38 | pub fn save(&self) -> Result<()> { 39 | let config_path = Self::get_config_path()?; 40 | 41 | // Ensure config directory exists 42 | if let Some(parent) = config_path.parent() { 43 | fs::create_dir_all(parent) 44 | .with_context(|| format!("Failed to create config directory: {:?}", parent))?; 45 | } 46 | 47 | let content = toml::to_string_pretty(self).with_context(|| "Failed to serialize config")?; 48 | 49 | fs::write(&config_path, content) 50 | .with_context(|| format!("Failed to write config file: {:?}", config_path))?; 51 | 52 | Ok(()) 53 | } 54 | 55 | pub fn add_provider( 56 | &mut self, 57 | name: String, 58 | api_url: String, 59 | token: String, 60 | model: Option, 61 | ) { 62 | let provider = Provider { 63 | name: name.clone(), 64 | api_url, 65 | token, 66 | model, 67 | }; 68 | self.providers.insert(name, provider); 69 | } 70 | 71 | pub fn remove_provider(&mut self, name: &str) -> bool { 72 | if let Some(current) = &self.current_provider { 73 | if current == name { 74 | self.current_provider = None; 75 | } 76 | } 77 | self.providers.remove(name).is_some() 78 | } 79 | 80 | pub fn set_current_provider(&mut self, name: &str) -> bool { 81 | if self.providers.contains_key(name) { 82 | self.current_provider = Some(name.to_string()); 83 | true 84 | } else { 85 | false 86 | } 87 | } 88 | 89 | pub fn clear_current_provider(&mut self) { 90 | self.current_provider = None; 91 | } 92 | 93 | #[allow(dead_code)] 94 | pub fn get_current_provider(&self) -> Option<&Provider> { 95 | self.current_provider 96 | .as_ref() 97 | .and_then(|name| self.providers.get(name)) 98 | } 99 | 100 | fn get_config_path() -> Result { 101 | let home_dir = 102 | dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Failed to get user home directory"))?; 103 | 104 | Ok(home_dir.join(".cce").join("config.toml")) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /readme_zh.md: -------------------------------------------------------------------------------- 1 | # CCE - Claude Config Environment 使用指南(中文) 2 | [English Version](README.md) 3 | 4 | CCE 是一个使用 Rust 编写的 Claude 服务提供方配置切换工具,可帮助你在本地安全管理多个 Claude API 账号,并在终端中快速切换相关环境变量。 5 | 6 | ## ✨ 主要特性 7 | 8 | - **极速切换**:一条命令即可在不同 Claude 服务提供方之间切换。 9 | - **多账号配置**:将账号信息保存在本地 `~/.cce/config.toml`,安全可控。 10 | - **即刻生效**:配合 shell 集成,`cce use` / `cce clear` 能立即更新当前终端环境变量。 11 | - **跨平台**:兼容 macOS、Linux(bash/zsh)、Windows PowerShell。 12 | 13 | ## 🚀 快速入门 14 | 15 | ```bash 16 | # 方式一:curl 安装(推荐) 17 | curl -sSL https://raw.githubusercontent.com/zhaopengme/cce/master/install.sh | bash 18 | 19 | # 方式二:源码构建 20 | git clone https://github.com/zhaopengme/cce.git 21 | cd cce 22 | cargo build --release 23 | cargo install --path . 24 | ``` 25 | 26 | ### Windows PowerShell 27 | 28 | ```powershell 29 | Invoke-WebRequest -Uri "https://raw.githubusercontent.com/zhaopengme/cce/master/install.ps1" -OutFile "install.ps1" 30 | .\install.ps1 31 | ``` 32 | 33 | 安装完成后,重新打开一个终端(或执行 `source ~/.zshrc` / `source ~/.bashrc`,PowerShell 下执行 `. $PROFILE`)即可生效。 34 | 35 | ## 🔧 Shell 集成 36 | 37 | ### 自动集成(推荐) 38 | 39 | ```bash 40 | cce install 41 | ``` 42 | 43 | 该命令会: 44 | - 自动识别当前 shell(bash 或 zsh) 45 | - 将 CCE 集成片段追加到对应的 profile 46 | - 在每次打开新终端时,根据 `current_provider` 自动加载环境变量 47 | - 包装 `cce` 函数,使 `cce use` / `cce clear` 立即更新当前会话 48 | 49 | ### 手动集成示例 50 | 51 | 若使用其它 shell,可手动在配置文件中加入: 52 | 53 | ```bash 54 | # ~/.zshrc 或 ~/.bashrc 55 | if command -v cce >/dev/null 2>&1; then 56 | if [[ -f "$HOME/.cce/config.toml" ]]; then 57 | current_provider=$(awk -F'"' '/^current_provider/ {print $2; exit}' "$HOME/.cce/config.toml") 58 | if [[ -n "$current_provider" ]]; then 59 | eval "$(CCE_SHELL_INTEGRATION=1 cce use "$current_provider")" 60 | fi 61 | fi 62 | fi 63 | 64 | # 可选:加载 cce shell 包装函数 65 | eval "$(cce shellenv)" 66 | ``` 67 | 68 | PowerShell 用户可参考 `install.ps1` 自动生成的片段,它会放置在 `$PROFILE` 中并在会话启动时自动执行。 69 | 70 | ## 📋 常用命令 71 | 72 | | 命令 | 说明 | 73 | |------|------| 74 | | `cce list` | 列出已配置的服务提供方并标记当前使用者 | 75 | | `cce add [--model ]` | 新增或更新服务提供方 | 76 | | `cce delete ` | 删除指定服务提供方 | 77 | | `cce use ` | 切换到指定服务提供方 | 78 | | `cce clear` | 清空当前服务提供方,恢复官方客户端 | 79 | | `cce check` | 检查环境变量与配置是否一致 | 80 | | `cce install [--force]` | 安装(或强制重装)shell 集成 | 81 | | `cce shellenv` | 输出 bash/zsh 包装函数定义 | 82 | 83 | > 提示:在脚本场景下,可使用 `CCE_SHELL_INTEGRATION=1 cce use ` / `CCE_SHELL_INTEGRATION=1 cce clear` 来获取 `export` / `unset` 命令并自动生效。 84 | 85 | ## 🐛 故障排查 86 | 87 | ### 安装失败 88 | - 确认 `curl`、`tar` 已安装; 89 | - 检查平台是否受支持(`uname -s && uname -m`); 90 | - 可改用源码构建或从 Release 页面下载二进制。 91 | 92 | ### Shell 集成未生效 93 | - 重新执行 `cce install --force`; 94 | - 确认 profile 中存在 “CCE Shell Integration” 区块: 95 | ```bash 96 | grep -n "CCE Shell Integration" ~/.zshrc 97 | ``` 98 | - 当前终端临时刷新: 99 | ```bash 100 | eval "$(CCE_SHELL_INTEGRATION=1 cce use )" 101 | ``` 102 | 103 | ### 环境变量未设置 104 | ```bash 105 | cce check 106 | echo $ANTHROPIC_AUTH_TOKEN 107 | echo $ANTHROPIC_BASE_URL 108 | ``` 109 | 110 | ### PATH 未包含安装目录 111 | ```bash 112 | echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc 113 | source ~/.zshrc 114 | ``` 115 | 116 | ## 📄 配置文件位置 117 | 118 | CCE 的配置文件保存在 `~/.cce/config.toml`,主要字段包括: 119 | 120 | ```toml 121 | current_provider = "anthropic" 122 | 123 | [providers.anthropic] 124 | name = "anthropic" 125 | api_url = "https://api.anthropic.com" 126 | token = "sk-ant-api03-your-token-here" 127 | 128 | [providers.custom] 129 | name = "custom" 130 | api_url = "https://custom-claude-api.com" 131 | token = "custom-token-123" 132 | ``` 133 | 134 | 可以通过 `cce add` 命令添加多个 provider,并使用 `cce use ` 激活其中之一。 135 | 136 | ## 🤝 反馈与贡献 137 | 138 | 如需反馈问题或提交改进,欢迎访问 GitHub 项目仓库提交 Issue 或 Pull Request。感谢使用 CCE,祝你切换 Claude 账号畅通无阻! 139 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | CCE (Claude Config Environment) is a Rust CLI tool for managing multiple Claude API service providers. It allows users to switch between different API endpoints and tokens via environment variables. 8 | 9 | ## Build and Development Commands 10 | 11 | ### Building the Project 12 | ```bash 13 | # Development build 14 | cargo build 15 | 16 | # Release build (optimized) 17 | cargo build --release 18 | 19 | # The binary will be in target/debug/cce or target/release/cce 20 | ``` 21 | 22 | ### Running the Project 23 | ```bash 24 | # Run directly with cargo 25 | cargo run -- 26 | 27 | # Examples: 28 | cargo run -- list 29 | cargo run -- add test-provider https://api.example.com test-token 30 | cargo run -- use test-provider 31 | cargo run -- tui # Launch interactive TUI 32 | ``` 33 | 34 | ### Testing 35 | ```bash 36 | # Run all tests 37 | cargo test 38 | 39 | # Run tests with output 40 | cargo test -- --nocapture 41 | 42 | # Run specific test 43 | cargo test 44 | ``` 45 | 46 | ### Code Quality 47 | ```bash 48 | # Check code without building 49 | cargo check 50 | 51 | # Format code 52 | cargo fmt 53 | 54 | # Lint code 55 | cargo clippy 56 | 57 | # Lint with all warnings 58 | cargo clippy -- -D warnings 59 | ``` 60 | 61 | ### Installing Locally 62 | ```bash 63 | # Install from source to ~/.cargo/bin 64 | cargo install --path . 65 | ``` 66 | 67 | ## Architecture 68 | 69 | ### Module Structure 70 | 71 | The codebase is organized into 5 main modules: 72 | 73 | 1. **main.rs** - Entry point that orchestrates command parsing and execution 74 | 2. **cli.rs** - Command-line interface definitions using Clap 75 | 3. **config.rs** - Configuration management and persistence 76 | 4. **provider.rs** - Core business logic for provider operations 77 | 5. **tui.rs** - Interactive Text User Interface using ratatui and crossterm 78 | 79 | ### Key Design Patterns 80 | 81 | **Configuration Storage**: Uses TOML format stored at `~/.cce/config.toml`. The Config struct manages: 82 | - A HashMap of provider configurations 83 | - Current provider selection 84 | - Serialization/deserialization with serde 85 | 86 | **Shell Integration Model**: CCE supports two execution modes: 87 | 1. **Normal mode**: Direct execution with user-friendly output 88 | 2. **Shell integration mode** (`CCE_SHELL_INTEGRATION=1`): Outputs shell commands (export/unset) to stdout for `eval` consumption 89 | 90 | The shell integration works by wrapping the `cce` command in a shell function (see `output_shellenv()` in provider.rs:417-456) that: 91 | - Intercepts `use` and `clear` commands 92 | - Executes the binary with `CCE_SHELL_INTEGRATION=1` 93 | - Evaluates the output to modify the parent shell's environment 94 | 95 | **Command Aliases**: The CLI supports aliases (e.g., `ls` for `list`, `del` for `delete`) defined via Clap's `#[command(alias = "...")]` attribute. 96 | 97 | **TUI Architecture**: The interactive TUI provides a full-screen terminal interface using: 98 | - **ratatui** - Terminal UI framework for rendering widgets and layouts 99 | - **crossterm** - Cross-platform terminal manipulation (keyboard input, raw mode, alternate screen) 100 | 101 | The TUI supports three input modes: 102 | 1. **Normal mode** - Navigate providers, use/clear/delete operations 103 | 2. **AddProvider mode** - Multi-field form for adding new providers (name, URL, token, model) 104 | 3. **DeleteConfirm mode** - Confirmation dialog overlay before deleting 105 | 106 | Key TUI features: 107 | - Arrow keys / j/k for navigation 108 | - Enter or 'u' to activate a provider 109 | - 'a' to add a new provider 110 | - 'd' to delete with confirmation 111 | - 'c' to clear the current provider 112 | - Tab/Shift-Tab to navigate between form fields 113 | - Real-time status messages and error handling 114 | 115 | ### Environment Variables 116 | 117 | CCE manages these environment variables: 118 | - `ANTHROPIC_AUTH_TOKEN` - API authentication token 119 | - `ANTHROPIC_BASE_URL` - API base URL 120 | - `ANTHROPIC_MODEL` - Model name (optional, set if provider has model configured) 121 | - `ANTHROPIC_DEFAULT_OPUS_MODEL` - Default Opus model (optional) 122 | - `ANTHROPIC_DEFAULT_SONNET_MODEL` - Default Sonnet model (optional) 123 | - `ANTHROPIC_DEFAULT_HAIKU_MODEL` - Default Haiku model (optional) 124 | 125 | Control variable: 126 | - `CCE_SHELL_INTEGRATION` - Set to "1" to enable shell integration mode 127 | 128 | ### Data Flow 129 | 130 | **Adding a provider**: 131 | 1. CLI parses `add` command with name, api_url, token, optional model (cli.rs:23-33) 132 | 2. ProviderManager::add_provider creates Provider struct (provider.rs:50-74) 133 | 3. Config::add_provider inserts into HashMap (config.rs:55-69) 134 | 4. Config::save serializes to ~/.cce/config.toml (config.rs:38-53) 135 | 136 | **Using a provider**: 137 | 1. CLI parses `use` command (cli.rs:48-51) 138 | 2. ProviderManager::use_provider checks shell integration mode (provider.rs:97-140) 139 | 3. Config::set_current_provider updates current selection (config.rs:80-87) 140 | 4. Config is saved to disk 141 | 5. If shell mode: emit export commands to stdout (provider.rs:282-292) 142 | 6. If normal mode: set env vars in process and print confirmation 143 | 144 | **Shell integration installation**: 145 | 1. Detects user's shell from $SHELL environment variable (provider.rs:331) 146 | 2. Determines appropriate config file (~/.zshrc, ~/.bashrc, etc.) 147 | 3. Checks if integration already exists to avoid duplicates (provider.rs:354-367) 148 | 4. Appends `eval "$(cce shellenv)"` to shell config (provider.rs:389-401) 149 | 5. The shellenv command outputs a wrapper function that intercepts cce use/clear 150 | 151 | ## Cross-Platform Considerations 152 | 153 | The project supports Linux, macOS (Intel and Apple Silicon), and Windows: 154 | - Shell integration varies by shell type (bash, zsh, fish) 155 | - macOS uses ~/.bash_profile instead of ~/.bashrc for bash 156 | - Windows uses PowerShell with a separate install.ps1 script 157 | - Path expansion handles tilde (~) differently across platforms 158 | 159 | ## Release Process 160 | 161 | Releases are automated via GitHub Actions (see .github/workflows/release.yml): 162 | - Triggered by pushing a tag matching `v*` pattern 163 | - Builds for multiple targets: linux-x86_64, macos-x86_64, macos-aarch64, windows-x86_64 164 | - Creates compressed archives (.tar.gz for Unix, .zip for Windows) 165 | - Uploads to GitHub releases with auto-generated release notes 166 | 167 | To create a release: 168 | ```bash 169 | git tag v0.x.x 170 | git push origin v0.x.x 171 | ``` 172 | -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | # CCE Installation Script for Windows PowerShell 2 | 3 | param( 4 | [string]$InstallPath = "$env:USERPROFILE\.cargo\bin" 5 | ) 6 | 7 | $ErrorActionPreference = "Stop" 8 | 9 | Write-Host "🧙 Installing CCE (Claude Config Environment)..." -ForegroundColor Blue 10 | 11 | # Check if Rust/Cargo is installed 12 | if (!(Get-Command cargo -ErrorAction SilentlyContinue)) { 13 | Write-Host "❌ Error: Rust/Cargo not found" -ForegroundColor Red 14 | Write-Host "Please install Rust first: https://rustup.rs/" -ForegroundColor Yellow 15 | exit 1 16 | } 17 | 18 | Write-Host "✅ Rust environment detected" -ForegroundColor Green 19 | 20 | # Build the project 21 | Write-Host "🔨 Building project..." -ForegroundColor Yellow 22 | cargo build --release 23 | 24 | if ($LASTEXITCODE -ne 0) { 25 | Write-Host "❌ Build failed" -ForegroundColor Red 26 | exit 1 27 | } 28 | 29 | Write-Host "✅ Build completed" -ForegroundColor Green 30 | 31 | # Install the binary 32 | Write-Host "📦 Installing to system..." -ForegroundColor Yellow 33 | cargo install --path . 34 | 35 | if ($LASTEXITCODE -ne 0) { 36 | Write-Host "❌ Installation failed" -ForegroundColor Red 37 | exit 1 38 | } 39 | 40 | Write-Host "✅ CCE binary installed successfully" -ForegroundColor Green 41 | 42 | # Check if CCE is available 43 | Write-Host "🧪 Verifying installation..." -ForegroundColor Yellow 44 | 45 | if (Get-Command cce -ErrorAction SilentlyContinue) { 46 | Write-Host "✅ CCE installed successfully!" -ForegroundColor Green 47 | Write-Host "" 48 | Write-Host "📖 Usage:" -ForegroundColor Blue 49 | Write-Host " cce list - List all service providers" 50 | Write-Host " cce add - Add a service provider" 51 | Write-Host " cce delete - Delete a service provider" 52 | Write-Host " cce use - Use specified service provider" 53 | Write-Host " cce check - Check environment variable status" 54 | Write-Host " cce --help - Show detailed help" 55 | Write-Host "" 56 | Write-Host "🔧 Configuring shell integration..." -ForegroundColor Cyan 57 | 58 | function Get-CceIntegrationBlock { 59 | @' 60 | # >>> CCE Shell Integration >>> 61 | if (Get-Command cce -ErrorAction SilentlyContinue) { 62 | $script:CceBinary = (Get-Command cce).Source 63 | 64 | function Apply-CceEnvironment { 65 | param( 66 | [string[]]$Lines 67 | ) 68 | 69 | $expanded = @() 70 | foreach ($entry in $Lines) { 71 | if ($null -eq $entry) { continue } 72 | $expanded += ($entry -split "`r?`n") 73 | } 74 | 75 | foreach ($line in $expanded) { 76 | if ([string]::IsNullOrWhiteSpace($line)) { continue } 77 | $trimmed = $line.Trim() 78 | if ($trimmed -match '^export\s+([A-Z0-9_]+)="([^"]*)"$') { 79 | $name = $matches[1] 80 | $value = $matches[2] 81 | [Environment]::SetEnvironmentVariable($name, $value, 'Process') 82 | } elseif ($trimmed -match '^unset\s+([A-Z0-9_]+)$') { 83 | $name = $matches[1] 84 | [Environment]::SetEnvironmentVariable($name, $null, 'Process') 85 | } 86 | } 87 | } 88 | 89 | function Invoke-CceBinary { 90 | param( 91 | [string[]]$Arguments 92 | ) 93 | 94 | $env:CCE_SHELL_INTEGRATION = '1' 95 | $output = & $script:CceBinary @Arguments 2>$null 96 | $status = $LASTEXITCODE 97 | Remove-Item Env:CCE_SHELL_INTEGRATION -ErrorAction SilentlyContinue 98 | return [PSCustomObject]@{ 99 | Status = $status 100 | Output = $output 101 | } 102 | } 103 | 104 | function cce { 105 | param( 106 | [Parameter(ValueFromRemainingArguments = $true)] 107 | [string[]]$Args 108 | ) 109 | 110 | if ($Args.Length -ge 2 -and $Args[0] -eq 'use') { 111 | $result = Invoke-CceBinary -Arguments $Args 112 | if ($result.Status -eq 0 -and $result.Output) { 113 | Apply-CceEnvironment -Lines $result.Output 114 | Write-Host "⚡ Switched to service provider '$($Args[1])'" 115 | Write-Host '✅ Environment variables are now active in current terminal' 116 | return 117 | } 118 | } elseif ($Args.Length -ge 1 -and $Args[0] -eq 'clear') { 119 | $result = Invoke-CceBinary -Arguments $Args 120 | if ($result.Status -eq 0 -and $result.Output) { 121 | Apply-CceEnvironment -Lines $result.Output 122 | Write-Host '🧹 Cleared service provider configuration' 123 | Write-Host '✅ Environment variables are now unset in current terminal' 124 | return 125 | } 126 | } 127 | 128 | & $script:CceBinary @Args 129 | } 130 | 131 | function Initialize-CceEnvironment { 132 | $configPath = Join-Path $env:USERPROFILE '.cce\config.toml' 133 | if (Test-Path $configPath) { 134 | $match = Select-String -Path $configPath -Pattern '^current_provider\s*=\s*"([^"]+)"' | Select-Object -First 1 135 | if ($match) { 136 | $provider = $match.Matches[0].Groups[1].Value 137 | if ($provider) { 138 | $result = Invoke-CceBinary -Arguments @('use', $provider) 139 | if ($result.Status -eq 0 -and $result.Output) { 140 | Apply-CceEnvironment -Lines $result.Output 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | Initialize-CceEnvironment 148 | } 149 | # <<< CCE Shell Integration <<< 150 | '@ 151 | } 152 | 153 | function Set-CceShellIntegration { 154 | param( 155 | [string]$ProfilePath 156 | ) 157 | 158 | if (-not (Test-Path $ProfilePath)) { 159 | New-Item -ItemType File -Path $ProfilePath -Force | Out-Null 160 | } 161 | 162 | $existing = Get-Content -Path $ProfilePath -Raw -ErrorAction SilentlyContinue 163 | if ($existing) { 164 | $existing = [regex]::Replace( 165 | $existing, 166 | '# >>> CCE Shell Integration >>>.*?# <<< CCE Shell Integration <<<\s*', 167 | '', 168 | [System.Text.RegularExpressions.RegexOptions]::Singleline 169 | ) 170 | } else { 171 | $existing = '' 172 | } 173 | 174 | $block = Get-CceIntegrationBlock 175 | if ($existing.Length -gt 0 -and -not $existing.EndsWith("`n")) { 176 | $existing += "`n" 177 | } 178 | $updated = $existing + $block + "`n" 179 | Set-Content -Path $ProfilePath -Value $updated -Encoding UTF8 180 | Write-Host "✅ Shell integration written to $ProfilePath" -ForegroundColor Green 181 | } 182 | 183 | Set-CceShellIntegration -ProfilePath $PROFILE 184 | Write-Host "" 185 | Write-Host "Restart PowerShell or run '. `$PROFILE' to apply the environment automatically." -ForegroundColor Yellow 186 | Write-Host "" 187 | Write-Host "💡 Start using: 'cce list' to manage your Claude configurations!" -ForegroundColor Green 188 | } else { 189 | Write-Host "⚠️ Installation may not be complete" -ForegroundColor Yellow 190 | Write-Host "Please ensure $InstallPath is in your PATH" -ForegroundColor Yellow 191 | } 192 | 193 | Write-Host "" 194 | Write-Host "🎉 Installation completed!" -ForegroundColor Green 195 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # CCE (Claude Config Environment) Installation Script 3 | # Supports Linux, macOS (Intel & Apple Silicon) 4 | 5 | set -e 6 | 7 | # Colors for output 8 | RED='\033[0;31m' 9 | GREEN='\033[0;32m' 10 | YELLOW='\033[1;33m' 11 | BLUE='\033[0;34m' 12 | CYAN='\033[0;36m' 13 | NC='\033[0m' # No Color 14 | 15 | # Configuration 16 | REPO="zhaopengme/cce" 17 | BINARY_NAME="cce" 18 | INSTALL_DIR="$HOME/.local/bin" 19 | 20 | # Function to print colored output 21 | print_status() { 22 | echo -e "${BLUE}🧙 $1${NC}" 23 | } 24 | 25 | print_success() { 26 | echo -e "${GREEN}✅ $1${NC}" 27 | } 28 | 29 | print_warning() { 30 | echo -e "${YELLOW}⚠️ $1${NC}" 31 | } 32 | 33 | print_error() { 34 | echo -e "${RED}❌ $1${NC}" 35 | } 36 | 37 | # Detect OS and architecture 38 | detect_platform() { 39 | local os 40 | local arch 41 | local platform 42 | 43 | case "$(uname -s)" in 44 | Linux*) os="linux" ;; 45 | Darwin*) os="macos" ;; 46 | *) 47 | print_error "Unsupported operating system: $(uname -s)" 48 | exit 1 49 | ;; 50 | esac 51 | 52 | case "$(uname -m)" in 53 | x86_64) arch="x86_64" ;; 54 | arm64|aarch64) 55 | if [[ "$os" == "macos" ]]; then 56 | arch="aarch64" 57 | else 58 | arch="x86_64" # Fallback to x86_64 for Linux ARM 59 | fi 60 | ;; 61 | *) 62 | print_warning "Unsupported architecture $(uname -m), falling back to x86_64" 63 | arch="x86_64" 64 | ;; 65 | esac 66 | 67 | if [[ "$os" == "linux" ]]; then 68 | platform="${BINARY_NAME}-linux-${arch}" 69 | elif [[ "$os" == "macos" ]]; then 70 | platform="${BINARY_NAME}-macos-${arch}" 71 | fi 72 | 73 | echo "$platform" 74 | } 75 | 76 | # Get latest release version 77 | get_latest_version() { 78 | local version 79 | version=$(curl -s "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') 80 | 81 | if [[ -z "$version" ]]; then 82 | print_error "Failed to get latest version from GitHub" 83 | exit 1 84 | fi 85 | 86 | echo "$version" 87 | } 88 | 89 | # Download and install binary 90 | install_cce() { 91 | local platform="$1" 92 | local version="$2" 93 | local download_url="https://github.com/${REPO}/releases/download/${version}/${platform}.tar.gz" 94 | local temp_dir 95 | 96 | temp_dir=$(mktemp -d) 97 | 98 | print_status "Downloading CCE ${version} for ${platform}..." 99 | 100 | if ! curl -L -o "${temp_dir}/${platform}.tar.gz" "$download_url"; then 101 | print_error "Failed to download CCE" 102 | rm -rf "$temp_dir" 103 | exit 1 104 | fi 105 | 106 | print_status "Extracting binary..." 107 | if ! tar -xzf "${temp_dir}/${platform}.tar.gz" -C "$temp_dir"; then 108 | print_error "Failed to extract binary" 109 | rm -rf "$temp_dir" 110 | exit 1 111 | fi 112 | 113 | # Create installation directory if it doesn't exist 114 | mkdir -p "$INSTALL_DIR" 115 | 116 | print_status "Installing to ${INSTALL_DIR}..." 117 | if ! cp "${temp_dir}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}"; then 118 | print_error "Failed to install binary" 119 | rm -rf "$temp_dir" 120 | exit 1 121 | fi 122 | 123 | # Make binary executable 124 | chmod +x "${INSTALL_DIR}/${BINARY_NAME}" 125 | 126 | # Cleanup 127 | rm -rf "$temp_dir" 128 | 129 | print_success "CCE installed successfully to ${INSTALL_DIR}/${BINARY_NAME}" 130 | } 131 | 132 | # Check if binary is in PATH 133 | check_path() { 134 | if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then 135 | print_success "Installation directory is already in PATH" 136 | return 0 137 | else 138 | print_warning "Installation directory is not in PATH" 139 | return 1 140 | fi 141 | } 142 | 143 | # Add to PATH instructions 144 | show_path_instructions() { 145 | echo "" 146 | print_status "To use CCE, you need to add it to your PATH:" 147 | echo "" 148 | echo -e "${CYAN}# For Bash users:${NC}" 149 | echo "echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc" 150 | echo "source ~/.bashrc" 151 | echo "" 152 | echo -e "${CYAN}# For Zsh users:${NC}" 153 | echo "echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.zshrc" 154 | echo "source ~/.zshrc" 155 | echo "" 156 | echo -e "${CYAN}# For Fish users:${NC}" 157 | echo "fish_add_path ~/.local/bin" 158 | echo "" 159 | } 160 | 161 | # Show usage instructions 162 | show_usage() { 163 | local profile 164 | if ! profile=$(detect_shell_profile 2>/dev/null); then 165 | profile="" 166 | fi 167 | 168 | echo "" 169 | print_success "CCE installation completed!" 170 | echo "" 171 | print_status "Usage:" 172 | echo " cce list - List all service providers" 173 | echo " cce add - Add a service provider" 174 | echo " cce delete - Delete a service provider" 175 | echo " cce use - Use specified service provider" 176 | echo " cce check - Check environment variable status" 177 | echo " cce --help - Show detailed help" 178 | echo "" 179 | print_status "Shell integration:" 180 | echo -e "${YELLOW}Installer has configured automatic provider loading for your shell.${NC}" 181 | echo "Open a new terminal to apply the changes." 182 | if [[ -n "$profile" ]]; then 183 | echo "To apply immediately, run:" 184 | echo " source \"$profile\"" 185 | fi 186 | echo "" 187 | print_status "Quick test:" 188 | echo "cce --version" 189 | echo "" 190 | } 191 | 192 | detect_shell_profile() { 193 | local shell_name profile 194 | shell_name=$(basename "${SHELL:-}") 195 | 196 | if [[ -z "$shell_name" ]]; then 197 | if [[ "$(uname -s)" == "Darwin" ]]; then 198 | shell_name="zsh" 199 | else 200 | shell_name="bash" 201 | fi 202 | fi 203 | 204 | case "$shell_name" in 205 | zsh) profile="$HOME/.zshrc" ;; 206 | bash) 207 | if [[ "$(uname -s)" == "Darwin" ]]; then 208 | profile="$HOME/.bash_profile" 209 | else 210 | profile="$HOME/.bashrc" 211 | fi 212 | ;; 213 | *) 214 | return 1 215 | esac 216 | 217 | echo "$profile" 218 | return 0 219 | } 220 | 221 | remove_existing_integration() { 222 | local file="$1" 223 | [[ -f "$file" ]] || return 0 224 | 225 | if grep -Fq ">>> CCE Shell Integration >>>" "$file"; then 226 | if sed --version >/dev/null 2>&1; then 227 | sed -i '/# >>> CCE Shell Integration >>>/,/# <<< CCE Shell Integration <<>> CCE Shell Integration >>>/,/# <<< CCE Shell Integration <<>"$profile" 240 | # >>> CCE Shell Integration >>> 241 | if command -v cce >/dev/null 2>&1; then 242 | _cce_binary="$(command -v cce)" 243 | 244 | cce() { 245 | if [[ "$1" == "use" && -n "$2" ]]; then 246 | local _output 247 | _output=$(CCE_SHELL_INTEGRATION=1 "$_cce_binary" use "$2" 2>/dev/null) 248 | if [[ $? -eq 0 && -n "$_output" ]]; then 249 | eval "$_output" 250 | echo "⚡ Switched to service provider '$2'" 251 | echo "✅ Environment variables are now active in current terminal" 252 | return 0 253 | fi 254 | elif [[ "$1" == "clear" ]]; then 255 | local _output 256 | _output=$(CCE_SHELL_INTEGRATION=1 "$_cce_binary" clear 2>/dev/null) 257 | if [[ $? -eq 0 && -n "$_output" ]]; then 258 | eval "$_output" 259 | echo "🧹 Cleared service provider configuration" 260 | echo "✅ Environment variables are now unset in current terminal" 261 | return 0 262 | fi 263 | fi 264 | 265 | "$_cce_binary" "$@" 266 | } 267 | 268 | __cce_apply_current_provider() { 269 | local _cfg="$HOME/.cce/config.toml" 270 | if [[ -f "$_cfg" ]]; then 271 | local _provider 272 | _provider=$(awk -F'"' '/^current_provider/ {print $2; exit}' "$_cfg") 273 | if [[ -n "$_provider" ]]; then 274 | local _boot_output 275 | _boot_output=$(CCE_SHELL_INTEGRATION=1 "$_cce_binary" use "$_provider" 2>/dev/null) 276 | if [[ $? -eq 0 && -n "$_boot_output" ]]; then 277 | eval "$_boot_output" 278 | fi 279 | fi 280 | fi 281 | } 282 | 283 | __cce_apply_current_provider 284 | unset -f __cce_apply_current_provider 285 | fi 286 | # <<< CCE Shell Integration <<< 287 | EOF 288 | } 289 | 290 | configure_shell_integration() { 291 | local profile status 292 | profile=$(detect_shell_profile 2>/dev/null) 293 | status=$? 294 | if [[ $status -ne 0 || -z "$profile" ]]; then 295 | print_warning "Automatic shell integration is supported for bash and zsh shells. Please configure other shells manually." 296 | return 297 | fi 298 | 299 | print_status "Configuring shell integration at ${profile}..." 300 | 301 | remove_existing_integration "$profile" 302 | append_shell_integration "$profile" 303 | 304 | print_success "Shell integration script added to ${profile}" 305 | } 306 | 307 | # Main installation process 308 | main() { 309 | print_status "Installing CCE (Claude Config Environment)..." 310 | echo "" 311 | 312 | # Detect platform 313 | local platform 314 | platform=$(detect_platform) 315 | print_status "Detected platform: $platform" 316 | 317 | # Get latest version 318 | local version 319 | version=$(get_latest_version) 320 | print_status "Latest version: $version" 321 | 322 | # Install 323 | install_cce "$platform" "$version" 324 | 325 | # Configure shell integration 326 | configure_shell_integration 327 | 328 | # Check PATH and show instructions 329 | if ! check_path; then 330 | show_path_instructions 331 | fi 332 | 333 | # Show usage 334 | show_usage 335 | } 336 | 337 | # Run main function 338 | main "$@" 339 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CCE - Claude Config Environment 2 | [中文版文档](readme_zh.md) 3 | 4 | 🧙 A Claude environment variable switching tool written in Rust, allowing you to easily manage multiple Claude API service providers. 5 | 6 | ## ✨ Features 7 | 8 | - 🔄 **Easy Switching** - Quickly switch between different Claude API service providers 9 | - 📝 **Configuration Management** - Securely store and manage multiple service provider configurations 10 | - 🎨 **User-Friendly Interface** - Colorful output and intuitive command-line interface 11 | - ⚡ **High Performance** - Built with Rust, fast startup and efficient execution 12 | - 🔒 **Secure & Reliable** - Local configuration storage to protect your API keys 13 | 14 | ## 🚀 Quick Start 15 | 16 | ### Installation 17 | 18 | #### Option 1: One-Click Install (Recommended) 19 | ```bash 20 | # Install with curl (supports Linux, macOS Intel & Apple Silicon) 21 | curl -sSL https://raw.githubusercontent.com/zhaopengme/cce/master/install.sh | bash 22 | ``` 23 | 24 | #### Option 2: Download from Releases 25 | ```bash 26 | # Visit https://github.com/zhaopengme/cce/releases 27 | # Download the appropriate binary for your platform: 28 | # - cce-linux-x86_64.tar.gz (Linux) 29 | # - cce-macos-x86_64.tar.gz (macOS Intel) 30 | # - cce-macos-aarch64.tar.gz (macOS Apple Silicon) 31 | # - cce-windows-x86_64.exe.zip (Windows) 32 | 33 | # Extract and install 34 | tar -xzf cce-*.tar.gz 35 | chmod +x cce 36 | mv cce ~/.local/bin/ # Make sure ~/.local/bin is in your PATH 37 | ``` 38 | 39 | #### Option 3: Build from Source 40 | ```bash 41 | # Clone the project 42 | git clone https://github.com/zhaopengme/cce.git 43 | cd cce 44 | 45 | # Build the project 46 | cargo build --release 47 | 48 | # Install (optional) 49 | cargo install --path . 50 | ``` 51 | 52 | #### Option 4: Windows PowerShell 53 | ```powershell 54 | # Download and run the PowerShell installer 55 | Invoke-WebRequest -Uri "https://raw.githubusercontent.com/zhaopengme/cce/master/install.ps1" -OutFile "install.ps1" 56 | .\install.ps1 57 | ``` 58 | 59 | ### Setup Shell Integration 60 | 61 | The key feature of CCE is the ability to make `cce use` and `cce clear` commands take effect immediately in your current terminal. 62 | 63 | **🚀 Automatic Setup (Recommended):** 64 | ```bash 65 | cce install 66 | ``` 67 | 68 | This command will automatically: 69 | - 🔍 Detect your shell (bash or zsh) 70 | - ✅ Append a CCE block to your shell profile 71 | - ⚡ Load the most recent provider every time a new terminal starts 72 | - 🪄 Wrap the `cce` command so `cce use` / `cce clear` update the current session instantly 73 | 74 | After installation completes, open a fresh terminal to pick up the changes. To activate immediately, run `source ~/.zshrc` or `source ~/.bashrc`. 75 | 76 | **🔧 Manual Setup (other shells or custom setups)** 77 | ```bash 78 | # Add this block to your shell configuration file (~/.zshrc, ~/.bashrc, etc.) 79 | if command -v cce >/dev/null 2>&1; then 80 | if [[ -f "$HOME/.cce/config.toml" ]]; then 81 | current_provider=$(awk -F'"' '/^current_provider/ {print $2; exit}' "$HOME/.cce/config.toml") 82 | if [[ -n "$current_provider" ]]; then 83 | eval "$(CCE_SHELL_INTEGRATION=1 cce use "$current_provider")" 84 | fi 85 | fi 86 | fi 87 | 88 | # Optional: add the shortcut wrapper identical to the installer 89 | eval "$(cce shellenv)" 90 | ``` 91 | 92 | **Note**: The Windows PowerShell installer writes the integration block to `$PROFILE` so each session automatically loads your last-used provider. 93 | 94 | ### Basic Usage 95 | 96 | #### 1. List all service providers 97 | ```bash 98 | cce list 99 | ``` 100 | 101 | #### 2. Add a service provider 102 | ```bash 103 | cce add [--model ] 104 | 105 | # Examples 106 | cce add anthropic https://api.anthropic.com sk-ant-api03-xxxx 107 | cce add custom https://custom-claude-api.com custom-token-123 108 | 109 | # With model specification (v0.2.0+) 110 | cce add my-provider https://api.example.com/v1 sk-token-123 --model claude-3-5-sonnet-20250229 111 | # or using short option 112 | cce add my-provider https://api.example.com/v1 sk-token-123 -m claude-3-5-sonnet-20250229 113 | ``` 114 | 115 | #### 3. Delete a service provider 116 | ```bash 117 | cce delete 118 | 119 | # Examples 120 | cce delete anthropic 121 | ``` 122 | 123 | #### 4. Switch service provider ⭐ 124 | 125 | **With shell integration (recommended)**: 126 | ```bash 127 | cce use anthropic 128 | # ⚡ Switched to service provider 'anthropic' 129 | # ✅ Environment variables are now active in current terminal 130 | ``` 131 | 132 | **Without shell integration**: 133 | ```bash 134 | eval "$(CCE_SHELL_INTEGRATION=1 cce use anthropic)" 135 | ``` 136 | 137 | #### 5. Clear environment variables (switch back to official Claude client) 138 | ```bash 139 | cce clear 140 | ``` 141 | 142 | This will unset `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL`, allowing you to use your Claude Pro/Max subscription with the official client. 143 | 144 | #### 6. Check environment variable status 145 | ```bash 146 | cce check 147 | ``` 148 | 149 | ## 📋 Command Reference 150 | 151 | ### `cce shellenv` 152 | Outputs the helper function the installer uses for bash/zsh. Run `eval "$(cce shellenv)"` if you need to install the wrapper manually or customize it. 153 | 154 | ### `cce list` 155 | Display all configured service providers with their status: 156 | - Provider name 157 | - API URL 158 | - Masked token preview 159 | - Current active status 160 | 161 | ### `cce add [--model ]` 162 | Add a new service provider: 163 | - `name`: Custom provider name 164 | - `api_url`: Claude API endpoint URL 165 | - `token`: API access token 166 | - `--model` / `-m`: Optional model name (v0.2.0+) 167 | 168 | If the provider already exists, it will be overwritten. When a model is specified, `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and `ANTHROPIC_DEFAULT_HAIKU_MODEL` environment variables will be exported when using this provider. 169 | 170 | ### `cce delete ` 171 | Remove the specified service provider. No confirmation required. 172 | 173 | ### `cce use ` 174 | Switch to the specified service provider. By default this command prints a short confirmation message. 175 | 176 | For scripts or shell integration, set `CCE_SHELL_INTEGRATION=1` to emit environment variable commands: 177 | 178 | ```bash 179 | eval "$(CCE_SHELL_INTEGRATION=1 cce use )" 180 | ``` 181 | 182 | ### `cce check` 183 | Verify current environment variable status: 184 | - Display current environment variables 185 | - Compare CCE configuration with actual environment variables 186 | - Provide suggestions when there are mismatches 187 | 188 | ### `cce clear` 189 | Clear environment variables to switch back to using the official Claude client. 190 | 191 | For scripts or shell integration, set `CCE_SHELL_INTEGRATION=1` to emit unset commands: 192 | 193 | ```bash 194 | eval "$(CCE_SHELL_INTEGRATION=1 cce clear)" 195 | ``` 196 | 197 | This command will: 198 | - Unset `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and `ANTHROPIC_DEFAULT_HAIKU_MODEL` environment variables 199 | - Clear the current provider selection in configuration 200 | - Allow you to use your Claude Pro/Max subscription with the official client 201 | 202 | ### `cce install [--force]` 203 | Automatically install shell integration for immediate environment variable effects: 204 | 205 | **Normal mode** (`cce install`): 206 | - 🔍 Detect your current shell (bash or zsh) 207 | - ✅ Check if integration is already installed 208 | - 📝 Add integration to appropriate config file 209 | - 💡 Provide activation instructions 210 | 211 | **Force mode** (`cce install --force`): 212 | - 🔄 Force reinstall even if already present 213 | - 📝 Add integration regardless of existing setup 214 | 215 | This command currently supports: 216 | - **Bash**: Appends to `~/.bashrc` (Linux) or `~/.bash_profile` (macOS) 217 | - **Zsh**: Appends to `~/.zshrc` 218 | - **PowerShell**: `install.ps1` writes to `$PROFILE` 219 | - Other shells: see the “Manual Setup” example above for guidance 220 | 221 | After installation, restart your terminal or run `source ~/.zshrc` (or equivalent) to activate. 222 | 223 | ## 🔧 Configuration 224 | 225 | Configuration file is stored at `~/.cce/config.toml`: 226 | 227 | ```toml 228 | current_provider = "anthropic" 229 | 230 | [providers.anthropic] 231 | name = "anthropic" 232 | api_url = "https://api.anthropic.com" 233 | token = "sk-ant-api03-your-token-here" 234 | 235 | [providers.custom] 236 | name = "custom" 237 | api_url = "https://custom-claude-api.com" 238 | token = "custom-token-123" 239 | 240 | [providers.my-provider] 241 | name = "my-provider" 242 | api_url = "https://api.example.com/v1" 243 | token = "sk-token-123" 244 | model = "claude-3-5-sonnet-20250229" 245 | ``` 246 | 247 | ## 🌍 Environment Variables 248 | 249 | After using `cce use` command, the following environment variables are automatically set: 250 | - `ANTHROPIC_AUTH_TOKEN`: API authentication token 251 | - `ANTHROPIC_BASE_URL`: API base URL 252 | - `ANTHROPIC_MODEL`: Model name (if specified with --model when adding provider) 253 | - `ANTHROPIC_DEFAULT_OPUS_MODEL`: Default Opus model (if specified with --model when adding provider) 254 | - `ANTHROPIC_DEFAULT_SONNET_MODEL`: Default Sonnet model (if specified with --model when adding provider) 255 | - `ANTHROPIC_DEFAULT_HAIKU_MODEL`: Default Haiku model (if specified with --model when adding provider) 256 | 257 | ## 💡 Usage Tips 258 | 259 | ### 1. Quick Switching 260 | ```bash 261 | # Add common providers 262 | cce add prod https://api.anthropic.com sk-ant-prod-xxx 263 | cce add dev https://dev-api.example.com dev-token-xxx 264 | 265 | # Quick switch (with shell integration) 266 | cce use prod 267 | cce use dev 268 | ``` 269 | 270 | ### 2. Script Usage 271 | ```bash 272 | #!/bin/bash 273 | eval "$(CCE_SHELL_INTEGRATION=1 cce use anthropic)" 274 | # Environment variables are now set and ready to use 275 | curl -H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" "$ANTHROPIC_BASE_URL/v1/messages" 276 | ``` 277 | 278 | ### 3. Verify Configuration 279 | ```bash 280 | cce check # Check current status 281 | echo $ANTHROPIC_AUTH_TOKEN # Verify token 282 | echo $ANTHROPIC_BASE_URL # Verify URL 283 | ``` 284 | 285 | ### 4. Backup Configuration 286 | ```bash 287 | cp ~/.cce/config.toml ~/.cce/config.toml.backup 288 | ``` 289 | 290 | ## 📥 Platform Support 291 | 292 | | Platform | Architecture | Binary | Status | 293 | |----------|-------------|---------|---------| 294 | | Linux | x86_64 | `cce-linux-x86_64.tar.gz` | ✅ | 295 | | macOS | Intel (x86_64) | `cce-macos-x86_64.tar.gz` | ✅ | 296 | | macOS | Apple Silicon (ARM64) | `cce-macos-aarch64.tar.gz` | ✅ | 297 | | Windows | x86_64 | `cce-windows-x86_64.exe.zip` | ✅ | 298 | 299 | All releases include automated CI/CD testing across multiple platforms to ensure reliability. 300 | 301 | ## 🐛 Troubleshooting 302 | 303 | ### Installation Issues 304 | If the one-click install fails: 305 | ```bash 306 | # Check if curl is available 307 | curl --version 308 | 309 | # Check if your platform is supported 310 | uname -s && uname -m 311 | 312 | # Try manual installation from releases page 313 | # https://github.com/zhaopengme/cce/releases 314 | ``` 315 | 316 | ### Shell Integration Not Working 317 | - Re-run `cce install --force` to regenerate the integration block in your profile. 318 | - Verify the CCE block exists in your profile file: 319 | ```bash 320 | grep -n "CCE Shell Integration" ~/.zshrc 321 | ``` 322 | - If you need to refresh the current terminal manually, run: 323 | ```bash 324 | eval "$(CCE_SHELL_INTEGRATION=1 cce use )" 325 | ``` 326 | 327 | ### Environment Variables Not Set 328 | Run `cce check` to diagnose the issue and follow the suggestions. 329 | 330 | ### PATH Issues 331 | If `cce` command is not found after installation: 332 | ```bash 333 | # Add ~/.local/bin to your PATH 334 | echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc 335 | source ~/.zshrc 336 | 337 | # Verify installation 338 | which cce 339 | cce --version 340 | ``` 341 | 342 | ### Configuration File Corrupted 343 | If the config file is corrupted, you can delete and recreate it: 344 | ```bash 345 | rm -rf ~/.cce 346 | ``` 347 | 348 | ## 🤝 Contributing 349 | 350 | Issues and Pull Requests are welcome! 351 | 352 | ## 📞 Contact 353 | 354 | - Author: [@zhaopengme](https://x.com/zhaopengme) 355 | - Twitter: https://x.com/zhaopengme 356 | 357 | ## 📄 License 358 | 359 | MIT License 360 | -------------------------------------------------------------------------------- /src/provider.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{Config, Provider}; 2 | use crate::constants::*; 3 | use anyhow::Result; 4 | use colored::*; 5 | 6 | pub struct ProviderManager; 7 | 8 | impl ProviderManager { 9 | pub fn list_providers(config: &Config) -> Result<()> { 10 | if config.providers.is_empty() { 11 | println!("{}", "No service providers configured".yellow()); 12 | return Ok(()); 13 | } 14 | 15 | println!("{}", "Configured service providers:".blue().bold()); 16 | println!(); 17 | 18 | for (name, provider) in &config.providers { 19 | let is_current = config.current_provider.as_ref() == Some(name); 20 | 21 | let marker = if is_current { 22 | "●".green() 23 | } else { 24 | "○".white() 25 | }; 26 | let name_color = if is_current { 27 | name.green().bold() 28 | } else { 29 | name.white() 30 | }; 31 | 32 | println!(" {} {}", marker, name_color); 33 | println!(" API URL: {}", provider.api_url.cyan()); 34 | println!( 35 | " Token: {}****", 36 | &provider.token[..provider.token.len().min(8)].dimmed() 37 | ); 38 | if let Some(ref model) = provider.model { 39 | println!(" Model: {}", model.cyan()); 40 | } 41 | 42 | if is_current { 43 | println!(" {}", "(currently active)".green().italic()); 44 | } 45 | println!(); 46 | } 47 | 48 | Ok(()) 49 | } 50 | 51 | pub fn add_provider( 52 | config: &mut Config, 53 | name: String, 54 | api_url: String, 55 | token: String, 56 | model: Option, 57 | ) -> Result<()> { 58 | if config.providers.contains_key(&name) { 59 | println!( 60 | "{} Service provider '{}' already exists, overwriting", 61 | "⚠️".yellow(), 62 | name.yellow() 63 | ); 64 | } 65 | 66 | config.add_provider(name.clone(), api_url, token, model); 67 | config.save()?; 68 | 69 | println!( 70 | "{} Successfully added service provider '{}'", 71 | "✅".green(), 72 | name.green().bold() 73 | ); 74 | Ok(()) 75 | } 76 | 77 | pub fn remove_provider(config: &mut Config, name: &str) -> Result<()> { 78 | if !config.providers.contains_key(name) { 79 | println!( 80 | "{} Service provider '{}' does not exist", 81 | "❌".red(), 82 | name.red() 83 | ); 84 | return Ok(()); 85 | } 86 | 87 | config.remove_provider(name); 88 | config.save()?; 89 | 90 | println!( 91 | "{} Successfully removed service provider '{}'", 92 | "🗑️".green(), 93 | name.green().bold() 94 | ); 95 | Ok(()) 96 | } 97 | 98 | pub fn use_provider(config: &mut Config, name: &str) -> Result<()> { 99 | if !config.providers.contains_key(name) { 100 | println!( 101 | "{} Service provider '{}' does not exist", 102 | "❌".red(), 103 | name.red() 104 | ); 105 | return Ok(()); 106 | } 107 | 108 | let shell_mode = Self::shell_integration_active(); 109 | 110 | if let Some(current) = &config.current_provider { 111 | if current == name && !shell_mode { 112 | println!( 113 | "{} Already using service provider '{}'", 114 | "ℹ️".blue(), 115 | name.blue().bold() 116 | ); 117 | return Ok(()); 118 | } 119 | } 120 | 121 | let provider = config 122 | .providers 123 | .get(name) 124 | .expect("Provider should exist after check") 125 | .clone(); 126 | 127 | // Set environment variables 128 | config.set_current_provider(name); 129 | config.save()?; 130 | 131 | set_provider_env_vars(&provider); 132 | 133 | if shell_mode { 134 | Self::emit_export_commands(&provider); 135 | } else { 136 | println!( 137 | "{} Switched to service provider '{}'", 138 | "🔄".green(), 139 | name.green().bold() 140 | ); 141 | println!(" API URL: {}", provider.api_url.cyan()); 142 | } 143 | 144 | Ok(()) 145 | } 146 | 147 | pub fn check_environment(config: &Config) -> Result<()> { 148 | println!( 149 | "{}", 150 | "🔍 Checking environment variable status".blue().bold() 151 | ); 152 | println!(); 153 | 154 | // Check current environment variables 155 | let current_api_key = std::env::var(ENV_AUTH_TOKEN); 156 | let current_api_url = std::env::var(ENV_BASE_URL); 157 | 158 | println!("{}", "Current environment variables:".cyan().bold()); 159 | match ¤t_api_key { 160 | Ok(key) => { 161 | let masked_key = if key.len() > 8 { 162 | format!("{}****", &key[..8]) 163 | } else { 164 | "****".to_string() 165 | }; 166 | println!(" {}: {}", ENV_AUTH_TOKEN, masked_key.green()); 167 | } 168 | Err(_) => { 169 | println!(" {}: {}", ENV_AUTH_TOKEN, "Not set".red()); 170 | } 171 | } 172 | 173 | match ¤t_api_url { 174 | Ok(url) => { 175 | println!(" {}: {}", ENV_BASE_URL, url.green()); 176 | } 177 | Err(_) => { 178 | println!(" {}: {}", ENV_BASE_URL, "Not set".red()); 179 | } 180 | } 181 | 182 | println!(); 183 | 184 | // Check configuration status 185 | if let Some(current_provider) = &config.current_provider { 186 | if let Some(provider) = config.providers.get(current_provider) { 187 | println!("{}", "CCE configuration status:".cyan().bold()); 188 | println!(" Current provider: {}", current_provider.green().bold()); 189 | println!(" Configured URL: {}", provider.api_url.cyan()); 190 | 191 | // Verify if environment variables match configuration 192 | let env_matches = match (¤t_api_key, ¤t_api_url) { 193 | (Ok(env_key), Ok(env_url)) => { 194 | env_key == &provider.token && env_url == &provider.api_url 195 | } 196 | _ => false, 197 | }; 198 | 199 | if env_matches { 200 | println!( 201 | " Status: {}", 202 | "✅ Environment variables match configuration".green() 203 | ); 204 | } else { 205 | println!( 206 | " Status: {}", 207 | "⚠️ Environment variables do not match configuration".yellow() 208 | ); 209 | println!( 210 | " Suggestion: Run 'cce use {}' to reset", 211 | current_provider.cyan() 212 | ); 213 | } 214 | } else { 215 | println!( 216 | "{}", 217 | "❌ Configuration error: Current provider does not exist".red() 218 | ); 219 | } 220 | } else { 221 | println!("{}", "CCE configuration status:".cyan().bold()); 222 | println!(" Current provider: {}", "None selected".yellow()); 223 | if !config.providers.is_empty() { 224 | println!(" Suggestion: Use 'cce use ' to select a provider"); 225 | } else { 226 | println!(" Suggestion: Use 'cce add' to add a service provider"); 227 | } 228 | } 229 | 230 | Ok(()) 231 | } 232 | 233 | pub fn clear_provider(config: &mut Config) -> Result<()> { 234 | // Check if there's a current provider to clear 235 | if config.current_provider.is_none() { 236 | println!("{} No service provider is currently active", "ℹ️".blue()); 237 | return Ok(()); 238 | } 239 | 240 | let previous_provider = config.current_provider.clone(); 241 | 242 | // Clear current provider in config 243 | config.clear_current_provider(); 244 | config.save()?; 245 | 246 | let shell_mode = Self::shell_integration_active(); 247 | 248 | if !shell_mode { 249 | if let Some(provider_name) = previous_provider { 250 | println!("{} Cleared service provider configuration", "🧹".green()); 251 | println!( 252 | "{} Removed '{}' as the active provider", 253 | "✓".green(), 254 | provider_name.yellow() 255 | ); 256 | } 257 | } 258 | 259 | clear_all_env_vars(); 260 | 261 | if !shell_mode { 262 | println!( 263 | "{}", 264 | "Environment variables cleared from current session".green() 265 | ); 266 | } 267 | 268 | if shell_mode { 269 | Self::emit_unset_commands(); 270 | } 271 | 272 | Ok(()) 273 | } 274 | 275 | fn emit_export_commands(provider: &Provider) { 276 | println!("{}", generate_export_commands(provider)); 277 | } 278 | 279 | fn emit_unset_commands() { 280 | // Output unset commands for shell 281 | println!("{}", generate_unset_commands()); 282 | } 283 | 284 | fn shell_integration_active() -> bool { 285 | std::env::var(ENV_SHELL_INTEGRATION) 286 | .map(|v| v == "1") 287 | .unwrap_or(false) 288 | } 289 | 290 | pub fn install_shell_integration(force: bool) -> Result<()> { 291 | use std::fs::{File, OpenOptions}; 292 | use std::io::{BufRead, BufReader, Write}; 293 | 294 | // Detect shell type 295 | let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()); 296 | let shell_name = shell.split('/').last().unwrap_or("bash"); 297 | 298 | let (config_file, comment_prefix) = match shell_name { 299 | "zsh" => ("~/.zshrc", "#"), 300 | "bash" => ("~/.bashrc", "#"), 301 | "fish" => ("~/.config/fish/config.fish", "#"), 302 | _ => ("~/.bashrc", "#"), 303 | }; 304 | 305 | // Expand tilde 306 | let config_path = if config_file.starts_with("~/") { 307 | let home = dirs::home_dir() 308 | .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; 309 | home.join(&config_file[2..]) 310 | } else { 311 | std::path::PathBuf::from(config_file) 312 | }; 313 | 314 | // Check if already installed 315 | let integration_line = r#"eval "$(cce shellenv)""#; 316 | let mut already_installed = false; 317 | 318 | if config_path.exists() { 319 | let file = File::open(&config_path)?; 320 | let reader = BufReader::new(file); 321 | 322 | for line in reader.lines() { 323 | let line = line?; 324 | let trimmed = line.trim(); 325 | // Skip commented lines 326 | if !trimmed.starts_with('#') && trimmed == integration_line { 327 | already_installed = true; 328 | break; 329 | } 330 | } 331 | } 332 | 333 | if already_installed && !force { 334 | println!( 335 | "{} Shell integration is already installed in {}", 336 | "ℹ️".blue(), 337 | config_file.cyan() 338 | ); 339 | println!( 340 | "{} Use {} to reinstall", 341 | "💡".blue(), 342 | "cce install --force".yellow() 343 | ); 344 | return Ok(()); 345 | } 346 | 347 | // Create config directory if it doesn't exist (for fish) 348 | if let Some(parent) = config_path.parent() { 349 | std::fs::create_dir_all(parent)?; 350 | } 351 | 352 | // Add shell integration 353 | let mut file = OpenOptions::new() 354 | .create(true) 355 | .append(true) 356 | .open(&config_path)?; 357 | 358 | let integration_block = format!( 359 | r#" 360 | {} CCE Shell Integration 361 | {}"#, 362 | comment_prefix, integration_line 363 | ); 364 | 365 | writeln!(file, "{}", integration_block)?; 366 | 367 | println!("{} Shell integration installed successfully!", "✅".green()); 368 | println!("📄 Added to: {}", config_path.display().to_string().cyan()); 369 | println!(); 370 | println!("{} To activate in current terminal:", "🔄".blue().bold()); 371 | println!(" {}", format!("source {}", config_file).yellow()); 372 | println!(); 373 | println!( 374 | "{} Or restart your terminal for changes to take effect.", 375 | "🆕".blue().bold() 376 | ); 377 | 378 | Ok(()) 379 | } 380 | 381 | pub fn output_shellenv() -> Result<()> { 382 | // Get current executable path 383 | let current_exe = 384 | std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("cce")); 385 | let cce_path = current_exe.display(); 386 | 387 | // Output complete shell function definition 388 | println!( 389 | r#"cce() {{ 390 | local cce_binary="{}" 391 | 392 | if [[ "$1" == "use" && -n "$2" ]]; then 393 | local env_output 394 | env_output=$(CCE_SHELL_INTEGRATION=1 "$cce_binary" use "$2" 2>/dev/null) 395 | if [[ $? -eq 0 && -n "$env_output" ]]; then 396 | eval "$env_output" 397 | echo "⚡ Switched to service provider '$2'" 398 | echo "✅ Environment variables are now active in current terminal" 399 | else 400 | "$cce_binary" "$@" 401 | fi 402 | elif [[ "$1" == "clear" ]]; then 403 | local env_output 404 | env_output=$(CCE_SHELL_INTEGRATION=1 "$cce_binary" clear 2>/dev/null) 405 | if [[ $? -eq 0 && -n "$env_output" ]]; then 406 | eval "$env_output" 407 | echo "🧹 Cleared service provider configuration" 408 | echo "✅ Environment variables are now unset in current terminal" 409 | else 410 | "$cce_binary" "$@" 411 | fi 412 | else 413 | "$cce_binary" "$@" 414 | fi 415 | }} 416 | 417 | # Auto-load current provider on shell startup 418 | if [[ -f ~/.cce/config.toml ]]; then 419 | _cce_current=$(awk -F\" '/^current_provider/ {{print $2}}' ~/.cce/config.toml) 420 | if [[ -n "$_cce_current" ]]; then 421 | eval "$(CCE_SHELL_INTEGRATION=1 "{}" use "$_cce_current" 2>/dev/null)" 422 | fi 423 | unset _cce_current 424 | fi"#, 425 | cce_path, cce_path 426 | ); 427 | 428 | Ok(()) 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{Config, Provider}; 2 | use crate::constants::*; 3 | use anyhow::Result; 4 | use crossterm::{ 5 | event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, 6 | execute, 7 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 8 | }; 9 | use ratatui::{ 10 | backend::{Backend, CrosstermBackend}, 11 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 12 | style::{Color, Modifier, Style}, 13 | text::{Line, Span}, 14 | widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, 15 | Frame, Terminal, 16 | }; 17 | use std::io; 18 | 19 | enum InputMode { 20 | Normal, 21 | AddProvider(AddProviderState), 22 | DeleteConfirm, 23 | } 24 | 25 | #[derive(Default)] 26 | struct AddProviderState { 27 | current_field: usize, // 0: name, 1: url, 2: token, 3: model (optional) 28 | name: String, 29 | url: String, 30 | token: String, 31 | model: String, 32 | } 33 | 34 | pub struct TuiApp { 35 | config: Config, 36 | list_state: ListState, 37 | input_mode: InputMode, 38 | message: Option, 39 | message_is_error: bool, 40 | } 41 | 42 | impl TuiApp { 43 | pub fn new(config: Config) -> Self { 44 | let mut list_state = ListState::default(); 45 | if !config.providers.is_empty() { 46 | list_state.select(Some(0)); 47 | } 48 | 49 | Self { 50 | config, 51 | list_state, 52 | input_mode: InputMode::Normal, 53 | message: None, 54 | message_is_error: false, 55 | } 56 | } 57 | 58 | fn next(&mut self) { 59 | if self.config.providers.is_empty() { 60 | return; 61 | } 62 | let i = match self.list_state.selected() { 63 | Some(i) => { 64 | if i >= self.config.providers.len() - 1 { 65 | 0 66 | } else { 67 | i + 1 68 | } 69 | } 70 | None => 0, 71 | }; 72 | self.list_state.select(Some(i)); 73 | } 74 | 75 | fn previous(&mut self) { 76 | if self.config.providers.is_empty() { 77 | return; 78 | } 79 | let i = match self.list_state.selected() { 80 | Some(i) => { 81 | if i == 0 { 82 | self.config.providers.len() - 1 83 | } else { 84 | i - 1 85 | } 86 | } 87 | None => 0, 88 | }; 89 | self.list_state.select(Some(i)); 90 | } 91 | 92 | fn get_selected_provider(&self) -> Option<&Provider> { 93 | if let Some(selected) = self.list_state.selected() { 94 | self.config.providers.values().nth(selected) 95 | } else { 96 | None 97 | } 98 | } 99 | 100 | fn use_provider(&mut self) -> Result<()> { 101 | if let Some(provider) = self.get_selected_provider() { 102 | let name = provider.name.clone(); 103 | let token = provider.token.clone(); 104 | let api_url = provider.api_url.clone(); 105 | let model = provider.model.clone(); 106 | 107 | self.config.set_current_provider(&name); 108 | self.config.save()?; 109 | 110 | // Set environment variables 111 | let provider = Provider { 112 | name: name.clone(), 113 | api_url: api_url.clone(), 114 | token: token.clone(), 115 | model: model.clone(), 116 | }; 117 | set_provider_env_vars(&provider); 118 | 119 | self.message = Some(format!("Switched to provider '{}'", name)); 120 | self.message_is_error = false; 121 | } 122 | Ok(()) 123 | } 124 | 125 | fn clear_provider(&mut self) -> Result<()> { 126 | self.config.clear_current_provider(); 127 | self.config.save()?; 128 | 129 | clear_all_env_vars(); 130 | 131 | self.message = Some("Cleared current provider".to_string()); 132 | self.message_is_error = false; 133 | Ok(()) 134 | } 135 | 136 | fn delete_provider(&mut self) -> Result<()> { 137 | if let Some(provider) = self.get_selected_provider() { 138 | let name = provider.name.clone(); 139 | self.config.remove_provider(&name); 140 | self.config.save()?; 141 | 142 | // Adjust selection 143 | if self.config.providers.is_empty() { 144 | self.list_state.select(None); 145 | } else if let Some(selected) = self.list_state.selected() { 146 | if selected >= self.config.providers.len() { 147 | self.list_state 148 | .select(Some(self.config.providers.len() - 1)); 149 | } 150 | } 151 | 152 | self.message = Some(format!("Deleted provider '{}'", name)); 153 | self.message_is_error = false; 154 | } 155 | Ok(()) 156 | } 157 | 158 | fn save_new_provider(&mut self) -> Result<()> { 159 | if let InputMode::AddProvider(state) = &self.input_mode { 160 | if state.name.is_empty() || state.url.is_empty() || state.token.is_empty() { 161 | self.message = Some("Name, URL, and Token are required".to_string()); 162 | self.message_is_error = true; 163 | return Ok(()); 164 | } 165 | 166 | let model = if state.model.is_empty() { 167 | None 168 | } else { 169 | Some(state.model.clone()) 170 | }; 171 | 172 | self.config.add_provider( 173 | state.name.clone(), 174 | state.url.clone(), 175 | state.token.clone(), 176 | model, 177 | ); 178 | self.config.save()?; 179 | 180 | // Select the newly added provider 181 | let index = self.config.providers.len() - 1; 182 | self.list_state.select(Some(index)); 183 | 184 | self.message = Some(format!("Added provider '{}'", state.name)); 185 | self.message_is_error = false; 186 | } 187 | 188 | self.input_mode = InputMode::Normal; 189 | Ok(()) 190 | } 191 | 192 | fn handle_input(&mut self, key: KeyCode, modifiers: KeyModifiers) -> Result { 193 | match &mut self.input_mode { 194 | InputMode::Normal => match key { 195 | KeyCode::Char('q') | KeyCode::Esc => return Ok(true), 196 | KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), 197 | KeyCode::Down | KeyCode::Char('j') => self.next(), 198 | KeyCode::Up | KeyCode::Char('k') => self.previous(), 199 | KeyCode::Enter | KeyCode::Char('u') => self.use_provider()?, 200 | KeyCode::Char('a') => { 201 | self.input_mode = InputMode::AddProvider(AddProviderState::default()); 202 | self.message = None; 203 | } 204 | KeyCode::Char('d') => { 205 | if self.get_selected_provider().is_some() { 206 | self.input_mode = InputMode::DeleteConfirm; 207 | self.message = None; 208 | } 209 | } 210 | KeyCode::Char('c') => self.clear_provider()?, 211 | _ => {} 212 | }, 213 | InputMode::AddProvider(state) => match key { 214 | KeyCode::Esc => { 215 | self.input_mode = InputMode::Normal; 216 | self.message = None; 217 | } 218 | KeyCode::Tab => { 219 | state.current_field = (state.current_field + 1) % 4; 220 | } 221 | KeyCode::BackTab => { 222 | state.current_field = if state.current_field == 0 { 223 | 3 224 | } else { 225 | state.current_field - 1 226 | }; 227 | } 228 | KeyCode::Enter => { 229 | if state.current_field == 3 { 230 | self.save_new_provider()?; 231 | } else { 232 | state.current_field += 1; 233 | } 234 | } 235 | KeyCode::Backspace => match state.current_field { 236 | 0 => { 237 | state.name.pop(); 238 | } 239 | 1 => { 240 | state.url.pop(); 241 | } 242 | 2 => { 243 | state.token.pop(); 244 | } 245 | 3 => { 246 | state.model.pop(); 247 | } 248 | _ => {} 249 | }, 250 | KeyCode::Char(c) => match state.current_field { 251 | 0 => state.name.push(c), 252 | 1 => state.url.push(c), 253 | 2 => state.token.push(c), 254 | 3 => state.model.push(c), 255 | _ => {} 256 | }, 257 | _ => {} 258 | }, 259 | InputMode::DeleteConfirm => match key { 260 | KeyCode::Char('y') | KeyCode::Char('Y') => { 261 | self.delete_provider()?; 262 | self.input_mode = InputMode::Normal; 263 | } 264 | KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { 265 | self.input_mode = InputMode::Normal; 266 | self.message = None; 267 | } 268 | _ => {} 269 | }, 270 | } 271 | 272 | Ok(false) 273 | } 274 | } 275 | 276 | pub fn run_tui(config: Config) -> Result<()> { 277 | // Setup terminal 278 | enable_raw_mode()?; 279 | let mut stdout = io::stdout(); 280 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 281 | let backend = CrosstermBackend::new(stdout); 282 | let mut terminal = Terminal::new(backend)?; 283 | 284 | // Create app state 285 | let mut app = TuiApp::new(config); 286 | 287 | // Run app 288 | let res = run_app(&mut terminal, &mut app); 289 | 290 | // Restore terminal 291 | disable_raw_mode()?; 292 | execute!( 293 | terminal.backend_mut(), 294 | LeaveAlternateScreen, 295 | DisableMouseCapture 296 | )?; 297 | terminal.show_cursor()?; 298 | 299 | if let Err(err) = res { 300 | println!("Error: {:?}", err); 301 | } 302 | 303 | Ok(()) 304 | } 305 | 306 | fn run_app(terminal: &mut Terminal, app: &mut TuiApp) -> Result<()> { 307 | loop { 308 | terminal.draw(|f| ui(f, app))?; 309 | 310 | if let Event::Key(key) = event::read()? { 311 | if app.handle_input(key.code, key.modifiers)? { 312 | return Ok(()); 313 | } 314 | } 315 | } 316 | } 317 | 318 | fn ui(f: &mut Frame, app: &mut TuiApp) { 319 | let chunks = Layout::default() 320 | .direction(Direction::Vertical) 321 | .margin(2) 322 | .constraints([ 323 | Constraint::Length(3), 324 | Constraint::Min(10), 325 | Constraint::Length(3), 326 | Constraint::Length(3), 327 | ]) 328 | .split(f.size()); 329 | 330 | // Title 331 | let title = Paragraph::new("CCE - Claude Config Environment") 332 | .style( 333 | Style::default() 334 | .fg(Color::Cyan) 335 | .add_modifier(Modifier::BOLD), 336 | ) 337 | .alignment(Alignment::Center) 338 | .block(Block::default().borders(Borders::ALL)); 339 | f.render_widget(title, chunks[0]); 340 | 341 | // Main content area 342 | match &app.input_mode { 343 | InputMode::Normal => { 344 | render_provider_list(f, app, chunks[1]); 345 | } 346 | InputMode::AddProvider(state) => { 347 | render_add_provider_form(f, state, chunks[1]); 348 | } 349 | InputMode::DeleteConfirm => { 350 | render_provider_list(f, app, chunks[1]); 351 | render_delete_confirmation(f, app, chunks[1]); 352 | } 353 | } 354 | 355 | // Status message 356 | if let Some(ref message) = app.message { 357 | let style = if app.message_is_error { 358 | Style::default().fg(Color::Red) 359 | } else { 360 | Style::default().fg(Color::Green) 361 | }; 362 | let msg = Paragraph::new(message.as_str()) 363 | .style(style) 364 | .alignment(Alignment::Center) 365 | .block(Block::default().borders(Borders::ALL).title("Status")); 366 | f.render_widget(msg, chunks[2]); 367 | } else { 368 | let msg = Paragraph::new("").block(Block::default().borders(Borders::ALL).title("Status")); 369 | f.render_widget(msg, chunks[2]); 370 | } 371 | 372 | // Help text 373 | let help_text = match &app.input_mode { 374 | InputMode::Normal => { 375 | "↑/↓: Navigate | Enter/u: Use Provider | a: Add | d: Delete | c: Clear | q/Esc: Quit" 376 | } 377 | InputMode::AddProvider(_) => "Tab/Shift+Tab: Next/Prev Field | Enter: Save | Esc: Cancel", 378 | InputMode::DeleteConfirm => "y: Confirm Delete | n/Esc: Cancel", 379 | }; 380 | let help = Paragraph::new(help_text) 381 | .style(Style::default().fg(Color::Yellow)) 382 | .alignment(Alignment::Center) 383 | .block(Block::default().borders(Borders::ALL).title("Help")); 384 | f.render_widget(help, chunks[3]); 385 | } 386 | 387 | fn render_provider_list(f: &mut Frame, app: &mut TuiApp, area: Rect) { 388 | let items: Vec = app 389 | .config 390 | .providers 391 | .values() 392 | .map(|provider| { 393 | let is_current = app.config.current_provider.as_ref() == Some(&provider.name); 394 | let marker = if is_current { "● " } else { "○ " }; 395 | 396 | let mut lines = vec![ 397 | Line::from(vec![ 398 | Span::styled( 399 | marker, 400 | Style::default().fg(if is_current { 401 | Color::Green 402 | } else { 403 | Color::White 404 | }), 405 | ), 406 | Span::styled( 407 | &provider.name, 408 | Style::default() 409 | .fg(Color::Cyan) 410 | .add_modifier(Modifier::BOLD), 411 | ), 412 | ]), 413 | Line::from(vec![ 414 | Span::raw(" URL: "), 415 | Span::styled(&provider.api_url, Style::default().fg(Color::Yellow)), 416 | ]), 417 | ]; 418 | 419 | let masked_token = if provider.token.len() > 8 { 420 | format!("{}****", &provider.token[..8]) 421 | } else { 422 | "****".to_string() 423 | }; 424 | lines.push(Line::from(vec![ 425 | Span::raw(" Token: "), 426 | Span::styled(masked_token, Style::default().fg(Color::DarkGray)), 427 | ])); 428 | 429 | if let Some(ref model) = provider.model { 430 | lines.push(Line::from(vec![ 431 | Span::raw(" Model: "), 432 | Span::styled(model, Style::default().fg(Color::Magenta)), 433 | ])); 434 | } 435 | 436 | if is_current { 437 | lines.push(Line::from(Span::styled( 438 | " (currently active)", 439 | Style::default() 440 | .fg(Color::Green) 441 | .add_modifier(Modifier::ITALIC), 442 | ))); 443 | } 444 | 445 | ListItem::new(lines).style(Style::default()) 446 | }) 447 | .collect(); 448 | 449 | let list = List::new(items) 450 | .block( 451 | Block::default() 452 | .borders(Borders::ALL) 453 | .title("Service Providers"), 454 | ) 455 | .highlight_style( 456 | Style::default() 457 | .bg(Color::DarkGray) 458 | .add_modifier(Modifier::BOLD), 459 | ) 460 | .highlight_symbol(">> "); 461 | 462 | f.render_stateful_widget(list, area, &mut app.list_state); 463 | } 464 | 465 | fn render_add_provider_form(f: &mut Frame, state: &AddProviderState, area: Rect) { 466 | let block = Block::default() 467 | .borders(Borders::ALL) 468 | .title("Add New Provider"); 469 | 470 | let inner = block.inner(area); 471 | f.render_widget(block, area); 472 | 473 | let chunks = Layout::default() 474 | .direction(Direction::Vertical) 475 | .margin(1) 476 | .constraints([ 477 | Constraint::Length(3), 478 | Constraint::Length(3), 479 | Constraint::Length(3), 480 | Constraint::Length(3), 481 | Constraint::Min(1), 482 | ]) 483 | .split(inner); 484 | 485 | let fields = [ 486 | ("Name", &state.name, 0), 487 | ("API URL", &state.url, 1), 488 | ("Token", &state.token, 2), 489 | ("Model (optional)", &state.model, 3), 490 | ]; 491 | 492 | for (i, (label, value, field_idx)) in fields.iter().enumerate() { 493 | let is_active = state.current_field == *field_idx; 494 | let style = if is_active { 495 | Style::default() 496 | .fg(Color::Yellow) 497 | .add_modifier(Modifier::BOLD) 498 | } else { 499 | Style::default() 500 | }; 501 | 502 | let border_style = if is_active { 503 | Style::default().fg(Color::Cyan) 504 | } else { 505 | Style::default() 506 | }; 507 | 508 | let input = Paragraph::new(value.as_str()).style(style).block( 509 | Block::default() 510 | .borders(Borders::ALL) 511 | .title(*label) 512 | .border_style(border_style), 513 | ); 514 | f.render_widget(input, chunks[i]); 515 | 516 | // Show cursor 517 | if is_active { 518 | f.set_cursor(chunks[i].x + value.len() as u16 + 1, chunks[i].y + 1); 519 | } 520 | } 521 | } 522 | 523 | fn render_delete_confirmation(f: &mut Frame, app: &TuiApp, area: Rect) { 524 | if let Some(provider) = app.get_selected_provider() { 525 | let block = Block::default() 526 | .borders(Borders::ALL) 527 | .title("Confirm Delete") 528 | .style(Style::default().bg(Color::Red).fg(Color::White)); 529 | 530 | let text = format!( 531 | "Are you sure you want to delete provider '{}'?\n\nPress 'y' to confirm, 'n' to cancel", 532 | provider.name 533 | ); 534 | 535 | let paragraph = Paragraph::new(text) 536 | .block(block) 537 | .wrap(Wrap { trim: true }) 538 | .alignment(Alignment::Center); 539 | 540 | let popup_area = centered_rect(60, 30, area); 541 | f.render_widget(paragraph, popup_area); 542 | } 543 | } 544 | 545 | fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 546 | let popup_layout = Layout::default() 547 | .direction(Direction::Vertical) 548 | .constraints([ 549 | Constraint::Percentage((100 - percent_y) / 2), 550 | Constraint::Percentage(percent_y), 551 | Constraint::Percentage((100 - percent_y) / 2), 552 | ]) 553 | .split(r); 554 | 555 | Layout::default() 556 | .direction(Direction::Horizontal) 557 | .constraints([ 558 | Constraint::Percentage((100 - percent_x) / 2), 559 | Constraint::Percentage(percent_x), 560 | Constraint::Percentage((100 - percent_x) / 2), 561 | ]) 562 | .split(popup_layout[1])[1] 563 | } 564 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "allocator-api2" 7 | version = "0.2.21" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 10 | 11 | [[package]] 12 | name = "anstream" 13 | version = "0.6.20" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 16 | dependencies = [ 17 | "anstyle", 18 | "anstyle-parse", 19 | "anstyle-query", 20 | "anstyle-wincon", 21 | "colorchoice", 22 | "is_terminal_polyfill", 23 | "utf8parse", 24 | ] 25 | 26 | [[package]] 27 | name = "anstyle" 28 | version = "1.0.11" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 31 | 32 | [[package]] 33 | name = "anstyle-parse" 34 | version = "0.2.7" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 37 | dependencies = [ 38 | "utf8parse", 39 | ] 40 | 41 | [[package]] 42 | name = "anstyle-query" 43 | version = "1.1.4" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 46 | dependencies = [ 47 | "windows-sys 0.60.2", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-wincon" 52 | version = "3.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 55 | dependencies = [ 56 | "anstyle", 57 | "once_cell_polyfill", 58 | "windows-sys 0.60.2", 59 | ] 60 | 61 | [[package]] 62 | name = "anyhow" 63 | version = "1.0.99" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "2.9.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" 72 | 73 | [[package]] 74 | name = "cassowary" 75 | version = "0.3.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 78 | 79 | [[package]] 80 | name = "castaway" 81 | version = "0.2.4" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" 84 | dependencies = [ 85 | "rustversion", 86 | ] 87 | 88 | [[package]] 89 | name = "cce" 90 | version = "0.2.6" 91 | dependencies = [ 92 | "anyhow", 93 | "clap", 94 | "colored", 95 | "crossterm", 96 | "dirs", 97 | "ratatui", 98 | "serde", 99 | "toml", 100 | ] 101 | 102 | [[package]] 103 | name = "cfg-if" 104 | version = "1.0.3" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 107 | 108 | [[package]] 109 | name = "clap" 110 | version = "4.5.45" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" 113 | dependencies = [ 114 | "clap_builder", 115 | "clap_derive", 116 | ] 117 | 118 | [[package]] 119 | name = "clap_builder" 120 | version = "4.5.44" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" 123 | dependencies = [ 124 | "anstream", 125 | "anstyle", 126 | "clap_lex", 127 | "strsim", 128 | ] 129 | 130 | [[package]] 131 | name = "clap_derive" 132 | version = "4.5.45" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" 135 | dependencies = [ 136 | "heck", 137 | "proc-macro2", 138 | "quote", 139 | "syn", 140 | ] 141 | 142 | [[package]] 143 | name = "clap_lex" 144 | version = "0.7.5" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 147 | 148 | [[package]] 149 | name = "colorchoice" 150 | version = "1.0.4" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 153 | 154 | [[package]] 155 | name = "colored" 156 | version = "2.2.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 159 | dependencies = [ 160 | "lazy_static", 161 | "windows-sys 0.59.0", 162 | ] 163 | 164 | [[package]] 165 | name = "compact_str" 166 | version = "0.7.1" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 169 | dependencies = [ 170 | "castaway", 171 | "cfg-if", 172 | "itoa", 173 | "ryu", 174 | "static_assertions", 175 | ] 176 | 177 | [[package]] 178 | name = "crossterm" 179 | version = "0.27.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 182 | dependencies = [ 183 | "bitflags", 184 | "crossterm_winapi", 185 | "libc", 186 | "mio", 187 | "parking_lot", 188 | "signal-hook", 189 | "signal-hook-mio", 190 | "winapi", 191 | ] 192 | 193 | [[package]] 194 | name = "crossterm_winapi" 195 | version = "0.9.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 198 | dependencies = [ 199 | "winapi", 200 | ] 201 | 202 | [[package]] 203 | name = "dirs" 204 | version = "5.0.1" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 207 | dependencies = [ 208 | "dirs-sys", 209 | ] 210 | 211 | [[package]] 212 | name = "dirs-sys" 213 | version = "0.4.1" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 216 | dependencies = [ 217 | "libc", 218 | "option-ext", 219 | "redox_users", 220 | "windows-sys 0.48.0", 221 | ] 222 | 223 | [[package]] 224 | name = "either" 225 | version = "1.15.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 228 | 229 | [[package]] 230 | name = "equivalent" 231 | version = "1.0.2" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 234 | 235 | [[package]] 236 | name = "foldhash" 237 | version = "0.1.5" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 240 | 241 | [[package]] 242 | name = "getrandom" 243 | version = "0.2.16" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 246 | dependencies = [ 247 | "cfg-if", 248 | "libc", 249 | "wasi", 250 | ] 251 | 252 | [[package]] 253 | name = "hashbrown" 254 | version = "0.15.5" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 257 | dependencies = [ 258 | "allocator-api2", 259 | "equivalent", 260 | "foldhash", 261 | ] 262 | 263 | [[package]] 264 | name = "heck" 265 | version = "0.5.0" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 268 | 269 | [[package]] 270 | name = "indexmap" 271 | version = "2.10.0" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 274 | dependencies = [ 275 | "equivalent", 276 | "hashbrown", 277 | ] 278 | 279 | [[package]] 280 | name = "is_terminal_polyfill" 281 | version = "1.70.1" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 284 | 285 | [[package]] 286 | name = "itertools" 287 | version = "0.12.1" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 290 | dependencies = [ 291 | "either", 292 | ] 293 | 294 | [[package]] 295 | name = "itertools" 296 | version = "0.13.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 299 | dependencies = [ 300 | "either", 301 | ] 302 | 303 | [[package]] 304 | name = "itoa" 305 | version = "1.0.15" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 308 | 309 | [[package]] 310 | name = "lazy_static" 311 | version = "1.5.0" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 314 | 315 | [[package]] 316 | name = "libc" 317 | version = "0.2.175" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 320 | 321 | [[package]] 322 | name = "libredox" 323 | version = "0.1.9" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" 326 | dependencies = [ 327 | "bitflags", 328 | "libc", 329 | ] 330 | 331 | [[package]] 332 | name = "lock_api" 333 | version = "0.4.14" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 336 | dependencies = [ 337 | "scopeguard", 338 | ] 339 | 340 | [[package]] 341 | name = "log" 342 | version = "0.4.28" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 345 | 346 | [[package]] 347 | name = "lru" 348 | version = "0.12.5" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 351 | dependencies = [ 352 | "hashbrown", 353 | ] 354 | 355 | [[package]] 356 | name = "memchr" 357 | version = "2.7.5" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 360 | 361 | [[package]] 362 | name = "mio" 363 | version = "0.8.11" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 366 | dependencies = [ 367 | "libc", 368 | "log", 369 | "wasi", 370 | "windows-sys 0.48.0", 371 | ] 372 | 373 | [[package]] 374 | name = "once_cell_polyfill" 375 | version = "1.70.1" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 378 | 379 | [[package]] 380 | name = "option-ext" 381 | version = "0.2.0" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 384 | 385 | [[package]] 386 | name = "parking_lot" 387 | version = "0.12.5" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 390 | dependencies = [ 391 | "lock_api", 392 | "parking_lot_core", 393 | ] 394 | 395 | [[package]] 396 | name = "parking_lot_core" 397 | version = "0.9.12" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 400 | dependencies = [ 401 | "cfg-if", 402 | "libc", 403 | "redox_syscall", 404 | "smallvec", 405 | "windows-link 0.2.1", 406 | ] 407 | 408 | [[package]] 409 | name = "paste" 410 | version = "1.0.15" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 413 | 414 | [[package]] 415 | name = "proc-macro2" 416 | version = "1.0.101" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 419 | dependencies = [ 420 | "unicode-ident", 421 | ] 422 | 423 | [[package]] 424 | name = "quote" 425 | version = "1.0.40" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 428 | dependencies = [ 429 | "proc-macro2", 430 | ] 431 | 432 | [[package]] 433 | name = "ratatui" 434 | version = "0.26.3" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" 437 | dependencies = [ 438 | "bitflags", 439 | "cassowary", 440 | "compact_str", 441 | "crossterm", 442 | "itertools 0.12.1", 443 | "lru", 444 | "paste", 445 | "stability", 446 | "strum", 447 | "unicode-segmentation", 448 | "unicode-truncate", 449 | "unicode-width", 450 | ] 451 | 452 | [[package]] 453 | name = "redox_syscall" 454 | version = "0.5.18" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 457 | dependencies = [ 458 | "bitflags", 459 | ] 460 | 461 | [[package]] 462 | name = "redox_users" 463 | version = "0.4.6" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 466 | dependencies = [ 467 | "getrandom", 468 | "libredox", 469 | "thiserror", 470 | ] 471 | 472 | [[package]] 473 | name = "rustversion" 474 | version = "1.0.22" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 477 | 478 | [[package]] 479 | name = "ryu" 480 | version = "1.0.20" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 483 | 484 | [[package]] 485 | name = "scopeguard" 486 | version = "1.2.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 489 | 490 | [[package]] 491 | name = "serde" 492 | version = "1.0.219" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 495 | dependencies = [ 496 | "serde_derive", 497 | ] 498 | 499 | [[package]] 500 | name = "serde_derive" 501 | version = "1.0.219" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 504 | dependencies = [ 505 | "proc-macro2", 506 | "quote", 507 | "syn", 508 | ] 509 | 510 | [[package]] 511 | name = "serde_spanned" 512 | version = "0.6.9" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 515 | dependencies = [ 516 | "serde", 517 | ] 518 | 519 | [[package]] 520 | name = "signal-hook" 521 | version = "0.3.18" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" 524 | dependencies = [ 525 | "libc", 526 | "signal-hook-registry", 527 | ] 528 | 529 | [[package]] 530 | name = "signal-hook-mio" 531 | version = "0.2.5" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" 534 | dependencies = [ 535 | "libc", 536 | "mio", 537 | "signal-hook", 538 | ] 539 | 540 | [[package]] 541 | name = "signal-hook-registry" 542 | version = "1.4.6" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 545 | dependencies = [ 546 | "libc", 547 | ] 548 | 549 | [[package]] 550 | name = "smallvec" 551 | version = "1.15.1" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 554 | 555 | [[package]] 556 | name = "stability" 557 | version = "0.2.1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" 560 | dependencies = [ 561 | "quote", 562 | "syn", 563 | ] 564 | 565 | [[package]] 566 | name = "static_assertions" 567 | version = "1.1.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 570 | 571 | [[package]] 572 | name = "strsim" 573 | version = "0.11.1" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 576 | 577 | [[package]] 578 | name = "strum" 579 | version = "0.26.3" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 582 | dependencies = [ 583 | "strum_macros", 584 | ] 585 | 586 | [[package]] 587 | name = "strum_macros" 588 | version = "0.26.4" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 591 | dependencies = [ 592 | "heck", 593 | "proc-macro2", 594 | "quote", 595 | "rustversion", 596 | "syn", 597 | ] 598 | 599 | [[package]] 600 | name = "syn" 601 | version = "2.0.106" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 604 | dependencies = [ 605 | "proc-macro2", 606 | "quote", 607 | "unicode-ident", 608 | ] 609 | 610 | [[package]] 611 | name = "thiserror" 612 | version = "1.0.69" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 615 | dependencies = [ 616 | "thiserror-impl", 617 | ] 618 | 619 | [[package]] 620 | name = "thiserror-impl" 621 | version = "1.0.69" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 624 | dependencies = [ 625 | "proc-macro2", 626 | "quote", 627 | "syn", 628 | ] 629 | 630 | [[package]] 631 | name = "toml" 632 | version = "0.8.23" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 635 | dependencies = [ 636 | "serde", 637 | "serde_spanned", 638 | "toml_datetime", 639 | "toml_edit", 640 | ] 641 | 642 | [[package]] 643 | name = "toml_datetime" 644 | version = "0.6.11" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 647 | dependencies = [ 648 | "serde", 649 | ] 650 | 651 | [[package]] 652 | name = "toml_edit" 653 | version = "0.22.27" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 656 | dependencies = [ 657 | "indexmap", 658 | "serde", 659 | "serde_spanned", 660 | "toml_datetime", 661 | "toml_write", 662 | "winnow", 663 | ] 664 | 665 | [[package]] 666 | name = "toml_write" 667 | version = "0.1.2" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" 670 | 671 | [[package]] 672 | name = "unicode-ident" 673 | version = "1.0.18" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 676 | 677 | [[package]] 678 | name = "unicode-segmentation" 679 | version = "1.12.0" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 682 | 683 | [[package]] 684 | name = "unicode-truncate" 685 | version = "1.1.0" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 688 | dependencies = [ 689 | "itertools 0.13.0", 690 | "unicode-segmentation", 691 | "unicode-width", 692 | ] 693 | 694 | [[package]] 695 | name = "unicode-width" 696 | version = "0.1.14" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 699 | 700 | [[package]] 701 | name = "utf8parse" 702 | version = "0.2.2" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 705 | 706 | [[package]] 707 | name = "wasi" 708 | version = "0.11.1+wasi-snapshot-preview1" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 711 | 712 | [[package]] 713 | name = "winapi" 714 | version = "0.3.9" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 717 | dependencies = [ 718 | "winapi-i686-pc-windows-gnu", 719 | "winapi-x86_64-pc-windows-gnu", 720 | ] 721 | 722 | [[package]] 723 | name = "winapi-i686-pc-windows-gnu" 724 | version = "0.4.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 727 | 728 | [[package]] 729 | name = "winapi-x86_64-pc-windows-gnu" 730 | version = "0.4.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 733 | 734 | [[package]] 735 | name = "windows-link" 736 | version = "0.1.3" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 739 | 740 | [[package]] 741 | name = "windows-link" 742 | version = "0.2.1" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 745 | 746 | [[package]] 747 | name = "windows-sys" 748 | version = "0.48.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 751 | dependencies = [ 752 | "windows-targets 0.48.5", 753 | ] 754 | 755 | [[package]] 756 | name = "windows-sys" 757 | version = "0.59.0" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 760 | dependencies = [ 761 | "windows-targets 0.52.6", 762 | ] 763 | 764 | [[package]] 765 | name = "windows-sys" 766 | version = "0.60.2" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 769 | dependencies = [ 770 | "windows-targets 0.53.3", 771 | ] 772 | 773 | [[package]] 774 | name = "windows-targets" 775 | version = "0.48.5" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 778 | dependencies = [ 779 | "windows_aarch64_gnullvm 0.48.5", 780 | "windows_aarch64_msvc 0.48.5", 781 | "windows_i686_gnu 0.48.5", 782 | "windows_i686_msvc 0.48.5", 783 | "windows_x86_64_gnu 0.48.5", 784 | "windows_x86_64_gnullvm 0.48.5", 785 | "windows_x86_64_msvc 0.48.5", 786 | ] 787 | 788 | [[package]] 789 | name = "windows-targets" 790 | version = "0.52.6" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 793 | dependencies = [ 794 | "windows_aarch64_gnullvm 0.52.6", 795 | "windows_aarch64_msvc 0.52.6", 796 | "windows_i686_gnu 0.52.6", 797 | "windows_i686_gnullvm 0.52.6", 798 | "windows_i686_msvc 0.52.6", 799 | "windows_x86_64_gnu 0.52.6", 800 | "windows_x86_64_gnullvm 0.52.6", 801 | "windows_x86_64_msvc 0.52.6", 802 | ] 803 | 804 | [[package]] 805 | name = "windows-targets" 806 | version = "0.53.3" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 809 | dependencies = [ 810 | "windows-link 0.1.3", 811 | "windows_aarch64_gnullvm 0.53.0", 812 | "windows_aarch64_msvc 0.53.0", 813 | "windows_i686_gnu 0.53.0", 814 | "windows_i686_gnullvm 0.53.0", 815 | "windows_i686_msvc 0.53.0", 816 | "windows_x86_64_gnu 0.53.0", 817 | "windows_x86_64_gnullvm 0.53.0", 818 | "windows_x86_64_msvc 0.53.0", 819 | ] 820 | 821 | [[package]] 822 | name = "windows_aarch64_gnullvm" 823 | version = "0.48.5" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 826 | 827 | [[package]] 828 | name = "windows_aarch64_gnullvm" 829 | version = "0.52.6" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 832 | 833 | [[package]] 834 | name = "windows_aarch64_gnullvm" 835 | version = "0.53.0" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 838 | 839 | [[package]] 840 | name = "windows_aarch64_msvc" 841 | version = "0.48.5" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 844 | 845 | [[package]] 846 | name = "windows_aarch64_msvc" 847 | version = "0.52.6" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 850 | 851 | [[package]] 852 | name = "windows_aarch64_msvc" 853 | version = "0.53.0" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 856 | 857 | [[package]] 858 | name = "windows_i686_gnu" 859 | version = "0.48.5" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 862 | 863 | [[package]] 864 | name = "windows_i686_gnu" 865 | version = "0.52.6" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 868 | 869 | [[package]] 870 | name = "windows_i686_gnu" 871 | version = "0.53.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 874 | 875 | [[package]] 876 | name = "windows_i686_gnullvm" 877 | version = "0.52.6" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 880 | 881 | [[package]] 882 | name = "windows_i686_gnullvm" 883 | version = "0.53.0" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 886 | 887 | [[package]] 888 | name = "windows_i686_msvc" 889 | version = "0.48.5" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 892 | 893 | [[package]] 894 | name = "windows_i686_msvc" 895 | version = "0.52.6" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 898 | 899 | [[package]] 900 | name = "windows_i686_msvc" 901 | version = "0.53.0" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 904 | 905 | [[package]] 906 | name = "windows_x86_64_gnu" 907 | version = "0.48.5" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 910 | 911 | [[package]] 912 | name = "windows_x86_64_gnu" 913 | version = "0.52.6" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 916 | 917 | [[package]] 918 | name = "windows_x86_64_gnu" 919 | version = "0.53.0" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 922 | 923 | [[package]] 924 | name = "windows_x86_64_gnullvm" 925 | version = "0.48.5" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 928 | 929 | [[package]] 930 | name = "windows_x86_64_gnullvm" 931 | version = "0.52.6" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 934 | 935 | [[package]] 936 | name = "windows_x86_64_gnullvm" 937 | version = "0.53.0" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 940 | 941 | [[package]] 942 | name = "windows_x86_64_msvc" 943 | version = "0.48.5" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 946 | 947 | [[package]] 948 | name = "windows_x86_64_msvc" 949 | version = "0.52.6" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 952 | 953 | [[package]] 954 | name = "windows_x86_64_msvc" 955 | version = "0.53.0" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 958 | 959 | [[package]] 960 | name = "winnow" 961 | version = "0.7.12" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" 964 | dependencies = [ 965 | "memchr", 966 | ] 967 | --------------------------------------------------------------------------------