Add local project config support (.svibe/config.toml)
Features: - Local config at .svibe/config.toml overrides global config - New 'svibe init --local' command creates project-specific config - Config merging: global -> local -> environment variables - Added .svibe to .gitignore (project configs typically not shared) Changes: - config/loader.rs: Added local config detection and merging logic - cli/commands.rs: Added --local flag to init command - main.rs: Updated init command handling - QWEN.md: Updated documentation with local config info Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/target
|
||||
.svibe
|
||||
|
||||
30
QWEN.md
30
QWEN.md
@@ -70,7 +70,35 @@ cargo run -- --help
|
||||
|
||||
## Configuration
|
||||
|
||||
Run `svibe init` to create a configuration file at `~/.config/svibe/config.toml`:
|
||||
### Global Configuration
|
||||
|
||||
Run `svibe init` to create a global configuration file at `~/.config/svibe/config.toml`:
|
||||
|
||||
```bash
|
||||
svibe init
|
||||
```
|
||||
|
||||
### Local (Project) Configuration
|
||||
|
||||
Run `svibe init --local` to create a project-specific configuration at `.svibe/config.toml`:
|
||||
|
||||
```bash
|
||||
svibe init --local
|
||||
```
|
||||
|
||||
Local configuration **overrides** global configuration. This is useful for:
|
||||
- Project-specific model settings
|
||||
- Team-specific API configurations
|
||||
- Different agents for different projects
|
||||
|
||||
### Configuration Priority
|
||||
|
||||
Settings are loaded and merged in this order (later overrides earlier):
|
||||
1. Global config (`~/.config/svibe/config.toml`)
|
||||
2. Local config (`.svibe/config.toml`) - if exists
|
||||
3. Environment variables (`OPENAI_API_KEY`, `OLLAMA_HOST`)
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```toml
|
||||
[providers.openai]
|
||||
|
||||
@@ -25,7 +25,11 @@ pub struct Cli {
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// Initialize configuration
|
||||
Init,
|
||||
Init {
|
||||
/// Create local config in .svibe/ (overrides global config)
|
||||
#[arg(short, long)]
|
||||
local: bool,
|
||||
},
|
||||
|
||||
/// Run a single task
|
||||
Run {
|
||||
|
||||
@@ -1,61 +1,113 @@
|
||||
use super::{Config, ConfigError, Result, OpenAiConfig, OllamaConfig, AgentConfig, AgentModels};
|
||||
use super::types::{default_openai_base_url, default_openai_model, default_ollama_base_url, default_ollama_model};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Configuration loader
|
||||
pub struct ConfigLoader {
|
||||
config_path: PathBuf,
|
||||
global_config_path: PathBuf,
|
||||
local_config_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ConfigLoader {
|
||||
/// Create a new config loader with default path
|
||||
/// Create a new config loader with default paths
|
||||
pub fn new() -> Self {
|
||||
let config_path = default_config_path();
|
||||
Self { config_path }
|
||||
let global_config_path = default_config_path();
|
||||
let local_config_path = find_local_config();
|
||||
Self {
|
||||
global_config_path,
|
||||
local_config_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a config loader with custom path
|
||||
/// Create a config loader with custom global path
|
||||
pub fn with_path(path: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
config_path: path.as_ref().to_path_buf(),
|
||||
global_config_path: path.as_ref().to_path_buf(),
|
||||
local_config_path: find_local_config(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load configuration from file
|
||||
/// Load configuration from global and local files (local overrides global)
|
||||
pub fn load(&self) -> Result<Config> {
|
||||
if !self.config_path.exists() {
|
||||
return Err(ConfigError::NotFound(
|
||||
self.config_path.display().to_string(),
|
||||
));
|
||||
}
|
||||
// Load global config
|
||||
let mut global_config = self.load_global()?;
|
||||
|
||||
let content = std::fs::read_to_string(&self.config_path)?;
|
||||
let mut config: Config = toml::from_str(&content)?;
|
||||
// Load and merge local config if it exists
|
||||
if let Some(ref local_path) = self.local_config_path {
|
||||
if local_path.exists() {
|
||||
let local_config = self.load_from_path(local_path)?;
|
||||
global_config.merge(local_config);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with environment variables
|
||||
config.merge_env();
|
||||
global_config.merge_env();
|
||||
|
||||
// Ensure default agent exists
|
||||
if config.agents.is_empty() {
|
||||
config.agents.insert("default".to_string(), default_agent_config());
|
||||
if global_config.agents.is_empty() {
|
||||
global_config.agents.insert("default".to_string(), default_agent_config());
|
||||
}
|
||||
|
||||
Ok(global_config)
|
||||
}
|
||||
|
||||
/// Load only the global configuration
|
||||
fn load_global(&self) -> Result<Config> {
|
||||
if !self.global_config_path.exists() {
|
||||
return Err(ConfigError::NotFound(
|
||||
self.global_config_path.display().to_string(),
|
||||
));
|
||||
}
|
||||
self.load_from_path(&self.global_config_path)
|
||||
}
|
||||
|
||||
/// Load configuration from a specific path
|
||||
fn load_from_path(&self, path: &Path) -> Result<Config> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let config: Config = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Save configuration to file
|
||||
pub fn save(&self, config: &Config) -> Result<()> {
|
||||
if let Some(parent) = self.config_path.parent() {
|
||||
/// Save global configuration to file
|
||||
pub fn save_global(&self, config: &Config) -> Result<()> {
|
||||
self.save_to_path(&self.global_config_path, config)
|
||||
}
|
||||
|
||||
/// Save local configuration to file
|
||||
pub fn save_local(&self, config: &Config) -> Result<()> {
|
||||
let local_path = self.local_config_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from(".svibe/config.toml"));
|
||||
self.save_to_path(&local_path, config)
|
||||
}
|
||||
|
||||
/// Save configuration to a specific path (public for CLI)
|
||||
pub fn save_to_path(&self, path: &Path, config: &Config) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let content = toml::to_string_pretty(config)?;
|
||||
std::fs::write(&self.config_path, content)?;
|
||||
std::fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the config path
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.config_path
|
||||
/// Get the global config path
|
||||
pub fn global_path(&self) -> &Path {
|
||||
&self.global_config_path
|
||||
}
|
||||
|
||||
/// Get the local config path (if exists)
|
||||
pub fn local_path(&self) -> Option<&Path> {
|
||||
self.local_config_path.as_deref()
|
||||
}
|
||||
|
||||
/// Check if local config exists
|
||||
pub fn has_local_config(&self) -> bool {
|
||||
self.local_config_path
|
||||
.as_ref()
|
||||
.map(|p| p.exists())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +117,7 @@ impl Default for ConfigLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default configuration path
|
||||
/// Get the default global configuration path
|
||||
fn default_config_path() -> PathBuf {
|
||||
dirs::config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
@@ -73,6 +125,16 @@ fn default_config_path() -> PathBuf {
|
||||
.join("config.toml")
|
||||
}
|
||||
|
||||
/// Find local config file in .svibe directory
|
||||
fn find_local_config() -> Option<PathBuf> {
|
||||
let local_path = PathBuf::from(".svibe/config.toml");
|
||||
if local_path.exists() {
|
||||
Some(local_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge environment variables into config
|
||||
impl Config {
|
||||
fn merge_env(&mut self) {
|
||||
@@ -92,6 +154,77 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge another config into this one (other overrides self)
|
||||
fn merge(&mut self, other: Config) {
|
||||
// Merge providers
|
||||
if let Some(other_openai) = other.providers.openai {
|
||||
if let Some(ref mut self_openai) = self.providers.openai {
|
||||
// Merge OpenAI configs
|
||||
if other_openai.api_key.is_some() {
|
||||
self_openai.api_key = other_openai.api_key;
|
||||
}
|
||||
if other_openai.base_url != default_openai_base_url() {
|
||||
self_openai.base_url = other_openai.base_url;
|
||||
}
|
||||
if other_openai.default_model != default_openai_model() {
|
||||
self_openai.default_model = other_openai.default_model;
|
||||
}
|
||||
} else {
|
||||
self.providers.openai = Some(other_openai);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(other_ollama) = other.providers.ollama {
|
||||
if let Some(ref mut self_ollama) = self.providers.ollama {
|
||||
// Merge Ollama configs
|
||||
if other_ollama.base_url != default_ollama_base_url() {
|
||||
self_ollama.base_url = other_ollama.base_url;
|
||||
}
|
||||
if other_ollama.default_model != default_ollama_model() {
|
||||
self_ollama.default_model = other_ollama.default_model;
|
||||
}
|
||||
if other_ollama.bearer_token.is_some() {
|
||||
self_ollama.bearer_token = other_ollama.bearer_token;
|
||||
}
|
||||
if other_ollama.username.is_some() {
|
||||
self_ollama.username = other_ollama.username;
|
||||
}
|
||||
if other_ollama.password.is_some() {
|
||||
self_ollama.password = other_ollama.password;
|
||||
}
|
||||
} else {
|
||||
self.providers.ollama = Some(other_ollama);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge agents (local completely overrides global agents with same name)
|
||||
for (name, agent) in other.agents {
|
||||
self.agents.insert(name, agent);
|
||||
}
|
||||
|
||||
// Override default agent if specified
|
||||
if other.default_agent != "default" {
|
||||
self.default_agent = other.default_agent;
|
||||
}
|
||||
|
||||
// Merge settings
|
||||
if other.settings.verbose {
|
||||
self.settings.verbose = true;
|
||||
}
|
||||
if other.settings.max_tokens != 0 {
|
||||
self.settings.max_tokens = other.settings.max_tokens;
|
||||
}
|
||||
if other.settings.temperature != 0.0 {
|
||||
self.settings.temperature = other.settings.temperature;
|
||||
}
|
||||
if other.settings.working_dir.is_some() {
|
||||
self.settings.working_dir = other.settings.working_dir;
|
||||
}
|
||||
if other.settings.session_dir.is_some() {
|
||||
self.settings.session_dir = other.settings.session_dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a default agent configuration
|
||||
|
||||
48
src/main.rs
48
src/main.rs
@@ -32,8 +32,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Handle init command first (before config loading)
|
||||
if matches!(cli.command, Some(Commands::Init)) {
|
||||
cmd_init()?;
|
||||
if let Some(Commands::Init { local }) = cli.command {
|
||||
cmd_init(local)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -79,8 +79,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
eprintln!("REPL error: {}", e);
|
||||
}
|
||||
}
|
||||
Some(Commands::Init) => {
|
||||
cmd_init()?;
|
||||
// Init is handled before config loading, but include for completeness
|
||||
Some(Commands::Init { local: _ }) => {
|
||||
// Already handled above
|
||||
}
|
||||
Some(Commands::Run { task, task_type }) => {
|
||||
cmd_run(&agent, &task, task_type.as_deref()).await?;
|
||||
@@ -130,14 +131,37 @@ fn load_config(config_path: Option<&str>) -> anyhow::Result<Config> {
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_init() -> anyhow::Result<()> {
|
||||
fn cmd_init(local: bool) -> anyhow::Result<()> {
|
||||
let loader = ConfigLoader::new();
|
||||
let config = create_default_config()?;
|
||||
|
||||
println!("Creating configuration at: {}", loader.path().display());
|
||||
loader.save(&config)?;
|
||||
if local {
|
||||
// Create local config in .svibe/
|
||||
let local_path = std::path::PathBuf::from(".svibe/config.toml");
|
||||
println!("Creating local configuration at: {}", local_path.display());
|
||||
loader.save_to_path(&local_path, &config)?;
|
||||
|
||||
println!(r#"
|
||||
println!(r#"
|
||||
Local configuration created successfully!
|
||||
|
||||
This config will override settings from your global config.
|
||||
|
||||
Location: .svibe/config.toml
|
||||
|
||||
Next steps:
|
||||
1. Edit .svibe/config.toml to customize project-specific settings
|
||||
2. Set your OpenAI API key in the global config or via environment:
|
||||
export OPENAI_API_KEY=your-key-here
|
||||
3. Run svibe:
|
||||
svibe repl
|
||||
svibe run "Help me with this task"
|
||||
"#);
|
||||
} else {
|
||||
// Create global config
|
||||
println!("Creating global configuration at: {}", loader.global_path().display());
|
||||
loader.save_global(&config)?;
|
||||
|
||||
println!(r#"
|
||||
Configuration created successfully!
|
||||
|
||||
Next steps:
|
||||
@@ -146,14 +170,18 @@ Next steps:
|
||||
|
||||
Or edit the config file and add your API key.
|
||||
|
||||
2. (Optional) Start Ollama for local models:
|
||||
2. (Optional) Create project-specific config:
|
||||
svibe init --local
|
||||
|
||||
3. (Optional) Start Ollama for local models:
|
||||
ollama serve
|
||||
ollama pull llama3
|
||||
|
||||
3. Run svibe:
|
||||
4. Run svibe:
|
||||
svibe repl
|
||||
svibe run "Help me with this task"
|
||||
"#);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user