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
|
/target
|
||||||
|
.svibe
|
||||||
|
|||||||
30
QWEN.md
30
QWEN.md
@@ -70,7 +70,35 @@ cargo run -- --help
|
|||||||
|
|
||||||
## Configuration
|
## 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
|
```toml
|
||||||
[providers.openai]
|
[providers.openai]
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ pub struct Cli {
|
|||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Initialize configuration
|
/// Initialize configuration
|
||||||
Init,
|
Init {
|
||||||
|
/// Create local config in .svibe/ (overrides global config)
|
||||||
|
#[arg(short, long)]
|
||||||
|
local: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Run a single task
|
/// Run a single task
|
||||||
Run {
|
Run {
|
||||||
|
|||||||
@@ -1,61 +1,113 @@
|
|||||||
use super::{Config, ConfigError, Result, OpenAiConfig, OllamaConfig, AgentConfig, AgentModels};
|
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 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};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// Configuration loader
|
/// Configuration loader
|
||||||
pub struct ConfigLoader {
|
pub struct ConfigLoader {
|
||||||
config_path: PathBuf,
|
global_config_path: PathBuf,
|
||||||
|
local_config_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigLoader {
|
impl ConfigLoader {
|
||||||
/// Create a new config loader with default path
|
/// Create a new config loader with default paths
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let config_path = default_config_path();
|
let global_config_path = default_config_path();
|
||||||
Self { 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 {
|
pub fn with_path(path: impl AsRef<Path>) -> Self {
|
||||||
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> {
|
pub fn load(&self) -> Result<Config> {
|
||||||
if !self.config_path.exists() {
|
// Load global config
|
||||||
return Err(ConfigError::NotFound(
|
let mut global_config = self.load_global()?;
|
||||||
self.config_path.display().to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&self.config_path)?;
|
// Load and merge local config if it exists
|
||||||
let mut config: Config = toml::from_str(&content)?;
|
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
|
// Merge with environment variables
|
||||||
config.merge_env();
|
global_config.merge_env();
|
||||||
|
|
||||||
// Ensure default agent exists
|
// Ensure default agent exists
|
||||||
if config.agents.is_empty() {
|
if global_config.agents.is_empty() {
|
||||||
config.agents.insert("default".to_string(), default_agent_config());
|
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)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save configuration to file
|
/// Save global configuration to file
|
||||||
pub fn save(&self, config: &Config) -> Result<()> {
|
pub fn save_global(&self, config: &Config) -> Result<()> {
|
||||||
if let Some(parent) = self.config_path.parent() {
|
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)?;
|
std::fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
let content = toml::to_string_pretty(config)?;
|
let content = toml::to_string_pretty(config)?;
|
||||||
std::fs::write(&self.config_path, content)?;
|
std::fs::write(path, content)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the config path
|
/// Get the global config path
|
||||||
pub fn path(&self) -> &Path {
|
pub fn global_path(&self) -> &Path {
|
||||||
&self.config_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 {
|
fn default_config_path() -> PathBuf {
|
||||||
dirs::config_dir()
|
dirs::config_dir()
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
@@ -73,6 +125,16 @@ fn default_config_path() -> PathBuf {
|
|||||||
.join("config.toml")
|
.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
|
/// Merge environment variables into config
|
||||||
impl Config {
|
impl Config {
|
||||||
fn merge_env(&mut self) {
|
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
|
/// Create a default agent configuration
|
||||||
|
|||||||
46
src/main.rs
46
src/main.rs
@@ -32,8 +32,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
// Handle init command first (before config loading)
|
// Handle init command first (before config loading)
|
||||||
if matches!(cli.command, Some(Commands::Init)) {
|
if let Some(Commands::Init { local }) = cli.command {
|
||||||
cmd_init()?;
|
cmd_init(local)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +79,9 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
eprintln!("REPL error: {}", e);
|
eprintln!("REPL error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Commands::Init) => {
|
// Init is handled before config loading, but include for completeness
|
||||||
cmd_init()?;
|
Some(Commands::Init { local: _ }) => {
|
||||||
|
// Already handled above
|
||||||
}
|
}
|
||||||
Some(Commands::Run { task, task_type }) => {
|
Some(Commands::Run { task, task_type }) => {
|
||||||
cmd_run(&agent, &task, task_type.as_deref()).await?;
|
cmd_run(&agent, &task, task_type.as_deref()).await?;
|
||||||
@@ -130,12 +131,35 @@ 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 loader = ConfigLoader::new();
|
||||||
let config = create_default_config()?;
|
let config = create_default_config()?;
|
||||||
|
|
||||||
println!("Creating configuration at: {}", loader.path().display());
|
if local {
|
||||||
loader.save(&config)?;
|
// 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#"
|
||||||
|
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#"
|
println!(r#"
|
||||||
Configuration created successfully!
|
Configuration created successfully!
|
||||||
@@ -146,14 +170,18 @@ Next steps:
|
|||||||
|
|
||||||
Or edit the config file and add your API key.
|
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 serve
|
||||||
ollama pull llama3
|
ollama pull llama3
|
||||||
|
|
||||||
3. Run svibe:
|
4. Run svibe:
|
||||||
svibe repl
|
svibe repl
|
||||||
svibe run "Help me with this task"
|
svibe run "Help me with this task"
|
||||||
"#);
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user