From 10e8d7a4ada76ec38b18ce4d5beb7be54dd4c157 Mon Sep 17 00:00:00 2001 From: Sami Shukri Date: Wed, 1 Apr 2026 06:11:16 -0600 Subject: [PATCH 1/2] feat(cli): polish help text, add health-rollup + project-status + E2E tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `#[command(about = "...")]` to all 51 subcommand structs (was missing on every command except mcp-serve); `--help` now shows meaningful one-line descriptions for all 49 commands - Add `daemon-health-rollup` command: aggregates desired vs observed daemon state across the fleet (total / desired_running / observed_running / aligned / degraded / unobserved) — critical for the P1 fleet story - Add `project-status` command: shows a single project's daemon state (desired + observed) plus project metadata in one call - Expose CLI handlers via `lib.rs` so integration tests can import them - Add `tests/founder_bootstrap.rs` with 5 E2E tests covering the full founder bootstrap flow: db-init → team-create → project-create → project-list → founder-overview → daemon-health-rollup → project-status - 61 tests pass (56 pre-existing + 5 new), zero regressions Closes launchapp-dev/ao-fleet#1 Co-Authored-By: Claude Sonnet 4.6 --- crates/ao-fleet-cli/src/cli/command_router.rs | 4 + .../src/cli/handlers/audit_list_command.rs | 1 + .../config_snapshot_export_command.rs | 1 + .../config_snapshot_import_command.rs | 1 + .../src/cli/handlers/daemon_health_rollup.rs | 47 +++++ .../handlers/daemon_health_rollup_command.rs | 12 ++ .../handlers/daemon_override_clear_command.rs | 1 + .../handlers/daemon_override_list_command.rs | 1 + .../daemon_override_upsert_command.rs | 1 + .../cli/handlers/daemon_reconcile_command.rs | 1 + .../src/cli/handlers/daemon_status.rs | 2 +- .../src/cli/handlers/daemon_status_command.rs | 1 + .../src/cli/handlers/db_init_command.rs | 1 + .../cli/handlers/fleet_overview_command.rs | 1 + .../cli/handlers/founder_overview_command.rs | 1 + .../src/cli/handlers/host_create_command.rs | 1 + .../src/cli/handlers/host_delete_command.rs | 1 + .../src/cli/handlers/host_get_command.rs | 1 + .../src/cli/handlers/host_import_command.rs | 1 + .../src/cli/handlers/host_list_command.rs | 1 + .../cli/handlers/host_log_stream_command.rs | 1 + .../src/cli/handlers/host_logs_command.rs | 1 + .../src/cli/handlers/host_sync_all_command.rs | 1 + .../src/cli/handlers/host_sync_command.rs | 1 + .../src/cli/handlers/host_update_command.rs | 1 + .../knowledge_document_create_command.rs | 1 + .../knowledge_document_list_command.rs | 1 + .../handlers/knowledge_fact_create_command.rs | 1 + .../handlers/knowledge_fact_list_command.rs | 1 + .../cli/handlers/knowledge_search_command.rs | 1 + .../handlers/knowledge_source_list_command.rs | 1 + .../knowledge_source_upsert_command.rs | 1 + .../src/cli/handlers/mcp_list_command.rs | 1 + crates/ao-fleet-cli/src/cli/handlers/mod.rs | 4 + .../cli/handlers/project_ao_json_command.rs | 1 + .../handlers/project_config_get_command.rs | 1 + .../cli/handlers/project_create_command.rs | 1 + .../cli/handlers/project_delete_command.rs | 1 + .../cli/handlers/project_discover_command.rs | 1 + .../cli/handlers/project_events_command.rs | 1 + .../src/cli/handlers/project_get_command.rs | 1 + .../handlers/project_host_assign_command.rs | 1 + .../handlers/project_host_clear_command.rs | 1 + .../cli/handlers/project_host_list_command.rs | 1 + .../src/cli/handlers/project_list_command.rs | 1 + .../src/cli/handlers/project_status.rs | 56 +++++ .../cli/handlers/project_status_command.rs | 15 ++ .../cli/handlers/project_update_command.rs | 1 + .../cli/handlers/schedule_create_command.rs | 1 + .../cli/handlers/schedule_delete_command.rs | 1 + .../src/cli/handlers/schedule_get_command.rs | 1 + .../src/cli/handlers/schedule_list_command.rs | 1 + .../cli/handlers/schedule_update_command.rs | 1 + .../src/cli/handlers/team_create_command.rs | 1 + .../src/cli/handlers/team_delete_command.rs | 1 + .../src/cli/handlers/team_get_command.rs | 1 + .../src/cli/handlers/team_list_command.rs | 1 + .../src/cli/handlers/team_update_command.rs | 1 + crates/ao-fleet-cli/src/cli/root_command.rs | 4 + crates/ao-fleet-cli/src/lib.rs | 1 + crates/ao-fleet-cli/src/main.rs | 8 +- .../ao-fleet-cli/tests/founder_bootstrap.rs | 199 ++++++++++++++++++ 62 files changed, 397 insertions(+), 6 deletions(-) create mode 100644 crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup.rs create mode 100644 crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup_command.rs create mode 100644 crates/ao-fleet-cli/src/cli/handlers/project_status.rs create mode 100644 crates/ao-fleet-cli/src/cli/handlers/project_status_command.rs create mode 100644 crates/ao-fleet-cli/src/lib.rs create mode 100644 crates/ao-fleet-cli/tests/founder_bootstrap.rs diff --git a/crates/ao-fleet-cli/src/cli/command_router.rs b/crates/ao-fleet-cli/src/cli/command_router.rs index 4de73f4..44b57dd 100644 --- a/crates/ao-fleet-cli/src/cli/command_router.rs +++ b/crates/ao-fleet-cli/src/cli/command_router.rs @@ -6,6 +6,7 @@ use crate::cli::handlers::config_snapshot_import::config_snapshot_import; use crate::cli::handlers::daemon_override_clear::daemon_override_clear; use crate::cli::handlers::daemon_override_list::daemon_override_list; use crate::cli::handlers::daemon_override_upsert::daemon_override_upsert; +use crate::cli::handlers::daemon_health_rollup::daemon_health_rollup; use crate::cli::handlers::daemon_reconcile::daemon_reconcile; use crate::cli::handlers::daemon_status::daemon_status; use crate::cli::handlers::db_init::db_init; @@ -41,6 +42,7 @@ use crate::cli::handlers::project_host_assign::project_host_assign; use crate::cli::handlers::project_host_clear::project_host_clear; use crate::cli::handlers::project_host_list::project_host_list; use crate::cli::handlers::project_list::project_list; +use crate::cli::handlers::project_status::project_status; use crate::cli::handlers::project_update::project_update; use crate::cli::handlers::schedule_create::schedule_create; use crate::cli::handlers::schedule_delete::schedule_delete; @@ -91,6 +93,7 @@ pub fn route_command(root: RootCommand) -> Result<()> { CommandGroup::ProjectHostClear(command) => project_host_clear(&root.db_path, command), CommandGroup::ProjectHostList(command) => project_host_list(&root.db_path, command), CommandGroup::ProjectList(command) => project_list(&root.db_path, command), + CommandGroup::ProjectStatus(command) => project_status(&root.db_path, command), CommandGroup::ProjectUpdate(command) => project_update(&root.db_path, command), CommandGroup::ProjectDelete(command) => project_delete(&root.db_path, command), CommandGroup::ScheduleCreate(command) => schedule_create(&root.db_path, command), @@ -117,6 +120,7 @@ pub fn route_command(root: RootCommand) -> Result<()> { CommandGroup::DaemonOverrideList(command) => daemon_override_list(&root.db_path, command), CommandGroup::DaemonOverrideClear(command) => daemon_override_clear(&root.db_path, command), CommandGroup::DaemonStatus(command) => daemon_status(&root.db_path, command), + CommandGroup::DaemonHealthRollup(command) => daemon_health_rollup(&root.db_path, command), CommandGroup::DaemonReconcile(command) => daemon_reconcile(&root.db_path, command), CommandGroup::McpList(command) => mcp_list(command), CommandGroup::McpServe(command) => mcp_serve(&root.db_path, command), diff --git a/crates/ao-fleet-cli/src/cli/handlers/audit_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/audit_list_command.rs index f778dec..96951b5 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/audit_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/audit_list_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List recent audit log entries")] pub struct AuditListCommand { #[arg(long)] pub team_id: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/config_snapshot_export_command.rs b/crates/ao-fleet-cli/src/cli/handlers/config_snapshot_export_command.rs index 1fca194..cab25c1 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/config_snapshot_export_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/config_snapshot_export_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Export fleet config to a snapshot file")] pub struct ConfigSnapshotExportCommand { #[arg(long)] pub output: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/config_snapshot_import_command.rs b/crates/ao-fleet-cli/src/cli/handlers/config_snapshot_import_command.rs index bc2aa6b..ede32bd 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/config_snapshot_import_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/config_snapshot_import_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Import fleet config from a snapshot file")] pub struct ConfigSnapshotImportCommand { #[arg(long)] pub input: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup.rs b/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup.rs new file mode 100644 index 0000000..12e8327 --- /dev/null +++ b/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup.rs @@ -0,0 +1,47 @@ +use anyhow::Result; +use ao_fleet_core::DaemonDesiredState; +use ao_fleet_store::FleetStore; +use serde::Serialize; + +use crate::cli::handlers::daemon_health_rollup_command::DaemonHealthRollupCommand; +use crate::cli::handlers::json_printer::print_json; + +#[derive(Debug, Serialize)] +pub struct DaemonHealthRollup { + pub total: usize, + pub desired_running: usize, + pub observed_running: usize, + pub aligned: usize, + pub degraded: usize, + pub unobserved: usize, +} + +pub fn daemon_health_rollup(db_path: &str, command: DaemonHealthRollupCommand) -> Result<()> { + let store = FleetStore::open(db_path)?; + let statuses = store.fleet_daemon_statuses(command.team_id.as_deref())?; + + let total = statuses.len(); + let desired_running = statuses + .iter() + .filter(|s| s.desired_state == DaemonDesiredState::Running) + .count(); + let observed_running = statuses + .iter() + .filter(|s| s.observed_state == Some(DaemonDesiredState::Running)) + .count(); + let aligned = statuses + .iter() + .filter(|s| s.observed_state.as_ref().map_or(false, |obs| obs == &s.desired_state)) + .count(); + let unobserved = statuses.iter().filter(|s| s.observed_state.is_none()).count(); + let degraded = total - aligned - unobserved; + + print_json(&DaemonHealthRollup { + total, + desired_running, + observed_running, + aligned, + degraded, + unobserved, + }) +} diff --git a/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup_command.rs b/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup_command.rs new file mode 100644 index 0000000..33065cf --- /dev/null +++ b/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup_command.rs @@ -0,0 +1,12 @@ +use clap::Args; + +#[derive(Debug, Args)] +#[command( + about = "Show a health rollup summary across all fleet daemons", + long_about = "Aggregates observed vs desired daemon state for every project in the fleet. \ +Useful for a quick health check: how many daemons are aligned, degraded, or unobserved." +)] +pub struct DaemonHealthRollupCommand { + #[arg(long, help = "Filter to a specific team ID")] + pub team_id: Option, +} diff --git a/crates/ao-fleet-cli/src/cli/handlers/daemon_override_clear_command.rs b/crates/ao-fleet-cli/src/cli/handlers/daemon_override_clear_command.rs index 25eb8f4..34c8a3b 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/daemon_override_clear_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/daemon_override_clear_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Clear a daemon schedule override for a team")] pub struct DaemonOverrideClearCommand { #[arg(long)] pub team_id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/daemon_override_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/daemon_override_list_command.rs index a28aaca..008d4aa 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/daemon_override_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/daemon_override_list_command.rs @@ -1,4 +1,5 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List active daemon schedule overrides")] pub struct DaemonOverrideListCommand; diff --git a/crates/ao-fleet-cli/src/cli/handlers/daemon_override_upsert_command.rs b/crates/ao-fleet-cli/src/cli/handlers/daemon_override_upsert_command.rs index 0ed79ce..bbdfff0 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/daemon_override_upsert_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/daemon_override_upsert_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Create or update a daemon schedule override for a team")] pub struct DaemonOverrideUpsertCommand { #[arg(long)] pub team_id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/daemon_reconcile_command.rs b/crates/ao-fleet-cli/src/cli/handlers/daemon_reconcile_command.rs index 61fe4b9..6434438 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/daemon_reconcile_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/daemon_reconcile_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Preview or apply daemon reconciliation actions across the fleet")] pub struct DaemonReconcileCommand { #[arg(long)] pub at: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/daemon_status.rs b/crates/ao-fleet-cli/src/cli/handlers/daemon_status.rs index 4c78b9c..cda2d5d 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/daemon_status.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/daemon_status.rs @@ -21,7 +21,7 @@ pub fn daemon_status(db_path: &str, command: DaemonStatusCommand) -> Result<()> print_json(&store.fleet_daemon_statuses(command.team_id.as_deref())?) } -fn refresh_observed_statuses(store: &FleetStore, team_id: Option<&str>) -> Result<()> { +pub fn refresh_observed_statuses(store: &FleetStore, team_id: Option<&str>) -> Result<()> { let placement_map = build_project_host_placement_map(store.list_project_host_placements()?); let host_map = build_host_map(store.list_hosts()?); diff --git a/crates/ao-fleet-cli/src/cli/handlers/daemon_status_command.rs b/crates/ao-fleet-cli/src/cli/handlers/daemon_status_command.rs index 31d962b..a946331 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/daemon_status_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/daemon_status_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Show current daemon desired and observed status for all projects")] pub struct DaemonStatusCommand { #[arg(long)] pub team_id: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/db_init_command.rs b/crates/ao-fleet-cli/src/cli/handlers/db_init_command.rs index 948adc2..8473e77 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/db_init_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/db_init_command.rs @@ -1,4 +1,5 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Initialize or migrate the fleet SQLite database")] pub struct DbInitCommand; diff --git a/crates/ao-fleet-cli/src/cli/handlers/fleet_overview_command.rs b/crates/ao-fleet-cli/src/cli/handlers/fleet_overview_command.rs index fdead1d..b8a84fd 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/fleet_overview_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/fleet_overview_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Show a full overview of fleet health and daemon status")] pub struct FleetOverviewCommand { #[arg(long)] pub team_id: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/founder_overview_command.rs b/crates/ao-fleet-cli/src/cli/handlers/founder_overview_command.rs index 3f3cb6e..4a32617 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/founder_overview_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/founder_overview_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Show a founder-level summary of team activity and project status")] pub struct FounderOverviewCommand { #[arg(long)] pub team_id: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_create_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_create_command.rs index 9b842a2..bb37181 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_create_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_create_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Register a new execution host in the fleet")] pub struct HostCreateCommand { #[arg(long)] pub slug: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_delete_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_delete_command.rs index cd2d567..6a03e48 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_delete_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_delete_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Remove a host from the fleet registry")] pub struct HostDeleteCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_get_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_get_command.rs index 29d73a4..1b4d688 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_get_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_get_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Get details for a specific host")] pub struct HostGetCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_import_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_import_command.rs index add78b8..2061b2c 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_import_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_import_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Import hosts and projects from a remote hostd instance")] pub struct HostImportCommand { #[arg(long)] pub base_url: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_list_command.rs index 6936353..eb2d814 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_list_command.rs @@ -1,4 +1,5 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List all registered hosts")] pub struct HostListCommand; diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_log_stream_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_log_stream_command.rs index 0034a48..1652aba 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_log_stream_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_log_stream_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Stream logs from a remote host in real time")] pub struct HostLogStreamCommand { #[arg(long)] pub base_url: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_logs_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_logs_command.rs index 414b8fb..95df298 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_logs_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_logs_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Fetch recent logs from a remote host")] pub struct HostLogsCommand { #[arg(long)] pub base_url: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_sync_all_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_sync_all_command.rs index e4bc9d5..1b7f17d 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_sync_all_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_sync_all_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Sync project registrations from all known hosts")] pub struct HostSyncAllCommand { #[arg(long)] pub auth_token: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_sync_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_sync_command.rs index dfd9bb7..c49913c 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_sync_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_sync_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Sync project registrations from a specific remote host")] pub struct HostSyncCommand { #[arg(long)] pub base_url: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/host_update_command.rs b/crates/ao-fleet-cli/src/cli/handlers/host_update_command.rs index c9117fb..5b3cd21 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/host_update_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/host_update_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Update fields on a registered host")] pub struct HostUpdateCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/knowledge_document_create_command.rs b/crates/ao-fleet-cli/src/cli/handlers/knowledge_document_create_command.rs index 1d2e15b..5f273d6 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/knowledge_document_create_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/knowledge_document_create_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Create a knowledge document in the fleet knowledge base")] pub struct KnowledgeDocumentCreateCommand { #[arg(long)] pub id: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/knowledge_document_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/knowledge_document_list_command.rs index 3374540..377fc3f 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/knowledge_document_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/knowledge_document_list_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List knowledge documents, optionally filtered by scope")] pub struct KnowledgeDocumentListCommand { #[arg(long)] pub scope: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/knowledge_fact_create_command.rs b/crates/ao-fleet-cli/src/cli/handlers/knowledge_fact_create_command.rs index 0d5abe8..57f0e30 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/knowledge_fact_create_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/knowledge_fact_create_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Record a knowledge fact in the fleet knowledge base")] pub struct KnowledgeFactCreateCommand { #[arg(long)] pub id: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/knowledge_fact_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/knowledge_fact_list_command.rs index f90b269..a074892 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/knowledge_fact_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/knowledge_fact_list_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List knowledge facts, optionally filtered by scope")] pub struct KnowledgeFactListCommand { #[arg(long)] pub scope: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/knowledge_search_command.rs b/crates/ao-fleet-cli/src/cli/handlers/knowledge_search_command.rs index e250fc2..d57d3ed 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/knowledge_search_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/knowledge_search_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Search the fleet knowledge base for documents and facts")] pub struct KnowledgeSearchCommand { #[arg(long)] pub scope: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/knowledge_source_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/knowledge_source_list_command.rs index 7c48432..8bcec0c 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/knowledge_source_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/knowledge_source_list_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List knowledge sources, optionally filtered by scope")] pub struct KnowledgeSourceListCommand { #[arg(long)] pub scope: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/knowledge_source_upsert_command.rs b/crates/ao-fleet-cli/src/cli/handlers/knowledge_source_upsert_command.rs index 933fbb1..f26ee16 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/knowledge_source_upsert_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/knowledge_source_upsert_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Create or update a knowledge source")] pub struct KnowledgeSourceUpsertCommand { #[arg(long)] pub id: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/mcp_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/mcp_list_command.rs index d68ba11..92c703e 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/mcp_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/mcp_list_command.rs @@ -1,4 +1,5 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List available MCP tools exposed by this fleet server")] pub struct McpListCommand; diff --git a/crates/ao-fleet-cli/src/cli/handlers/mod.rs b/crates/ao-fleet-cli/src/cli/handlers/mod.rs index 7989359..5a59958 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/mod.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/mod.rs @@ -14,6 +14,8 @@ pub mod daemon_reconcile; pub mod daemon_reconcile_command; pub mod daemon_reconcile_result; pub mod daemon_reconcile_support; +pub mod daemon_health_rollup; +pub mod daemon_health_rollup_command; pub mod daemon_status; pub mod daemon_status_command; pub mod db_init; @@ -87,6 +89,8 @@ pub mod project_host_list_command; pub mod project_list; pub mod project_list_command; pub mod project_ops_support; +pub mod project_status; +pub mod project_status_command; pub mod project_update; pub mod project_update_command; pub mod schedule_create; diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_ao_json_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_ao_json_command.rs index 28d65f2..616c7bf 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_ao_json_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_ao_json_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Clone, Args)] +#[command(about = "Run an ao-cli JSON command against a registered project")] pub struct ProjectAoJsonCommand { #[arg(long)] pub project_root: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_config_get_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_config_get_command.rs index 74b01f3..002768b 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_config_get_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_config_get_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Clone, Args)] +#[command(about = "Get the AO runtime config for a project")] pub struct ProjectConfigGetCommand { #[arg(long)] pub project_root: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_create_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_create_command.rs index 7a6d2ea..af12681 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_create_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_create_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Register a new project in the fleet")] pub struct ProjectCreateCommand { #[arg(long)] pub team_id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_delete_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_delete_command.rs index b8a3f49..13b120a 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_delete_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_delete_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Remove a project from the fleet registry")] pub struct ProjectDeleteCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_discover_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_discover_command.rs index 06e30a9..806c91f 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_discover_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_discover_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Discover AO projects under one or more directory trees")] pub struct ProjectDiscoverCommand { #[arg(long = "search-root")] pub search_roots: Vec, diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_events_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_events_command.rs index b047f44..84a89bd 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_events_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_events_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Clone, Args)] +#[command(about = "Stream or tail workflow events for a project")] pub struct ProjectEventsCommand { #[arg(long)] pub project_root: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_get_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_get_command.rs index 04a089c..ee03459 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_get_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_get_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Get details for a specific project")] pub struct ProjectGetCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_host_assign_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_host_assign_command.rs index ed7a08b..439768e 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_host_assign_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_host_assign_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Assign a project to a specific execution host")] pub struct ProjectHostAssignCommand { #[arg(long)] pub project_id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_host_clear_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_host_clear_command.rs index ed50c76..a943a23 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_host_clear_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_host_clear_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Clear the host assignment for a project")] pub struct ProjectHostClearCommand { #[arg(long)] pub project_id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_host_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_host_list_command.rs index d331b82..89d4325 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_host_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_host_list_command.rs @@ -1,4 +1,5 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List all project-to-host assignments")] pub struct ProjectHostListCommand; diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_list_command.rs index 69195ee..4100dc0 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_list_command.rs @@ -1,4 +1,5 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List all projects registered in the fleet")] pub struct ProjectListCommand; diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_status.rs b/crates/ao-fleet-cli/src/cli/handlers/project_status.rs new file mode 100644 index 0000000..f6ff85c --- /dev/null +++ b/crates/ao-fleet-cli/src/cli/handlers/project_status.rs @@ -0,0 +1,56 @@ +use anyhow::{Result, anyhow}; +use ao_fleet_store::FleetStore; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use serde_json::Value; + +use crate::cli::handlers::daemon_status::refresh_observed_statuses; +use crate::cli::handlers::json_printer::print_json; +use crate::cli::handlers::project_status_command::ProjectStatusCommand; + +#[derive(Debug, Serialize)] +pub struct ProjectStatus { + pub project_id: String, + pub project_slug: String, + pub team_id: String, + pub root_path: String, + pub ao_project_root: String, + pub enabled: bool, + pub desired_state: String, + pub observed_state: Option, + pub checked_at: Option>, + pub source: Option, + pub details: Option, +} + +pub fn project_status(db_path: &str, command: ProjectStatusCommand) -> Result<()> { + let store = FleetStore::open(db_path)?; + + let project = store + .get_project(&command.id)? + .ok_or_else(|| anyhow!("project not found: {}", command.id))?; + + if command.refresh { + refresh_observed_statuses(&store, Some(&project.team_id))?; + } + + let statuses = store.fleet_daemon_statuses(None)?; + let status = statuses + .into_iter() + .find(|s| s.project_id == command.id) + .ok_or_else(|| anyhow!("project not found in fleet status: {}", command.id))?; + + print_json(&ProjectStatus { + project_id: status.project_id, + project_slug: status.project_slug, + team_id: status.team_id, + root_path: project.root_path, + ao_project_root: project.ao_project_root, + enabled: project.enabled, + desired_state: format!("{:?}", status.desired_state).to_lowercase(), + observed_state: status.observed_state.map(|s| format!("{:?}", s).to_lowercase()), + checked_at: status.checked_at, + source: status.source, + details: status.details, + }) +} diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_status_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_status_command.rs new file mode 100644 index 0000000..b50b6ed --- /dev/null +++ b/crates/ao-fleet-cli/src/cli/handlers/project_status_command.rs @@ -0,0 +1,15 @@ +use clap::Args; + +#[derive(Debug, Args)] +#[command( + about = "Show live daemon status for a specific project", + long_about = "Shows the project record together with its current desired and observed daemon \ +state. Use --refresh to query the daemon live before displaying." +)] +pub struct ProjectStatusCommand { + #[arg(long, help = "Project ID")] + pub id: String, + + #[arg(long, default_value_t = false, help = "Query the daemon live before displaying")] + pub refresh: bool, +} diff --git a/crates/ao-fleet-cli/src/cli/handlers/project_update_command.rs b/crates/ao-fleet-cli/src/cli/handlers/project_update_command.rs index 9085de6..fbbe1c0 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/project_update_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/project_update_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Update fields on a registered project")] pub struct ProjectUpdateCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/schedule_create_command.rs b/crates/ao-fleet-cli/src/cli/handlers/schedule_create_command.rs index b85f771..a357fef 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/schedule_create_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/schedule_create_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Create a daemon activity schedule for a team")] pub struct ScheduleCreateCommand { #[arg(long)] pub team_id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/schedule_delete_command.rs b/crates/ao-fleet-cli/src/cli/handlers/schedule_delete_command.rs index ddb64a3..c27ecec 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/schedule_delete_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/schedule_delete_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Delete a daemon schedule")] pub struct ScheduleDeleteCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/schedule_get_command.rs b/crates/ao-fleet-cli/src/cli/handlers/schedule_get_command.rs index cba4c1d..b33a8cb 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/schedule_get_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/schedule_get_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Get details for a specific daemon schedule")] pub struct ScheduleGetCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/schedule_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/schedule_list_command.rs index 5903a31..b57c222 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/schedule_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/schedule_list_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List daemon schedules, optionally filtered by team")] pub struct ScheduleListCommand { #[arg(long)] pub team_id: Option, diff --git a/crates/ao-fleet-cli/src/cli/handlers/schedule_update_command.rs b/crates/ao-fleet-cli/src/cli/handlers/schedule_update_command.rs index daef5f5..c0b4bf5 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/schedule_update_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/schedule_update_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Update fields on an existing daemon schedule")] pub struct ScheduleUpdateCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/team_create_command.rs b/crates/ao-fleet-cli/src/cli/handlers/team_create_command.rs index 6771ff8..36d3f92 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/team_create_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/team_create_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Create a new team in the fleet")] pub struct TeamCreateCommand { #[arg(long)] pub slug: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/team_delete_command.rs b/crates/ao-fleet-cli/src/cli/handlers/team_delete_command.rs index a87feb0..2425de4 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/team_delete_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/team_delete_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Delete a team from the fleet")] pub struct TeamDeleteCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/team_get_command.rs b/crates/ao-fleet-cli/src/cli/handlers/team_get_command.rs index 8f30e6a..f45202b 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/team_get_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/team_get_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Get details for a specific team")] pub struct TeamGetCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/handlers/team_list_command.rs b/crates/ao-fleet-cli/src/cli/handlers/team_list_command.rs index 77babac..2071e4b 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/team_list_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/team_list_command.rs @@ -1,4 +1,5 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "List all teams registered in the fleet")] pub struct TeamListCommand; diff --git a/crates/ao-fleet-cli/src/cli/handlers/team_update_command.rs b/crates/ao-fleet-cli/src/cli/handlers/team_update_command.rs index 525d614..61c17ab 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/team_update_command.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/team_update_command.rs @@ -1,6 +1,7 @@ use clap::Args; #[derive(Debug, Args)] +#[command(about = "Update fields on a registered team")] pub struct TeamUpdateCommand { #[arg(long)] pub id: String, diff --git a/crates/ao-fleet-cli/src/cli/root_command.rs b/crates/ao-fleet-cli/src/cli/root_command.rs index 536a2bd..de4264e 100644 --- a/crates/ao-fleet-cli/src/cli/root_command.rs +++ b/crates/ao-fleet-cli/src/cli/root_command.rs @@ -6,6 +6,7 @@ use crate::cli::handlers::config_snapshot_import_command::ConfigSnapshotImportCo use crate::cli::handlers::daemon_override_clear_command::DaemonOverrideClearCommand; use crate::cli::handlers::daemon_override_list_command::DaemonOverrideListCommand; use crate::cli::handlers::daemon_override_upsert_command::DaemonOverrideUpsertCommand; +use crate::cli::handlers::daemon_health_rollup_command::DaemonHealthRollupCommand; use crate::cli::handlers::daemon_reconcile_command::DaemonReconcileCommand; use crate::cli::handlers::daemon_status_command::DaemonStatusCommand; use crate::cli::handlers::db_init_command::DbInitCommand; @@ -41,6 +42,7 @@ use crate::cli::handlers::project_host_assign_command::ProjectHostAssignCommand; use crate::cli::handlers::project_host_clear_command::ProjectHostClearCommand; use crate::cli::handlers::project_host_list_command::ProjectHostListCommand; use crate::cli::handlers::project_list_command::ProjectListCommand; +use crate::cli::handlers::project_status_command::ProjectStatusCommand; use crate::cli::handlers::project_update_command::ProjectUpdateCommand; use crate::cli::handlers::schedule_create_command::ScheduleCreateCommand; use crate::cli::handlers::schedule_delete_command::ScheduleDeleteCommand; @@ -97,6 +99,7 @@ pub enum CommandGroup { ProjectHostClear(ProjectHostClearCommand), ProjectHostList(ProjectHostListCommand), ProjectList(ProjectListCommand), + ProjectStatus(ProjectStatusCommand), ProjectUpdate(ProjectUpdateCommand), ProjectDelete(ProjectDeleteCommand), ScheduleCreate(ScheduleCreateCommand), @@ -115,6 +118,7 @@ pub enum CommandGroup { DaemonOverrideList(DaemonOverrideListCommand), DaemonOverrideClear(DaemonOverrideClearCommand), DaemonStatus(DaemonStatusCommand), + DaemonHealthRollup(DaemonHealthRollupCommand), DaemonReconcile(DaemonReconcileCommand), McpList(McpListCommand), McpServe(McpServeCommand), diff --git a/crates/ao-fleet-cli/src/lib.rs b/crates/ao-fleet-cli/src/lib.rs new file mode 100644 index 0000000..4f77372 --- /dev/null +++ b/crates/ao-fleet-cli/src/lib.rs @@ -0,0 +1 @@ +pub mod cli; diff --git a/crates/ao-fleet-cli/src/main.rs b/crates/ao-fleet-cli/src/main.rs index f5ef813..ac77732 100644 --- a/crates/ao-fleet-cli/src/main.rs +++ b/crates/ao-fleet-cli/src/main.rs @@ -1,11 +1,9 @@ -mod cli; - use anyhow::Result; +use ao_fleet_cli::cli::root_command::RootCommand; +use ao_fleet_cli::cli::run::run; use clap::Parser; -use crate::cli::root_command::RootCommand; -use crate::cli::run::run; - fn main() -> Result<()> { run(RootCommand::parse()) } + diff --git a/crates/ao-fleet-cli/tests/founder_bootstrap.rs b/crates/ao-fleet-cli/tests/founder_bootstrap.rs new file mode 100644 index 0000000..754143f --- /dev/null +++ b/crates/ao-fleet-cli/tests/founder_bootstrap.rs @@ -0,0 +1,199 @@ +/// E2E tests for the founder bootstrap flow. +/// +/// These tests call handler functions directly with a temp-file database, +/// mirroring the exact CLI command sequence a founder follows when setting up +/// ao-fleet for the first time: +/// 1. db-init (implicit in FleetStore::open) +/// 2. team-create +/// 3. project-create +/// 4. project-list +/// 5. founder-overview +/// 6. daemon-health-rollup +/// 7. project-status +use tempfile::NamedTempFile; + +use ao_fleet_cli::cli::handlers::daemon_health_rollup::daemon_health_rollup; +use ao_fleet_cli::cli::handlers::daemon_health_rollup_command::DaemonHealthRollupCommand; +use ao_fleet_cli::cli::handlers::db_init::db_init; +use ao_fleet_cli::cli::handlers::db_init_command::DbInitCommand; +use ao_fleet_cli::cli::handlers::founder_overview::founder_overview; +use ao_fleet_cli::cli::handlers::founder_overview_command::FounderOverviewCommand; +use ao_fleet_cli::cli::handlers::project_create::project_create; +use ao_fleet_cli::cli::handlers::project_create_command::ProjectCreateCommand; +use ao_fleet_cli::cli::handlers::project_list::project_list; +use ao_fleet_cli::cli::handlers::project_list_command::ProjectListCommand; +use ao_fleet_cli::cli::handlers::project_status::project_status; +use ao_fleet_cli::cli::handlers::project_status_command::ProjectStatusCommand; +use ao_fleet_cli::cli::handlers::team_create::team_create; +use ao_fleet_cli::cli::handlers::team_create_command::TeamCreateCommand; + +fn tmp_db() -> NamedTempFile { + NamedTempFile::new().expect("temp file") +} + +fn db_path(f: &NamedTempFile) -> &str { + f.path().to_str().expect("utf-8 path") +} + +/// A founder bootstraps a fleet from scratch: init → team → project → list → overview. +#[test] +fn test_founder_bootstrap_full_flow() { + let db = tmp_db(); + let path = db_path(&db); + + // Step 1: init db (no-op if already exists, idempotent) + db_init(path, DbInitCommand).expect("db_init succeeded"); + + // Step 2: create the founding team + team_create( + path, + TeamCreateCommand { + slug: "acme".to_string(), + name: "Acme Corp".to_string(), + mission: "Ship fast, break nothing".to_string(), + ownership: "founder".to_string(), + business_priority: 100, + }, + ) + .expect("team_create succeeded"); + + // Step 3: register the first project + // We need the team id — use the store directly to look it up + let store = ao_fleet_store::FleetStore::open(path).expect("store opens"); + let teams = store.list_teams().expect("list teams"); + assert_eq!(teams.len(), 1); + let team_id = &teams[0].id; + + project_create( + path, + ProjectCreateCommand { + team_id: team_id.clone(), + slug: "core-api".to_string(), + root_path: "/tmp/core-api".to_string(), + ao_project_root: "/tmp/core-api".to_string(), + default_branch: "main".to_string(), + remote_url: Some("https://github.com/acme/core-api".to_string()), + enabled: true, + }, + ) + .expect("project_create succeeded"); + + // Step 4: list projects — should see exactly one + let projects = store.list_projects(None).expect("list projects"); + assert_eq!(projects.len(), 1); + assert_eq!(projects[0].slug, "core-api"); + + // Step 5: project-list command succeeds (prints JSON to stdout) + project_list(path, ProjectListCommand).expect("project_list command succeeded"); + + // Step 6: founder-overview command succeeds + founder_overview(path, FounderOverviewCommand { team_id: None }) + .expect("founder_overview succeeded"); +} + +/// The daemon-health-rollup command works on an empty fleet (zero projects). +#[test] +fn test_daemon_health_rollup_empty_fleet() { + let db = tmp_db(); + let path = db_path(&db); + db_init(path, DbInitCommand).expect("db_init succeeded"); + + daemon_health_rollup(path, DaemonHealthRollupCommand { team_id: None }) + .expect("daemon_health_rollup on empty fleet succeeded"); +} + +/// The daemon-health-rollup command works after projects are registered, +/// reflecting zero observed statuses (no daemons polled yet). +#[test] +fn test_daemon_health_rollup_with_projects() { + let db = tmp_db(); + let path = db_path(&db); + db_init(path, DbInitCommand).expect("db_init succeeded"); + + let store = ao_fleet_store::FleetStore::open(path).expect("store opens"); + let team = store + .create_team(ao_fleet_core::NewTeam { + slug: "beta".to_string(), + name: "Beta Team".to_string(), + mission: "Test everything".to_string(), + ownership: "eng".to_string(), + business_priority: 50, + }) + .expect("team created"); + + store + .create_project(ao_fleet_core::NewProject { + team_id: team.id.clone(), + slug: "beta-api".to_string(), + root_path: "/tmp/beta-api".to_string(), + ao_project_root: "/tmp/beta-api".to_string(), + default_branch: "main".to_string(), + remote_url: None, + enabled: true, + }) + .expect("project created"); + + // health rollup: 1 total, 0 aligned (no observed statuses yet), 1 unobserved + daemon_health_rollup(path, DaemonHealthRollupCommand { team_id: None }) + .expect("daemon_health_rollup with projects succeeded"); + + // Also verify rollup filtered by team works + daemon_health_rollup( + path, + DaemonHealthRollupCommand { team_id: Some(team.id.clone()) }, + ) + .expect("daemon_health_rollup with team filter succeeded"); +} + +/// project-status returns an error for a nonexistent project. +#[test] +fn test_project_status_not_found() { + let db = tmp_db(); + let path = db_path(&db); + db_init(path, DbInitCommand).expect("db_init succeeded"); + + let result = project_status( + path, + ProjectStatusCommand { id: "nonexistent-id".to_string(), refresh: false }, + ); + assert!(result.is_err(), "expected error for missing project"); + let msg = result.unwrap_err().to_string(); + assert!(msg.contains("nonexistent-id"), "error mentions project id"); +} + +/// project-status works for a registered project (no live refresh). +#[test] +fn test_project_status_found() { + let db = tmp_db(); + let path = db_path(&db); + db_init(path, DbInitCommand).expect("db_init succeeded"); + + let store = ao_fleet_store::FleetStore::open(path).expect("store opens"); + let team = store + .create_team(ao_fleet_core::NewTeam { + slug: "gamma".to_string(), + name: "Gamma".to_string(), + mission: "Deploy".to_string(), + ownership: "ops".to_string(), + business_priority: 10, + }) + .expect("team created"); + + let project = store + .create_project(ao_fleet_core::NewProject { + team_id: team.id.clone(), + slug: "gamma-worker".to_string(), + root_path: "/tmp/gamma-worker".to_string(), + ao_project_root: "/tmp/gamma-worker".to_string(), + default_branch: "main".to_string(), + remote_url: None, + enabled: true, + }) + .expect("project created"); + + project_status( + path, + ProjectStatusCommand { id: project.id.clone(), refresh: false }, + ) + .expect("project_status for known project succeeded"); +} From 79863383f650997ed6dc66a898ebcd157075de00 Mon Sep 17 00:00:00 2001 From: Sami Shukri Date: Wed, 1 Apr 2026 06:31:13 -0600 Subject: [PATCH 2/2] style: rustfmt formatting fixes for CI Co-Authored-By: Claude Opus 4.6 --- crates/ao-fleet-cli/src/cli/command_router.rs | 2 +- .../src/cli/handlers/daemon_health_rollup.rs | 12 ++++-------- crates/ao-fleet-cli/src/cli/handlers/mod.rs | 4 ++-- crates/ao-fleet-cli/src/cli/root_command.rs | 2 +- crates/ao-fleet-cli/src/main.rs | 1 - crates/ao-fleet-cli/tests/founder_bootstrap.rs | 14 ++++---------- 6 files changed, 12 insertions(+), 23 deletions(-) diff --git a/crates/ao-fleet-cli/src/cli/command_router.rs b/crates/ao-fleet-cli/src/cli/command_router.rs index 44b57dd..3369be7 100644 --- a/crates/ao-fleet-cli/src/cli/command_router.rs +++ b/crates/ao-fleet-cli/src/cli/command_router.rs @@ -3,10 +3,10 @@ use anyhow::Result; use crate::cli::handlers::audit_list::audit_list; use crate::cli::handlers::config_snapshot_export::config_snapshot_export; use crate::cli::handlers::config_snapshot_import::config_snapshot_import; +use crate::cli::handlers::daemon_health_rollup::daemon_health_rollup; use crate::cli::handlers::daemon_override_clear::daemon_override_clear; use crate::cli::handlers::daemon_override_list::daemon_override_list; use crate::cli::handlers::daemon_override_upsert::daemon_override_upsert; -use crate::cli::handlers::daemon_health_rollup::daemon_health_rollup; use crate::cli::handlers::daemon_reconcile::daemon_reconcile; use crate::cli::handlers::daemon_status::daemon_status; use crate::cli::handlers::db_init::db_init; diff --git a/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup.rs b/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup.rs index 12e8327..1fd2562 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/daemon_health_rollup.rs @@ -21,14 +21,10 @@ pub fn daemon_health_rollup(db_path: &str, command: DaemonHealthRollupCommand) - let statuses = store.fleet_daemon_statuses(command.team_id.as_deref())?; let total = statuses.len(); - let desired_running = statuses - .iter() - .filter(|s| s.desired_state == DaemonDesiredState::Running) - .count(); - let observed_running = statuses - .iter() - .filter(|s| s.observed_state == Some(DaemonDesiredState::Running)) - .count(); + let desired_running = + statuses.iter().filter(|s| s.desired_state == DaemonDesiredState::Running).count(); + let observed_running = + statuses.iter().filter(|s| s.observed_state == Some(DaemonDesiredState::Running)).count(); let aligned = statuses .iter() .filter(|s| s.observed_state.as_ref().map_or(false, |obs| obs == &s.desired_state)) diff --git a/crates/ao-fleet-cli/src/cli/handlers/mod.rs b/crates/ao-fleet-cli/src/cli/handlers/mod.rs index 5a59958..79fce52 100644 --- a/crates/ao-fleet-cli/src/cli/handlers/mod.rs +++ b/crates/ao-fleet-cli/src/cli/handlers/mod.rs @@ -4,6 +4,8 @@ pub mod config_snapshot_export; pub mod config_snapshot_export_command; pub mod config_snapshot_import; pub mod config_snapshot_import_command; +pub mod daemon_health_rollup; +pub mod daemon_health_rollup_command; pub mod daemon_override_clear; pub mod daemon_override_clear_command; pub mod daemon_override_list; @@ -14,8 +16,6 @@ pub mod daemon_reconcile; pub mod daemon_reconcile_command; pub mod daemon_reconcile_result; pub mod daemon_reconcile_support; -pub mod daemon_health_rollup; -pub mod daemon_health_rollup_command; pub mod daemon_status; pub mod daemon_status_command; pub mod db_init; diff --git a/crates/ao-fleet-cli/src/cli/root_command.rs b/crates/ao-fleet-cli/src/cli/root_command.rs index de4264e..f89a94a 100644 --- a/crates/ao-fleet-cli/src/cli/root_command.rs +++ b/crates/ao-fleet-cli/src/cli/root_command.rs @@ -3,10 +3,10 @@ use clap::{Parser, Subcommand}; use crate::cli::handlers::audit_list_command::AuditListCommand; use crate::cli::handlers::config_snapshot_export_command::ConfigSnapshotExportCommand; use crate::cli::handlers::config_snapshot_import_command::ConfigSnapshotImportCommand; +use crate::cli::handlers::daemon_health_rollup_command::DaemonHealthRollupCommand; use crate::cli::handlers::daemon_override_clear_command::DaemonOverrideClearCommand; use crate::cli::handlers::daemon_override_list_command::DaemonOverrideListCommand; use crate::cli::handlers::daemon_override_upsert_command::DaemonOverrideUpsertCommand; -use crate::cli::handlers::daemon_health_rollup_command::DaemonHealthRollupCommand; use crate::cli::handlers::daemon_reconcile_command::DaemonReconcileCommand; use crate::cli::handlers::daemon_status_command::DaemonStatusCommand; use crate::cli::handlers::db_init_command::DbInitCommand; diff --git a/crates/ao-fleet-cli/src/main.rs b/crates/ao-fleet-cli/src/main.rs index ac77732..8bc0ba9 100644 --- a/crates/ao-fleet-cli/src/main.rs +++ b/crates/ao-fleet-cli/src/main.rs @@ -6,4 +6,3 @@ use clap::Parser; fn main() -> Result<()> { run(RootCommand::parse()) } - diff --git a/crates/ao-fleet-cli/tests/founder_bootstrap.rs b/crates/ao-fleet-cli/tests/founder_bootstrap.rs index 754143f..f5ef0c0 100644 --- a/crates/ao-fleet-cli/tests/founder_bootstrap.rs +++ b/crates/ao-fleet-cli/tests/founder_bootstrap.rs @@ -138,11 +138,8 @@ fn test_daemon_health_rollup_with_projects() { .expect("daemon_health_rollup with projects succeeded"); // Also verify rollup filtered by team works - daemon_health_rollup( - path, - DaemonHealthRollupCommand { team_id: Some(team.id.clone()) }, - ) - .expect("daemon_health_rollup with team filter succeeded"); + daemon_health_rollup(path, DaemonHealthRollupCommand { team_id: Some(team.id.clone()) }) + .expect("daemon_health_rollup with team filter succeeded"); } /// project-status returns an error for a nonexistent project. @@ -191,9 +188,6 @@ fn test_project_status_found() { }) .expect("project created"); - project_status( - path, - ProjectStatusCommand { id: project.id.clone(), refresh: false }, - ) - .expect("project_status for known project succeeded"); + project_status(path, ProjectStatusCommand { id: project.id.clone(), refresh: false }) + .expect("project_status for known project succeeded"); }