cellstate/
main.rs

1//! CELLSTATE CLI — Universal installer and local development tool.
2//!
3//! Install:
4//!   curl -fsSL https://cellstate.batterypack.dev/install | sh
5//!
6//! Commands:
7//!   cellstate setup      <- Interactive first-run installer (detect env, install SDKs)
8//!   cellstate init       <- Scaffold .cellstate/ config in current project
9//!   cellstate start      <- Start local CELLSTATE server (Docker)
10//!   cellstate stop       <- Stop local CELLSTATE server
11//!   cellstate inspect    <- View live memory state
12//!   cellstate use        <- Set or display session scope
13//!   cellstate status     <- Show session state and server connectivity
14//!   cellstate trajectory <- Manage trajectories
15//!   cellstate agent      <- Manage agents
16//!   cellstate note       <- Manage notes
17//!   cellstate context    <- Context commit and recall
18
19mod commands;
20pub(crate) mod entity;
21pub(crate) mod http;
22pub(crate) mod session;
23mod ui;
24
25use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
26
27// ============================================================================
28// CLI definition (derive-based infra commands)
29// ============================================================================
30
31#[derive(Parser)]
32#[command(
33    name = "cellstate",
34    about = "Agent memory that doesn't forget, lie, or black-box you.",
35    long_about = None,
36    version,
37    propagate_version = true,
38)]
39struct Cli {
40    /// Output as JSON (for agent/script consumption)
41    #[arg(long, global = true)]
42    json: bool,
43
44    #[command(subcommand)]
45    command: Option<Commands>,
46}
47
48#[derive(Subcommand)]
49enum Commands {
50    /// Interactive first-run setup: detect environment, install SDKs, start server.
51    ///
52    /// Run this first. It detects Python, Node, Rust, and Docker,
53    /// asks what you're building with, installs only what you need,
54    /// and optionally starts a local CELLSTATE server.
55    Setup,
56
57    /// Scaffold a CELLSTATE project in the current directory.
58    ///
59    /// Creates .cellstate/config.toml, .env.example, and cellstate.lock.
60    Init,
61
62    /// Start the local CELLSTATE server (PostgreSQL + API) via Docker.
63    ///
64    /// Equivalent to docker compose up with a minimal production-like stack.
65    /// Runs in the background. Use `cellstate stop` to shut down.
66    Start,
67
68    /// Stop the local CELLSTATE server.
69    Stop,
70
71    /// View live memory state from a running CELLSTATE instance.
72    ///
73    /// Polls the API and displays active memories with Blake3 receipts.
74    /// For streaming TUI: see `cellstate watch` (coming soon).
75    Inspect(commands::inspect::InspectArgs),
76
77    /// Pack compilation and AAIF convergence output commands.
78    ///
79    /// Compile a CELLSTATE pack and emit convergence outputs: AGENTS.md,
80    /// llms.txt, A2A Agent Card, SKILL.md, A2UI schema.
81    Pack(commands::pack::PackArgs),
82
83    /// Set or display session scope (tenant, agent, trajectory, scope).
84    ///
85    /// Persists scope selections so they don't need to be passed as flags
86    /// on every command. Use `cellstate use tenant <uuid>` to set scope,
87    /// `cellstate use --clear` to reset, or `cellstate use` to display.
88    #[command(name = "use")]
89    Use(commands::use_cmd::UseArgs),
90
91    /// Show session state and server connectivity.
92    ///
93    /// Displays current scope, server health, version, and auth status.
94    Status,
95}
96
97// ============================================================================
98// Known entity command names (Phase 3 pilot)
99// ============================================================================
100
101/// Entity command names that are handled by the builder-API entity module.
102const ENTITY_COMMANDS: &[&str] = &[
103    "trajectory",
104    "agent",
105    "scope",
106    "artifact",
107    "note",
108    "turn",
109    "context",
110    "lock",
111    "message",
112    "delegation",
113    "handoff",
114    "edge",
115    "team",
116    "tenant",
117    "api-key",
118    "event-dag",
119    "search",
120    "batch",
121    "summarization-policy",
122    "summarization-request",
123    "deployment",
124    "working-set",
125    "config",
126    "gates",
127    "models",
128];
129
130fn is_entity_command(name: &str) -> bool {
131    ENTITY_COMMANDS.contains(&name)
132}
133
134// ============================================================================
135// Entry point
136// ============================================================================
137
138#[tokio::main]
139async fn main() {
140    // Build the full command tree: derive commands + builder entity commands.
141    // This lets `cellstate --help` show both infra and entity commands.
142    let mut app = Cli::command();
143    for cmd in entity::build_entity_commands() {
144        app = app.subcommand(cmd);
145    }
146    let mut matches = app.get_matches();
147
148    // Extract global --json flag before dispatching
149    let json = matches.get_flag("json");
150
151    // Try derive-based infra commands first.
152    // from_arg_matches may fail if the subcommand is a builder entity command,
153    // which is expected — we fall through to entity dispatch.
154    if let Ok(cli) = Cli::from_arg_matches_mut(&mut matches) {
155        // Infra commands are sync and some use reqwest::blocking::Client which
156        // creates its own tokio runtime. We must run them on a blocking thread
157        // to avoid "cannot drop a runtime in async context" panics.
158        let result = tokio::task::spawn_blocking(move || match cli.command {
159            Some(Commands::Setup) | None => commands::setup::run(),
160            Some(Commands::Init) => commands::init::run(),
161            Some(Commands::Start) => commands::start::run(),
162            Some(Commands::Stop) => commands::start::stop(),
163            Some(Commands::Inspect(args)) => commands::inspect::run(&args),
164            Some(Commands::Pack(args)) => commands::pack::run(&args),
165            Some(Commands::Use(args)) => commands::use_cmd::run_with_json(&args, cli.json),
166            Some(Commands::Status) => commands::status::run_with_json(cli.json),
167        })
168        .await;
169        match result {
170            Ok(Ok(())) => {}
171            Ok(Err(e)) => {
172                eprintln!("{} {}", console::style("error:").red().bold(), e);
173                std::process::exit(1);
174            }
175            Err(e) => {
176                eprintln!("{} {}", console::style("error:").red().bold(), e);
177                std::process::exit(1);
178            }
179        }
180        return;
181    }
182
183    // Try entity commands (builder-API, async)
184    if let Some((sub_name, sub_matches)) = matches.subcommand() {
185        if is_entity_command(sub_name) {
186            let session = match session::CliSession::load() {
187                Ok(s) => s,
188                Err(e) => {
189                    eprintln!("{} {}", console::style("error:").red().bold(), e);
190                    std::process::exit(1);
191                }
192            };
193            let client = entity::client::ApiClient::new(
194                session.effective_base_url(None),
195                session.effective_api_key(None),
196                session.tenant_id.clone(),
197            );
198            let output = entity::output::OutputConfig { json };
199            if let Err(e) =
200                entity::dispatch(sub_name, sub_matches, &client, &output, &session).await
201            {
202                eprintln!("{} {}", console::style("error:").red().bold(), e);
203                std::process::exit(1);
204            }
205            return;
206        }
207    }
208
209    // No command matched — show help
210    let _ = Cli::command().print_help();
211}