cellstate/commands/
use_cmd.rs

1//! `cellstate use` — Set or display session scope.
2//!
3//! Persists tenant, agent, trajectory, and scope selections so they don't
4//! need to be passed as flags on every subsequent command.
5//!
6//! Examples:
7//!   cellstate use tenant 550e8400-e29b-41d4-a716-446655440000
8//!   cellstate use agent  6ba7b810-9dad-11d1-80b4-00c04fd430c8
9//!   cellstate use --clear
10//!   cellstate use                # display current session
11
12use crate::session::{self, CliSession};
13use crate::ui;
14use clap::{Parser, Subcommand};
15use console::style;
16
17// ============================================================================
18// Command definition
19// ============================================================================
20
21#[derive(Debug, Parser)]
22pub struct UseArgs {
23    /// Clear all session scope (tenant, agent, trajectory, scope)
24    #[arg(long)]
25    pub clear: bool,
26
27    #[command(subcommand)]
28    pub command: Option<UseCommand>,
29}
30
31#[derive(Debug, Subcommand)]
32pub enum UseCommand {
33    /// Set the active tenant scope
34    Tenant {
35        /// Tenant ID (UUID)
36        id: String,
37    },
38    /// Set the active agent scope
39    Agent {
40        /// Agent ID (UUID)
41        id: String,
42    },
43    /// Set the active trajectory scope
44    Trajectory {
45        /// Trajectory ID (UUID)
46        id: String,
47    },
48    /// Set the active scope
49    Scope {
50        /// Scope ID (UUID)
51        id: String,
52    },
53}
54
55// ============================================================================
56// Execution
57// ============================================================================
58
59pub fn run_with_json(args: &UseArgs, json: bool) -> anyhow::Result<()> {
60    let mut session = CliSession::load()?;
61
62    if args.clear {
63        session.clear_scope();
64        session.save()?;
65        if json {
66            println!(r#"{{"cleared": true}}"#);
67        } else {
68            ui::ok("Session scope cleared.");
69        }
70        return Ok(());
71    }
72
73    match &args.command {
74        None => {
75            if json {
76                let out = serde_json::json!({
77                    "base_url": session.base_url.as_deref().unwrap_or(crate::http::DEFAULT_BASE_URL),
78                    "api_key": session.api_key.as_deref().map(mask_api_key),
79                    "tenant_id": session.tenant_id,
80                    "agent_id": session.agent_id,
81                    "trajectory_id": session.trajectory_id,
82                    "scope_id": session.scope_id,
83                });
84                println!("{}", serde_json::to_string_pretty(&out).unwrap());
85            } else {
86                print_session(&session);
87            }
88        }
89        Some(cmd) => {
90            let (field, id) = match cmd {
91                UseCommand::Tenant { id } => {
92                    session::validate_uuid(id)?;
93                    session.tenant_id = Some(id.clone());
94                    ("tenant", id.as_str())
95                }
96                UseCommand::Agent { id } => {
97                    session::validate_uuid(id)?;
98                    session.agent_id = Some(id.clone());
99                    ("agent", id.as_str())
100                }
101                UseCommand::Trajectory { id } => {
102                    session::validate_uuid(id)?;
103                    session.trajectory_id = Some(id.clone());
104                    ("trajectory", id.as_str())
105                }
106                UseCommand::Scope { id } => {
107                    session::validate_uuid(id)?;
108                    session.scope_id = Some(id.clone());
109                    ("scope", id.as_str())
110                }
111            };
112            session.save()?;
113            if json {
114                println!(r#"{{"{}": "{}"}}"#, field, id);
115            } else {
116                ui::ok(&format!(
117                    "{} set to {}",
118                    field.to_uppercase().chars().next().unwrap().to_string() + &field[1..],
119                    style(id).cyan()
120                ));
121            }
122        }
123    }
124
125    Ok(())
126}
127
128// ============================================================================
129// Display helpers
130// ============================================================================
131
132fn print_session(session: &CliSession) {
133    ui::print_banner();
134    ui::section("Session");
135
136    print_field(
137        "Base URL",
138        session.base_url.as_deref(),
139        Some(crate::http::DEFAULT_BASE_URL),
140    );
141    print_field(
142        "API Key",
143        session.api_key.as_deref().map(mask_api_key).as_deref(),
144        None,
145    );
146
147    ui::section("Scope");
148
149    print_field("Tenant", session.tenant_id.as_deref(), None);
150    print_field("Agent", session.agent_id.as_deref(), None);
151    print_field("Trajectory", session.trajectory_id.as_deref(), None);
152    print_field("Scope", session.scope_id.as_deref(), None);
153
154    println!();
155    println!("{}", style("Set scope: cellstate use tenant <uuid>").dim());
156    println!("{}", style("Clear all: cellstate use --clear").dim());
157    println!();
158}
159
160fn print_field(label: &str, value: Option<&str>, default: Option<&str>) {
161    match value {
162        Some(v) => ui::kv(label, v),
163        None => match default {
164            Some(d) => {
165                println!(
166                    "  {:<20} {} {}",
167                    style(label).dim(),
168                    style(d).cyan(),
169                    style("(default)").dim()
170                );
171            }
172            None => ui::kv_missing(label),
173        },
174    }
175}
176
177/// Mask an API key for display: show prefix and last 4 chars only.
178fn mask_api_key(key: &str) -> String {
179    if key.len() <= 8 {
180        return "****".to_string();
181    }
182    let prefix = &key[..4];
183    let suffix = &key[key.len() - 4..];
184    format!("{prefix}...{suffix}")
185}