use std::io::{IsTerminal, Write};
use serde_json::{json, Value};
pub mod exit {
pub const OK: i32 = 0;
pub const VALIDATION: i32 = 2;
pub const NOT_FOUND: i32 = 4;
pub const CONFLICT: i32 = 5;
pub const DATA: i32 = 6;
}
#[derive(Debug)]
pub struct CliError {
pub code: i32,
pub message: String,
pub hint: Option<String>,
pub retryable: bool,
}
impl CliError {
pub fn validation(message: String, hint: Option<String>) -> Self {
Self { code: exit::VALIDATION, message, hint, retryable: false }
}
pub fn not_found(message: String, hint: Option<String>) -> Self {
Self { code: exit::NOT_FOUND, message, hint, retryable: false }
}
pub fn conflict(message: String, hint: Option<String>) -> Self {
Self { code: exit::CONFLICT, message, hint, retryable: false }
}
pub fn data(message: String, hint: Option<String>) -> Self {
Self { code: exit::DATA, message, hint, retryable: false }
}
}
impl std::fmt::Display for CliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for CliError {}
#[derive(Clone, Copy)]
pub struct Ctx {
pub json: bool,
pub quiet: bool,
pub full: bool,
}
impl Ctx {
pub fn new(json_flag: bool, quiet: bool, full: bool, _no_interactive: bool) -> Self {
let stdout_tty = std::io::stdout().is_terminal();
Self {
json: json_flag || !stdout_tty,
quiet: quiet || !stdout_tty,
full,
}
}
}
pub fn emit(ctx: Ctx, value: &Value) {
if ctx.json {
let mut out = std::io::stdout().lock();
let _ = serde_json::to_writer_pretty(&mut out, value);
let _ = out.write_all(b"\n");
return;
}
print_pretty(value, ctx.full);
}
fn print_pretty(value: &Value, full: bool) {
match value {
Value::Array(arr) => {
if arr.is_empty() {
println!("(empty)");
return;
}
for item in arr {
print_brief(item, false);
}
}
Value::Object(map) => {
let has_record_key = map.contains_key("name") || map.contains_key("slug");
if has_record_key {
print_brief(value, full);
} else {
for (k, v) in map {
if let Some(s) = v.as_str() {
println!(" {k}: {s}");
} else {
println!(" {k}: {v}");
}
}
}
}
Value::String(s) => println!("{s}"),
Value::Null => println!("(null)"),
other => println!("{other}"),
}
}
pub fn print_brief(item: &Value, full: bool) {
let obj = match item.as_object() {
Some(o) => o,
None => {
println!(" {item}");
return;
}
};
let s = |k: &str| obj.get(k).and_then(|v| v.as_str()).unwrap_or("");
let has = |k: &str| obj.get(k).map(|v| !v.is_null()).unwrap_or(false);
let is_repo = has("slug") && (has("url") || has("visibility"));
let is_customer = has("slug")
&& (obj.contains_key("related_products") || obj.contains_key("chats"));
if is_repo {
println!(
" {:<30} [{:<10}] {}",
s("slug"),
s("visibility"),
s("description")
);
if full {
if has("url") {
println!(" url: {}", s("url"));
}
}
} else if is_customer {
let mut line = format!(" {:<14} {}", s("slug"), s("name"));
if has("name_en") {
line.push_str(&format!(" ({})", s("name_en")));
}
println!("{line}");
if full {
if let Some(arr) = obj.get("related_products").and_then(|v| v.as_array()) {
if !arr.is_empty() {
println!(" products: {}", join_str(arr));
}
}
if let Some(arr) = obj.get("chats").and_then(|v| v.as_array()) {
if !arr.is_empty() {
println!(" chats: {}", join_str(arr));
}
}
if has("notes") {
println!(" notes: {}", s("notes"));
}
if let Some(chat_ids) = obj.get("chat_ids").and_then(|v| v.as_object()) {
for (name, id) in chat_ids {
println!(" chat: {name} -> {}", id.as_str().unwrap_or(""));
}
}
if let Some(prods) = obj.get("products_detail").and_then(|v| v.as_array()) {
for p in prods {
print_brief(p, false);
}
}
}
} else if has("slug") {
println!(" {:<24} {}", s("slug"), s("name"));
if full && has("description") {
println!(" {}", s("description"));
}
} else if has("name") {
let mut line = format!(" {:<10}", s("name"));
if has("department") {
line.push_str(&format!(" ({})", s("department")));
}
if has("github") {
line.push_str(&format!(" gh:{}", s("github")));
}
println!("{line}");
if full {
for key in ["user_id", "open_id", "email"] {
if has(key) {
println!(" {key:<8} {}", s(key));
}
}
if let Some(arr) = obj.get("git_aliases").and_then(|v| v.as_array()) {
if !arr.is_empty() {
println!(" aliases: {}", join_str(arr));
}
}
if has("role") {
println!(" role: {}", s("role"));
}
}
} else {
println!(" {item}");
}
}
fn join_str(arr: &[Value]) -> String {
arr.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join(", ")
}
pub fn emit_no_op(ctx: Ctx, message: &str) {
if ctx.json {
emit(ctx, &json!({ "ok": true, "no_op": true, "message": message }));
} else if !ctx.quiet {
println!("{message}");
}
}
pub fn emit_ok(ctx: Ctx, payload: Value) {
if ctx.json {
emit(ctx, &payload);
} else if !ctx.quiet {
if let Some(msg) = payload.get("message").and_then(|v| v.as_str()) {
println!("{msg}");
} else {
print_pretty(&payload, ctx.full);
}
}
}
pub fn report(err: &CliError) {
let stderr_tty = std::io::stderr().is_terminal();
let want_json = !stderr_tty || std::env::var("LIBER_JSON_ERRORS").is_ok();
let mut err_out = std::io::stderr().lock();
if want_json {
let payload = json!({
"error": {
"code": err.code,
"message": err.message,
"hint": err.hint,
"retryable": err.retryable,
}
});
let _ = serde_json::to_writer(&mut err_out, &payload);
let _ = err_out.write_all(b"\n");
} else {
let _ = writeln!(err_out, "error: {}", err.message);
if let Some(h) = &err.hint {
let _ = writeln!(err_out, "hint: {h}");
}
}
}