Skip to main content

objectiveai_cli/command/swarms/
list.rs

1//! `swarms list <source>` — enumerate swarms from a given source.
2//! Streams one `ResponseItem` per swarm. Sources:
3//!
4//! - `Filesystem` / `Objectiveai` / `Mock` — delegates to the SDK's
5//!   `list_swarms` HTTP endpoint with the matching `ListSwarmsSource`.
6//! - `All` — filesystem + objectiveai, **de-duplicated**: ObjectiveAI
7//!   items skip anything already covered by a filesystem item. The
8//!   fetches run concurrently.
9
10use std::pin::Pin;
11
12use futures::Stream;
13use objectiveai_sdk::cli::command::swarms::list::{
14    Request, RequestSource, ResponseItem,
15};
16use objectiveai_sdk::swarm::request::{ListSwarmsRequest, ListSwarmsSource};
17use objectiveai_sdk::{RemotePath, swarm::list_swarms};
18
19use crate::context::Context;
20use crate::error::Error;
21
22type ItemStream = Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>>;
23
24pub async fn execute(ctx: &Context, request: Request) -> Result<ItemStream, Error> {
25    let stream: ItemStream = match request.source {
26        RequestSource::Filesystem => paths_to_stream(
27            fetch_paths(ctx, ListSwarmsSource::Filesystem).await?,
28        ),
29        RequestSource::Objectiveai => paths_to_stream(
30            fetch_paths(ctx, ListSwarmsSource::Objectiveai).await?,
31        ),
32        RequestSource::Mock => {
33            paths_to_stream(fetch_paths(ctx, ListSwarmsSource::Mock).await?)
34        }
35        RequestSource::All => {
36            // the legacy noted this should be joined into the same
37            // future tree — porting that improvement is out of scope.
38            let (fs_items, oai_items) = tokio::try_join!(
39                fetch_paths(ctx, ListSwarmsSource::Filesystem),
40                fetch_paths(ctx, ListSwarmsSource::Objectiveai),
41            )?;
42            let items = merge_all(fs_items, oai_items);
43            Box::pin(futures::stream::iter(items.into_iter().map(Ok)))
44        }
45    };
46    Ok(stream)
47}
48
49fn paths_to_stream(paths: Vec<RemotePath>) -> ItemStream {
50    Box::pin(futures::stream::iter(
51        paths.into_iter().map(Ok),
52    ))
53}
54
55async fn fetch_paths(
56    ctx: &Context,
57    source: ListSwarmsSource,
58) -> Result<Vec<RemotePath>, Error> {
59    let resp = list_swarms(
60        ctx.api_client().await?,
61        ListSwarmsRequest {
62            source: Some(source),
63        },
64    )
65    .await?;
66    Ok(resp.data)
67}
68
69/// Merge filesystem + objectiveai into a single de-duplicated list:
70/// filesystem items first, then objectiveai items not already
71/// covered by a filesystem item.
72fn merge_all(
73    fs_items: Vec<RemotePath>,
74    oai_items: Vec<RemotePath>,
75) -> Vec<ResponseItem> {
76    // `ResponseItem` is an alias of `RemotePath`, so items are paths.
77    let mut items: Vec<ResponseItem> = fs_items;
78
79    for path in oai_items {
80        let dominated = items.iter().any(|existing| existing == &path);
81        if !dominated {
82            items.push(path);
83        }
84    }
85
86    items
87}
88
89pub mod request_schema {
90    use objectiveai_sdk::cli::command::swarms::list as sdk;
91    use objectiveai_sdk::cli::command::swarms::list::request_schema::{Request, Response};
92
93    use crate::context::Context;
94    use crate::error::Error;
95
96    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
97        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
98    }
99}
100
101pub mod response_schema {
102    use objectiveai_sdk::cli::command::swarms::list as sdk;
103    use objectiveai_sdk::cli::command::swarms::list::response_schema::{Request, Response};
104
105    use crate::context::Context;
106    use crate::error::Error;
107
108    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
109        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
110    }
111}