use anyhow::Result;
use crate::auth;
use crate::auth::token::TokenStore;
use crate::config::Config;
use super::USAGE;
pub async fn run(args: &[String]) -> Result<()> {
let token_store = TokenStore::new()?;
match args.first().map(String::as_str) {
Some("login") => {
let tokens = auth::oauth::login().await?;
let viewer = auth::add_account(&token_store, tokens).await?;
println!("{}", auth::signed_in(&viewer));
Ok(())
}
Some("status") => auth_status(&token_store).await,
Some("list") => {
list(&token_store)?;
Ok(())
}
Some("switch") => {
let Some(name) = args.get(1) else {
list(&token_store)?;
anyhow::bail!("Usage: linear-tui auth switch <workspace>");
};
let account = switch(&token_store, name)?;
println!("Switched to {}.", account.label());
Ok(())
}
Some("token") => {
let Some(key) = args.get(1) else {
anyhow::bail!("Usage: linear-tui auth token <api-key>");
};
let viewer = auth::setup::save_api_key(key).await?;
println!("API key saved. {}", auth::signed_in(&viewer));
Ok(())
}
Some("set-oauth") => {
let Some(client_id) = args.get(1) else {
anyhow::bail!("Usage: linear-tui auth set-oauth <client-id> [client-secret]");
};
let mut config = Config::load()?;
config.auth.oauth_client_id = Some(client_id.clone());
config.auth.oauth_client_secret = args.get(2).cloned();
config.save()?;
println!("Logins will now use your application ({client_id}).");
println!("Register these redirect URIs on it:");
for port in auth::oauth::callback_ports() {
println!(" http://localhost:{port}/callback");
}
Ok(())
}
Some("logout") => {
let clear_key = args.iter().any(|arg| arg == "--all");
let name = args[1..].iter().find(|arg| !arg.starts_with("--"));
let mut config = Config::load()?;
if clear_key {
token_store.clear()?;
config.auth.api_key = None;
config.save()?;
println!("Logged out of every workspace. Stored tokens and API key removed.");
return Ok(());
}
let removed = token_store.update(|accounts| {
let key = match name {
Some(name) => accounts.find(name).map(|a| a.key().cloned()),
None => accounts.current_account().map(|a| a.key().cloned()),
};
let removed = key.and_then(|key| accounts.remove(key.as_ref()));
let next = accounts.current_account().map(|a| a.label());
(removed, next)
})?;
match removed {
(Some(account), next) => {
println!("Logged out of {}.", account.label());
if let Some(next) = next {
println!("Now using {next}.");
}
}
(None, _) => match name {
Some(name) => anyhow::bail!("No workspace named '{name}' is signed in."),
None => println!("No OAuth token was stored."),
},
}
if config.auth.api_key.is_some() && token_store.load()?.is_empty() {
println!(
"An API key is still set in config.toml — \
`linear-tui auth logout --all` removes that too."
);
}
Ok(())
}
_ => {
print!("{USAGE}");
Ok(())
}
}
}
fn list(token_store: &TokenStore) -> Result<()> {
let accounts = token_store.load()?;
if accounts.is_empty() {
println!("No workspace is signed in through the browser. Run `linear-tui auth login`.");
return Ok(());
}
for account in &accounts.accounts {
let mark = if accounts.is_current(account) {
"*"
} else {
" "
};
match &account.user {
Some(user) => println!("{mark} {} as {user}", account.label()),
None => println!("{mark} {}", account.label()),
}
}
Ok(())
}
pub fn switch(token_store: &TokenStore, name: &str) -> Result<auth::token::Account> {
token_store
.update(|accounts| {
let account = accounts.find(name).cloned()?;
accounts.current = account.key().cloned();
Some(account)
})?
.ok_or_else(|| {
anyhow::anyhow!(
"No workspace named '{name}' is signed in. \
`linear-tui auth list` shows them; `linear-tui auth login` adds one."
)
})
}
async fn auth_status(token_store: &TokenStore) -> Result<()> {
let config = Config::load()?;
let accounts = token_store.load()?;
println!("Application {}", auth::oauth::application_summary()?);
match accounts.current_account() {
Some(account) => {
println!("Workspace {}", account.label());
let remaining = account.tokens.seconds_until_expiry();
if remaining > 0 {
println!("OAuth token valid for {}", format_duration(remaining));
} else {
println!(
"OAuth token expired {} ago, refreshes on next use",
format_duration(-remaining)
);
}
if accounts.accounts.len() > 1 {
println!(
"Workspaces {} signed in (`linear-tui auth list`)",
accounts.accounts.len()
);
}
println!("Stored in {}", token_store.path().display());
}
None => println!("OAuth token none"),
}
println!(
"API key {}",
match config.auth.api_key {
Some(_) => "set in config.toml",
None => "none",
}
);
println!();
match auth::resolve_auth(token_store, config.auth.api_key.as_deref()).await {
Ok(method) => match auth::identify(&method).await {
Ok(viewer) => println!("{} Via {}.", auth::signed_in(&viewer), method.label()),
Err(e) => println!("Stored credentials were rejected by the API: {e}"),
},
Err(e) => println!("{e}"),
}
Ok(())
}
fn format_duration(seconds: i64) -> String {
match seconds {
s if s >= 3600 => format!("{}h {}m", s / 3600, (s % 3600) / 60),
s if s >= 60 => format!("{}m", s / 60),
s => format!("{s}s"),
}
}