Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Build

on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-build-
${{ runner.os }}-cargo-

- name: Build
run: cargo build --release

- name: Run tests
run: cargo test
34 changes: 34 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Lint

on:
pull_request:
branches: [main, develop]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-lint-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-lint-
${{ runner.os }}-cargo-

- name: Check formatting
run: cargo fmt -- --check

- name: Run clippy
run: cargo clippy -- -D warnings
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Manage multiple provider configs (e.g. qwen, openai, deepseek) in `~/.cc-use/` a

```
~/.cc-use/
base.json # Shared configuration (optional)
qwen.json
openai.json
deepseek.json
Expand All @@ -17,6 +18,19 @@ Manage multiple provider configs (e.g. qwen, openai, deepseek) in `~/.cc-use/` a

On first use, your existing `~/.claude/settings.json` is backed up to `settings.json.bak`, then replaced with a symlink pointing to the active config.

### Base Configuration

You can define a `base.json` that contains shared settings across all providers. When switching, configurations are merged:

```
base.json: { "env": { "ANTHROPIC_API_KEY": "sk-..." }, "permissions": { "allow": ["*"] } }
qwen.json: { "env": { "ANTHROPIC_BASE_URL": "https://api.qwen.ai" } }

→ Merged: { "env": { "ANTHROPIC_API_KEY": "sk-...", "ANTHROPIC_BASE_URL": "https://api.qwen.ai" }, "permissions": { "allow": ["*"] } }
```

When `base.json` exists, switching creates a merged file instead of a symlink. Provider-specific settings override base settings.

## Install

Requires [Rust toolchain](https://rustup.rs/).
Expand Down Expand Up @@ -44,7 +58,7 @@ cc-use add openai
# List all configs
cc-use ls

# Show current (or specified) config content
# Show current (or specified) config content (merged with base if exists)
cc-use show
cc-use show qwen

Expand All @@ -53,6 +67,11 @@ cc-use edit qwen

# Remove a config (refuses if active)
cc-use rm openai

# Base configuration management
cc-use base # Edit base config (opens $EDITOR)
cc-use base show # Show base config content
cc-use base rm # Remove base config
```

## Quick start
Expand All @@ -64,6 +83,9 @@ cc-use add qwen # editor opens, paste your settings, save & quit
# Add another provider
cc-use add deepseek # paste deepseek settings

# (Optional) Set up shared base config
cc-use base # editor opens, add shared settings like API keys, permissions

# Switch between them
cc-use qwen
cc-use deepseek
Expand All @@ -74,4 +96,4 @@ cc-use

## License

MIT
MIT
15 changes: 14 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ pub enum Command {
/// Name for the new configuration
name: String,
},
/// Manage base configuration (shared across all configs)
Base {
#[command(subcommand)]
action: Option<BaseAction>,
},
/// List all configurations
Ls,
/// Remove a configuration
Rm {
/// Name of the configuration to remove
name: String,
},
/// Show configuration content
/// Show configuration content (merged with base if exists)
Show {
/// Name of the configuration (defaults to current)
name: Option<String>,
Expand All @@ -39,3 +44,11 @@ pub enum Command {
name: String,
},
}

#[derive(Subcommand)]
pub enum BaseAction {
/// Show base configuration
Show,
/// Remove base configuration
Rm,
}
6 changes: 5 additions & 1 deletion src/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ pub fn run(name: &str) -> Result<()> {
config::ensure_init()?;

if config::config_exists(name)? {
bail!("configuration '{}' already exists. Use 'cc-use edit {}' to modify it.", name, name);
bail!(
"configuration '{}' already exists. Use 'cc-use edit {}' to modify it.",
name,
name
);
}

let path = config::config_path(name)?;
Expand Down
55 changes: 55 additions & 0 deletions src/commands/base.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use anyhow::{bail, Result};
use colored::Colorize;

use crate::config;
use crate::editor;

pub fn edit() -> Result<()> {
config::ensure_init()?;

let path = config::base_config_path()?;

// Create empty file if doesn't exist
if !path.exists() {
std::fs::write(&path, "{}\n")?;
}

editor::open_editor(&path)?;
editor::validate_json(&path)?;

if config::base_config_exists()? {
println!("Base configuration updated");
} else {
// User deleted content, remove the file
std::fs::remove_file(&path).ok();
println!("Base configuration removed");
}
Ok(())
}

pub fn show() -> Result<()> {
if !config::base_config_exists()? {
bail!("no base configuration. Use 'cc-use base' to create one.");
}

let path = config::base_config_path()?;
let content = std::fs::read_to_string(&path)?;
let value: serde_json::Value = serde_json::from_str(&content)?;
let pretty = serde_json::to_string_pretty(&value)?;

println!("{}:", "base".green().bold());
println!("{}", pretty);
Ok(())
}

pub fn remove() -> Result<()> {
if !config::base_config_exists()? {
bail!("no base configuration to remove");
}

let path = config::base_config_path()?;
std::fs::remove_file(&path)?;

println!("Base configuration removed");
Ok(())
}
6 changes: 5 additions & 1 deletion src/commands/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ use crate::editor;
pub fn run(name: &str) -> Result<()> {
let path = config::config_path(name)?;
if !path.exists() {
bail!("configuration '{}' does not exist. Use 'cc-use add {}' to create it.", name, name);
bail!(
"configuration '{}' does not exist. Use 'cc-use add {}' to create it.",
name,
name
);
}

editor::open_editor(&path)?;
Expand Down
5 changes: 5 additions & 0 deletions src/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ pub fn run() -> Result<()> {
return Ok(());
}

// Show base config status
if config::base_config_exists()? {
println!(" {} {}", "base".cyan(), "(shared)".cyan());
}

let current = config::current_config()?;

for name in &configs {
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod add;
pub mod base;
pub mod edit;
pub mod list;
pub mod remove;
Expand Down
16 changes: 11 additions & 5 deletions src/commands/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@ use crate::config;
pub fn run(name: Option<&str>) -> Result<()> {
let resolved_name = match name {
Some(n) => n.to_string(),
None => config::current_config()?
.ok_or_else(|| anyhow::anyhow!("no active configuration. Specify a name or switch to one first."))?,
None => config::current_config()?.ok_or_else(|| {
anyhow::anyhow!("no active configuration. Specify a name or switch to one first.")
})?,
};

let path = config::config_path(&resolved_name)?;
if !path.exists() {
bail!("configuration '{}' does not exist", resolved_name);
}

let content = std::fs::read_to_string(&path)?;
let value: serde_json::Value = serde_json::from_str(&content)?;
// Show merged config if base exists
let value = config::get_merged_config(&resolved_name)?;
let pretty = serde_json::to_string_pretty(&value)?;

println!("{}:", resolved_name.green().bold());
// Indicate if this is a merged view
if config::base_config_exists()? {
println!("{} (merged with base):", resolved_name.green().bold());
} else {
println!("{}:", resolved_name.green().bold());
}
println!("{}", pretty);
Ok(())
}
Loading
Loading