Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,23 @@ pub struct InitArgs {}

pub async fn run(base: BaseArgs, _args: InitArgs) -> Result<()> {
let bt_dir = std::env::current_dir()?.join(".bt");
if bt_dir.join("config.json").exists() {
print_command_status(CommandStatus::Warning, "Already Initialized");
let config_path = bt_dir.join("config.json");
if config_path.exists() {
if base.json {
let existing = config::load_file(&config_path);
println!(
"{}",
serde_json::json!({
"initialized": false,
"status": "already-initialized",
"org": existing.org,
"project": existing.project,
"path": config_path.display().to_string(),
})
);
} else {
print_command_status(CommandStatus::Warning, "Already Initialized");
}
return Ok(());
}

Expand Down Expand Up @@ -61,11 +76,24 @@ pub async fn run(base: BaseArgs, _args: InitArgs) -> Result<()> {

config::save_local(&cfg, true)?;

print_command_status(
CommandStatus::Success,
&format!("Project linked to {org}/{project}"),
);
print_command_status(CommandStatus::Success, "Created .bt/config.json");
if base.json {
println!(
"{}",
serde_json::json!({
"initialized": true,
"status": "created",
"org": org,
"project": project,
"path": config_path.display().to_string(),
})
);
} else {
print_command_status(
CommandStatus::Success,
&format!("Project linked to {org}/{project}"),
);
print_command_status(CommandStatus::Success, "Created .bt/config.json");
}

Ok(())
}
55 changes: 54 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ mod utils;
use crate::args::{has_explicit_profile_arg, ArgValueSource, BaseArgs, CLIArgs};

const DEFAULT_CANARY_VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "-canary.dev");
const CLI_VERSION: &str = match option_env!("BT_VERSION_STRING") {
pub(crate) const CLI_VERSION: &str = match option_env!("BT_VERSION_STRING") {
Some(version) => version,
None => DEFAULT_CANARY_VERSION,
};
Expand Down Expand Up @@ -247,6 +247,23 @@ fn main() {
std::process::exit(exit_code as i32);
}

fn handle_version_json(argv: &[OsString]) -> bool {
let mut saw_version = false;
let mut saw_json = false;
for arg in argv.iter().skip(1).filter_map(|a| a.to_str()) {
if arg == "--" {
break;
}
saw_version |= arg == "--version" || arg == "-V";
saw_json |= arg == "--json";
}
if !(saw_version && saw_json) {
return false;
}
println!("{}", serde_json::json!({ "version": CLI_VERSION }));
true
}

fn apply_runtime_env_overrides(base: &BaseArgs) {
// Apply the CLI-owned override once so reqwest and inherited child
// commands consistently observe BRAINTRUST_CA_CERT/--ca-cert precedence
Expand All @@ -260,6 +277,10 @@ fn try_main() -> Result<()> {
let argv: Vec<OsString> = std::env::args_os().collect();
env::bootstrap_from_args(&argv)?;

if handle_version_json(&argv) {
return Ok(());
}

let matches = Cli::command().get_matches_from(&argv);
let mut cli = Cli::from_arg_matches(&matches).expect("clap matches should parse");
apply_base_arg_sources(&matches, cli.command.base_mut());
Expand Down Expand Up @@ -588,4 +609,36 @@ mod tests {
assert!(!cli.command.base().quiet);
assert!(cli.command.base().verbose);
}

fn argv(parts: &[&str]) -> Vec<OsString> {
parts.iter().map(OsString::from).collect()
}

#[test]
fn handle_version_json_detects_long_form() {
assert!(handle_version_json(&argv(&["bt", "--version", "--json"])));
assert!(handle_version_json(&argv(&["bt", "--json", "--version"])));
}

#[test]
fn handle_version_json_detects_short_form() {
assert!(handle_version_json(&argv(&["bt", "-V", "--json"])));
}

#[test]
fn handle_version_json_requires_both_flags() {
assert!(!handle_version_json(&argv(&["bt", "--version"])));
assert!(!handle_version_json(&argv(&["bt", "--json", "status"])));
}

#[test]
fn handle_version_json_ignores_args_after_double_dash() {
assert!(!handle_version_json(&argv(&[
"bt",
"eval",
"--",
"--version",
"--json",
])));
}
}
74 changes: 67 additions & 7 deletions src/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ const BUILD_UPDATE_CHANNEL: Option<&str> = option_env!("BT_UPDATE_CHANNEL");
#[derive(Debug, Deserialize)]
struct GitHubRelease {
tag_name: String,
#[serde(default)]
target_commitish: Option<String>,
}

pub async fn run(base: BaseArgs, args: SelfArgs) -> Result<()> {
Expand All @@ -104,7 +106,7 @@ async fn run_update(base: &BaseArgs, args: UpdateArgs) -> Result<()> {
Ok(release) => {
let current = env!("CARGO_PKG_VERSION");
if stable_is_up_to_date(current, &release.tag_name) {
println!("{}", stable_check_message(current, &release.tag_name));
print_stable_check(base, current, &release.tag_name);
return Ok(());
}
}
Expand Down Expand Up @@ -140,17 +142,43 @@ async fn check_for_update(base: &BaseArgs, channel: UpdateChannel) -> Result<()>
let current = env!("CARGO_PKG_VERSION");

match channel {
UpdateChannel::Stable => {
println!("{}", stable_check_message(current, &release.tag_name));
}
UpdateChannel::Canary => {
println!("{}", canary_check_message(&release.tag_name));
}
UpdateChannel::Stable => print_stable_check(base, current, &release.tag_name),
UpdateChannel::Canary => print_canary_check(base, &release),
}

Ok(())
}

fn print_stable_check(base: &BaseArgs, current: &str, release_tag: &str) {
if base.json {
let payload = serde_json::json!({
"channel": "stable",
"current": current,
"latest": release_tag,
"up_to_date": stable_is_up_to_date(current, release_tag),
});
println!("{payload}");
} else {
println!("{}", stable_check_message(current, release_tag));
}
}

fn print_canary_check(base: &BaseArgs, release: &GitHubRelease) {
if base.json {
let payload = serde_json::json!({
"channel": "canary",
"latest": release.tag_name,
"up_to_date": canary_is_up_to_date(
crate::CLI_VERSION,
release.target_commitish.as_deref(),
),
});
println!("{payload}");
} else {
println!("{}", canary_check_message(&release.tag_name));
}
}

async fn fetch_release(_base: &BaseArgs, channel: UpdateChannel) -> Result<GitHubRelease> {
let client = crate::http::build_http_client_from_builder(
Client::builder()
Expand Down Expand Up @@ -329,6 +357,16 @@ fn stable_check_message(current: &str, release_tag: &str) -> String {
format!("update available on stable channel: current={current}, latest={release_tag}")
}

fn canary_is_up_to_date(current_version: &str, target_commitish: Option<&str>) -> bool {
let Some((_, local_sha)) = current_version.rsplit_once("-canary.") else {
return false;
};
if local_sha.is_empty() || local_sha == "dev" {
return false;
}
target_commitish.is_some_and(|target| target.starts_with(local_sha))
}

fn stable_is_up_to_date(current: &str, release_tag: &str) -> bool {
let latest = release_tag.trim_start_matches('v');
latest == current
Expand Down Expand Up @@ -432,6 +470,28 @@ mod tests {
assert!(msg.contains("latest=v0.2.0"));
}

#[test]
fn canary_up_to_date_matches_target_commitish() {
assert!(canary_is_up_to_date(
"0.1.0-canary.abc123def456",
Some("abc123def456789012345678901234567890aaaa"),
));
assert!(!canary_is_up_to_date(
"0.1.0-canary.abc123def456",
Some("ffffffffffffffffffffffffffffffffffffffff"),
));
}

#[test]
fn canary_up_to_date_false_for_dev_or_stable_builds() {
assert!(!canary_is_up_to_date("0.1.0-canary.dev", Some("abc")));
assert!(!canary_is_up_to_date(
"0.1.0",
Some("abc123def456789012345678901234567890aaaa"),
));
assert!(!canary_is_up_to_date("0.1.0-canary.abc123def456", None));
}

#[test]
fn canary_check_message_contains_guidance() {
let msg = canary_check_message("canary-deadbeef");
Expand Down
40 changes: 31 additions & 9 deletions src/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,27 @@ pub async fn run(base: BaseArgs, args: SwitchArgs) -> Result<()> {
}
};

let path = if args.local {
config::local_path().ok_or_else(|| {
anyhow::anyhow!(
"No local .bt directory found. Use bt init to initialize this directory."
)
})?
let (path, scope) = if args.local {
(
config::local_path().ok_or_else(|| {
anyhow::anyhow!(
"No local .bt directory found. Use bt init to initialize this directory."
)
})?,
"local",
)
} else if args.global {
config::global_path()?
(config::global_path()?, "global")
} else if interactive && config::local_path().is_some() {
select_scope()?
let chosen = select_scope()?;
let scope = if chosen == config::global_path()? {
"global"
} else {
"local"
};
(chosen, scope)
} else {
config::global_path()?
(config::global_path()?, "global")
};

let mut cfg = config::load_file(&path);
Expand All @@ -151,6 +160,19 @@ pub async fn run(base: BaseArgs, args: SwitchArgs) -> Result<()> {
config::save_file(&path, &cfg)
.context(format!("Could not save config to {}", path.display()))?;

if base.json {
let payload = serde_json::json!({
"org": org_name,
"project": project.name,
"project_id": project.id,
"profile": config_profile,
"scope": scope,
"path": path.display().to_string(),
});
println!("{payload}");
return Ok(());
}

let display = format!("{org_name}/{}", project.name);
print_command_status(CommandStatus::Success, &format!("Switched to {display}"));
if base.verbose {
Expand Down
Loading