Skip to main content

agentstatedeveloper_cli/commands/
endpoints.rs

1//! `asd endpoints` — list cross-service endpoints detected in this repo, show
2//! the in-repo matched edges, and export a service manifest.
3//!
4//! Endpoints (HTTP routes/clients, pub-sub) are written to the registry at
5//! `/asd/v1/index/endpoints` during `asd index`. This command reads them back,
6//! runs in-repo contract matching ([`match_edges`]), and can emit a
7//! [`ServiceManifest`] — the unit a Team-tier tool imports for cross-repo
8//! matching. (This OSS command does in-repo matching + export only.)
9
10use anyhow::Result;
11use clap::Args;
12use serde_json::{Value, json};
13
14use agentstatedeveloper_core::{Engine, ServiceEndpoint, ServiceManifest, match_edges};
15
16use crate::config::Config;
17
18#[derive(Debug, Args)]
19pub struct EndpointsArgs {
20    /// Emit a JSON ServiceManifest (repo_id + endpoints) for cross-repo import.
21    #[arg(long)]
22    pub export: bool,
23
24    /// Machine-readable JSON: { endpoints, edges }.
25    #[arg(long)]
26    pub agent: bool,
27}
28
29pub fn run(cfg: &Config, args: EndpointsArgs) -> Result<()> {
30    let engine = Engine::open_sqlite(&cfg.db_path)?;
31    let endpoints = load_endpoints(&engine);
32
33    if args.export {
34        let repo_id = endpoints
35            .first()
36            .map(|e| e.repo_id.clone())
37            .unwrap_or_default();
38        let manifest = ServiceManifest { repo_id, endpoints };
39        println!("{}", serde_json::to_string_pretty(&manifest)?);
40        return Ok(());
41    }
42
43    let edges = match_edges(&endpoints);
44
45    if args.agent {
46        println!(
47            "{}",
48            serde_json::to_string_pretty(&json!({
49                "endpoints": endpoints,
50                "edges": edges,
51            }))?
52        );
53        return Ok(());
54    }
55
56    if endpoints.is_empty() {
57        println!(
58            "No cross-service endpoints detected. Run `asd index` first; \
59             HTTP detection currently covers Python (FastAPI/Flask + requests/httpx)."
60        );
61        return Ok(());
62    }
63
64    println!("{} endpoint(s):", endpoints.len());
65    for e in &endpoints {
66        println!(
67            "  {:?}/{:?}  {}  in {}  (conf {:.2})",
68            e.transport, e.direction, e.contract, e.qname, e.confidence
69        );
70    }
71    println!();
72    if edges.is_empty() {
73        println!(
74            "No in-repo matched edges. (A match needs a client call and a server route \
75             in THIS repo sharing a contract; cross-repo matching is a Team-tier feature.)"
76        );
77    } else {
78        println!("{} in-repo matched edge(s):", edges.len());
79        for ed in &edges {
80            println!(
81                "  {}  ->  {}   [{}]",
82                ed.from.qname, ed.to.qname, ed.contract
83            );
84        }
85    }
86    Ok(())
87}
88
89/// Read the endpoint registry (`contract_hash → repo_id → symbol_id → endpoint`)
90/// and flatten it into a sorted list.
91fn load_endpoints(engine: &Engine) -> Vec<ServiceEndpoint> {
92    let tree = engine
93        .repo
94        .get_tree(&engine.ref_name, "/asd/v1/index/endpoints")
95        .unwrap_or(Value::Null);
96
97    let mut out = Vec::new();
98    if let Some(by_contract) = tree.as_object() {
99        for by_repo in by_contract.values() {
100            let Some(by_repo) = by_repo.as_object() else {
101                continue;
102            };
103            for by_sym in by_repo.values() {
104                let Some(by_sym) = by_sym.as_object() else {
105                    continue;
106                };
107                for ep_val in by_sym.values() {
108                    if let Ok(ep) = serde_json::from_value::<ServiceEndpoint>(ep_val.clone()) {
109                        out.push(ep);
110                    }
111                }
112            }
113        }
114    }
115    out.sort_by(|a, b| a.contract.cmp(&b.contract).then(a.qname.cmp(&b.qname)));
116    out
117}