|
| 1 | +# Root Cause Analysis: Debug Output Breaking CLI UI |
| 2 | + |
| 3 | +## Issue Summary |
| 4 | + |
| 5 | +When running `agent auth list`, the command outputs debug logging information that breaks the CLI user interface: |
| 6 | + |
| 7 | +``` |
| 8 | +INFO 2025-12-16T12:43:13 +38ms service=models.dev file={} refreshing |
| 9 | +``` |
| 10 | + |
| 11 | +This debug output should only be shown when the `--verbose` flag is provided. |
| 12 | + |
| 13 | +## Root Cause Analysis |
| 14 | + |
| 15 | +### The Problem Chain |
| 16 | + |
| 17 | +1. **Default Logging Behavior (src/util/log.ts:55)** |
| 18 | + |
| 19 | + ```typescript |
| 20 | + let write = (msg: any) => Bun.stderr.write(msg); |
| 21 | + ``` |
| 22 | + |
| 23 | + - By default, all log messages write directly to `stderr` |
| 24 | + - This happens **before** `Log.init()` is called |
| 25 | + - The default log level is `INFO` (line 19) |
| 26 | + |
| 27 | +2. **Missing Log Initialization for CLI Commands** |
| 28 | + - **Agent mode** (src/index.js:182-185): Properly calls `Log.init()` |
| 29 | + - **Run command** (src/cli/cmd/run.ts:115): Properly calls `Log.init()` |
| 30 | + - **Auth commands**: **NEVER** calls `Log.init()` |
| 31 | + - **Other CLI commands (mcp, models, stats, export, etc.)**: Also don't call `Log.init()` |
| 32 | + |
| 33 | +3. **Early Logger Creation (src/provider/models.ts:8)** |
| 34 | + |
| 35 | + ```typescript |
| 36 | + const log = Log.create({ service: 'models.dev' }); |
| 37 | + ``` |
| 38 | + |
| 39 | + - Logger is created at module import time |
| 40 | + - Uses default `write` function pointing to `stderr` |
| 41 | + - Any calls to `log.info()` will output to stderr by default |
| 42 | + |
| 43 | +4. **Auth List Command Flow** |
| 44 | + |
| 45 | + **Call Chain:** |
| 46 | + |
| 47 | + ``` |
| 48 | + user runs: agent auth list |
| 49 | + ↓ |
| 50 | + src/index.js:519 → AuthCommand is invoked |
| 51 | + ↓ |
| 52 | + src/cli/cmd/auth.ts:26 → AuthListCommand.handler() |
| 53 | + ↓ |
| 54 | + src/cli/cmd/auth.ts:35 → ModelsDev.get() is called |
| 55 | + ↓ |
| 56 | + src/provider/models.ts:71 → refresh() is called |
| 57 | + ↓ |
| 58 | + src/provider/models.ts:81 → log.info('refreshing', { file }) |
| 59 | + ↓ |
| 60 | + PROBLEM: write() still points to Bun.stderr.write() |
| 61 | + ↓ |
| 62 | + Debug output appears in CLI: "INFO 2025-12-16T12:43:13 +38ms service=models.dev file={} refreshing" |
| 63 | + ``` |
| 64 | + |
| 65 | +5. **Why Log.init() Matters** |
| 66 | + |
| 67 | + When `Log.init({ print: false })` is called (src/util/log.ts:57-75): |
| 68 | + |
| 69 | + ```typescript |
| 70 | + export async function init(options: Options) { |
| 71 | + if (options.level) level = options.level; |
| 72 | + cleanup(Global.Path.log); |
| 73 | + if (options.print) return; // If print=true, keep using stderr |
| 74 | + |
| 75 | + // Create log file path |
| 76 | + logpath = path.join(Global.Path.log, ...); |
| 77 | + const logfile = Bun.file(logpath); |
| 78 | + const writer = logfile.writer(); |
| 79 | + |
| 80 | + // REDIRECT write function to log file instead of stderr |
| 81 | + write = async (msg: any) => { |
| 82 | + const num = writer.write(msg); |
| 83 | + writer.flush(); |
| 84 | + return num; |
| 85 | + }; |
| 86 | + } |
| 87 | + ``` |
| 88 | + |
| 89 | + **Before Log.init()**: `write` → `Bun.stderr.write()` ✗ visible in CLI |
| 90 | + **After Log.init({ print: false })**: `write` → `logfile.writer()` ✓ hidden in log file |
| 91 | + |
| 92 | +### Timeline of Events |
| 93 | + |
| 94 | +1. **User runs command**: `agent auth list` |
| 95 | +2. **Module imports happen**: |
| 96 | + - `src/provider/models.ts` is imported |
| 97 | + - Line 8: `const log = Log.create({ service: 'models.dev' })` executes |
| 98 | + - Logger is created with default `write = Bun.stderr.write` |
| 99 | +3. **yargs parses command**: Identifies `auth list` subcommand |
| 100 | +4. **Command handler executes**: `AuthListCommand.handler()` at line 26 |
| 101 | +5. **Models are fetched**: Line 35 calls `ModelsDev.get()` |
| 102 | +6. **Refresh is triggered**: Line 71 calls `refresh()` |
| 103 | +7. **Log output occurs**: Line 81 calls `log.info('refreshing', { file })` |
| 104 | +8. **Output goes to stderr**: Because `Log.init()` was never called |
| 105 | +9. **User sees debug output**: Breaking the clean CLI UI |
| 106 | + |
| 107 | +### Additional Issues |
| 108 | + |
| 109 | +**Background Refresh Timer (src/provider/models.ts:98)** |
| 110 | + |
| 111 | +```typescript |
| 112 | +setInterval(() => ModelsDev.refresh(), 60 * 1000 * 60).unref(); |
| 113 | +``` |
| 114 | + |
| 115 | +This timer runs every 60 minutes in the background and will also output to stderr if `Log.init()` hasn't been called, potentially interrupting long-running processes or piped output. |
| 116 | + |
| 117 | +## Affected Commands |
| 118 | + |
| 119 | +Based on analysis, the following CLI commands are affected (they don't initialize logging): |
| 120 | + |
| 121 | +1. **auth** commands: |
| 122 | + - `agent auth list` ✗ |
| 123 | + - `agent auth login` ✗ |
| 124 | + - `agent auth logout` ✗ |
| 125 | + - `agent auth status` ✗ |
| 126 | + |
| 127 | +2. **mcp** commands (src/cli/cmd/mcp.ts): |
| 128 | + - `agent mcp list` ✗ |
| 129 | + - `agent mcp install` ✗ |
| 130 | + - Any logging in MCP operations will leak to stderr |
| 131 | + |
| 132 | +3. **models** commands (src/cli/cmd/models.ts): |
| 133 | + - Any model-related operations ✗ |
| 134 | + |
| 135 | +4. **stats** commands (src/cli/cmd/stats.ts): |
| 136 | + - Any stats operations ✗ |
| 137 | + |
| 138 | +5. **export** commands (src/cli/cmd/export.ts): |
| 139 | + - Any export operations ✗ |
| 140 | + |
| 141 | +**Not Affected:** |
| 142 | + |
| 143 | +- `agent` (default mode) ✓ - Calls Log.init() at line 182 |
| 144 | +- `agent run` ✓ - Calls Log.init() at line 115 (when verbose) |
| 145 | + |
| 146 | +## Why This Violates Best Practices |
| 147 | + |
| 148 | +According to CLI best practices research: |
| 149 | + |
| 150 | +1. **Default Behavior**: CLIs should only show warnings and errors by default |
| 151 | +2. **Verbose Flag**: INFO/DEBUG logs should only appear with `--verbose` flag |
| 152 | +3. **Clean Output**: Regular users should enjoy a clean, focused experience |
| 153 | +4. **Stderr vs Stdout**: Debug output mixing with command output disrupts piping and scripting |
| 154 | + |
| 155 | +The current behavior violates all of these principles for CLI commands that don't initialize logging. |
| 156 | + |
| 157 | +## Solution Requirements |
| 158 | + |
| 159 | +To fix this issue, we need to: |
| 160 | + |
| 161 | +1. **Initialize logging early** for ALL CLI commands, not just agent mode |
| 162 | +2. **Respect the verbose flag** - Only print to stderr when `--verbose` is used |
| 163 | +3. **Default to file logging** - Write logs to files by default (not stderr) |
| 164 | +4. **Ensure consistency** - All commands should have the same logging behavior |
| 165 | +5. **Maintain backward compatibility** - Don't break existing verbose flag behavior |
| 166 | + |
| 167 | +## Proposed Solution |
| 168 | + |
| 169 | +Add a global middleware or initialization in `src/index.js` that: |
| 170 | + |
| 171 | +- Calls `Log.init({ print: false })` before any commands execute |
| 172 | +- Allows `--verbose` flag to enable `Log.init({ print: true, level: 'DEBUG' })` |
| 173 | +- Applies to ALL commands, not just agent mode |
| 174 | + |
| 175 | +This ensures: |
| 176 | + |
| 177 | +- By default: logs go to files in `~/.local/share/opencode/log/` |
| 178 | +- With `--verbose`: logs print to stderr for debugging |
| 179 | +- Clean CLI UI for all commands without debug pollution |
0 commit comments