use crate::auth::Auth;
use crate::local_store::LocalStore;
use asterai_runtime::resource::ResourceId;
use eyre::{Context, OptionExt, bail};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Debug)]
pub struct PushArgs {
env_name: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct PutEnvironmentRequest {
components: Vec<String>,
vars: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PutEnvironmentResponse {
namespace: String,
name: String,
version: String,
previous_version: Option<String>,
change_type: Option<String>,
change_reason: String,
}
impl PushArgs {
pub fn parse(args: impl Iterator<Item = String>) -> eyre::Result<Self> {
let mut env_name: Option<String> = None;
for arg in args {
match arg.as_str() {
"--help" | "-h" | "help" => {
print_help();
std::process::exit(0);
}
other => {
if other.starts_with('-') {
bail!("unknown flag: {}", other);
}
if env_name.is_some() {
bail!("unexpected argument: {}", other);
}
env_name = Some(other.to_string());
}
}
}
let env_name = env_name.ok_or_eyre(
"missing environment name\n\nUsage: asterai env push <name>\n\
Example: asterai env push my-env",
)?;
Ok(Self { env_name })
}
pub async fn execute(&self, api_endpoint: &str) -> eyre::Result<()> {
let api_key = Auth::read_stored_api_key()
.ok_or_eyre("API key not found. Run 'asterai auth login' to authenticate.")?;
let target_id = ResourceId::from_str(&self.env_name)
.or_else(|_| {
let with_namespace = format!(
"{}:{}",
Auth::read_user_or_fallback_namespace(),
self.env_name
);
ResourceId::from_str(&with_namespace)
})
.wrap_err("invalid environment name")?;
let environment = LocalStore::fetch_environment(&target_id).or_else(|_| {
let local_id =
ResourceId::new_from_parts("local".to_string(), target_id.name().to_string())?;
LocalStore::fetch_environment(&local_id)
});
let environment = environment
.wrap_err_with(|| format!("environment '{}' not found locally", self.env_name))?;
let namespace = target_id.namespace();
let name = target_id.name();
println!("pushing environment {}:{}...", namespace, name);
let components: Vec<String> = environment.component_refs();
let request = PutEnvironmentRequest {
components,
vars: environment.vars.clone(),
};
let base_url = api_endpoint;
let client = reqwest::Client::new();
let response = client
.put(format!(
"{}/v1/environment/{}/{}",
base_url, namespace, name
))
.header("Authorization", api_key.trim())
.json(&request)
.send()
.await
.wrap_err("failed to send push request")?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "unknown error".to_string());
let message = format_error_message(status, &error_text, namespace);
bail!("{message}");
}
let result: PutEnvironmentResponse =
response.json().await.wrap_err("failed to parse response")?;
if let Some(prev) = &result.previous_version {
println!(
"updated {}:{}@{} (was {})",
result.namespace, result.name, result.version, prev
);
if let Some(change_type) = &result.change_type {
println!(" change: {} ({})", change_type, result.change_reason);
}
} else {
println!(
"created {}:{}@{}",
result.namespace, result.name, result.version
);
}
Ok(())
}
}
fn format_error_message(status: StatusCode, error_text: &str, namespace: &str) -> String {
let text = error_text
.trim()
.strip_prefix("bad request: ")
.or_else(|| error_text.strip_prefix("Bad Request: "))
.unwrap_or(error_text);
if status == StatusCode::FORBIDDEN {
return format!(
"you don't have permission to push to namespace '{}'",
namespace
);
}
if status == StatusCode::CONFLICT {
return text.to_string();
}
if text.contains("not found in registry")
&& let Some(component) = extract_component_name(text)
{
return format!(
"component '{}' not found in registry\n\
hint: push the component first with: asterai component push {}",
component, component
);
}
text.to_string()
}
fn extract_component_name(text: &str) -> Option<&str> {
let start = text.find('\'')?;
let end = text[start + 1..].find('\'')?;
Some(&text[start + 1..start + 1 + end])
}
fn print_help() {
println!(
r#"Push a local environment to the registry.
Usage: asterai env push <name> [options]
Arguments:
<name> Local environment name (e.g., my-env or namespace:my-env)
Options:
-h, --help Show this help message
Examples:
asterai env push my-env
asterai env push myteam:production-env
asterai env push my-env --staging
"#
);
}