agentstatedeveloper_cli/commands/
endpoints.rs1use 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 #[arg(long)]
22 pub export: bool,
23
24 #[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
89fn 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}