Skip to content

Commit 9743cd6

Browse files
authored
Flag restructuring for bt setup (#105)
Mechanical rename/removal of CLI flags: - `--quiet` → `--verbose` for bt setup (invert default) - `--agents` (repeatable) → `--agent` (single), remove `AgentArg::All` - `--no-mcp-skill` → `--no-skills` + `--no-mcp` - Add `--no-instrument`, `--tui`/`--background`, `--no-workflow` tui (default) launches the agent in its tui, background launches a headless agent tui and json conflict - Move `--yes` to `#[arg(skip)]` - ```--interactive/-i``` flag, that more or less preserves the previous behavior of asking all questions Part 1 of #94
1 parent c3a197e commit 9743cd6

9 files changed

Lines changed: 1385 additions & 507 deletions

File tree

src/args.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,34 @@ use clap::Args;
44

55
pub use braintrust_sdk_rust::{DEFAULT_API_URL, DEFAULT_APP_URL};
66

7+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8+
pub enum ArgValueSource {
9+
CommandLine,
10+
EnvVariable,
11+
}
12+
713
#[derive(Debug, Clone, Args)]
814
pub struct BaseArgs {
915
/// Output as JSON
1016
#[arg(long, global = true)]
1117
pub json: bool,
1218

13-
/// Suppress non-essential output
19+
/// Verbose mode — set at runtime by subcommands that support it
20+
#[arg(skip)]
21+
pub verbose: bool,
22+
23+
/// Reduce interactive UI output
1424
#[arg(long, short = 'q', env = "BRAINTRUST_QUIET", global = true, value_parser = clap::builder::BoolishValueParser::new(), default_value_t = false)]
1525
pub quiet: bool,
1626

1727
/// Disable ANSI color output
1828
#[arg(long, env = "BRAINTRUST_NO_COLOR", global = true, value_parser = clap::builder::BoolishValueParser::new(), default_value_t = false)]
1929
pub no_color: bool,
2030

31+
/// Disable all interactive prompts
32+
#[arg(long, env = "BRAINTRUST_NO_INPUT", global = true, value_parser = clap::builder::BoolishValueParser::new(), default_value_t = false)]
33+
pub no_input: bool,
34+
2135
/// Use a saved login profile (or via BRAINTRUST_PROFILE)
2236
#[arg(long, env = "BRAINTRUST_PROFILE", global = true)]
2337
pub profile: Option<String>,
@@ -40,14 +54,13 @@ pub struct BaseArgs {
4054
#[arg(long, env = "BRAINTRUST_API_KEY", global = true, hide = true)]
4155
pub api_key: Option<String>,
4256

57+
#[arg(skip)]
58+
pub api_key_source: Option<ArgValueSource>,
59+
4360
/// Prefer profile credentials even if BRAINTRUST_API_KEY/--api-key is set.
4461
#[arg(long, global = true)]
4562
pub prefer_profile: bool,
4663

47-
/// Disable all interactive prompts
48-
#[arg(long, global = true)]
49-
pub no_input: bool,
50-
5164
/// Override API URL (or via BRAINTRUST_API_URL)
5265
#[arg(
5366
long,

src/auth.rs

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::BTreeMap;
2+
use std::error::Error as StdError;
23
use std::fs;
34
use std::io::{IsTerminal, Write};
45
use std::path::{Path, PathBuf};
@@ -67,6 +68,48 @@ pub struct AvailableOrg {
6768
pub api_url: Option<String>,
6869
}
6970

71+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72+
enum RecoverableAuthErrorKind {
73+
OauthProfileSelection,
74+
OauthClientId,
75+
OauthRefreshToken,
76+
StoredCredential,
77+
}
78+
79+
#[derive(Debug)]
80+
struct RecoverableAuthError {
81+
kind: RecoverableAuthErrorKind,
82+
message: String,
83+
}
84+
85+
impl std::fmt::Display for RecoverableAuthError {
86+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87+
f.write_str(&self.message)
88+
}
89+
}
90+
91+
impl StdError for RecoverableAuthError {}
92+
93+
fn recoverable_auth_error(kind: RecoverableAuthErrorKind, message: String) -> anyhow::Error {
94+
anyhow::Error::new(RecoverableAuthError { kind, message })
95+
}
96+
97+
pub fn is_missing_credential_error(err: &anyhow::Error) -> bool {
98+
err.chain().any(|source| {
99+
source
100+
.downcast_ref::<RecoverableAuthError>()
101+
.is_some_and(|err| {
102+
matches!(
103+
err.kind,
104+
RecoverableAuthErrorKind::OauthProfileSelection
105+
| RecoverableAuthErrorKind::OauthClientId
106+
| RecoverableAuthErrorKind::OauthRefreshToken
107+
| RecoverableAuthErrorKind::StoredCredential
108+
)
109+
})
110+
})
111+
}
112+
70113
pub fn list_profiles() -> Result<Vec<ProfileInfo>> {
71114
let store = load_auth_store()?;
72115
Ok(store
@@ -494,15 +537,23 @@ pub async fn resolve_auth(base: &BaseArgs) -> Result<ResolvedAuth> {
494537
.or_else(|| {
495538
(store.profiles.len() == 1).then(|| store.profiles.keys().next().unwrap().as_str())
496539
})
497-
.ok_or_else(|| anyhow::anyhow!("oauth profile requested but none selected"))?
540+
.ok_or_else(|| {
541+
recoverable_auth_error(
542+
RecoverableAuthErrorKind::OauthProfileSelection,
543+
"oauth profile requested but none selected".to_string(),
544+
)
545+
})?
498546
.to_string();
499547
let profile = store
500548
.profiles
501549
.get(profile_name.as_str())
502550
.ok_or_else(|| anyhow::anyhow!("profile '{profile_name}' not found"))?;
503551
let client_id = profile.oauth_client_id.as_deref().ok_or_else(|| {
504-
anyhow::anyhow!(
505-
"oauth profile '{profile_name}' is missing client_id; re-run `bt auth login --oauth --profile {profile_name}`"
552+
recoverable_auth_error(
553+
RecoverableAuthErrorKind::OauthClientId,
554+
format!(
555+
"oauth profile '{profile_name}' is missing client_id; re-run `bt auth login --oauth --profile {profile_name}`"
556+
),
506557
)
507558
})?;
508559
let cached_expires_at = profile.oauth_access_expires_at;
@@ -519,8 +570,11 @@ pub async fn resolve_auth(base: &BaseArgs) -> Result<ResolvedAuth> {
519570
}
520571

521572
let refresh_token = load_profile_oauth_refresh_token(&profile_name)?.ok_or_else(|| {
522-
anyhow::anyhow!(
523-
"oauth refresh token missing for profile '{profile_name}'; re-run `bt auth login --oauth --profile {profile_name}`"
573+
recoverable_auth_error(
574+
RecoverableAuthErrorKind::OauthRefreshToken,
575+
format!(
576+
"oauth refresh token missing for profile '{profile_name}'; re-run `bt auth login --oauth --profile {profile_name}`"
577+
),
524578
)
525579
})?;
526580
let refreshed = refresh_oauth_access_token(&api_url, &refresh_token, client_id).await?;
@@ -640,8 +694,11 @@ where
640694
None
641695
} else {
642696
Some(load_secret(profile_name)?.ok_or_else(|| {
643-
anyhow::anyhow!(
644-
"no keychain credential found for profile '{profile_name}'; re-run `bt auth login --profile {profile_name}`"
697+
recoverable_auth_error(
698+
RecoverableAuthErrorKind::StoredCredential,
699+
format!(
700+
"no keychain credential found for profile '{profile_name}'; re-run `bt auth login --profile {profile_name}`"
701+
),
645702
)
646703
})?)
647704
};
@@ -831,6 +888,7 @@ async fn run_login_oauth(base: &BaseArgs, args: AuthLoginArgs) -> Result<()> {
831888
Ok(())
832889
}
833890

891+
#[allow(dead_code)]
834892
pub async fn login_interactive(base: &mut BaseArgs) -> Result<String> {
835893
let methods = ["OAuth (browser)", "API key"];
836894
let selected = ui::fuzzy_select("Select login method", &methods, 0)?;
@@ -842,6 +900,11 @@ pub async fn login_interactive(base: &mut BaseArgs) -> Result<String> {
842900
}
843901
}
844902

903+
pub async fn login_setup_oauth(base: &mut BaseArgs) -> Result<String> {
904+
login_interactive_oauth(base).await
905+
}
906+
907+
#[allow(dead_code)]
845908
async fn login_interactive_api_key(base: &mut BaseArgs) -> Result<String> {
846909
let api_key = prompt_api_key()?;
847910

@@ -2642,14 +2705,16 @@ mod tests {
26422705
fn make_base() -> BaseArgs {
26432706
BaseArgs {
26442707
json: false,
2708+
verbose: false,
26452709
quiet: false,
26462710
no_color: false,
2711+
no_input: false,
26472712
profile: None,
26482713
project: None,
26492714
org_name: None,
26502715
api_key: None,
2716+
api_key_source: None,
26512717
prefer_profile: false,
2652-
no_input: false,
26532718
api_url: None,
26542719
app_url: None,
26552720
env_file: None,
@@ -2710,6 +2775,34 @@ mod tests {
27102775
assert_err_contains(result, "invalid api_url");
27112776
}
27122777

2778+
#[test]
2779+
fn missing_credential_error_helper_detects_typed_auth_errors() {
2780+
let err = recoverable_auth_error(
2781+
RecoverableAuthErrorKind::OauthRefreshToken,
2782+
"oauth refresh token missing".to_string(),
2783+
);
2784+
2785+
assert!(is_missing_credential_error(&err));
2786+
}
2787+
2788+
#[test]
2789+
fn missing_credential_error_helper_ignores_unrelated_errors() {
2790+
let err = anyhow::anyhow!("some unrelated error");
2791+
2792+
assert!(!is_missing_credential_error(&err));
2793+
}
2794+
2795+
#[test]
2796+
fn missing_credential_error_helper_detects_errors_through_context() {
2797+
let err = recoverable_auth_error(
2798+
RecoverableAuthErrorKind::StoredCredential,
2799+
"missing stored credential".to_string(),
2800+
)
2801+
.context("while resolving auth");
2802+
2803+
assert!(is_missing_credential_error(&err));
2804+
}
2805+
27132806
fn restore_env_var(key: &str, previous: Option<OsString>) {
27142807
match previous {
27152808
Some(value) => env::set_var(key, value),

src/functions/push.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3433,14 +3433,16 @@ mod tests {
34333433
fn test_base_args() -> BaseArgs {
34343434
BaseArgs {
34353435
json: false,
3436+
verbose: false,
34363437
quiet: false,
34373438
no_color: false,
3439+
no_input: false,
34383440
profile: None,
34393441
org_name: None,
34403442
project: None,
34413443
api_key: None,
3444+
api_key_source: None,
34423445
prefer_profile: false,
3443-
no_input: false,
34443446
api_url: None,
34453447
app_url: None,
34463448
env_file: None,

0 commit comments

Comments
 (0)