homeboy 0.69.0

CLI for multi-component deployment and development workflow automation
Documentation
use crate::config::{self, ConfigEntity};
use crate::error::{Error, Result};
use crate::output::{CreateOutput, MergeOutput, RemoveResult};
use crate::project;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Fleet {
    #[serde(skip_deserializing, default)]
    pub id: String,

    #[serde(default)]
    pub project_ids: Vec<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

impl Fleet {
    pub fn new(id: String, project_ids: Vec<String>) -> Self {
        Self {
            id,
            project_ids,
            description: None,
        }
    }
}

impl ConfigEntity for Fleet {
    const ENTITY_TYPE: &'static str = "fleet";
    const DIR_NAME: &'static str = "fleets";

    fn id(&self) -> &str {
        &self.id
    }
    fn set_id(&mut self, id: String) {
        self.id = id;
    }
    fn not_found_error(id: String, suggestions: Vec<String>) -> Error {
        Error::fleet_not_found(id, suggestions)
    }
}

// ============================================================================
// Core CRUD - Generated by entity_crud! macro
// ============================================================================

entity_crud!(Fleet; list_ids, merge);

// ============================================================================
// Operations
// ============================================================================

/// Add a project to a fleet
pub fn add_project(fleet_id: &str, project_id: &str) -> Result<Fleet> {
    let mut fleet = load(fleet_id)?;

    // Validate project exists
    if !project::exists(project_id) {
        let suggestions = config::find_similar_ids::<crate::project::Project>(project_id);
        return Err(Error::project_not_found(project_id, suggestions));
    }

    // Don't add duplicates
    if !fleet.project_ids.contains(&project_id.to_string()) {
        fleet.project_ids.push(project_id.to_string());
        save(&fleet)?;
    }

    Ok(fleet)
}

/// Remove a project from a fleet
pub fn remove_project(fleet_id: &str, project_id: &str) -> Result<Fleet> {
    let mut fleet = load(fleet_id)?;

    fleet.project_ids.retain(|id| id != project_id);
    save(&fleet)?;

    Ok(fleet)
}

/// Get all projects in a fleet with full project data
pub fn get_projects(fleet_id: &str) -> Result<Vec<crate::project::Project>> {
    let fleet = load(fleet_id)?;
    let mut projects = Vec::new();

    for project_id in &fleet.project_ids {
        if let Ok(project) = project::load(project_id) {
            projects.push(project);
        }
    }

    Ok(projects)
}

/// Get component usage across a fleet (component_id -> Vec<project_id>)
pub fn component_usage(fleet_id: &str) -> Result<std::collections::HashMap<String, Vec<String>>> {
    let fleet = load(fleet_id)?;
    let mut usage: std::collections::HashMap<String, Vec<String>> =
        std::collections::HashMap::new();

    for project_id in &fleet.project_ids {
        if let Ok(project) = project::load(project_id) {
            for component_id in &project.component_ids {
                usage
                    .entry(component_id.clone())
                    .or_default()
                    .push(project_id.clone());
            }
        }
    }

    Ok(usage)
}

// Fleet Sync was removed in #101 — it had OpenClaw-specific logic hardcoded
// in core, violating homeboy's platform-agnostic design. Use `homeboy deploy`
// to sync files across servers instead.