Skip to main content

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}