debugger-cli 0.1.3

LLM-friendly debugger CLI using the Debug Adapter Protocol
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! IPC protocol message types
//!
//! Defines the request/response format for CLI ↔ daemon communication.
//! Uses a simple length-prefixed JSON protocol.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::common::error::IpcError;

/// IPC request from CLI to daemon
#[derive(Debug, Serialize, Deserialize)]
pub struct Request {
    /// Request ID for matching responses
    pub id: u64,
    /// The command to execute
    pub command: Command,
}

/// IPC response from daemon to CLI
#[derive(Debug, Serialize, Deserialize)]
pub struct Response {
    /// Request ID this response corresponds to
    pub id: u64,
    /// Whether the command succeeded
    pub success: bool,
    /// Result data on success
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<serde_json::Value>,
    /// Error information on failure
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<IpcError>,
}

impl Response {
    /// Create a success response
    pub fn success(id: u64, result: serde_json::Value) -> Self {
        Self {
            id,
            success: true,
            result: Some(result),
            error: None,
        }
    }

    /// Create an error response
    pub fn error(id: u64, error: IpcError) -> Self {
        Self {
            id,
            success: false,
            result: None,
            error: Some(error),
        }
    }

    /// Create a success response with no data
    pub fn ok(id: u64) -> Self {
        Self {
            id,
            success: true,
            result: Some(serde_json::json!({})),
            error: None,
        }
    }
}

/// Commands that can be sent from CLI to daemon
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Command {
    // === Session Management ===
    /// Start debugging a program
    Start {
        program: PathBuf,
        args: Vec<String>,
        adapter: Option<String>,
        stop_on_entry: bool,
        /// Initial breakpoints to set before program starts (file:line or function name)
        #[serde(default)]
        initial_breakpoints: Vec<String>,
    },

    /// Attach to a running process
    Attach {
        pid: u32,
        adapter: Option<String>,
    },

    /// Detach from process (keeps it running)
    Detach,

    /// Stop debugging (terminates debuggee)
    Stop,

    /// Restart program with same arguments
    Restart,

    /// Get session status
    Status,

    // === Breakpoints ===
    /// Add a breakpoint
    BreakpointAdd {
        location: BreakpointLocation,
        condition: Option<String>,
        hit_count: Option<u32>,
    },

    /// Remove a breakpoint
    BreakpointRemove {
        id: Option<u32>,
        all: bool,
    },

    /// List all breakpoints
    BreakpointList,

    /// Enable a breakpoint
    BreakpointEnable { id: u32 },

    /// Disable a breakpoint
    BreakpointDisable { id: u32 },

    // === Execution Control ===
    /// Continue execution
    Continue,

    /// Step over (next line, skip function calls)
    Next,

    /// Step into (next line, enter function calls)
    StepIn,

    /// Step out (run until function returns)
    StepOut,

    /// Pause execution
    Pause,

    // === State Inspection ===
    /// Get stack trace
    StackTrace {
        thread_id: Option<i64>,
        limit: usize,
    },

    /// Get local variables
    Locals { frame_id: Option<i64> },

    /// Evaluate expression
    Evaluate {
        expression: String,
        frame_id: Option<i64>,
        context: EvaluateContext,
    },

    /// Get scopes for a frame
    Scopes { frame_id: i64 },

    /// Get variables in a scope
    Variables { reference: i64 },

    // === Thread/Frame Management ===
    /// List all threads
    Threads,

    /// Switch to thread
    ThreadSelect { id: i64 },

    /// Select stack frame
    FrameSelect { number: usize },

    /// Move up the stack (to caller)
    FrameUp,

    /// Move down the stack (toward current frame)
    FrameDown,

    // === Context ===
    /// Get current position with source context
    Context { lines: usize },

    // === Async ===
    /// Wait for next stop event
    Await { timeout_secs: u64 },

    // === Output ===
    /// Get buffered output
    GetOutput {
        tail: Option<usize>,
        clear: bool,
    },

    /// Subscribe to output events (for --follow)
    SubscribeOutput,

    // === Shutdown ===
    /// Shutdown the daemon
    Shutdown,
}

/// Breakpoint location specification
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BreakpointLocation {
    /// File and line number
    Line { file: PathBuf, line: u32 },
    /// Function name
    Function { name: String },
}

impl BreakpointLocation {
    /// Parse a location string like "file.rs:42" or "main"
    pub fn parse(s: &str) -> Result<Self, crate::common::Error> {
        // Handle file:line format, careful with Windows paths like "C:\path\file.rs:10"
        // Strategy: find the last ':' that's followed by digits only
        if let Some(colon_idx) = s.rfind(':') {
            let (file_part, line_part) = s.split_at(colon_idx);
            let line_str = &line_part[1..]; // Skip the ':'

            // Only treat as file:line if the part after ':' is a valid line number
            if !line_str.is_empty() && line_str.chars().all(|c| c.is_ascii_digit()) {
                let line: u32 = line_str.parse().map_err(|_| {
                    crate::common::Error::InvalidLocation(format!(
                        "invalid line number: {}",
                        line_str
                    ))
                })?;
                return Ok(Self::Line {
                    file: PathBuf::from(file_part),
                    line,
                });
            }
        }

        // No valid file:line pattern, treat as function name
        Ok(Self::Function {
            name: s.to_string(),
        })
    }
}

