From fcc28fbcd6448203574b00cfbf12897dc854b213 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:16:38 -0700 Subject: [PATCH 1/7] feat(auth): require explicit `login` subcommand instead of bare-`auth` shortcut Bare `hotdata auth` previously triggered an automatic browser login. Now it prints the `auth` help (listing login/register/logout/status), matching how other commands behave when invoked with no subcommand. Users must type `hotdata auth login` to authenticate. Also exempt bare `auth` from the update gate since it no longer hits the API. --- src/command.rs | 2 +- src/main.rs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/command.rs b/src/command.rs index 32495ad..7bae106 100644 --- a/src/command.rs +++ b/src/command.rs @@ -263,7 +263,7 @@ pub enum QueryCommands { #[derive(Subcommand)] pub enum AuthCommands { - /// Log in via browser (same as `hotdata auth` with no subcommand) + /// Log in via browser Login, /// Create a new account via browser (defaults to GitHub OAuth) diff --git a/src/main.rs b/src/main.rs index 5080bbf..7131693 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,9 @@ fn main() { // never blocked (see `update::should_check`). let gate_update = !matches!( &cli.command, - None | Some(Commands::Upgrade) | Some(Commands::Completions { .. }) + None | Some(Commands::Upgrade) + | Some(Commands::Completions { .. }) + | Some(Commands::Auth { command: None }) ); if gate_update { update::enforce_latest_or_exit(); @@ -170,10 +172,19 @@ fn main() { } Some(cmd) => match cmd { Commands::Auth { command } => match command { - None | Some(AuthCommands::Login) => auth::login(), + Some(AuthCommands::Login) => auth::login(), Some(AuthCommands::Register { email }) => auth::register(email), Some(AuthCommands::Status) => auth::status("default"), Some(AuthCommands::Logout) => auth::logout("default"), + None => { + use clap::CommandFactory; + let mut cmd = Cli::command(); + cmd.build(); + cmd.find_subcommand_mut("auth") + .unwrap() + .print_help() + .unwrap(); + } }, Commands::Query { sql, From e51a6049bed444a9051ede63c76a4f6b20707902 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:19:47 -0700 Subject: [PATCH 2/7] docs(changelog): note auth login subcommand requirement (#182) --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc36bb..51b5423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [Unreleased] + +### πŸš€ Features + +- *(auth)* [**breaking**] Require explicit `login` subcommand; bare `hotdata auth` now prints help (#182) ## [0.8.0] - 2026-06-24 ### πŸš€ Features From 5e13502236f5d3db39c2f2d98e0b2d7b666fb76f Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:24:20 -0700 Subject: [PATCH 3/7] feat(auth): add `profiles` subcommand to scaffold profiles.yml `hotdata auth profiles` writes ~/.hotdata/profiles.yml seeded with a `default` profile pointing at the production API and app URLs: profiles: default: HOTDATA_API_URL: https://api.hotdata.dev/v1 HOTDATA_APP_URL: https://app.hotdata.dev Refuses to overwrite an existing file. Local-only, so it is exempt from the API update gate. --- CHANGELOG.md | 1 + src/auth.rs | 12 ++++++++++++ src/command.rs | 3 +++ src/config.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 4 ++++ 5 files changed, 63 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b5423..7547e10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### πŸš€ Features - *(auth)* [**breaking**] Require explicit `login` subcommand; bare `hotdata auth` now prints help (#182) +- *(auth)* Add `auth profiles` to scaffold a `profiles.yml` with a `default` profile (#182) ## [0.8.0] - 2026-06-24 ### πŸš€ Features diff --git a/src/auth.rs b/src/auth.rs index bd535ca..01fcae0 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -8,6 +8,18 @@ use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::io::stdout; +pub fn create_profiles() { + match config::create_profiles_file() { + Ok(path) => { + println!("{} {}", "Created".green(), path.display()); + } + Err(e) => { + eprintln!("error: {e}"); + std::process::exit(1); + } + } +} + pub fn logout(profile: &str) { crate::jwt::clear_session(); if let Err(e) = config::clear_workspaces(profile) { diff --git a/src/command.rs b/src/command.rs index 7bae106..7baed9e 100644 --- a/src/command.rs +++ b/src/command.rs @@ -278,6 +278,9 @@ pub enum AuthCommands { /// Show authentication status Status, + + /// Create a profiles.yml seeded with a `default` profile + Profiles, } #[derive(Subcommand)] diff --git a/src/config.rs b/src/config.rs index 7ff1662..af33af1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,6 +21,27 @@ fn config_path() -> Result { Ok(config_dir()?.join("config.yml")) } +fn profiles_path() -> Result { + Ok(config_dir()?.join("profiles.yml")) +} + +/// Create a `profiles.yml` in the config dir seeded with a `default` +/// profile pointing at the production API and app URLs. Returns the path +/// it wrote to. Refuses to overwrite an existing file so a user's +/// customizations are never clobbered. +pub fn create_profiles_file() -> Result { + let path = profiles_path()?; + if path.exists() { + return Err(format!("{} already exists", path.display())); + } + + let content = format!( + "profiles:\n default:\n HOTDATA_API_URL: {DEFAULT_API_URL}\n HOTDATA_APP_URL: {DEFAULT_APP_URL}\n" + ); + write_config(&path, &content)?; + Ok(path) +} + pub const DEFAULT_API_URL: &str = "https://api.hotdata.dev/v1"; pub const DEFAULT_APP_URL: &str = "https://app.hotdata.dev"; @@ -495,6 +516,28 @@ mod tests { assert_eq!(result, "ws-1"); } + #[test] + fn create_profiles_file_writes_default_profile() { + let (_tmp, _guard) = with_temp_config_dir(); + + let path = create_profiles_file().unwrap(); + assert!(path.exists()); + + let content = fs::read_to_string(&path).unwrap(); + assert!(content.contains("default:")); + assert!(content.contains(&format!("HOTDATA_API_URL: {DEFAULT_API_URL}"))); + assert!(content.contains(&format!("HOTDATA_APP_URL: {DEFAULT_APP_URL}"))); + } + + #[test] + fn create_profiles_file_refuses_to_overwrite() { + let (_tmp, _guard) = with_temp_config_dir(); + + create_profiles_file().unwrap(); + let err = create_profiles_file().unwrap_err(); + assert!(err.contains("already exists"), "got: {err}"); + } + #[test] fn resolve_workspace_id_errors_when_none() { let profile = ProfileConfig::default(); diff --git a/src/main.rs b/src/main.rs index 7131693..423a717 100644 --- a/src/main.rs +++ b/src/main.rs @@ -159,6 +159,9 @@ fn main() { None | Some(Commands::Upgrade) | Some(Commands::Completions { .. }) | Some(Commands::Auth { command: None }) + | Some(Commands::Auth { + command: Some(AuthCommands::Profiles) + }) ); if gate_update { update::enforce_latest_or_exit(); @@ -176,6 +179,7 @@ fn main() { Some(AuthCommands::Register { email }) => auth::register(email), Some(AuthCommands::Status) => auth::status("default"), Some(AuthCommands::Logout) => auth::logout("default"), + Some(AuthCommands::Profiles) => auth::create_profiles(), None => { use clap::CommandFactory; let mut cmd = Cli::command(); From 82ed0a642322124ebd3577a50f31cf4e82827a54 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:34:02 -0700 Subject: [PATCH 4/7] Revert "feat(auth): add `profiles` subcommand to scaffold profiles.yml" The profiles.yml duplicated existing behavior: api_url/app_url are already overridable via the HOTDATA_API_URL / HOTDATA_APP_URL env vars, and the scaffolded file restated the built-in defaults without anything reading it. --- CHANGELOG.md | 1 - src/auth.rs | 12 ------------ src/command.rs | 3 --- src/config.rs | 43 ------------------------------------------- src/main.rs | 4 ---- 5 files changed, 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7547e10..51b5423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ ### πŸš€ Features - *(auth)* [**breaking**] Require explicit `login` subcommand; bare `hotdata auth` now prints help (#182) -- *(auth)* Add `auth profiles` to scaffold a `profiles.yml` with a `default` profile (#182) ## [0.8.0] - 2026-06-24 ### πŸš€ Features diff --git a/src/auth.rs b/src/auth.rs index 01fcae0..bd535ca 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -8,18 +8,6 @@ use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::io::stdout; -pub fn create_profiles() { - match config::create_profiles_file() { - Ok(path) => { - println!("{} {}", "Created".green(), path.display()); - } - Err(e) => { - eprintln!("error: {e}"); - std::process::exit(1); - } - } -} - pub fn logout(profile: &str) { crate::jwt::clear_session(); if let Err(e) = config::clear_workspaces(profile) { diff --git a/src/command.rs b/src/command.rs index 7baed9e..7bae106 100644 --- a/src/command.rs +++ b/src/command.rs @@ -278,9 +278,6 @@ pub enum AuthCommands { /// Show authentication status Status, - - /// Create a profiles.yml seeded with a `default` profile - Profiles, } #[derive(Subcommand)] diff --git a/src/config.rs b/src/config.rs index af33af1..7ff1662 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,27 +21,6 @@ fn config_path() -> Result { Ok(config_dir()?.join("config.yml")) } -fn profiles_path() -> Result { - Ok(config_dir()?.join("profiles.yml")) -} - -/// Create a `profiles.yml` in the config dir seeded with a `default` -/// profile pointing at the production API and app URLs. Returns the path -/// it wrote to. Refuses to overwrite an existing file so a user's -/// customizations are never clobbered. -pub fn create_profiles_file() -> Result { - let path = profiles_path()?; - if path.exists() { - return Err(format!("{} already exists", path.display())); - } - - let content = format!( - "profiles:\n default:\n HOTDATA_API_URL: {DEFAULT_API_URL}\n HOTDATA_APP_URL: {DEFAULT_APP_URL}\n" - ); - write_config(&path, &content)?; - Ok(path) -} - pub const DEFAULT_API_URL: &str = "https://api.hotdata.dev/v1"; pub const DEFAULT_APP_URL: &str = "https://app.hotdata.dev"; @@ -516,28 +495,6 @@ mod tests { assert_eq!(result, "ws-1"); } - #[test] - fn create_profiles_file_writes_default_profile() { - let (_tmp, _guard) = with_temp_config_dir(); - - let path = create_profiles_file().unwrap(); - assert!(path.exists()); - - let content = fs::read_to_string(&path).unwrap(); - assert!(content.contains("default:")); - assert!(content.contains(&format!("HOTDATA_API_URL: {DEFAULT_API_URL}"))); - assert!(content.contains(&format!("HOTDATA_APP_URL: {DEFAULT_APP_URL}"))); - } - - #[test] - fn create_profiles_file_refuses_to_overwrite() { - let (_tmp, _guard) = with_temp_config_dir(); - - create_profiles_file().unwrap(); - let err = create_profiles_file().unwrap_err(); - assert!(err.contains("already exists"), "got: {err}"); - } - #[test] fn resolve_workspace_id_errors_when_none() { let profile = ProfileConfig::default(); diff --git a/src/main.rs b/src/main.rs index 423a717..7131693 100644 --- a/src/main.rs +++ b/src/main.rs @@ -159,9 +159,6 @@ fn main() { None | Some(Commands::Upgrade) | Some(Commands::Completions { .. }) | Some(Commands::Auth { command: None }) - | Some(Commands::Auth { - command: Some(AuthCommands::Profiles) - }) ); if gate_update { update::enforce_latest_or_exit(); @@ -179,7 +176,6 @@ fn main() { Some(AuthCommands::Register { email }) => auth::register(email), Some(AuthCommands::Status) => auth::status("default"), Some(AuthCommands::Logout) => auth::logout("default"), - Some(AuthCommands::Profiles) => auth::create_profiles(), None => { use clap::CommandFactory; let mut cmd = Cli::command(); From 7315db898715b5030d8e8f57e7f6ad239bbc1180 Mon Sep 17 00:00:00 2001 From: Eddie Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:54:21 -0700 Subject: [PATCH 5/7] docs(auth): drop stale bare-`auth` login references after subcommand change Bare `hotdata auth` now prints help instead of logging in, so the runtime hints, bundled docs/skills, and internal comments that told users `hotdata auth` (no subcommand) authenticates were misleading. Point them all at `hotdata auth login`, and tighten two tests that asserted the weaker substring. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 8 +++----- skills/hotdata/SKILL.md | 7 +++---- skills/hotdata/references/WORKFLOWS.md | 2 +- src/config.rs | 4 ++-- src/jwt.rs | 10 +++++----- src/sdk.rs | 10 +++++----- 6 files changed, 19 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c0baf6c..96306a3 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,13 @@ cp target/release/hotdata /usr/local/bin/hotdata ## Connect -Run either of the following (they are equivalent): +Run: ```sh hotdata auth login -# or -hotdata auth ``` -This launches a browser window where you can authorize the CLI to access your Hotdata account. +This launches a browser window where you can authorize the CLI to access your Hotdata account. (Bare `hotdata auth` prints the `auth` subcommand help.) Alternatively, authenticate with an API key using the `--api-key` flag: @@ -62,7 +60,7 @@ API key priority (lowest to highest): config file β†’ `HOTDATA_API_KEY` env var | Command | Subcommands | Description | | :-- | :-- | :-- | -| `auth` | `login`, `status`, `logout` | `login` or bare `auth` opens browser login; `status` / `logout` manage the saved profile | +| `auth` | `login`, `status`, `logout` | `login` opens browser login; `status` / `logout` manage the saved profile | | `workspaces` | `list`, `set` | Manage workspaces | | `connections` | `list`, `create`, `refresh`, `new` | Manage connections | | `databases` | `list`, `create`, `delete`, `tables` | Managed databases (create and load tables via parquet) | diff --git a/skills/hotdata/SKILL.md b/skills/hotdata/SKILL.md index a049bf7..5013197 100644 --- a/skills/hotdata/SKILL.md +++ b/skills/hotdata/SKILL.md @@ -27,10 +27,10 @@ Install all skills with **`hotdata skills install`**. Load specialized skills on ## Authentication -Run **`hotdata auth login`** (or **`hotdata auth`** with no subcommandβ€”same behavior) to authenticate via browser login. Config is stored in `~/.hotdata/config.yml`. +Run **`hotdata auth login`** to authenticate via browser login. Config is stored in `~/.hotdata/config.yml`. API key resolution (lowest to highest priority): -1. Config file (saved by `hotdata auth login` / `hotdata auth`) +1. Config file (saved by `hotdata auth login`) 2. `HOTDATA_API_KEY` environment variable (or `.env` file) 3. `--api-key ` flag (works on any command) @@ -336,8 +336,7 @@ A newer release can be incompatible with the API, so in an **interactive termina ### Auth ``` -hotdata auth login # Browser-based login (same as: hotdata auth) -hotdata auth # Browser-based login (same as: hotdata auth login) +hotdata auth login # Browser-based login hotdata auth status # Check current auth status hotdata auth logout # Remove saved auth for the default profile ``` diff --git a/skills/hotdata/references/WORKFLOWS.md b/skills/hotdata/references/WORKFLOWS.md index d0f0ffe..90cde70 100644 --- a/skills/hotdata/references/WORKFLOWS.md +++ b/skills/hotdata/references/WORKFLOWS.md @@ -34,7 +34,7 @@ End-to-end checklists. Use the linked sections for command detail and guardrails **Skill:** **`hotdata`** (optional **`hotdata-analytics`** for first queries) -1. [ ] `hotdata auth login` (or `hotdata auth`) +1. [ ] `hotdata auth login` 2. [ ] `hotdata workspaces list` β†’ `hotdata workspaces set` if not on the right workspace 3. [ ] `hotdata connections list` β€” note connection ids and names 4. [ ] (Optional) `hotdata connections create …` β€” see **`hotdata`** skill **Create a Connection** diff --git a/src/config.rs b/src/config.rs index 7ff1662..02005be 100644 --- a/src/config.rs +++ b/src/config.rs @@ -264,7 +264,7 @@ pub fn resolve_workspace_id( .workspaces .first() .map(|w| w.public_id.clone()) - .ok_or_else(|| "no workspace-id provided and no default workspace found. Run 'hotdata auth login' (or 'hotdata auth') or specify --workspace-id.".to_string()) + .ok_or_else(|| "no workspace-id provided and no default workspace found. Run 'hotdata auth login' or specify --workspace-id.".to_string()) } /// Global API key override set via --api-key flag. @@ -284,7 +284,7 @@ pub fn load(profile: &str) -> Result { let config_file: ConfigFile = serde_yaml::from_str(&content).unwrap_or_else(|_| { eprintln!("{}", "error parsing config file.".red()); eprintln!( - "Run 'hotdata auth login' (or 'hotdata auth') to generate a new config file." + "Run 'hotdata auth login' to generate a new config file." ); std::process::exit(1); }); diff --git a/src/jwt.rs b/src/jwt.rs index e5b2514..99e1a22 100644 --- a/src/jwt.rs +++ b/src/jwt.rs @@ -12,7 +12,7 @@ //! | Access token valid for > 30 s | return it directly | //! | Access expiring or expired, refresh token valid | call `/o/token/` with `grant_type=refresh_token` | //! | Refresh token dead, `api_key` present | re-mint via `grant_type=api_token` | -//! | Refresh token dead, no `api_key` | return an error β€” user must `hotdata auth` again | +//! | Refresh token dead, no `api_key` | return an error β€” user must `hotdata auth login` again | //! //! The raw `hd_...` API token (flow 3 in the design doc) is *never* //! persisted to the session file β€” it stays in the main config or the @@ -302,7 +302,7 @@ pub fn ensure_access_token( // 0) An explicit identity override (`--api-key`, `HOTDATA_API_KEY`, // or `.env`) is asserting a specific identity for *this invocation*. // The on-disk session may belong to a completely different user - // from a prior `hotdata auth` and must not be reused. Mint fresh + // from a prior `hotdata auth login` and must not be reused. Mint fresh // and deliberately skip persisting so we don't clobber the // interactive session. Surface the real mint error here too β€” if // the override key is bad, "HTTP 401" is more useful than the @@ -358,7 +358,7 @@ pub fn ensure_access_token( // API token rejected (revoked, expired, or invalid). // Fall through to the re-auth hint β€” hide the raw HTTP // error from the user; the api.rs caller appends a - // `hotdata auth` hint. + // `hotdata auth login` hint. } } } @@ -435,7 +435,7 @@ impl hotdata::auth::BearerTokenProvider for CliTokenProvider { resolved.map_err(|body| { // Surface as a 401 so `Configuration::resolve_bearer_token` logs the // cause and the request proceeds to a 401 the wrapper shapes into - // the "run hotdata auth" hint (the same end-state as the old + // the "run hotdata auth login" hint (the same end-state as the old // ApiClient refresher returning None). hotdata::auth::TokenExchangeError::Status { status: 401, body } }) @@ -1235,7 +1235,7 @@ mod tests { let (_tmp, _guard) = with_temp_config_dir(); // No session, no api_key fallback -> ensure_access_token errors, the // provider maps it to a 401 so the request proceeds to the wrapper's - // "run hotdata auth" hint. + // "run hotdata auth login" hint. let profile = mock_profile("http://127.0.0.1:1"); let provider = session_provider(&profile, None); match bearer(&provider).unwrap_err() { diff --git a/src/sdk.rs b/src/sdk.rs index 5401ad6..6597ef1 100644 --- a/src/sdk.rs +++ b/src/sdk.rs @@ -249,7 +249,7 @@ impl ApiError { /// `ApiClient::fail_response`'s formatting. /// /// On a 4xx, re-probe the auth status so a masked 404/403 is upgraded into - /// the "run hotdata auth" hint; otherwise surface the server body. Split out + /// the "run hotdata auth login" hint; otherwise surface the server body. Split out /// from [`exit`](Self::exit) so callers that want to append their own hint /// after the error (e.g. the query cross-source hint) can print, add the /// hint, then exit. @@ -483,7 +483,7 @@ impl Api { eprintln!("{}", format!("error: {e}").red()); eprintln!( "Run {} to log in, or pass --api-key.", - "hotdata auth".cyan() + "hotdata auth login".cyan() ); std::process::exit(1); } @@ -840,7 +840,7 @@ pub fn format_fail_message( if status.is_client_error() && let Some(auth::AuthStatus::Invalid(_)) = auth_status { - return "error: API key is invalid. Run 'hotdata auth login' (or 'hotdata auth') to re-authenticate.".to_string(); + return "error: API key is invalid. Run 'hotdata auth login' to re-authenticate.".to_string(); } // A 403 ACCESS_DENIED is the allow-list guard rejecting an operation the // credential can't perform β€” typically a database API token (which is @@ -886,7 +886,7 @@ mod tests { Some(&AuthStatus::Invalid(401)), ); assert!(msg.contains("API key is invalid")); - assert!(msg.contains("hotdata auth login") || msg.contains("hotdata auth")); + assert!(msg.contains("hotdata auth login")); } #[test] @@ -962,7 +962,7 @@ mod tests { msg.to_lowercase().contains("database api token"), "got: {msg}" ); - assert!(msg.contains("hotdata auth"), "got: {msg}"); + assert!(msg.contains("hotdata auth login"), "got: {msg}"); } #[test] From 64672123e37a2db1554ab7e02e3f5782476732b8 Mon Sep 17 00:00:00 2001 From: Eddie Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:58:38 -0700 Subject: [PATCH 6/7] test(auth): cover bare `auth` printing help instead of logging in Adds an integration test for the new no-subcommand arm (the 0%-patch-coverage lines Codecov flagged): `hotdata auth` exits 0 with the clap help block listing its subcommands and does NOT start the browser login flow. Runs offline and without credentials. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/auth_no_subcommand_help.rs | 48 ++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/auth_no_subcommand_help.rs diff --git a/tests/auth_no_subcommand_help.rs b/tests/auth_no_subcommand_help.rs new file mode 100644 index 0000000..c64f8ef --- /dev/null +++ b/tests/auth_no_subcommand_help.rs @@ -0,0 +1,48 @@ +//! `hotdata auth` with no subcommand prints the `auth` help (listing its +//! subcommands) instead of triggering a browser login β€” the behavior change +//! from PR #182. +//! +//! Runs fully offline and without credentials: bare `auth` is exempt from the +//! update gate and hits no API. Before the change this arm called +//! `auth::login()`, which prints "Opening browser to log in..." and starts a +//! local callback server; the test asserts that flow is *not* started. + +mod common; + +use common::{unauthenticated_output, DEFAULT_API_URL}; + +#[test] +fn bare_auth_prints_subcommand_help() { + let output = unauthenticated_output(DEFAULT_API_URL, &["auth"]); + + assert!( + output.status.success(), + "`hotdata auth` should exit 0 printing help\n--- stderr ---\n{}", + String::from_utf8_lossy(&output.stderr), + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + + // Clap help block, listing every auth subcommand. + assert!( + stdout.contains("Usage"), + "expected help output with a usage line; got:\n{stdout}" + ); + for sub in ["login", "register", "status", "logout"] { + assert!( + stdout.contains(sub), + "auth help missing `{sub}` subcommand; got:\n{stdout}" + ); + } + + // The login flow must NOT have started. `auth::login()` prints this banner + // before opening a browser / spinning up the callback server. + let combined = format!( + "{stdout}{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !combined.contains("Opening browser to log in"), + "bare `auth` should print help, not start a login flow; got:\n{combined}" + ); +} From 5dce860b714029a152abc8fc912c6d3aabe34972 Mon Sep 17 00:00:00 2001 From: Eddie Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:00:01 -0700 Subject: [PATCH 7/7] style: rustfmt the auth-hint and test changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the now-shorter hint strings and sort the test import β€” fixes the fmt CI check. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/config.rs | 4 +--- src/sdk.rs | 3 ++- tests/auth_no_subcommand_help.rs | 7 ++----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/config.rs b/src/config.rs index 02005be..cf62f6c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -283,9 +283,7 @@ pub fn load(profile: &str) -> Result { .map_err(|e| format!("error reading config file: {e}"))?; let config_file: ConfigFile = serde_yaml::from_str(&content).unwrap_or_else(|_| { eprintln!("{}", "error parsing config file.".red()); - eprintln!( - "Run 'hotdata auth login' to generate a new config file." - ); + eprintln!("Run 'hotdata auth login' to generate a new config file."); std::process::exit(1); }); config_file diff --git a/src/sdk.rs b/src/sdk.rs index 6597ef1..47b65e4 100644 --- a/src/sdk.rs +++ b/src/sdk.rs @@ -840,7 +840,8 @@ pub fn format_fail_message( if status.is_client_error() && let Some(auth::AuthStatus::Invalid(_)) = auth_status { - return "error: API key is invalid. Run 'hotdata auth login' to re-authenticate.".to_string(); + return "error: API key is invalid. Run 'hotdata auth login' to re-authenticate." + .to_string(); } // A 403 ACCESS_DENIED is the allow-list guard rejecting an operation the // credential can't perform β€” typically a database API token (which is diff --git a/tests/auth_no_subcommand_help.rs b/tests/auth_no_subcommand_help.rs index c64f8ef..9e845bf 100644 --- a/tests/auth_no_subcommand_help.rs +++ b/tests/auth_no_subcommand_help.rs @@ -9,7 +9,7 @@ mod common; -use common::{unauthenticated_output, DEFAULT_API_URL}; +use common::{DEFAULT_API_URL, unauthenticated_output}; #[test] fn bare_auth_prints_subcommand_help() { @@ -37,10 +37,7 @@ fn bare_auth_prints_subcommand_help() { // The login flow must NOT have started. `auth::login()` prints this banner // before opening a browser / spinning up the callback server. - let combined = format!( - "{stdout}{}", - String::from_utf8_lossy(&output.stderr) - ); + let combined = format!("{stdout}{}", String::from_utf8_lossy(&output.stderr)); assert!( !combined.contains("Opening browser to log in"), "bare `auth` should print help, not start a login flow; got:\n{combined}"