use base64::Engine;
use clap::Args;
use serde::Serialize;
use serde_json::{json, Map, Value};
pub type CmdResult<T> = homeboy::Result<(T, i32)>;
#[derive(Serialize)]
pub struct ProjectsSummary {
pub total_projects: u32,
pub succeeded: u32,
pub failed: u32,
}
pub fn parse_key_val(s: &str) -> Result<(String, String), String> {
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
Ok((s[..pos].to_string(), s[pos + 1..].to_string()))
}
pub(crate) struct GlobalArgs {}
#[derive(Args, Default, Debug)]
pub struct DynamicSetArgs {
pub id: Option<String>,
pub spec: Option<String>,
#[arg(long, value_name = "JSON")]
pub json: Option<String>,
#[arg(long, value_name = "BASE64")]
pub base64: Option<String>,
#[arg(long, value_name = "FIELD")]
pub replace: Vec<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub extra: Vec<String>,
}
impl DynamicSetArgs {
pub fn json_spec(&self) -> Result<Option<String>, homeboy::Error> {
if let Some(b64) = &self.base64 {
let decoded_bytes = base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(|e| {
homeboy::Error::validation_invalid_argument(
"base64",
format!("Invalid base64 encoding: {}", e),
None,
Some(vec!["Encode with: echo '{...}' | base64".to_string()]),
)
})?;
let decoded_str = String::from_utf8(decoded_bytes).map_err(|e| {
homeboy::Error::validation_invalid_argument(
"base64",
format!("Decoded base64 is not valid UTF-8: {}", e),
None,
None,
)
})?;
return Ok(Some(decoded_str));
}
if let Some(ref s) = self.spec {
if s.starts_with("--") {
return Ok(self.json.clone());
}
}
Ok(self.json.clone().or_else(|| self.spec.clone()))
}
pub fn effective_extra(&self) -> Vec<String> {
match &self.spec {
Some(s) if s.starts_with("--") => {
let mut combined = vec![s.clone()];
combined.extend(self.extra.iter().cloned());
combined
}
_ => self.extra.clone(),
}
}
}
fn parse_kv_flags(extra: &[String]) -> homeboy::Result<Value> {
let mut obj = Map::new();
let mut iter = extra.iter().peekable();
while let Some(arg) = iter.next() {
if let Some(key) = arg.strip_prefix("--") {
let value = iter.next().ok_or_else(|| {
homeboy::Error::validation_invalid_argument(
key,
format!("Missing value for flag --{}", key),
None,
None,
)
})?;
let parsed = parse_value(value);
obj.insert(key.to_string(), parsed);
}
}
Ok(Value::Object(obj))
}
fn parse_value(s: &str) -> Value {
if let Ok(v) = serde_json::from_str(s) {
return v;
}
if s == "true" {
return json!(true);
}
if s == "false" {
return json!(false);
}
if let Ok(n) = s.parse::<i64>() {
return json!(n);
}
if let Ok(n) = s.parse::<f64>() {
return json!(n);
}
json!(s)
}
pub fn merge_json_sources(spec: Option<&str>, extra: &[String]) -> homeboy::Result<Value> {
let mut base = if let Some(spec) = spec {
let raw = homeboy::config::read_json_spec_to_string(spec)?;
serde_json::from_str(&raw).map_err(|e| {
let hint = if raw.contains('\\') {
Some(
"For patterns with backslashes, use --base64 to bypass shell escaping:\n \
echo '{...}' | base64\n \
homeboy <command> set ID --base64 \"<encoded>\""
.to_string(),
)
} else {
None
};
homeboy::Error::validation_invalid_json(
e,
Some("parse JSON spec".to_string()),
Some(format!(
"{}{}",
raw.chars().take(200).collect::<String>(),
hint.map(|h| format!("\n\nTip: {}", h)).unwrap_or_default()
)),
)
})?
} else {
Value::Object(Map::new())
};
if !extra.is_empty() {
let flags = parse_kv_flags(extra)?;
if let (Value::Object(base_obj), Value::Object(flags_obj)) = (&mut base, flags) {
for (k, v) in flags_obj {
base_obj.insert(k, v);
}
}
}
Ok(base)
}
pub fn merge_dynamic_args(args: &DynamicSetArgs) -> homeboy::Result<Option<Value>> {
let spec = args.json_spec()?;
let extra = args.effective_extra();
if spec.is_none() && extra.is_empty() {
return Ok(None);
}
Ok(Some(merge_json_sources(spec.as_deref(), &extra)?))
}
pub fn finalize_set_spec(
merged: &Value,
explicit_replace: &[String],
) -> homeboy::Result<(String, Vec<String>)> {
let json_string = homeboy::config::to_json_string(merged)?;
let mut replace_fields = explicit_replace.to_vec();
for field in homeboy::config::collect_array_fields(merged) {
if !replace_fields.contains(&field) {
replace_fields.push(field);
}
}
Ok((json_string, replace_fields))
}
pub mod api;
pub mod audit;
pub mod auth;
pub mod build;
pub mod changelog;
pub mod changes;
pub mod cleanup;
pub mod cli;
pub mod component;
pub mod config;
pub mod db;
pub mod deploy;
pub mod docs;
pub mod file;
pub mod fleet;
pub mod git;
pub mod init;
pub mod lint;
pub mod logs;
pub mod module;
pub mod project;
pub mod release;
pub mod server;
pub mod ssh;
pub mod status;
pub mod test;
pub mod transfer;
pub mod upgrade;
pub mod version;
pub(crate) fn run_markdown(
command: crate::Commands,
_global: &GlobalArgs,
) -> homeboy::Result<(String, i32)> {
match command {
crate::Commands::Docs(args) => docs::run_markdown(args),
crate::Commands::Changelog(args) => changelog::run_markdown(args),
_ => Err(homeboy::Error::validation_invalid_argument(
"output_mode",
"Command does not support markdown output",
None,
None,
)),
}
}
macro_rules! dispatch {
($args:expr, $module:ident) => {
crate::output::map_cmd_result_to_json($module::run_json($args))
};
($args:expr, $global:expr, $module:ident) => {
crate::output::map_cmd_result_to_json($module::run($args, $global))
};
}
pub(crate) fn run_json(
command: crate::Commands,
global: &GlobalArgs,
) -> (homeboy::Result<serde_json::Value>, i32) {
crate::tty::status("homeboy is working...");
match command {
crate::Commands::Init(args) => dispatch!(args, init),
crate::Commands::Status(args) => dispatch!(args, status),
crate::Commands::Test(args) => dispatch!(args, test),
crate::Commands::Lint(args) => dispatch!(args, lint),
crate::Commands::Cleanup(args) => dispatch!(args, cleanup),
crate::Commands::Project(args) => dispatch!(args, global, project),
crate::Commands::Ssh(args) => dispatch!(args, global, ssh),
crate::Commands::Server(args) => dispatch!(args, global, server),
crate::Commands::Db(args) => dispatch!(args, global, db),
crate::Commands::File(args) => dispatch!(args, global, file),
crate::Commands::Fleet(args) => dispatch!(args, global, fleet),
crate::Commands::Logs(args) => dispatch!(args, global, logs),
crate::Commands::Transfer(args) => dispatch!(args, global, transfer),
crate::Commands::Deploy(args) => dispatch!(args, global, deploy),
crate::Commands::Component(args) => dispatch!(args, global, component),
crate::Commands::Config(args) => dispatch!(args, global, config),
crate::Commands::Module(args) => dispatch!(args, global, module),
crate::Commands::Docs(args) => dispatch!(args, global, docs),
crate::Commands::Changelog(args) => dispatch!(args, global, changelog),
crate::Commands::Git(args) => dispatch!(args, global, git),
crate::Commands::Version(args) => dispatch!(args, global, version),
crate::Commands::Build(args) => dispatch!(args, global, build),
crate::Commands::Changes(args) => dispatch!(args, global, changes),
crate::Commands::Release(args) => dispatch!(args, global, release),
crate::Commands::Audit(args) => dispatch!(args, global, audit),
crate::Commands::Auth(args) => dispatch!(args, global, auth),
crate::Commands::Api(args) => dispatch!(args, global, api),
crate::Commands::Upgrade(args) | crate::Commands::Update(args) => {
dispatch!(args, global, upgrade)
}
crate::Commands::List => {
let err = homeboy::Error::validation_invalid_argument(
"output_mode",
"List command uses raw output mode",
None,
None,
);
crate::output::map_cmd_result_to_json::<serde_json::Value>(Err(err))
}
}
}