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
12 changes: 6 additions & 6 deletions crates/csa-config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ fn merge_toml_values(base: toml::Value, overlay: toml::Value) -> toml::Value {
impl ProjectConfig {
/// Load config with fallback chain:
///
/// 1. If both `.csa/config.toml` (project) and `~/.config/csa/config.toml` (user) exist,
/// deep-merge them with project settings overriding user settings.
/// 1. If both `.csa/config.toml` (project) and `~/.config/cli-sub-agent/config.toml` (user)
/// exist, deep-merge them with project settings overriding user settings.
/// 2. If only project config exists, use it directly.
/// 3. If only user config exists, use it as fallback.
/// 4. If neither exists, return None.
Expand Down Expand Up @@ -267,12 +267,12 @@ impl ProjectConfig {
Ok(Some(config))
}

/// Path to user-level config: `~/.config/csa/config.toml`.
/// Path to user-level config: `~/.config/cli-sub-agent/config.toml`.
///
/// Returns None if the config directory cannot be determined
/// (e.g., no HOME in containers).
pub fn user_config_path() -> Option<PathBuf> {
directories::ProjectDirs::from("", "", "csa")
directories::ProjectDirs::from("", "", "cli-sub-agent")
.map(|dirs| dirs.config_dir().join("config.toml"))
}

Expand Down Expand Up @@ -374,7 +374,7 @@ impl ProjectConfig {
.unwrap_or(false)
}

/// Save a user-level config template to `~/.config/csa/config.toml`.
/// Save a user-level config template to `~/.config/cli-sub-agent/config.toml`.
///
/// Creates the directory if needed. Returns the path written, or None
/// if the config directory cannot be determined.
Expand All @@ -396,7 +396,7 @@ impl ProjectConfig {
/// Generate a commented template for user-level config.
pub fn user_config_template() -> String {
r#"# CSA User-Level Configuration
# Location: ~/.config/csa/config.toml
# Location: ~/.config/cli-sub-agent/config.toml
#
# This file provides default tiers, tool settings, and aliases
# that apply across all CSA projects unless overridden by
Expand Down
86 changes: 85 additions & 1 deletion crates/csa-config/src/config_merge_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ fn test_user_config_path_returns_some() {
if std::env::var("HOME").is_ok() {
assert!(path.is_some());
let p = path.unwrap();
assert!(p.to_string_lossy().contains("csa"));
assert!(p.to_string_lossy().contains("cli-sub-agent"));
assert!(p.to_string_lossy().contains("config.toml"));
}
// In containers without HOME, it's OK to return None
Expand All @@ -202,4 +202,88 @@ fn test_user_config_template_is_valid() {
assert!(template.contains("[resources]"));
assert!(template.contains("suppress_notify"));
assert!(template.contains("# [tiers."));
// Template location comment should point to unified path
assert!(template.contains("cli-sub-agent/config.toml"));
}

#[test]
fn test_user_config_path_matches_global_config_dir() {
// After unification, user-level ProjectConfig and GlobalConfig share the same directory.
if std::env::var("HOME").is_err() {
return; // Skip in containers
}
let user_path = ProjectConfig::user_config_path().unwrap();
let global_path = crate::GlobalConfig::config_path().unwrap();
assert_eq!(
user_path.parent(),
global_path.parent(),
"User and global config should share the same directory"
);
}

#[test]
fn test_load_user_tiers_without_project_config() {
// When only user-level config exists (no project config),
// load should return user-level tiers.
let tmp = tempfile::tempdir().unwrap();
let user_path = tmp.path().join("user.toml");
let project_path = tmp
.path()
.join("nonexistent")
.join(".csa")
.join("config.toml");

std::fs::write(
&user_path,
r#"
schema_version = 1
[tiers.user-tier]
description = "User-level tier"
models = ["codex/openai/o3/medium"]
"#,
)
.unwrap();

let config = ProjectConfig::load_with_paths(Some(&user_path), &project_path)
.unwrap()
.expect("Should load from user path");
assert!(config.tiers.contains_key("user-tier"));
}

#[test]
fn test_load_project_overrides_user_tiers() {
// When both user and project configs exist, project tiers
// override user tiers (deep merge).
let tmp = tempfile::tempdir().unwrap();
let user_path = tmp.path().join("user.toml");
let project_path = tmp.path().join("project.toml");

std::fs::write(
&user_path,
r#"
schema_version = 1
[tiers.shared-tier]
description = "User version"
models = ["codex/openai/o3/medium"]
"#,
)
.unwrap();

std::fs::write(
&project_path,
r#"
schema_version = 1
[tiers.shared-tier]
description = "Project version"
models = ["codex/openai/o3/high"]
"#,
)
.unwrap();

let config = ProjectConfig::load_with_paths(Some(&user_path), &project_path)
.unwrap()
.expect("Should load merged config");
let tier = config.tiers.get("shared-tier").unwrap();
assert_eq!(tier.description, "Project version");
assert_eq!(tier.models, vec!["codex/openai/o3/high"]);
}
2 changes: 1 addition & 1 deletion crates/csa-config/src/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use tempfile::tempdir;
#[test]
fn test_load_nonexistent_returns_none() {
let dir = tempdir().unwrap();
// Use load_with_paths to isolate from real ~/.config/csa/config.toml on host.
// Use load_with_paths to isolate from real ~/.config/cli-sub-agent/config.toml on host.
let project_path = dir.path().join(".csa").join("config.toml");
let result = ProjectConfig::load_with_paths(None, &project_path).unwrap();
assert!(result.is_none());
Expand Down
10 changes: 5 additions & 5 deletions skills/install-update-csa/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ capability tiers, and sets tool-specific restrictions.

- First time installing or setting up CSA
- Updating CSA binary to a new version
- Checking or creating global user config (`~/.config/csa/config.toml`)
- Checking or creating global user config (`~/.config/cli-sub-agent/config.toml`)
- First time setting up CSA in a project (project-level config)
- Reconfiguring tool/model selection for changed requirements
- Adding newly installed tools or models

## Output

- CSA binary verified in PATH
- `~/.config/csa/config.toml` — global user configuration (TOML)
- `~/.config/cli-sub-agent/config.toml` — global user configuration (TOML)
- `.csa/config.toml` — project configuration (TOML)
- `.csa/` added to `.gitignore`

Expand Down Expand Up @@ -63,10 +63,10 @@ Report the version. If it looks outdated, suggest updating.

### Phase 0.5: Check Global Config

Check if the global user config exists at `~/.config/csa/config.toml`:
Check if the global user config exists at `~/.config/cli-sub-agent/config.toml`:

```bash
ls ~/.config/csa/config.toml 2>/dev/null
ls ~/.config/cli-sub-agent/config.toml 2>/dev/null
```

- If **not found**:
Expand All @@ -81,7 +81,7 @@ ls ~/.config/csa/config.toml 2>/dev/null

**Key settings to include in global config**:
```toml
# ~/.config/csa/config.toml
# ~/.config/cli-sub-agent/config.toml
# Global defaults — project configs override these values.

[tools.codex]
Expand Down
Loading