use crate::api::bindings;
use crate::api::probe;
use crate::auth::binding_secrets::{
default_file_dir, BindingSecretStore, FileBindingSecretStore, KeyringBindingSecretStore,
};
use crate::cli::commands::shared::{hard_confirm, make_client};
use crate::cli::{BindingsAction, GlobalArgs};
use crate::config;
use crate::error::OlError;
use crate::ui::color;
use crate::ui::output::OutputConfig;
use secrecy::SecretString;
use tabled::Table;
pub async fn run(g: &GlobalArgs, action: BindingsAction) -> Result<(), OlError> {
match action {
BindingsAction::List => list(g).await,
BindingsAction::RotateSecret { id, yes } => rotate_secret(g, &id, yes).await,
BindingsAction::DeleteSecret { id, yes } => delete_secret(g, &id, yes).await,
BindingsAction::Probe { id } => probe_cmd(g, &id).await,
BindingsAction::Metrics { id } => metrics_cmd(g, &id).await,
BindingsAction::Delete { id, yes } => delete(g, &id, yes).await,
}
}
async fn list(g: &GlobalArgs) -> Result<(), OlError> {
let out = OutputConfig::resolve(g);
let client = make_client().await?;
let bindings = bindings::list(&client).await?;
if out.is_machine() {
out.print_json(&bindings);
return Ok(());
}
if bindings.is_empty() {
out.print_info("No bindings found.");
return Ok(());
}
let rows: Vec<crate::ui::tables::BindingRow> = bindings
.into_iter()
.map(|b| crate::ui::tables::BindingRow {
id: b.id,
tool: b.tool,
provider: b.provider,
state: b.state.unwrap_or_else(|| "active".into()),
score: b
.routing_score
.map(|s| format!("{s:.1}"))
.unwrap_or_else(|| "—".into()),
})
.collect();
println!("{}", Table::new(rows));
Ok(())
}
async fn rotate_secret(g: &GlobalArgs, id: &str, yes_flag: bool) -> Result<(), OlError> {
let out = OutputConfig::resolve(g);
let yes = g.yes || yes_flag;
if g.dry_run {
out.print_step(&format!(
"--dry-run: would POST /api/v1/editor/bindings/{id}/regenerate-secret"
));
return Ok(());
}
if !yes && !out.interactive {
return Err(OlError::new(
crate::error::OL_4200_TOKEN_EXPIRED,
"rotating a secret in non-interactive mode requires --yes",
));
}
let client = make_client().await?;
let reveal = bindings::regenerate_secret(&client, id).await?;
persist_secret_locally(&reveal.binding_id, &reveal.secret)?;
if out.is_machine() {
out.print_json(&reveal);
return Ok(());
}
println!();
println!(
"{} {}",
color::yellow("⚠", out.color),
color::bold("STORE THIS SECRET — it will not be shown again", out.color)
);
println!();
println!(
" {} {}",
color::cyan("🔑", out.color),
color::bold(&reveal.secret, out.color)
);
println!();
out.print_step(&format!("Secret stored locally for binding {id}"));
crate::telemetry::capture_global(crate::telemetry::Event::binding_secret_rotated());
Ok(())
}
async fn delete_secret(g: &GlobalArgs, id: &str, yes_flag: bool) -> Result<(), OlError> {
let out = OutputConfig::resolve(g);
let yes = g.yes || yes_flag;
if g.dry_run {
out.print_step(&format!(
"--dry-run: would DELETE /api/v1/editor/bindings/{id}/secret"
));
return Ok(());
}
hard_confirm(id, out.interactive, yes)?;
let client = make_client().await?;
match bindings::delete_secret(&client, id).await {
Ok(()) => {}
Err(e) if e.code.code == "OL-4234" => {
out.print_step(&format!(
"Binding `{id}` already in secret_missing state — server-side no-op"
));
}
Err(e) => return Err(e),
}
let _ = clear_local_secret(id);
out.print_step(&format!("Secret deleted for binding {id}"));
Ok(())
}
async fn probe_cmd(g: &GlobalArgs, id: &str) -> Result<(), OlError> {
let out = OutputConfig::resolve(g);
let client = make_client().await?;
let detail = bindings::get(&client, id).await?;
let url = detail.endpoint_url.unwrap_or_default();
if url.is_empty() {
return Err(OlError::new(
crate::error::OL_4212_INVALID_ENDPOINT_URL,
format!("binding `{id}` has no endpoint_url (provider may be missing one)"),
));
}
let result = probe::probe(&url);
match (out.is_machine(), &result) {
(true, Ok(report)) => out.print_json(report),
(true, Err(e)) => out.print_json(&serde_json::json!({
"passed": false,
"endpoint_url": url,
"error": { "code": e.code.code, "message": &e.message }
})),
(false, Ok(report)) => {
out.print_step(&format!(
"Probe passed for {url} ({} resolved)",
report.resolved_ips.len()
));
for f in &report.findings {
out.print_substep(&format!("[{}] {}", f.code, f.message));
}
}
(false, Err(e)) => {
out.print_error(e);
}
}
result.map(|_| ())
}
async fn metrics_cmd(g: &GlobalArgs, id: &str) -> Result<(), OlError> {
let out = OutputConfig::resolve(g);
let client = make_client().await?;
let metrics = bindings::metrics(&client, id).await?;
if out.is_machine() {
out.print_json(&metrics);
return Ok(());
}
out.print_step(&format!("Binding {} metrics", metrics.binding_id));
if let Some(latency) = metrics.latency_ms {
if let Some(p50) = latency.p50 {
out.print_substep(&format!("Latency p50: {p50} ms"));
}
if let Some(p95) = latency.p95 {
out.print_substep(&format!("Latency p95: {p95} ms"));
}
if let Some(p99) = latency.p99 {
out.print_substep(&format!("Latency p99: {p99} ms"));
}
}
if let Some(s) = metrics.success_rate_24h {
out.print_substep(&format!("Success rate (24h): {:.1}%", s * 100.0));
}
if let Some(t) = metrics.thumbs_down_7d {
out.print_substep(&format!("Thumbs-down (7d): {:.1}%", t * 100.0));
}
if let Some(score) = metrics.score {
out.print_substep(&format!("Routing score: {score:.1}"));
}
if let Some(p) = metrics.active_penalty {
out.print_substep(&format!("Active penalty: {p:.1}"));
}
Ok(())
}
async fn delete(g: &GlobalArgs, id: &str, yes_flag: bool) -> Result<(), OlError> {
let out = OutputConfig::resolve(g);
let yes = g.yes || yes_flag;
if g.dry_run {
out.print_step(&format!(
"--dry-run: would DELETE /api/v1/editor/bindings/{id}"
));
return Ok(());
}
hard_confirm(id, out.interactive, yes)?;
let client = make_client().await?;
match bindings::delete(&client, id).await {
Ok(()) => {
out.print_step(&format!("Deleted binding `{id}`"));
let _ = clear_local_secret(id);
Ok(())
}
Err(e) if e.code.code == "OL-4234" => {
out.print_step(&format!("Binding `{id}` already absent — no-op"));
Ok(())
}
Err(e) => Err(e),
}
}
fn persist_secret_locally(binding_id: &str, secret: &str) -> Result<(), OlError> {
let secret = SecretString::from(secret.to_string());
let keyring = KeyringBindingSecretStore::new();
if let Ok(()) = keyring.store(binding_id, secret.clone()) {
return Ok(());
}
let dir = default_file_dir(&config::provider_dir());
let machine_id = config::machine_id_or_init().unwrap_or_else(|_| "unknown".into());
let file = FileBindingSecretStore::new(dir, machine_id);
file.store(binding_id, secret)
}
fn clear_local_secret(binding_id: &str) -> Result<(), OlError> {
let _ = KeyringBindingSecretStore::new().delete(binding_id);
let dir = default_file_dir(&config::provider_dir());
let machine_id = config::machine_id_or_init().unwrap_or_else(|_| "unknown".into());
FileBindingSecretStore::new(dir, machine_id).delete(binding_id)
}