use std::process::ExitCode;
use std::sync::Arc;
use std::time::Duration;
mod auth_cli;
use axum::middleware::from_fn_with_state;
use link_assistant_router::accounts::{AccountRouter, AccountRouterOptions};
use link_assistant_router::cli::{AccountOp, Cli, Command, ProviderOp, TokenOp};
use link_assistant_router::config::{Config, RoutingMode, StoragePolicy};
use link_assistant_router::crater::{ForgeFedTaskProvider, TaskProvider};
use link_assistant_router::login::LoginManager;
use link_assistant_router::metrics::Metrics;
use link_assistant_router::oauth::OAuthProvider;
use link_assistant_router::providers::{ProviderStore, ProviderUpsert};
use link_assistant_router::proxy::AppState;
use link_assistant_router::storage::{TokenStore, build_token_store};
use link_assistant_router::subscription::{SubscriptionProvider, SubscriptionReader};
use link_assistant_router::token::{ADMIN_SCOPE, IssueRequest, TokenManager};
use log_lazy::LogLazy;
use tower_http::trace::TraceLayer;
type SharedState = (Arc<dyn TokenStore>, Option<AccountRouter>);
type AnyError = Box<dyn std::error::Error>;
#[tokio::main]
async fn main() -> ExitCode {
let arguments =
link_assistant_router::cli::protect_client_arguments(std::env::args_os().collect(), true);
let cli = <Cli as lino_arguments::Parser>::parse_from(arguments);
match cli.command.as_ref() {
Some(Command::With(args)) => {
return link_assistant_router::with_command::run(args).await;
}
Some(Command::Server { op }) => {
return link_assistant_router::server_command::run(op);
}
_ => {}
}
let verbose = cli.verbose;
let request_log = cli.request_log.clone();
let request_log_max_bytes = cli.request_log_max_bytes;
link_assistant_router::logging::init(verbose);
let logger = link_assistant_router::logging::build_lazy(verbose);
tracing::info!("Link.Assistant.Router v{}", link_assistant_router::VERSION);
if verbose {
tracing::info!("Verbose logging enabled");
}
let config = match cli.into_config() {
Ok(c) => c,
Err(e) => {
tracing::error!("Configuration error: {e}");
return ExitCode::from(2);
}
};
match cli.command.as_ref() {
None | Some(Command::Serve) => match run_server(
config,
logger,
request_log.as_deref(),
request_log_max_bytes,
)
.await
{
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
tracing::error!("server error: {e}");
ExitCode::from(1)
}
},
Some(Command::Tokens { op }) => run_tokens(&config, op),
Some(Command::Accounts { op }) => run_accounts(&config, op),
Some(Command::Providers { op }) => run_providers(&config, op),
Some(Command::Clients { op }) => {
link_assistant_router::client_command::run(&config, op).await
}
Some(Command::With(_) | Command::Server { .. }) => unreachable!("handled before config"),
Some(Command::Auth { op }) => auth_cli::run(&config, op).await,
Some(Command::Doctor) => run_doctor(&config).await,
}
}
fn subscription_pool(config: &Config) -> (SubscriptionProvider, std::path::PathBuf) {
let provider = config
.upstream_provider
.subscription_provider()
.unwrap_or(SubscriptionProvider::Claude);
let primary = if provider == SubscriptionProvider::Claude {
std::path::PathBuf::from(&config.claude_code_home)
} else {
let user_home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
provider.resolve_home(&user_home)
};
(provider, primary)
}
fn build_shared_state(config: &Config) -> Result<SharedState, AnyError> {
if !config.data_dir.exists() {
std::fs::create_dir_all(&config.data_dir)?;
}
let store = build_token_store(config.storage_policy, &config.data_dir)?;
let account_router =
if config.additional_account_dirs.is_empty() && config.account_request_limits.is_empty() {
None
} else {
let (provider, primary) = subscription_pool(config);
let options = AccountRouterOptions {
strategy: config.account_routing_strategy,
cooldown: Duration::from_secs(config.account_cooldown_secs),
session_affinity_ttl: Duration::from_secs(config.session_affinity_ttl_secs),
request_limits: config
.account_request_limits
.iter()
.map(|limit| (*limit != 0).then_some(*limit))
.collect(),
};
Some(AccountRouter::new_for_provider(
primary,
&config.additional_account_dirs,
provider,
options,
))
};
Ok((store, account_router))
}
const BOOTSTRAP_ADMIN_TTL_HOURS: i64 = 24 * 365;
const BOOTSTRAP_ADMIN_LABEL: &str = "bootstrap-admin";
fn announce_admin_access(config: &Config, token_manager: &TokenManager) {
if config.allow_anonymous_admin {
tracing::warn!(
"--allow-anonymous-admin is set: /api/tokens*, /api/providers* and /api/login* accept unauthenticated requests"
);
return;
}
if config.admin_key.is_some() {
tracing::info!("Admin access: TOKEN_ADMIN_KEY configured (bootstrap credential)");
return;
}
match token_manager.has_active_admin_token() {
Ok(true) => {
tracing::info!("Admin access: existing admin token found in the token store");
return;
}
Ok(false) => {}
Err(e) => {
tracing::warn!("could not inspect the token store for admin tokens: {e}");
return;
}
}
match token_manager.issue_admin_token(BOOTSTRAP_ADMIN_TTL_HOURS, BOOTSTRAP_ADMIN_LABEL) {
Ok(token) => {
println!("─────────────────────────────────────────────────────────────");
println!("Admin token (shown once, store it now): {token}");
println!("Use it as: Authorization: Bearer <token>");
println!("Rotate it with: link-assistant-router tokens rotate <id>");
println!("─────────────────────────────────────────────────────────────");
tracing::info!("Generated a bootstrap admin token; admin endpoints are closed");
}
Err(e) => tracing::error!("failed to generate a bootstrap admin token: {e}"),
}
}
async fn run_server(
config: Config,
logger: LogLazy,
request_log: Option<&std::path::Path>,
request_log_max_bytes: u64,
) -> Result<(), Box<dyn std::error::Error>> {
tracing::info!("Upstream: {}", config.upstream_base_url);
tracing::info!("Upstream provider: {:?}", config.upstream_provider);
let (subscription_provider, subscription_home) = subscription_pool(&config);
tracing::info!(
"Subscription home ({subscription_provider}): {}",
subscription_home.display()
);
tracing::info!("Routing mode: {:?}", config.routing_mode);
tracing::info!("Storage policy: {:?}", config.storage_policy);
if config.routing_mode == RoutingMode::Cli || config.routing_mode == RoutingMode::Hybrid {
tracing::warn!(
"RoutingMode::{:?} is configured but the CLI backend is not yet wired; falling back to direct.",
config.routing_mode
);
}
let (store, account_router) = build_shared_state(&config)?;
if let Some(router) = account_router.as_ref() {
tracing::info!("Multi-account routing enabled ({} accounts)", router.len());
}
let token_manager = TokenManager::with_store(&config.token_secret, store);
announce_admin_access(&config, &token_manager);
let oauth_provider = OAuthProvider::new(&config.claude_code_home);
let metrics = Arc::new(Metrics::default());
let provider_store = ProviderStore::open(&config.data_dir, &config.token_secret)?;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?;
let crater_provider =
if config.upstream_provider == link_assistant_router::config::UpstreamProvider::Crater {
Some(Arc::new(ForgeFedTaskProvider::new(
client.clone(),
config.crater.clone(),
)) as Arc<dyn TaskProvider>)
} else {
None
};
let user_home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
let subscription_readers = link_assistant_router::subscription::all_subscription_readers(
&config.claude_code_home,
&user_home,
);
for reader in &subscription_readers {
tracing::info!(
"Subscription provider {}: reading credentials from {}",
reader.provider(),
reader.home().display()
);
}
let subscription_reader = link_assistant_router::subscription::active_subscription_reader(
config.upstream_provider,
&subscription_readers,
);
let model_catalogs = Arc::new(link_assistant_router::model_catalog::ModelCatalogCache::new());
let admin_claim = Arc::new(link_assistant_router::admin::AdminClaim::load(
config.admin_key.clone(),
&config.data_dir,
config.admin_ui.candidate_ttl,
));
let state = AppState {
client,
token_manager,
oauth_provider,
account_router,
subscription_reader,
subscription_base_url: None,
subscription_readers,
model_catalogs: Arc::clone(&model_catalogs),
subscription_cache: Arc::new(link_assistant_router::refresh::TokenCache::new()),
upstream_base_url: config.upstream_base_url.clone(),
upstream_provider: config.upstream_provider,
gonka: link_assistant_router::gonka::GonkaConfig::new(
config.gonka_private_key.clone(),
&config.gonka_source_url,
config.gonka_model.clone(),
),
bridge_model: config.bridge_model.clone(),
audit: std::sync::Arc::new(link_assistant_router::audit::AuditLog::to_path(
config.audit_log.as_deref(),
)),
request_log: link_assistant_router::logging::request_log(
&config.data_dir,
request_log,
request_log_max_bytes,
),
crater: crater_provider,
openai_compatible: config.openai_compatible.clone(),
provider_store,
logger,
max_proxy_request_bytes: config.max_proxy_request_bytes,
admin: Arc::clone(&admin_claim),
admin_key: config.admin_key.clone(),
allow_anonymous_admin: config.allow_anonymous_admin,
metrics: Arc::clone(&metrics),
activitypub_actor_base_url: config.activitypub_actor_base_url.clone(),
activitypub_public_key_pem: config.activitypub_public_key_pem.clone(),
mpp: config.mpp.clone(),
login_manager: LoginManager::new(config.login.clone()),
github: link_assistant_router::github_proxy::GitHubProxyConfig::from_env()
.map_err(std::io::Error::other)?,
};
let catalog_refresh = tokio::spawn(
link_assistant_router::model_catalog::refresh_catalogs_forever(
state.client.clone(),
state.subscription_readers.clone(),
Arc::clone(&state.subscription_cache),
Arc::clone(&state.model_catalogs),
),
);
let app = link_assistant_router::server_router::router(state.clone(), &config)
.layer(from_fn_with_state(
state.clone(),
link_assistant_router::request_log::log_http_exchange,
))
.layer(TraceLayer::new_for_http());
tracing::info!("Listening on {}", config.listen_addr);
let admin_server = if config.admin_ui.enabled {
let admin_addr = config.admin_ui.listen_addr;
let admin_app = link_assistant_router::admin_api::router(state.clone())
.layer(from_fn_with_state(
state.clone(),
link_assistant_router::request_log::log_http_exchange,
))
.layer(TraceLayer::new_for_http());
let admin_listener = tokio::net::TcpListener::bind(admin_addr).await?;
tracing::info!("Admin UI listening on {admin_addr}");
if admin_claim.is_claimed() {
tracing::info!("Admin credential present; bootstrap is closed");
} else {
tracing::warn!(
"Admin is unclaimed: the first visitor to {admin_addr} that confirms a claim becomes admin"
);
}
Some(tokio::spawn(async move {
if let Err(e) = axum::serve(admin_listener, admin_app)
.with_graceful_shutdown(shutdown_signal())
.await
{
tracing::error!("admin UI server error: {e}");
}
}))
} else {
tracing::info!("Admin UI disabled (set --admin-port / ADMIN_PORT to enable)");
None
};
let chat_channels = spawn_chat_channels(&config, &state, Arc::clone(&admin_claim));
let listener = tokio::net::TcpListener::bind(config.listen_addr).await?;
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
if let Some(handle) = admin_server {
handle.abort();
}
for handle in chat_channels {
handle.abort();
}
catalog_refresh.abort();
Ok(())
}
fn spawn_chat_channels(
config: &Config,
state: &AppState,
admin_claim: Arc<link_assistant_router::admin::AdminClaim>,
) -> Vec<tokio::task::JoinHandle<()>> {
let chat_config = config.chat_admin.clone();
if !chat_config.telegram_enabled() && !chat_config.vk_enabled() {
tracing::info!(
"Chat admin channels disabled (set TELEGRAM_BOT_TOKEN and/or VK_BOT_TOKEN to enable)"
);
return Vec::new();
}
let chat = Arc::new(
link_assistant_router::chat_admin::ChatAdmin::new(
admin_claim,
state.token_manager.clone(),
config.admin_key.clone(),
chat_config.clone(),
)
.with_status(Arc::new(state.clone())),
);
let mut handles = Vec::new();
if chat_config.telegram_enabled() {
let chat = Arc::clone(&chat);
let client = state.client.clone();
handles.push(tokio::spawn(async move {
link_assistant_router::telegram::run(chat, client).await;
}));
}
if chat_config.vk_enabled() {
let chat = Arc::clone(&chat);
let client = state.client.clone();
handles.push(tokio::spawn(async move {
link_assistant_router::vk::run(chat, client).await;
}));
}
if chat.admin_claim().is_claimed() {
tracing::info!("Chat admin: a credential exists; /start will ask for one");
} else {
tracing::warn!(
"Chat admin: unclaimed — the first private-chat user to confirm a /start becomes admin"
);
}
handles
}
fn run_tokens(config: &Config, op: &TokenOp) -> ExitCode {
let (store, _account_router) = match build_shared_state(config) {
Ok(v) => v,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(1);
}
};
let mgr = TokenManager::with_store(&config.token_secret, store);
match op {
TokenOp::Issue {
ttl_hours,
label,
account,
max_requests,
max_tokens,
rate_limit_per_minute,
admin,
} => match mgr.issue(&IssueRequest {
ttl_hours: *ttl_hours,
label,
account: account.as_deref(),
max_requests: *max_requests,
max_tokens: *max_tokens,
rate_limit_per_minute: *rate_limit_per_minute,
scope: if *admin { ADMIN_SCOPE } else { "" },
}) {
Ok(t) => {
println!("{t}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
TokenOp::Rotate {
id,
ttl_hours,
label,
} => match mgr.rotate_token(id, *ttl_hours, label) {
Ok(t) => {
println!("{t}");
eprintln!("revoked {id}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
TokenOp::List => match mgr.list_tokens() {
Ok(records) => {
println!(
"{:<36} {:<10} {:<10} {:<8} {:<13} {:<15} {:<8} {:<6} label",
"id",
"issued_at",
"expires_at",
"revoked",
"requests",
"tokens",
"rpm",
"scope"
);
for r in records {
let requests = r.max_requests.map_or_else(
|| format!("{}/-", r.used_requests),
|max| format!("{}/{max}", r.used_requests),
);
let scope = if r.scope.is_empty() {
"client"
} else {
r.scope.as_str()
};
let tokens = r.max_tokens.map_or_else(
|| format!("{}/-", r.used_tokens),
|max| format!("{}/{max}", r.used_tokens),
);
let rpm = r
.rate_limit_per_minute
.map_or_else(|| "-".to_string(), |limit| limit.to_string());
println!(
"{:<36} {:<10} {:<10} {:<8} {:<13} {:<15} {:<8} {scope:<6} {}",
r.id, r.issued_at, r.expires_at, r.revoked, requests, tokens, rpm, r.label
);
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
TokenOp::Revoke { id } | TokenOp::Expire { id } => match mgr.revoke_token(id) {
Ok(()) => {
println!("revoked {id}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
TokenOp::Show { id } => match mgr.list_tokens() {
Ok(records) => records.into_iter().find(|r| r.id == *id).map_or_else(
|| {
eprintln!("not found: {id}");
ExitCode::from(2)
},
|r| {
println!("{}", serde_json::to_string_pretty(&r).unwrap_or_default());
ExitCode::SUCCESS
},
),
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
}
}
fn run_accounts(config: &Config, op: &AccountOp) -> ExitCode {
let router = match build_shared_state(config) {
Ok((_, Some(r))) => r,
Ok((_, None)) => {
let (provider, primary) = subscription_pool(config);
AccountRouter::new_for_provider(
primary,
&[],
provider,
AccountRouterOptions {
strategy: config.account_routing_strategy,
cooldown: Duration::from_secs(config.account_cooldown_secs),
session_affinity_ttl: Duration::from_secs(config.session_affinity_ttl_secs),
request_limits: config
.account_request_limits
.iter()
.map(|limit| (*limit != 0).then_some(*limit))
.collect(),
},
)
}
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(1);
}
};
match op {
AccountOp::List => {
let snap = router.health_snapshot();
println!(
"{:<16} {:<8} {:<6} {:<9} {:<9} home",
"name", "healthy", "used", "limit", "remaining"
);
for h in snap {
let limit = h
.request_limit
.map_or_else(|| "-".to_string(), |value| value.to_string());
let remaining = h
.remaining_requests
.map_or_else(|| "-".to_string(), |value| value.to_string());
println!(
"{:<16} {:<8} {:<6} {:<9} {:<9} {}",
h.name,
h.healthy,
h.used,
limit,
remaining,
h.home.display()
);
}
ExitCode::SUCCESS
}
}
}
fn run_providers(config: &Config, op: &ProviderOp) -> ExitCode {
let store = match ProviderStore::open(&config.data_dir, &config.token_secret) {
Ok(store) => store,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(1);
}
};
match op {
ProviderOp::List => match store.list_redacted() {
Ok(records) => {
println!(
"{:<20} {:<18} {:<32} {:<10} default_model",
"name", "kind", "base_url", "enabled"
);
for record in records {
println!(
"{:<20} {:<18} {:<32} {:<10} {}",
record.name,
record.kind.as_str(),
record.base_url,
record.enabled,
record.default_model.unwrap_or_default()
);
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
ProviderOp::Add {
name,
kind,
base_url,
model,
models,
api_key,
api_key_env,
enabled,
} => {
let input = ProviderUpsert {
name: name.clone(),
kind: Some(kind.clone()),
base_url: base_url.clone(),
default_model: model.clone(),
models: Some(models.clone()),
api_key: api_key.clone(),
api_key_env: api_key_env.clone(),
encrypted_api_key: None,
enabled: Some(*enabled),
};
match store.upsert(input) {
Ok(record) => {
println!(
"{}",
serde_json::to_string_pretty(&record.redacted()).unwrap_or_default()
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
}
}
ProviderOp::Show { name } => match store.get(name) {
Ok(Some(record)) => {
println!(
"{}",
serde_json::to_string_pretty(&record.redacted()).unwrap_or_default()
);
ExitCode::SUCCESS
}
Ok(None) => {
eprintln!("not found: {name}");
ExitCode::from(2)
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
ProviderOp::Remove { name } => match store.delete(name) {
Ok(true) => {
println!("removed {name}");
ExitCode::SUCCESS
}
Ok(false) => {
eprintln!("not found: {name}");
ExitCode::from(2)
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
ProviderOp::Import { path } => match store.import_file(path) {
Ok(count) => {
println!("imported {count} provider(s)");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
},
}
}
async fn run_doctor(config: &Config) -> ExitCode {
println!("Link.Assistant.Router v{}", link_assistant_router::VERSION);
println!("listen_addr : {}", config.listen_addr);
println!("upstream_base_url : {}", config.upstream_base_url);
println!("upstream_provider : {:?}", config.upstream_provider);
println!(
"openai_provider : {} ({})",
config.openai_compatible.provider_name, config.openai_compatible.base_url
);
println!(
"crater_inbox : {}",
config.crater.inbox.as_deref().unwrap_or("<unset>")
);
println!("crater_actor : {}", config.crater.actor);
println!("claude_code_home : {}", config.claude_code_home);
println!("routing_mode : {:?}", config.routing_mode);
println!("storage_policy : {:?}", config.storage_policy);
println!("data_dir : {}", config.data_dir.display());
println!("enable_openai_api : {}", config.enable_openai_api);
println!("enable_anthropic_api : {}", config.enable_anthropic_api);
println!("enable_metrics : {}", config.enable_metrics);
println!(
"additional_account_dirs: {} configured",
config.additional_account_dirs.len()
);
println!(
"account_routing_strategy: {:?}",
config.account_routing_strategy
);
println!("account_cooldown_secs : {}", config.account_cooldown_secs);
println!(
"account_request_limits : {:?}",
config.account_request_limits
);
println!(
"session_affinity_ttl : {}",
config.session_affinity_ttl_secs
);
println!(
"admin_key : {}",
if config.admin_key.is_some() {
"set"
} else {
"<unset>"
}
);
println!(
"admin_ui : {}",
if config.admin_ui.enabled {
format!("enabled on {}", config.admin_ui.listen_addr)
} else {
"disabled".to_string()
}
);
{
let claim = link_assistant_router::admin::AdminClaim::load(
config.admin_key.clone(),
&config.data_dir,
config.admin_ui.candidate_ttl,
);
let status = claim.status();
println!(
"admin_credential : {}",
if status.provisioned_by_environment {
"provisioned by environment"
} else if status.claimed {
"claimed (first-visitor bootstrap closed)"
} else {
"UNCLAIMED (bootstrap open)"
}
);
}
println!(
"admin_endpoints : {}",
if config.allow_anonymous_admin {
"OPEN (--allow-anonymous-admin)"
} else {
"closed (admin key or admin-scoped token required)"
}
);
println!(
"telegram_admin_bot : {}",
if config.chat_admin.telegram_enabled() {
"enabled (private chats only)"
} else {
"disabled"
}
);
println!(
"vk_admin_bot : {}",
if config.chat_admin.vk_enabled() {
"enabled (private chats only)"
} else {
"disabled"
}
);
println!(
"login_api : {}",
if config.login.enabled {
"enabled (POST /api/login)"
} else {
"disabled"
}
);
println!(
"login_cli : {} {}",
config.login.command,
config.login.args.join(" ")
);
println!(
"mpp_openai_charge : {}",
if config.mpp.is_configured() {
"enabled"
} else {
"disabled"
}
);
let (active_provider, primary_home) = subscription_pool(config);
let primary = SubscriptionReader::new(active_provider, &primary_home);
match primary.discover_credential_path() {
Some(path) => {
let status = primary.read_token().map_or("found, NO TOKEN", |token| {
if token.is_expired(chrono::Utc::now().timestamp_millis()) {
"found, token EXPIRED on disk"
} else {
"found, token OK"
}
});
println!("primary credentials : {} ({})", path.display(), status);
}
None => println!(
"primary credentials : {} (MISSING)",
primary_home.display()
),
}
for (i, dir) in config.additional_account_dirs.iter().enumerate() {
let reader = SubscriptionReader::new(active_provider, dir);
match reader.discover_credential_path() {
Some(path) => println!(
"extra account {} : {} (found)",
i + 1,
path.display()
),
None => println!(
"extra account {} : {} (MISSING)",
i + 1,
dir.display()
),
}
}
let user_home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
let catalog_error = link_assistant_router::doctor::subscription_catalog_diagnostics(
active_provider,
&config.claude_code_home,
&user_home,
)
.await;
if config.data_dir.exists() {
println!("data_dir : present");
} else {
println!("data_dir : will be created on first write");
}
if matches!(
config.storage_policy,
StoragePolicy::Text | StoragePolicy::Both
) {
let p = config.data_dir.join("tokens.lino");
println!(
"lino store : {} ({})",
p.display(),
if p.exists() { "present" } else { "<empty>" }
);
}
if matches!(
config.storage_policy,
StoragePolicy::Binary | StoragePolicy::Both
) {
let p = config.data_dir.join("tokens.bin");
println!(
"binary store : {} ({})",
p.display(),
if p.exists() { "present" } else { "<empty>" }
);
}
let p = config.data_dir.join("providers.lenv");
println!(
"provider store : {} ({})",
p.display(),
if p.exists() { "present" } else { "<empty>" }
);
if catalog_error {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("Failed to install CTRL+C signal handler");
tracing::info!("Shutdown signal received");
}