impl std::fmt::Display for BreakpointLocation {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Line { file, line } => write!(f, "{}:{}", file.display(), line),
            Self::Function { name } => write!(f, "{}", name),
        }
    }
}

/// Context for expression evaluation
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EvaluateContext {
    /// Watch expression (read-only evaluation)
    #[default]
    Watch,
    /// REPL evaluation (can have side effects)
    Repl,
    /// Hover evaluation
    Hover,
}

// === Result types for responses ===

/// Status response
#[derive(Debug, Serialize, Deserialize)]
pub struct StatusResult {
    pub daemon_running: bool,
    pub session_active: bool,
    pub state: Option<String>,
    pub program: Option<String>,
    pub adapter: Option<String>,
    pub stopped_thread: Option<i64>,
    pub stopped_reason: Option<String>,
}

/// Breakpoint information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BreakpointInfo {
    pub id: u32,
    pub verified: bool,
    pub source: Option<String>,
    pub line: Option<u32>,
    pub message: Option<String>,
    pub enabled: bool,
    pub condition: Option<String>,
    pub hit_count: Option<u32>,
}

/// Stack frame information
#[derive(Debug, Serialize, Deserialize)]
pub struct StackFrameInfo {
    pub id: i64,
    pub name: String,
    pub source: Option<String>,
    pub line: Option<u32>,
    pub column: Option<u32>,
}

/// Thread information
#[derive(Debug, Serialize, Deserialize)]
pub struct ThreadInfo {
    pub id: i64,
    pub name: String,
    pub state: Option<String>,
}

/// Variable information
#[derive(Debug, Serialize, Deserialize)]
pub struct VariableInfo {
    pub name: String,
    pub value: String,
    pub type_name: Option<String>,
    pub variables_reference: i64,
}

/// Stop event result
#[derive(Debug, Serialize, Deserialize)]
pub struct StopResult {
    pub reason: String,
    pub description: Option<String>,
    #[serde(default)]
    pub thread_id: Option<i64>,
    #[serde(default)]
    pub all_threads_stopped: bool,
    #[serde(default)]
    pub hit_breakpoint_ids: Vec<u32>,
    /// Current location info
    pub source: Option<String>,
    pub line: Option<u32>,
    pub column: Option<u32>,
}

/// Evaluate result
#[derive(Debug, Serialize, Deserialize)]
pub struct EvaluateResult {
    pub result: String,
    pub type_name: Option<String>,
    pub variables_reference: i64,
}

/// Context result with source code
#[derive(Debug, Serialize, Deserialize)]
pub struct ContextResult {
    pub thread_id: i64,
    pub source: Option<String>,
    pub line: u32,
    pub column: Option<u32>,
    pub function: Option<String>,
    /// Source lines with line numbers
    pub source_lines: Vec<SourceLine>,
    /// Local variables
    pub locals: Vec<VariableInfo>,
}

/// A source line with its number
#[derive(Debug, Serialize, Deserialize)]
pub struct SourceLine {
    pub number: u32,
    pub content: String,
    pub is_current: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_file_line() {
        let loc = BreakpointLocation::parse("src/main.rs:42").unwrap();
        match loc {
            BreakpointLocation::Line { file, line } => {
                assert_eq!(file, PathBuf::from("src/main.rs"));
                assert_eq!(line, 42);
            }
            _ => panic!("Expected Line variant"),
        }
    }

    #[test]
    fn test_parse_function() {
        let loc = BreakpointLocation::parse("main").unwrap();
        match loc {
            BreakpointLocation::Function { name } => {
                assert_eq!(name, "main");
            }
            _ => panic!("Expected Function variant"),
        }
    }

    #[test]
    fn test_parse_namespaced_function() {
        let loc = BreakpointLocation::parse("mymod::MyStruct::method").unwrap();
        match loc {
            BreakpointLocation::Function { name } => {
                assert_eq!(name, "mymod::MyStruct::method");
            }
            _ => panic!("Expected Function variant"),
        }
    }

    #[cfg(windows)]
    #[test]
    fn test_parse_windows_path() {
        let loc = BreakpointLocation::parse(r"C:\Users\test\src\main.rs:42").unwrap();
        match loc {
            BreakpointLocation::Line { file, line } => {
                assert_eq!(file, PathBuf::from(r"C:\Users\test\src\main.rs"));
                assert_eq!(line, 42);
            }
            _ => panic!("Expected Line variant"),
        }
    }
}