use std::process::ExitCode;
use crate::cli::ConfigureArgs;
use crate::clients::{ClientKind, ClientManager, ManagedCredential, TokenSource};
use crate::managed_server::{ResolvedServer, prepare_run_credential, resolve};
type AnyError = Box<dyn std::error::Error + Send + Sync>;
fn unconfigurable(client: ClientKind) -> Option<String> {
match client {
ClientKind::Cursor | ClientKind::GeminiCli => Some(
client
.setup_limitation()
.unwrap_or("this client cannot be configured through a file")
.to_string(),
),
_ => None,
}
}
const fn environment_only(client: ClientKind) -> bool {
matches!(client, ClientKind::GrokCli)
}
pub async fn run(args: &ConfigureArgs) -> ExitCode {
match run_inner(args).await {
Ok(code) => code,
Err(error) => {
eprintln!(
"error: {}",
crate::login_url::redact_secrets(&error.to_string())
);
ExitCode::from(1)
}
}
}
async fn run_inner(args: &ConfigureArgs) -> Result<ExitCode, AnyError> {
let manager = ClientManager::from_env()?;
if args.undo {
return undo(args, &manager).await;
}
let explicit_token = if args.token_stdin {
Some(crate::server_command::read_token()?)
} else {
args.token.clone()
};
let server = target(args, explicit_token).await?;
println!("router: {} (from {})", server.base_url, server.source);
let mut configured = 0_usize;
let mut skipped = Vec::new();
let mut failed = Vec::new();
for client in args.clients() {
if let Some(reason) = unconfigurable(client) {
skipped.push((client, reason));
continue;
}
if args.all && !manager.status(client).is_ok_and(|status| status.installed) {
skipped.push((client, "not installed on this machine".to_string()));
continue;
}
match configure_one(args, &manager, &server, client).await {
Ok(()) => configured += 1,
Err(error) if args.all => failed.push((client, error.to_string())),
Err(error) => return Err(error),
}
}
for (client, reason) in &skipped {
println!("skipped {}: {reason}", client.display_name());
}
for (client, error) in &failed {
eprintln!("error: {}: {error}", client.display_name());
}
if args.all {
println!("configured {configured} client(s); undo: router configure --undo <CLIENT>");
}
Ok(if failed.is_empty() {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
})
}
async fn target(
args: &ConfigureArgs,
explicit_token: Option<String>,
) -> Result<ResolvedServer, AnyError> {
if args.target.local {
let mut server = crate::managed_server::discovered_local_router()
.await
.ok_or("no router is listening on this machine; start one with `router serve`, or drop --local to use the selected server")?;
if let Some(token) = explicit_token {
server.token = Some(token);
}
return Ok(server);
}
let server = resolve(
args.target.server.as_deref(),
args.target.management_server.as_deref(),
explicit_token,
None,
args.target.managed,
)
.await?;
if server.source == "managed local container" {
crate::managed_server::start_managed()?;
}
Ok(server)
}
async fn configure_one(
args: &ConfigureArgs,
manager: &ClientManager,
server: &ResolvedServer,
client: ClientKind,
) -> Result<(), AnyError> {
if manager.managed_target_matches(client, &server.base_url)?
&& (environment_only(client)
|| crate::client_global::undo_state_path(&manager.config_path(client)).exists())
&& let Some(token) = manager.managed_token(client)?
&& manager
.catalog(client, &server.base_url, &token)
.await
.is_ok()
{
println!(
"{} is already configured in {}",
client.display_name(),
manager.config_path(client).display()
);
println!(
"credentials: {} (mode 0600)",
manager.environment_path(client).display()
);
println!("undo: router configure --undo {client}");
return Ok(());
}
let credential = prepare_run_credential(
server,
client,
&format!("configure-{client}"),
args.ttl_hours,
false,
)
.await?;
let record = ManagedCredential {
client: client.to_string(),
source: if credential.was_minted() {
TokenSource::Minted
} else {
TokenSource::Supplied
},
token_id: credential.id(),
label: Some(format!("configure-{client}")),
issued_at: Some(chrono::Utc::now().timestamp()),
router: Some(server.base_url.clone()),
principal_id: Some(crate::credential_recovery_store::PRIMARY_ACCOUNT.to_string()),
config_sha256: None,
};
let models = crate::clients::usable_models(client, credential.models());
let configured = if environment_only(client) {
manager
.apply_setup_transaction(
client,
&server.base_url,
&credential.token,
&record,
&models,
)
.map(|_| None)
} else {
manager
.apply_configure_transaction(
client,
&server.base_url,
&credential.token,
&record,
&models,
)
.map(Some)
};
let configured = match configured {
Ok(configured) => configured,
Err(error) => {
return match crate::managed_server::cleanup_run_credential(credential).await {
Ok(()) => Err(error.into()),
Err(cleanup) => Err(format!(
"{error}; the unused minted credential could not be revoked: {cleanup}"
)
.into()),
};
}
};
if let Some(path) = configured {
println!("configured {} in {}", client.display_name(), path.display());
}
let environment = manager.environment_path(client);
println!("credentials: {} (mode 0600)", environment.display());
if environment_only(client) {
println!(
"{} has no persistent base-URL setting, so the exports above are the whole \
configuration; source them from your shell profile",
client.display_name()
);
}
println!("undo: router configure --undo {client}");
Ok(())
}
async fn undo(args: &ConfigureArgs, manager: &ClientManager) -> Result<ExitCode, AnyError> {
let mut restored = 0_usize;
let mut failed = Vec::new();
for client in args.clients() {
if let Some(reason) = unconfigurable(client) {
if args.all {
continue;
}
return Err(reason.into());
}
match undo_one(args, manager, client).await {
Ok(true) => restored += 1,
Ok(false) => {}
Err(error) if args.all => failed.push((client, error.to_string())),
Err(error) => return Err(error),
}
}
for (client, error) in &failed {
eprintln!("error: {}: {error}", client.display_name());
}
if args.all {
println!("restored {restored} client(s)");
}
Ok(if failed.is_empty() {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
})
}
async fn undo_one(
args: &ConfigureArgs,
manager: &ClientManager,
client: ClientKind,
) -> Result<bool, AnyError> {
let record = manager.credential_metadata(client).ok().flatten();
let config = crate::client_global::undo(client)?;
if let Some(record) = record
.as_ref()
.filter(|record| record.revocable_by_default())
{
report_revocation(args, record).await;
}
let environment = manager.environment_path(client);
let had_credential = environment.exists();
if had_credential {
std::fs::remove_file(&environment)?;
}
let metadata = manager.credential_metadata_path(client);
if metadata.exists() {
std::fs::remove_file(&metadata)?;
}
match config {
Some(path) => {
println!("restored {} exactly", path.display());
Ok(true)
}
None if had_credential => {
println!(
"removed the stored credential for {}",
client.display_name()
);
Ok(true)
}
None if !args.all => Err(format!(
"no configuration saved by `configure` exists for {client}; nothing was restored"
)
.into()),
None => Ok(false),
}
}
async fn report_revocation(args: &ConfigureArgs, record: &ManagedCredential) {
let (Some(router), Some(id)) = (record.router.as_deref(), record.token_id.as_deref()) else {
return;
};
let Some(admin) = admin_token_for(args, router) else {
println!(
"note: token {id} on {router} was left in place; revoke it with \
`router tokens revoke {id} --server {router}`"
);
return;
};
let management = management_origin_for(args, router);
match crate::managed_server::revoke(&management, &admin, id).await {
Ok(()) => println!("revoked token {id} on {router}"),
Err(error) => {
eprintln!("warning: {error}");
println!(
"note: token {id} on {router} is still live; revoke it with \
`router tokens revoke {id} --server {router}`"
);
}
}
}
fn management_origin_for(args: &ConfigureArgs, router: &str) -> String {
if let Some(origin) = args.target.management_server.as_deref()
&& let Ok(origin) = crate::managed_server::canonical_server_origin(origin)
{
return origin;
}
crate::managed_server::load_persisted()
.ok()
.flatten()
.filter(|persisted| {
crate::managed_server::canonical_server_origin(&persisted.server).ok()
== crate::managed_server::canonical_server_origin(router).ok()
})
.and_then(|persisted| persisted.management_server)
.unwrap_or_else(|| router.to_string())
}
fn admin_token_for(args: &ConfigureArgs, router: &str) -> Option<String> {
if let Some(token) = args.token.clone() {
return Some(token);
}
if let Ok(token) = std::env::var("LINK_ASSISTANT_ROUTER_TOKEN")
.or_else(|_| std::env::var("LINK_ASSISTANT_TOKEN"))
{
return Some(token);
}
crate::managed_server::load_persisted()
.ok()
.flatten()
.filter(|persisted| {
crate::managed_server::canonical_server_origin(&persisted.server).ok()
== crate::managed_server::canonical_server_origin(router).ok()
})
.and_then(|persisted| persisted.token)
}
#[cfg(test)]
#[path = "configure_tests.rs"]
mod tests;