Skip to main content

stacksdapp_shell/
lib.rs

1//! Shared shell utilities for the stacksdapp CLI and libraries.
2//! Init once from `main` via [`init`]. Commands and crates then use [`status`],
3//! [`warn`], [`error`], [`debug`], and [`emit_json`].
4
5pub mod mnemonic;
6pub mod project;
7pub mod steps;
8
9pub use mnemonic::{
10    check_deployer_mnemonic, inspect_settings_file, is_mnemonic_placeholder,
11    is_public_devnet_mnemonic, parse_deployer_mnemonic, settings_relative_path,
12    validate_mnemonic_word_format, MnemonicCheck, ParsedDeployerMnemonic, PUBLIC_DEVNET_MNEMONICS,
13};
14pub use project::{
15    default_config_toml, enter_scaffold_root, find_init_root, find_scaffold_root, load_config,
16    project_root, resolve_scaffold_root, validate_network, StacksdappConfig, CONFIG_FILE,
17};
18pub use steps::{
19    begin_step, grey, kv, lavender, mint, print_banner, println_human_safe, rule, step_ok, LiveStep,
20};
21
22use colored::control;
23use serde::Serialize;
24use std::io::{self, IsTerminal, Write};
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::OnceLock;
27
28static SHELL: OnceLock<Shell> = OnceLock::new();
29static JSON_EMITTED: AtomicBool = AtomicBool::new(false);
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ColorMode {
33    Auto,
34    Always,
35    Never,
36}
37
38impl ColorMode {
39    pub fn parse(s: &str) -> Option<Self> {
40        match s.to_ascii_lowercase().as_str() {
41            "auto" => Some(Self::Auto),
42            "always" => Some(Self::Always),
43            "never" => Some(Self::Never),
44            _ => None,
45        }
46    }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Format {
51    Human,
52    Json,
53}
54
55#[derive(Debug, Clone)]
56pub struct Shell {
57    pub verbosity: u8,
58    pub quiet: bool,
59    pub format: Format,
60    pub color: ColorMode,
61}
62
63impl Default for Shell {
64    fn default() -> Self {
65        Self {
66            verbosity: 0,
67            quiet: false,
68            format: Format::Human,
69            color: ColorMode::Auto,
70        }
71    }
72}
73
74/// Initialize global shell settings. Safe to call once; later calls are ignored.
75pub fn init(shell: Shell) {
76    apply_color(shell.color);
77    let _ = SHELL.set(shell);
78}
79
80fn apply_color(mode: ColorMode) {
81    match mode {
82        ColorMode::Always => control::set_override(true),
83        ColorMode::Never => control::set_override(false),
84        ColorMode::Auto => {
85            // Respect TTY: colored crate already defaults to auto when unset.
86            if !io::stdout().is_terminal() && !io::stderr().is_terminal() {
87                control::set_override(false);
88            } else {
89                control::unset_override();
90            }
91        }
92    }
93}
94
95pub fn get() -> &'static Shell {
96    SHELL.get_or_init(Shell::default)
97}
98
99pub fn verbosity() -> u8 {
100    get().verbosity
101}
102
103pub fn is_quiet() -> bool {
104    get().quiet || get().format == Format::Json
105}
106
107pub fn is_json() -> bool {
108    get().format == Format::Json
109}
110
111/// Human status line on stdout (suppressed when quiet/json).
112pub fn status(msg: impl AsRef<str>) {
113    if is_quiet() {
114        return;
115    }
116    println!("{}", msg.as_ref());
117}
118
119/// Human warning on stderr (suppressed when quiet/json).
120pub fn warn(msg: impl AsRef<str>) {
121    if is_quiet() {
122        return;
123    }
124    eprintln!("{}", msg.as_ref());
125}
126
127/// Errors always print unless JSON mode (then prefer [`emit_json`] / [`emit_error_json`]).
128pub fn error(msg: impl AsRef<str>) {
129    if is_json() {
130        return;
131    }
132    eprintln!("{}", msg.as_ref());
133}
134
135/// Verbose detail when `-v` / `-vv` … is set (and not quiet/json).
136pub fn debug(level: u8, msg: impl AsRef<str>) {
137    if is_quiet() || verbosity() < level {
138        return;
139    }
140    eprintln!("{}", msg.as_ref());
141}
142
143/// Emit a JSON value on stdout when `--json` is active.
144pub fn emit_json<T: Serialize>(value: &T) {
145    if !is_json() {
146        return;
147    }
148    match serde_json::to_string(value) {
149        Ok(s) => {
150            println!("{s}");
151            let _ = io::stdout().flush();
152            JSON_EMITTED.store(true, Ordering::SeqCst);
153        }
154        Err(e) => eprintln!("{{\"ok\":false,\"error\":\"json serialize failed: {e}\"}}"),
155    }
156}
157
158/// True if a command already wrote a JSON payload this process.
159pub fn json_already_emitted() -> bool {
160    JSON_EMITTED.load(Ordering::SeqCst)
161}
162
163/// Emit a JSON error object (for command failures under `--json`).
164pub fn emit_error_json(command: &str, message: &str) {
165    if !is_json() || json_already_emitted() {
166        return;
167    }
168    let payload = serde_json::json!({
169        "ok": false,
170        "command": command,
171        "error": message,
172    });
173    emit_json(&payload);
174}
175
176/// Pretty-print JSON when not in machine mode helpers need human fallback.
177pub fn println_human(msg: impl AsRef<str>) {
178    status(msg);
179}