feat: implement Hermes config module and commands (Phase 3)

Add hermes_config.rs (~1190 lines) with YAML section-level replacement
that preserves comments and formatting in unmanaged sections:
- Type definitions: HermesModelConfig, HermesAgentConfig, HermesEnvConfig
- YAML section finder (find_yaml_section_range) with column-0 key detection
- Provider CRUD on custom_providers array (indexed by name field)
- Model/Agent config get/set via yaml<->json conversion
- .env dotenv read/write preserving comments and line ordering
- Health check, backup with rotation, write lock (OnceLock<Mutex>)
- MCP section access stubs for Phase 4
- 19 unit tests

Add commands/hermes.rs with 10 Tauri commands registered in lib.rs.
Replace all Hermes TODO stubs in services/provider/live.rs with real
implementations (import, remove, write-to-live, read-live-settings).
This commit is contained in:
Jason
2026-04-15 17:13:03 +08:00
parent a2e9e1938b
commit 6d0e9f4c74
6 changed files with 1378 additions and 28 deletions
+77 -11
View File
@@ -42,10 +42,8 @@ pub(crate) fn provider_exists_in_live_config(
.map(|providers| providers.contains_key(provider_id)),
AppType::OpenClaw => crate::openclaw_config::get_providers()
.map(|providers| providers.contains_key(provider_id)),
AppType::Hermes => {
// TODO: hermes_config module not yet implemented
Ok(false)
}
AppType::Hermes => crate::hermes_config::get_providers()
.map(|providers| providers.contains_key(provider_id)),
_ => Ok(false),
}
}
@@ -795,11 +793,8 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
}
}
AppType::Hermes => {
// TODO: hermes_config module not yet implemented
log::debug!(
"Hermes provider '{}' write to live config not yet implemented",
provider.id
);
crate::hermes_config::set_provider(&provider.id, provider.settings_config.clone())?;
log::debug!("Hermes provider '{}' written to live config", provider.id);
}
}
Ok(())
@@ -1005,8 +1000,9 @@ pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
"Hermes configuration file not found",
));
}
// Return empty object until hermes_config is implemented
Ok(json!({}))
let yaml_config = crate::hermes_config::read_hermes_config()?;
let config = crate::hermes_config::yaml_to_json(&yaml_config)?;
Ok(config)
}
}
}
@@ -1339,6 +1335,76 @@ pub fn import_openclaw_providers_from_live(state: &AppState) -> Result<usize, Ap
Ok(imported)
}
/// Import all providers from Hermes live config to database
///
/// This imports existing providers from ~/.hermes/config.yaml
/// into the CC Switch database. Each provider found will be added to the
/// database with is_current set to false.
pub fn import_hermes_providers_from_live(state: &AppState) -> Result<usize, AppError> {
use crate::hermes_config;
let providers = hermes_config::get_providers()?;
if providers.is_empty() {
return Ok(0);
}
let mut imported = 0;
let existing_ids = state.db.get_provider_ids("hermes")?;
for (name, config) in providers {
// Validate: skip entries with empty name
if name.trim().is_empty() {
log::warn!("Skipping Hermes provider with empty name");
continue;
}
// Skip if already exists in database
if existing_ids.contains(&name) {
log::debug!("Hermes provider '{name}' already exists in database, skipping");
continue;
}
// Create provider
let mut provider = Provider::with_id(name.clone(), name.clone(), config, None);
provider.meta = Some(crate::provider::ProviderMeta {
live_config_managed: Some(true),
..Default::default()
});
// Save to database
if let Err(e) = state.db.save_provider("hermes", &provider) {
log::warn!("Failed to import Hermes provider '{name}': {e}");
continue;
}
imported += 1;
log::info!("Imported Hermes provider '{name}' from live config");
}
Ok(imported)
}
/// Remove a Hermes provider from live config
///
/// This removes a specific provider from ~/.hermes/config.yaml
/// without affecting other providers in the file.
pub fn remove_hermes_provider_from_live(provider_id: &str) -> Result<(), AppError> {
use crate::hermes_config;
// Check if Hermes config directory exists
if !hermes_config::get_hermes_dir().exists() {
log::debug!(
"Hermes config directory doesn't exist, skipping removal of '{provider_id}'"
);
return Ok(());
}
hermes_config::remove_provider(provider_id)?;
log::info!("Hermes provider '{provider_id}' removed from live config");
Ok(())
}
/// Remove an OpenClaw provider from live config
///
/// This removes a specific provider from ~/.openclaw/openclaw.json
+8 -15
View File
@@ -21,8 +21,9 @@ use crate::store::AppState;
// Re-export sub-module functions for external access
pub use live::{
import_default_config, import_openclaw_providers_from_live,
import_opencode_providers_from_live, read_live_settings, sync_current_to_live,
import_default_config, import_hermes_providers_from_live,
import_openclaw_providers_from_live, import_opencode_providers_from_live, read_live_settings,
sync_current_to_live,
};
// Internal re-exports (pub(crate))
@@ -35,7 +36,8 @@ pub(crate) use live::{
// Internal re-exports
use live::{
remove_openclaw_provider_from_live, remove_opencode_provider_from_live, write_gemini_live,
remove_hermes_provider_from_live, remove_openclaw_provider_from_live,
remove_opencode_provider_from_live, write_gemini_live,
};
use usage::validate_usage_script;
@@ -1282,12 +1284,7 @@ impl ProviderService {
match app_type {
AppType::OpenCode => remove_opencode_provider_from_live(id)?,
AppType::OpenClaw => remove_openclaw_provider_from_live(id)?,
AppType::Hermes => {
// TODO: hermes_config module not yet implemented
log::debug!(
"Hermes provider '{id}' removal from live config not yet implemented"
);
}
AppType::Hermes => remove_hermes_provider_from_live(id)?,
_ => {}
}
}
@@ -1351,8 +1348,7 @@ impl ProviderService {
remove_openclaw_provider_from_live(id)?;
}
AppType::Hermes => {
// TODO: hermes_config module not yet implemented
log::debug!("Hermes provider '{id}' removal from live config not yet implemented");
remove_hermes_provider_from_live(id)?;
}
_ => {
return Err(AppError::Message(format!(
@@ -1542,10 +1538,7 @@ impl ProviderService {
let rollback_result = match app_type {
AppType::OpenCode => remove_opencode_provider_from_live(&provider.id),
AppType::OpenClaw => remove_openclaw_provider_from_live(&provider.id),
AppType::Hermes => {
// TODO: hermes_config module not yet implemented
Ok(())
}
AppType::Hermes => remove_hermes_provider_from_live(&provider.id),
_ => Ok(()),
};