ai_memory/cli/io_writer.rs
1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4//! # Public API
5//!
6//! `CliOutput` is the parameterized output abstraction every `cmd_*`
7//! handler writes to. It owns mutable references to `dyn Write` for
8//! both stdout and stderr so production code can pass `io::stdout()` /
9//! `io::stderr()` locks while unit tests pass `Vec<u8>` capture buffers.
10//!
11//! The struct is the **stable contract** between W5a (this module) and
12//! the downstream cmd_* migrations in W5b/c/d. Do not change the field
13//! visibility or method signatures without coordinating across closers.
14//!
15//! ## Stable surface
16//!
17//! ```ignore
18//! pub struct CliOutput<'a> {
19//! pub stdout: &'a mut dyn Write,
20//! pub stderr: &'a mut dyn Write,
21//! }
22//!
23//! impl<'a> CliOutput<'a> {
24//! pub fn from_std(stdout: &'a mut dyn Write, stderr: &'a mut dyn Write) -> Self;
25//! }
26//! ```
27//!
28//! ## Usage in handlers
29//!
30//! Every `cmd_*` replaces `println!(...)` with `writeln!(out.stdout, ...)?`
31//! and `eprintln!(...)` with `writeln!(out.stderr, ...)?`. The `?`
32//! propagates I/O errors instead of panicking on broken-pipe (closing
33//! a long-running pager mid-output, etc.).
34
35use std::io::Write;
36
37/// Output abstraction passed to every CLI command. Carries mutable
38/// references to stdout and stderr writers so handlers can be unit-tested
39/// by capturing into `Vec<u8>` buffers.
40pub struct CliOutput<'a> {
41 pub stdout: &'a mut dyn Write,
42 pub stderr: &'a mut dyn Write,
43}
44
45impl<'a> CliOutput<'a> {
46 /// Construct from explicit stdout/stderr writer references. Both must
47 /// outlive the resulting `CliOutput` borrow.
48 pub fn from_std(stdout: &'a mut dyn Write, stderr: &'a mut dyn Write) -> Self {
49 Self { stdout, stderr }
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 #[test]
57 fn test_capture_roundtrip() {
58 let mut stdout = Vec::<u8>::new();
59 let mut stderr = Vec::<u8>::new();
60 let out = CliOutput {
61 stdout: &mut stdout,
62 stderr: &mut stderr,
63 };
64 writeln!(out.stdout, "hello").unwrap();
65 writeln!(out.stderr, "warn").unwrap();
66 assert_eq!(String::from_utf8(stdout).unwrap(), "hello\n");
67 assert_eq!(String::from_utf8(stderr).unwrap(), "warn\n");
68 }
69
70 #[test]
71 fn test_from_std_constructor() {
72 let mut stdout = Vec::<u8>::new();
73 let mut stderr = Vec::<u8>::new();
74 {
75 let out = CliOutput::from_std(&mut stdout, &mut stderr);
76 writeln!(out.stdout, "ok").unwrap();
77 writeln!(out.stderr, "err").unwrap();
78 }
79 assert_eq!(String::from_utf8(stdout).unwrap(), "ok\n");
80 assert_eq!(String::from_utf8(stderr).unwrap(), "err\n");
81 }
82
83 #[test]
84 fn test_independent_streams() {
85 let mut stdout = Vec::<u8>::new();
86 let mut stderr = Vec::<u8>::new();
87 {
88 let out = CliOutput::from_std(&mut stdout, &mut stderr);
89 writeln!(out.stdout, "one").unwrap();
90 writeln!(out.stdout, "two").unwrap();
91 writeln!(out.stderr, "warn-1").unwrap();
92 }
93 assert_eq!(String::from_utf8(stdout).unwrap(), "one\ntwo\n");
94 assert_eq!(String::from_utf8(stderr).unwrap(), "warn-1\n");
95 }
96}