diff --git a/src/commands/module.rs b/src/commands/module.rs index 0f629d2..1bf317e 100644 --- a/src/commands/module.rs +++ b/src/commands/module.rs @@ -16,7 +16,7 @@ use dialoguer::{Confirm, Input, theme::ColorfulTheme}; use indicatif::{ProgressBar, ProgressStyle}; use crate::api::{self, ApiError, CreateModuleInput}; -use crate::credentials::{self, LoadError}; +use crate::credentials; use super::{ DEFAULT_API_BASE, DEFAULT_APPS_API_BASE, DEFAULT_WEB_BASE, ENV_API_URL, ENV_APPS_API_URL, @@ -64,16 +64,7 @@ pub fn run(args: ModuleArgs) -> Result<()> { fn init(args: InitArgs) -> Result<()> { let theme = ColorfulTheme::default(); - let creds = match credentials::load() { - Ok(c) => c, - Err(LoadError::NotFound) => { - return Err(anyhow!( - "not signed in. Run `mirrorstack login` to sign in." - )); - } - Err(e) => return Err(e.into()), - }; - + let creds = credentials::load_or_login_hint()?; let api_base = resolve_base(ENV_API_URL, DEFAULT_API_BASE); let apps_base = resolve_base(ENV_APPS_API_URL, DEFAULT_APPS_API_BASE); let web_base = resolve_base(ENV_WEB_URL, DEFAULT_WEB_BASE); diff --git a/src/commands/whoami.rs b/src/commands/whoami.rs index a85e2be..e3faeca 100644 --- a/src/commands/whoami.rs +++ b/src/commands/whoami.rs @@ -9,7 +9,7 @@ use anyhow::{Result, anyhow}; use clap::Args; use crate::api::{self, ApiError}; -use crate::credentials::{self, LoadError}; +use crate::credentials; use super::{DEFAULT_API_BASE, ENV_API_URL, resolve_base}; @@ -17,16 +17,7 @@ use super::{DEFAULT_API_BASE, ENV_API_URL, resolve_base}; pub struct WhoamiArgs {} pub fn run(_args: WhoamiArgs) -> Result<()> { - let creds = match credentials::load() { - Ok(c) => c, - Err(LoadError::NotFound) => { - return Err(anyhow!( - "not signed in. Run `mirrorstack login` to sign in." - )); - } - Err(e) => return Err(e.into()), - }; - + let creds = credentials::load_or_login_hint()?; let api_base = resolve_base(ENV_API_URL, DEFAULT_API_BASE); match api::me(&api_base, &creds.access_token) { diff --git a/src/credentials/mod.rs b/src/credentials/mod.rs index 016637a..9205e65 100644 --- a/src/credentials/mod.rs +++ b/src/credentials/mod.rs @@ -77,6 +77,19 @@ pub fn load() -> Result { } } +/// Wrap [`load`] with the auth-required command idiom: a missing file +/// becomes a "run `mirrorstack login`" hint instead of the raw error. +/// Used by every command that needs an authenticated session. +pub fn load_or_login_hint() -> Result { + match load() { + Ok(c) => Ok(c), + Err(LoadError::NotFound) => Err(anyhow::anyhow!( + "not signed in. Run `mirrorstack login` to sign in." + )), + Err(e) => Err(e.into()), + } +} + /// Encode `SystemTime` as RFC 3339 in JSON. Avoids pulling chrono just /// for this; serde_json's default for SystemTime is a tagged struct /// which is ugly on disk. diff --git a/src/credentials/tests.rs b/src/credentials/tests.rs index aaf58e5..95cd581 100644 --- a/src/credentials/tests.rs +++ b/src/credentials/tests.rs @@ -33,6 +33,13 @@ fn credentials_lifecycle() { // load() before save() returns NotFound. assert!(matches!(load(), Err(LoadError::NotFound))); + // load_or_login_hint() before save() surfaces the login hint. + let hint_err = load_or_login_hint().unwrap_err(); + assert!( + hint_err.to_string().contains("mirrorstack login"), + "expected login hint, got: {hint_err}" + ); + // save → load roundtrip preserves token strings + expires_at. let want = Credentials { access_token: "AT".into(), @@ -41,6 +48,10 @@ fn credentials_lifecycle() { }; save(&want).expect("save"); let got = load().expect("load"); + + // load_or_login_hint() after save() returns the same data as load(). + let got_hint = load_or_login_hint().expect("load_or_login_hint"); + assert_eq!(got_hint, got); assert_eq!(got.access_token, want.access_token); assert_eq!(got.refresh_token, want.refresh_token); let skew = got