tana_stdio/
lib.rs

1//! # tana-stdio
2//!
3//! Terminal output utilities for Tana projects.
4//! Consistent formatting across CLI, services, and tools.
5//!
6//! ## Format
7//!
8//! ```text
9//! [action] message
10//! ```
11//!
12//! ## Usage
13//!
14//! ```rust
15//! use tana_stdio::{log, error, warn, success, fail};
16//!
17//! log("build", "compiling contract...");
18//! success("build complete");
19//! error("build", "compilation failed");
20//! ```
21//!
22//! ## Log Levels
23//!
24//! Control output with `LOG_LEVEL` environment variable:
25//! - `error` - Errors only
26//! - `info` - Default (startup + important messages)
27//! - `debug` - Verbose output
28
29use std::env;
30use std::sync::OnceLock;
31
32/// Log level for tana services
33#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
34pub enum LogLevel {
35    Error = 0,
36    Info = 1,
37    Debug = 2,
38}
39
40impl LogLevel {
41    fn from_str(s: &str) -> Self {
42        match s.to_lowercase().as_str() {
43            "error" => LogLevel::Error,
44            "debug" => LogLevel::Debug,
45            _ => LogLevel::Info,
46        }
47    }
48}
49
50static LOG_LEVEL: OnceLock<LogLevel> = OnceLock::new();
51
52/// Get the current log level (cached from LOG_LEVEL env var)
53pub fn log_level() -> LogLevel {
54    *LOG_LEVEL.get_or_init(|| {
55        env::var("LOG_LEVEL")
56            .map(|s| LogLevel::from_str(&s))
57            .unwrap_or(LogLevel::Info)
58    })
59}
60
61/// Check if debug logging is enabled
62pub fn is_debug() -> bool {
63    log_level() >= LogLevel::Debug
64}
65
66/// Check if info logging is enabled
67pub fn is_info() -> bool {
68    log_level() >= LogLevel::Info
69}
70
71// ============================================================
72// Core logging functions (match @tananetwork/stdio API)
73// ============================================================
74
75/// Log an action with a message
76/// Format: `[action] message`
77///
78/// # Example
79/// ```
80/// tana_stdio::log("build", "compiling contract...");
81/// // Output: [build] compiling contract...
82/// ```
83pub fn log(action: &str, message: &str) {
84    if log_level() >= LogLevel::Info {
85        eprintln!("[{}] {}", action, message);
86    }
87}
88
89/// Log an error
90/// Format: `[action] message`
91///
92/// # Example
93/// ```
94/// tana_stdio::error("build", "compilation failed");
95/// // Output: [build] compilation failed
96/// ```
97pub fn error(action: &str, message: &str) {
98    eprintln!("[{}] {}", action, message);
99}
100
101/// Log a warning
102/// Format: `[warn] message` or `[name] message`
103///
104/// # Example
105/// ```
106/// tana_stdio::warn("cache", "stale entries detected");
107/// // Output: [warn] [cache] stale entries detected
108/// ```
109pub fn warn(name: &str, message: &str) {
110    eprintln!("[warn] [{}] {}", name, message);
111}
112
113/// Log a simple warning without component name
114/// Format: `[warn] message`
115pub fn warn_simple(message: &str) {
116    eprintln!("[warn] {}", message);
117}
118
119/// Log a status line with success/failure indicator
120/// Format: `[ok] message` or `[fail] message`
121///
122/// # Example
123/// ```
124/// tana_stdio::status("database", "connected", true);
125/// // Output: [ok] [database] connected
126/// ```
127pub fn status(name: &str, message: &str, ok: bool) {
128    if ok {
129        eprintln!("[ok] [{}] {}", name, message);
130    } else {
131        eprintln!("[fail] [{}] {}", name, message);
132    }
133}
134
135/// Print a section header
136///
137/// # Example
138/// ```
139/// tana_stdio::header("configuration");
140/// // Output:
141/// //
142/// // configuration
143/// // ----------------------------------------
144/// ```
145pub fn header(title: &str) {
146    eprintln!();
147    eprintln!("{}", title);
148    eprintln!("{}", "-".repeat(40));
149}
150
151/// Print a blank line
152pub fn blank() {
153    eprintln!();
154}
155
156/// Success message
157/// Format: `[ok] message`
158///
159/// # Example
160/// ```
161/// tana_stdio::success("build complete");
162/// // Output: [ok] build complete
163/// ```
164pub fn success(message: &str) {
165    eprintln!("[ok] {}", message);
166}
167
168/// Failure message
169/// Format: `[fail] message`
170///
171/// # Example
172/// ```
173/// tana_stdio::fail("build failed");
174/// // Output: [fail] build failed
175/// ```
176pub fn fail(message: &str) {
177    eprintln!("[fail] {}", message);
178}
179
180/// Info line with label
181/// Format: `  label     value`
182///
183/// # Example
184/// ```
185/// tana_stdio::info("port", "8506");
186/// // Output:   port       8506
187/// ```
188pub fn info(label: &str, value: &str) {
189    eprintln!("  {:<10} {}", label, value);
190}
191
192/// Hint in subdued format
193/// Format: `  message`
194pub fn hint(message: &str) {
195    eprintln!("  {}", message);
196}
197
198/// Detail line with arrow
199/// Format: `    -> message`
200pub fn detail(message: &str) {
201    eprintln!("    -> {}", message);
202}
203
204/// Suggest a next step
205/// Format: `  -> description: command`
206///
207/// # Example
208/// ```
209/// tana_stdio::next_step("start the server", "npm run dev");
210/// // Output:   -> start the server: npm run dev
211/// ```
212pub fn next_step(description: &str, command: &str) {
213    eprintln!("  -> {}: {}", description, command);
214}
215
216/// Diagnostic warning
217/// Format: `[warn] [component] message`
218pub fn diagnostic(component: &str, message: &str) {
219    eprintln!("[warn] [{}] {}", component, message);
220}
221
222// ============================================================
223// Debug-level logging (only shown when LOG_LEVEL=debug)
224// ============================================================
225
226/// Debug log (only shown when LOG_LEVEL=debug)
227///
228/// # Example
229/// ```
230/// tana_stdio::debug("cache", "hit for key: user_123");
231/// // Output (only if LOG_LEVEL=debug): [cache] hit for key: user_123
232/// ```
233pub fn debug(action: &str, message: &str) {
234    if log_level() >= LogLevel::Debug {
235        eprintln!("[{}] {}", action, message);
236    }
237}
238
239// ============================================================
240// Macros for convenient formatting
241// ============================================================
242
243/// Log with format string support
244///
245/// # Example
246/// ```
247/// tana_stdio::logf!("build", "compiled {} files in {}ms", 42, 150);
248/// ```
249#[macro_export]
250macro_rules! logf {
251    ($action:expr, $($arg:tt)*) => {
252        if $crate::log_level() >= $crate::LogLevel::Info {
253            eprintln!(concat!("[", $action, "] {}"), format!($($arg)*));
254        }
255    };
256}
257
258/// Error with format string support
259#[macro_export]
260macro_rules! errorf {
261    ($action:expr, $($arg:tt)*) => {
262        eprintln!(concat!("[", $action, "] {}"), format!($($arg)*));
263    };
264}
265
266/// Debug with format string support (only shown when LOG_LEVEL=debug)
267#[macro_export]
268macro_rules! debugf {
269    ($action:expr, $($arg:tt)*) => {
270        if $crate::log_level() >= $crate::LogLevel::Debug {
271            eprintln!(concat!("[", $action, "] {}"), format!($($arg)*));
272        }
273    };
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn test_log_level_parsing() {
282        assert_eq!(LogLevel::from_str("error"), LogLevel::Error);
283        assert_eq!(LogLevel::from_str("info"), LogLevel::Info);
284        assert_eq!(LogLevel::from_str("debug"), LogLevel::Debug);
285        assert_eq!(LogLevel::from_str("INFO"), LogLevel::Info);
286        assert_eq!(LogLevel::from_str("unknown"), LogLevel::Info);
287    }
288
289    #[test]
290    fn test_log_level_ordering() {
291        assert!(LogLevel::Error < LogLevel::Info);
292        assert!(LogLevel::Info < LogLevel::Debug);
293    }
294}