kagi-vault 0.1.1

Encrypted secrets and environment variable manager for teams — a secure, team-ready dotenv alternative with per-service isolation
use crate::domain::error::DomainError;
use crate::domain::repository::secret_repo::SecretRepository;
use crate::domain::runner::CommandRunner;

pub struct RunCommandService<R: SecretRepository, C: CommandRunner> {
    repo: R,
    runner: C,
}

impl<R: SecretRepository, C: CommandRunner> RunCommandService<R, C> {
    pub fn new(repo: R, runner: C) -> Self {
        Self { repo, runner }
    }

    pub fn execute(
        &self,
        service_name: &str,
        command: &str,
        args: &[String],
    ) -> Result<i32, DomainError> {
        let service = self.repo.load(service_name)?;
        let env_vars: Vec<_> = service
            .secrets
            .values()
            .map(|s| (s.key.clone(), s.value.clone()))
            .collect();
        self.runner.run(&env_vars, command, args)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::crypto::encryptor::mock::XorEncryptor;
    use crate::domain::entity::secret::Secret;
    use crate::domain::entity::service::Service;
    use crate::infrastructure::env_injector::mock::MockCommandRunner;
    use crate::infrastructure::fs_store::FileStore;
    use tempfile::TempDir;

    fn setup(
        dir: &TempDir,
    ) -> (
        RunCommandService<FileStore, MockCommandRunner>,
        MockCommandRunner,
    ) {
        let base = dir.path().join(".kagi");
        std::fs::create_dir(&base).unwrap();
        let config = serde_json::json!({"version": "2", "project_id": "kgp_test", "services": {}});
        std::fs::write(
            base.join(crate::domain::config::KAGI_CONFIG_FILE),
            serde_json::to_string(&config).unwrap(),
        )
        .unwrap();
        let store = FileStore::new(base, Box::new(XorEncryptor::new(0xAB)));
        let mut svc = Service::new("api");
        svc.set_secret(Secret::new("KEY", "val"));
        store.save(&svc).unwrap();
        let runner = MockCommandRunner::default();
        (RunCommandService::new(store, runner.clone()), runner)
    }

    #[test]
    fn test_run_injects_env() {
        let dir = TempDir::new().unwrap();
        let (svc, runner) = setup(&dir);
        let exit_code = svc.execute("api", "echo", &["hello".into()]).unwrap();
        assert_eq!(exit_code, 0);
        let calls = runner.calls.lock().unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].0, vec![("KEY".to_string(), "val".to_string())]);
        assert_eq!(calls[0].1, "echo");
    }
}