Skip to main content

lit/
response.rs

1use serde::{Deserialize, Serialize};
2
3/// Output format for command responses
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum OutputFormat {
6    Json,
7    Human,
8}
9
10impl OutputFormat {
11    /// Determine output format from CLI flags and environment
12    pub fn resolve(json: bool, human: bool) -> Self {
13        if human {
14            return OutputFormat::Human;
15        }
16        if json {
17            return OutputFormat::Json;
18        }
19        // Check environment variable
20        match std::env::var("LIT_OUTPUT").as_deref() {
21            Ok("human") => OutputFormat::Human,
22            _ => OutputFormat::Json, // Default: JSON (agent-first)
23        }
24    }
25}
26
27/// Unified response wrapper for all command output
28#[derive(Debug, Serialize, Deserialize)]
29pub struct CommandOutput {
30    pub status: &'static str,
31    pub command: &'static str,
32    #[serde(flatten)]
33    pub data: serde_json::Value,
34}
35
36/// Trait for command responses that can be rendered in multiple formats
37pub trait CommandResponse: Serialize {
38    /// The command name for the response envelope
39    fn command_name(&self) -> &'static str;
40
41    /// Render as human-readable text
42    fn human_readable(&self) -> String;
43
44    /// Render as JSON (default implementation via serde).
45    ///
46    /// Output is compact (single line, no extra whitespace) by default — this
47    /// is the agent-first, token-efficient representation and is also valid
48    /// JSONL. Use [`CommandResponse::to_json_output_pretty`] for human-readable
49    /// indented JSON.
50    fn to_json_output(&self) -> String {
51        let data = serde_json::to_value(self).unwrap_or(serde_json::Value::Null);
52        let output = CommandOutput {
53            status: "ok",
54            command: self.command_name(),
55            data,
56        };
57        serde_json::to_string(&output).unwrap_or_default()
58    }
59
60    /// Render as indented, human-readable JSON (opt-in via `--pretty`).
61    fn to_json_output_pretty(&self) -> String {
62        let data = serde_json::to_value(self).unwrap_or(serde_json::Value::Null);
63        let output = CommandOutput {
64            status: "ok",
65            command: self.command_name(),
66            data,
67        };
68        serde_json::to_string_pretty(&output).unwrap_or_default()
69    }
70}
71
72/// Render a response in the specified format
73pub fn render<R: CommandResponse>(response: &R, format: OutputFormat) -> String {
74    match format {
75        OutputFormat::Json => response.to_json_output(),
76        OutputFormat::Human => response.human_readable(),
77    }
78}
79
80/// Render an error in the specified format
81pub fn render_error(
82    error: &crate::errors::LitError,
83    command: &str,
84    format: OutputFormat,
85) -> String {
86    match format {
87        OutputFormat::Json => {
88            let err_obj = serde_json::json!({
89                "status": "error",
90                "command": command,
91                "error": {
92                    "code": error.error_code(),
93                    "message": error.user_message(),
94                    "suggestions": error.suggestions(),
95                }
96            });
97            serde_json::to_string(&err_obj).unwrap_or_default()
98        }
99        OutputFormat::Human => {
100            let mut out = format!("error: {}", error.user_message());
101            let suggestions = error.suggestions();
102            if !suggestions.is_empty() {
103                out.push_str("\n\nhint:");
104                for s in suggestions {
105                    out.push_str(&format!("\n  {}", s));
106                }
107            }
108            out
109        }
110    }
111}
112
113// ─── Response types ─────────────────────────────────────────────
114
115#[derive(Debug, Serialize, Deserialize)]
116pub struct InitResponse {
117    pub path: String,
118    pub bare: bool,
119}
120
121impl CommandResponse for InitResponse {
122    fn command_name(&self) -> &'static str {
123        "init"
124    }
125    fn human_readable(&self) -> String {
126        if self.bare {
127            format!("Initialized empty bare Lit repository in {}", self.path)
128        } else {
129            format!("Initialized empty Lit repository in {}", self.path)
130        }
131    }
132}
133
134#[derive(Debug, Serialize, Deserialize)]
135pub struct AddResponse {
136    pub files_added: usize,
137}
138
139impl CommandResponse for AddResponse {
140    fn command_name(&self) -> &'static str {
141        "add"
142    }
143    fn human_readable(&self) -> String {
144        format!("Added {} file(s) to staging area", self.files_added)
145    }
146}
147
148#[derive(Debug, Serialize, Deserialize)]
149pub struct CommitResponse {
150    pub hash: String,
151    pub short_hash: String,
152    pub tree: String,
153    pub parent: Option<String>,
154    pub author: String,
155    pub message: String,
156    pub timestamp: i64,
157}
158
159impl CommandResponse for CommitResponse {
160    fn command_name(&self) -> &'static str {
161        "commit"
162    }
163    fn human_readable(&self) -> String {
164        format!("[{}] {}", self.short_hash, self.message)
165    }
166}
167
168#[derive(Debug, Serialize, Deserialize)]
169pub struct StatusResponse {
170    pub branch: Option<String>,
171    pub head: Option<String>,
172    pub staged: Vec<String>,
173    pub modified: Vec<String>,
174    pub untracked: Vec<String>,
175    pub clean: bool,
176}
177
178impl CommandResponse for StatusResponse {
179    fn command_name(&self) -> &'static str {
180        "status"
181    }
182    fn human_readable(&self) -> String {
183        // ANSI color codes (matches git's palette)
184        const GREEN: &str = "\x1b[32m";
185        const ORANGE: &str = "\x1b[33m";
186        const RED: &str = "\x1b[31m";
187        const BOLD: &str = "\x1b[1m";
188        const RESET: &str = "\x1b[0m";
189
190        let mut out = String::new();
191        if let Some(branch) = &self.branch {
192            out.push_str(&format!("On branch {BOLD}{branch}{RESET}\n"));
193        } else {
194            out.push_str(&format!("{BOLD}HEAD detached{RESET}\n"));
195        }
196
197        if self.clean {
198            out.push_str("nothing to commit, working tree clean\n");
199            return out;
200        }
201
202        if !self.staged.is_empty() {
203            out.push_str(&format!("\n{BOLD}Changes to be committed:{RESET}\n"));
204            for f in &self.staged {
205                out.push_str(&format!("{GREEN}  new file:   {f}{RESET}\n"));
206            }
207        }
208        if !self.modified.is_empty() {
209            out.push_str(&format!("\n{BOLD}Changes not staged for commit:{RESET}\n"));
210            for f in &self.modified {
211                out.push_str(&format!("{ORANGE}  modified:   {f}{RESET}\n"));
212            }
213        }
214        if !self.untracked.is_empty() {
215            out.push_str(&format!("\n{BOLD}Untracked files:{RESET}\n"));
216            for f in &self.untracked {
217                out.push_str(&format!("{RED}  {f}{RESET}\n"));
218            }
219        }
220        out
221    }
222}
223
224#[derive(Debug, Serialize, Deserialize)]
225pub struct CommitEntry {
226    pub hash: String,
227    pub short_hash: String,
228    pub author: String,
229    pub timestamp: i64,
230    pub message: String,
231    pub is_head: bool,
232}
233
234#[derive(Debug, Serialize, Deserialize)]
235pub struct LogResponse {
236    pub branch: Option<String>,
237    pub commits: Vec<CommitEntry>,
238}
239
240impl CommandResponse for LogResponse {
241    fn command_name(&self) -> &'static str {
242        "log"
243    }
244    fn human_readable(&self) -> String {
245        if self.commits.is_empty() {
246            return "No commits yet\n".to_string();
247        }
248        let mut out = String::new();
249        for entry in &self.commits {
250            out.push_str(&format!("commit {}\n", entry.hash));
251            if entry.is_head {
252                if let Some(branch) = &self.branch {
253                    out.push_str(&format!("  (HEAD -> {})\n", branch));
254                }
255            }
256            out.push_str(&format!("Author: {}\n", entry.author));
257            if let Some(dt) = chrono::DateTime::<chrono::Utc>::from_timestamp(entry.timestamp, 0) {
258                out.push_str(&format!(
259                    "Date:   {}\n",
260                    dt.format("%a %b %d %H:%M:%S %Y %z")
261                ));
262            }
263            out.push('\n');
264            for line in entry.message.lines() {
265                out.push_str(&format!("    {}\n", line));
266            }
267            out.push('\n');
268        }
269        out
270    }
271}
272
273#[derive(Debug, Serialize, Deserialize)]
274pub struct BranchEntry {
275    pub name: String,
276    pub is_current: bool,
277}
278
279#[derive(Debug, Serialize, Deserialize)]
280#[serde(tag = "action")]
281pub enum BranchResponse {
282    #[serde(rename = "list")]
283    List { branches: Vec<BranchEntry> },
284    #[serde(rename = "create")]
285    Create { name: String },
286    #[serde(rename = "delete")]
287    Delete { name: String },
288}
289
290impl CommandResponse for BranchResponse {
291    fn command_name(&self) -> &'static str {
292        "branch"
293    }
294    fn human_readable(&self) -> String {
295        match self {
296            BranchResponse::List { branches } => {
297                if branches.is_empty() {
298                    return "No branches yet\n".to_string();
299                }
300                let mut out = String::new();
301                for b in branches {
302                    let marker = if b.is_current { "* " } else { "  " };
303                    out.push_str(&format!("{}{}\n", marker, b.name));
304                }
305                out
306            }
307            BranchResponse::Create { name } => format!("Created branch '{}'\n", name),
308            BranchResponse::Delete { name } => format!("Deleted branch '{}'\n", name),
309        }
310    }
311}
312
313#[derive(Debug, Serialize, Deserialize)]
314pub struct CheckoutResponse {
315    pub target: String,
316    pub is_new_branch: bool,
317    pub is_detached: bool,
318}
319
320impl CommandResponse for CheckoutResponse {
321    fn command_name(&self) -> &'static str {
322        "checkout"
323    }
324    fn human_readable(&self) -> String {
325        if self.is_new_branch {
326            format!("Switched to a new branch '{}'\n", self.target)
327        } else if self.is_detached {
328            format!(
329                "HEAD is now at {} (detached)\n",
330                &self.target[..16.min(self.target.len())]
331            )
332        } else {
333            format!("Switched to branch '{}'\n", self.target)
334        }
335    }
336}
337
338#[derive(Debug, Serialize, Deserialize)]
339#[serde(tag = "object_type")]
340pub enum ShowResponse {
341    #[serde(rename = "commit")]
342    Commit {
343        hash: String,
344        author: String,
345        timestamp: i64,
346        message: String,
347    },
348    #[serde(rename = "tree")]
349    Tree {
350        hash: String,
351        entries: Vec<TreeEntryInfo>,
352    },
353    #[serde(rename = "blob")]
354    Blob {
355        hash: String,
356        size: usize,
357        content: Option<String>,
358        is_binary: bool,
359    },
360}
361
362#[derive(Debug, Serialize, Deserialize)]
363pub struct TreeEntryInfo {
364    pub mode: String,
365    pub object_type: String,
366    pub hash: String,
367    pub name: String,
368}
369
370impl CommandResponse for ShowResponse {
371    fn command_name(&self) -> &'static str {
372        "show"
373    }
374    fn human_readable(&self) -> String {
375        match self {
376            ShowResponse::Commit {
377                hash,
378                author,
379                timestamp,
380                message,
381            } => {
382                let mut out = format!("commit {}\nAuthor: {}\n", hash, author);
383                if let Some(dt) = chrono::DateTime::<chrono::Utc>::from_timestamp(*timestamp, 0) {
384                    out.push_str(&format!(
385                        "Date:   {}\n",
386                        dt.format("%a %b %d %H:%M:%S %Y %z")
387                    ));
388                }
389                out.push_str(&format!("\n{}\n", message));
390                out
391            }
392            ShowResponse::Tree { hash, entries } => {
393                let mut out = format!("tree {}\n\n", hash);
394                for e in entries {
395                    out.push_str(&format!(
396                        "{} {} {}\t{}\n",
397                        e.mode,
398                        e.object_type,
399                        &e.hash[..16.min(e.hash.len())],
400                        e.name
401                    ));
402                }
403                out
404            }
405            ShowResponse::Blob {
406                hash,
407                size,
408                content,
409                is_binary,
410            } => {
411                let mut out = format!("blob {}\n\n", hash);
412                if *is_binary {
413                    out.push_str(&format!("(binary content, {} bytes)\n", size));
414                } else if let Some(text) = content {
415                    out.push_str(text);
416                    out.push('\n');
417                }
418                out
419            }
420        }
421    }
422}
423
424#[derive(Debug, Serialize, Deserialize)]
425pub struct RemoteEntry {
426    pub name: String,
427    pub url: String,
428}
429
430#[derive(Debug, Serialize, Deserialize)]
431#[serde(tag = "action")]
432pub enum RemoteResponse {
433    #[serde(rename = "list")]
434    List { remotes: Vec<RemoteEntry> },
435    #[serde(rename = "add")]
436    Add { name: String, url: String },
437    #[serde(rename = "remove")]
438    Remove { name: String },
439}
440
441impl CommandResponse for RemoteResponse {
442    fn command_name(&self) -> &'static str {
443        "remote"
444    }
445    fn human_readable(&self) -> String {
446        match self {
447            RemoteResponse::List { remotes } => {
448                if remotes.is_empty() {
449                    return "No remotes configured\n".to_string();
450                }
451                let mut out = String::new();
452                for r in remotes {
453                    out.push_str(&format!("{}\t{}\n", r.name, r.url));
454                }
455                out
456            }
457            RemoteResponse::Add { name, .. } => format!("Added remote '{}'\n", name),
458            RemoteResponse::Remove { name } => format!("Removed remote '{}'\n", name),
459        }
460    }
461}
462
463#[derive(Debug, Serialize, Deserialize)]
464pub struct ConfigEntry {
465    pub key: String,
466    pub value: String,
467}
468
469#[derive(Debug, Serialize, Deserialize)]
470#[serde(tag = "action")]
471pub enum ConfigResponse {
472    #[serde(rename = "show")]
473    Show { entries: Vec<ConfigEntry> },
474    #[serde(rename = "get")]
475    Get { key: String, value: String },
476    #[serde(rename = "set")]
477    Set { key: String, value: String },
478}
479
480impl CommandResponse for ConfigResponse {
481    fn command_name(&self) -> &'static str {
482        "config"
483    }
484    fn human_readable(&self) -> String {
485        match self {
486            ConfigResponse::Show { entries } => {
487                let mut out = String::from("Lit Configuration\n==================\n\n");
488                for e in entries {
489                    out.push_str(&format!("{} = {}\n", e.key, e.value));
490                }
491                out
492            }
493            ConfigResponse::Get { key, value } => format!("{} = {}\n", key, value),
494            ConfigResponse::Set { key, value } => format!("Set {} = {}\n", key, value),
495        }
496    }
497}
498
499#[derive(Debug, Serialize, Deserialize)]
500pub struct MergeResponse {
501    pub merged: bool,
502    pub fast_forward: bool,
503    pub commit_hash: Option<String>,
504    pub message: String,
505    pub has_conflicts: bool,
506    pub file_results: Vec<FileMergeInfo>,
507    pub strategy: String,
508}
509
510#[derive(Debug, Serialize, Deserialize)]
511pub struct FileMergeInfo {
512    pub path: String,
513    pub status: String,
514    pub conflict_count: usize,
515}
516
517impl CommandResponse for MergeResponse {
518    fn command_name(&self) -> &'static str {
519        "merge"
520    }
521    fn human_readable(&self) -> String {
522        let mut out = String::new();
523        out.push_str(&self.message);
524        out.push('\n');
525
526        if !self.file_results.is_empty() {
527            for f in &self.file_results {
528                let icon = match f.status.as_str() {
529                    "conflict" => "C",
530                    "added" => "A",
531                    "deleted" => "D",
532                    "autoresolved" => "M",
533                    _ => " ",
534                };
535                out.push_str(&format!("  {} {}\n", icon, f.path));
536            }
537        }
538
539        out
540    }
541}
542
543#[derive(Debug, Serialize, Deserialize)]
544pub struct ResolveResponse {
545    pub resolved_files: Vec<String>,
546    pub remaining_conflicts: usize,
547    pub merge_complete: bool,
548    pub message: String,
549}
550
551impl CommandResponse for ResolveResponse {
552    fn command_name(&self) -> &'static str {
553        "resolve"
554    }
555    fn human_readable(&self) -> String {
556        let mut out = String::new();
557        out.push_str(&self.message);
558        out.push('\n');
559
560        for f in &self.resolved_files {
561            out.push_str(&format!("  Resolved: {}\n", f));
562        }
563
564        if self.remaining_conflicts > 0 {
565            out.push_str(&format!(
566                "  {} conflict(s) remaining\n",
567                self.remaining_conflicts
568            ));
569        }
570
571        out
572    }
573}
574
575#[derive(Debug, Serialize, Deserialize)]
576pub struct PushResponse {
577    pub remote: String,
578    pub branch: String,
579    pub objects_transferred: usize,
580    pub updated: bool,
581    pub message: String,
582}
583
584impl CommandResponse for PushResponse {
585    fn command_name(&self) -> &'static str {
586        "push"
587    }
588    fn human_readable(&self) -> String {
589        format!("{}\n", self.message)
590    }
591}
592
593#[derive(Debug, Serialize, Deserialize)]
594pub struct PullResponse {
595    pub remote: String,
596    pub branch: String,
597    pub objects_fetched: usize,
598    pub fast_forward: bool,
599    pub has_conflicts: bool,
600    pub merge_message: String,
601    pub message: String,
602}
603
604impl CommandResponse for PullResponse {
605    fn command_name(&self) -> &'static str {
606        "pull"
607    }
608    fn human_readable(&self) -> String {
609        format!("{}\n", self.message)
610    }
611}
612
613#[derive(Debug, Serialize, Deserialize)]
614pub struct CloneResponse {
615    pub url: String,
616    pub directory: String,
617    pub branches_cloned: Vec<String>,
618    pub objects_transferred: usize,
619    pub message: String,
620}
621
622impl CommandResponse for CloneResponse {
623    fn command_name(&self) -> &'static str {
624        "clone"
625    }
626    fn human_readable(&self) -> String {
627        format!("{}\n", self.message)
628    }
629}
630
631#[derive(Debug, Serialize, Deserialize)]
632pub struct FetchResponse {
633    pub remote: String,
634    pub branches_updated: Vec<String>,
635    pub objects_transferred: usize,
636    pub message: String,
637}
638
639impl CommandResponse for FetchResponse {
640    fn command_name(&self) -> &'static str {
641        "fetch"
642    }
643    fn human_readable(&self) -> String {
644        format!("{}\n", self.message)
645    }
646}
647
648#[derive(Debug, Serialize, Deserialize)]
649pub struct DiffResponse {
650    pub files: Vec<crate::core::diff::FileDiff>,
651    pub stats: Vec<crate::core::diff::DiffStat>,
652    pub stat_only: bool,
653    pub word_diff: bool,
654    pub files_changed: usize,
655    pub total_additions: usize,
656    pub total_deletions: usize,
657}
658
659impl CommandResponse for DiffResponse {
660    fn command_name(&self) -> &'static str {
661        "diff"
662    }
663    fn human_readable(&self) -> String {
664        use crate::core::diff::{annotate_hunk_word_diff, DiffLineKind, FileStatus};
665
666        if self.files.is_empty() {
667            return String::new(); // No output for no changes (like git)
668        }
669
670        let mut out = String::new();
671
672        if self.stat_only {
673            // --stat mode: compact summary
674            for stat in &self.stats {
675                let changes = stat.additions + stat.deletions;
676                let bar: String = std::iter::repeat_n('+', stat.additions.min(40))
677                    .chain(std::iter::repeat_n('-', stat.deletions.min(40)))
678                    .collect();
679                out.push_str(&format!(" {:<40} | {:>4} {}\n", stat.path, changes, bar));
680            }
681            out.push_str(&format!(
682                " {} file(s) changed, {} insertions(+), {} deletions(-)\n",
683                self.files_changed, self.total_additions, self.total_deletions
684            ));
685            return out;
686        }
687
688        for file in &self.files {
689            let header = match file.status {
690                FileStatus::Added => format!("--- /dev/null\n+++ b/{}\n", file.path),
691                FileStatus::Deleted => format!("--- a/{}\n+++ /dev/null\n", file.path),
692                FileStatus::Modified => {
693                    format!("--- a/{}\n+++ b/{}\n", file.path, file.path)
694                }
695            };
696            out.push_str(&header);
697
698            if file.is_binary {
699                out.push_str("Binary files differ\n");
700                continue;
701            }
702
703            for hunk in &file.hunks {
704                out.push_str(&format!(
705                    "@@ -{},{} +{},{} @@\n",
706                    hunk.old_start, hunk.old_count, hunk.new_start, hunk.new_count
707                ));
708                if self.word_diff {
709                    let annotated = annotate_hunk_word_diff(hunk);
710                    for (line, word_segs) in &annotated {
711                        if let Some(segs) = word_segs {
712                            let prefix = match line.kind {
713                                DiffLineKind::Add => '+',
714                                DiffLineKind::Remove => '-',
715                                _ => ' ',
716                            };
717                            out.push(prefix);
718                            for seg in segs {
719                                match seg.kind {
720                                    DiffLineKind::Remove => {
721                                        out.push_str(&format!("[-{}-]", seg.text));
722                                    }
723                                    DiffLineKind::Add => {
724                                        out.push_str(&format!("{{+{}+}}", seg.text));
725                                    }
726                                    DiffLineKind::Context => {
727                                        out.push_str(&seg.text);
728                                    }
729                                }
730                            }
731                            out.push('\n');
732                        } else {
733                            let prefix = match line.kind {
734                                DiffLineKind::Context => ' ',
735                                DiffLineKind::Add => '+',
736                                DiffLineKind::Remove => '-',
737                            };
738                            out.push_str(&format!("{}{}\n", prefix, line.content));
739                        }
740                    }
741                } else {
742                    for line in &hunk.lines {
743                        let prefix = match line.kind {
744                            DiffLineKind::Context => ' ',
745                            DiffLineKind::Add => '+',
746                            DiffLineKind::Remove => '-',
747                        };
748                        out.push_str(&format!("{}{}\n", prefix, line.content));
749                    }
750                }
751            }
752        }
753
754        // Summary line
755        out.push_str(&format!(
756            "\n{} file(s) changed, {} insertions(+), {} deletions(-)\n",
757            self.files_changed, self.total_additions, self.total_deletions
758        ));
759
760        out
761    }
762}
763
764#[derive(Debug, Serialize, Deserialize)]
765#[serde(tag = "action")]
766pub enum TagResponse {
767    #[serde(rename = "create")]
768    Create {
769        name: String,
770        hash: String,
771        annotated: bool,
772        signed: bool,
773        message: String,
774    },
775    #[serde(rename = "list")]
776    List { tags: Vec<String> },
777    #[serde(rename = "delete")]
778    Delete { name: String, message: String },
779    #[serde(rename = "verify")]
780    Verify {
781        name: String,
782        valid: bool,
783        algorithm: String,
784        message: String,
785    },
786}
787
788impl CommandResponse for TagResponse {
789    fn command_name(&self) -> &'static str {
790        "tag"
791    }
792    fn human_readable(&self) -> String {
793        match self {
794            TagResponse::Create { message, .. } => format!("{}\n", message),
795            TagResponse::List { tags } => {
796                if tags.is_empty() {
797                    return String::new();
798                }
799                tags.iter().map(|t| format!("{}\n", t)).collect()
800            }
801            TagResponse::Delete { message, .. } => format!("{}\n", message),
802            TagResponse::Verify { message, .. } => format!("{}\n", message),
803        }
804    }
805}
806
807#[derive(Debug, Serialize, Deserialize)]
808pub struct RotateKeyResponse {
809    pub objects_rotated: usize,
810    pub refs_rotated: usize,
811}
812
813impl CommandResponse for RotateKeyResponse {
814    fn command_name(&self) -> &'static str {
815        "rotate-key"
816    }
817    fn human_readable(&self) -> String {
818        format!(
819            "Passphrase rotation complete!\n  {} objects re-encrypted\n  {} refs re-encrypted\n  Old passphrase is no longer valid.\n",
820            self.objects_rotated, self.refs_rotated
821        )
822    }
823}
824
825// ============================================================================
826// Phase 1.5-1.8 Response Types
827// ============================================================================
828
829#[derive(Debug, Serialize, Deserialize)]
830pub struct StashEntryInfo {
831    pub index: usize,
832    pub message: String,
833    pub branch: Option<String>,
834    pub timestamp: i64,
835}
836
837#[derive(Debug, Serialize, Deserialize)]
838#[serde(tag = "action")]
839pub enum StashResponse {
840    #[serde(rename = "push")]
841    Push { index: usize, message: String },
842    #[serde(rename = "pop")]
843    Pop { index: usize, message: String },
844    #[serde(rename = "apply")]
845    Apply { index: usize, message: String },
846    #[serde(rename = "list")]
847    List { entries: Vec<StashEntryInfo> },
848    #[serde(rename = "drop")]
849    Drop { index: usize, message: String },
850}
851
852impl CommandResponse for StashResponse {
853    fn command_name(&self) -> &'static str {
854        "stash"
855    }
856    fn human_readable(&self) -> String {
857        match self {
858            StashResponse::Push { index, message } => {
859                format!(
860                    "Saved working directory to stash@{{{}}}: {}",
861                    index, message
862                )
863            }
864            StashResponse::Pop { index, message } => {
865                format!("Applied and dropped stash@{{{}}}: {}", index, message)
866            }
867            StashResponse::Apply { index, message } => {
868                format!("Applied stash@{{{}}}: {}", index, message)
869            }
870            StashResponse::List { entries } => {
871                if entries.is_empty() {
872                    "No stash entries".to_string()
873                } else {
874                    entries
875                        .iter()
876                        .map(|e| format!("stash@{{{}}}: {}", e.index, e.message))
877                        .collect::<Vec<_>>()
878                        .join("\n")
879                }
880            }
881            StashResponse::Drop { index, message } => {
882                format!("Dropped stash@{{{}}}: {}", index, message)
883            }
884        }
885    }
886}
887
888#[derive(Debug, Serialize, Deserialize)]
889pub struct ResetResponse {
890    pub target: String,
891    pub mode: String,
892    pub message: String,
893}
894
895impl CommandResponse for ResetResponse {
896    fn command_name(&self) -> &'static str {
897        "reset"
898    }
899    fn human_readable(&self) -> String {
900        format!(
901            "HEAD is now at {} ({})\n{}",
902            self.target, self.mode, self.message
903        )
904    }
905}
906
907#[derive(Debug, Serialize, Deserialize)]
908pub struct RevertResponse {
909    pub reverted_commit: String,
910    pub new_commit: String,
911    pub files_changed: usize,
912    pub message: String,
913}
914
915impl CommandResponse for RevertResponse {
916    fn command_name(&self) -> &'static str {
917        "revert"
918    }
919    fn human_readable(&self) -> String {
920        format!(
921            "Reverted {}\nNew commit: {}\n{} file(s) changed\n{}",
922            self.reverted_commit, self.new_commit, self.files_changed, self.message
923        )
924    }
925}
926
927#[derive(Debug, Serialize, Deserialize)]
928pub struct CherryPickResponse {
929    pub source_commit: String,
930    pub new_commit: String,
931    pub files_changed: usize,
932    pub message: String,
933}
934
935impl CommandResponse for CherryPickResponse {
936    fn command_name(&self) -> &'static str {
937        "cherry-pick"
938    }
939    fn human_readable(&self) -> String {
940        format!(
941            "Cherry-picked {}\nNew commit: {}\n{} file(s) changed\n{}",
942            self.source_commit, self.new_commit, self.files_changed, self.message
943        )
944    }
945}
946
947#[derive(Debug, Serialize, Deserialize)]
948pub struct RebaseResponse {
949    pub rebased_commits: usize,
950    pub onto: String,
951    pub branch: String,
952    pub message: String,
953    #[serde(skip_serializing_if = "Option::is_none")]
954    pub todo: Option<serde_json::Value>,
955}
956
957impl CommandResponse for RebaseResponse {
958    fn command_name(&self) -> &'static str {
959        "rebase"
960    }
961    fn human_readable(&self) -> String {
962        let mut out = format!("{}\n", self.message);
963        if self.rebased_commits > 0 {
964            out.push_str(&format!(
965                "Rebased {} commit(s) onto {}\n",
966                self.rebased_commits, self.onto
967            ));
968        }
969        if let Some(ref todo) = self.todo {
970            out.push_str(&format!(
971                "Todo: {}\n",
972                serde_json::to_string_pretty(todo).unwrap_or_default()
973            ));
974        }
975        out
976    }
977}
978
979#[derive(Debug, Serialize, Deserialize)]
980pub struct BlameLineInfo {
981    pub line_number: usize,
982    pub content: String,
983    pub commit_hash: String,
984    pub author: String,
985    pub timestamp: i64,
986}
987
988#[derive(Debug, Serialize, Deserialize)]
989pub struct BlameResponse {
990    pub file: String,
991    pub lines: Vec<BlameLineInfo>,
992}
993
994impl CommandResponse for BlameResponse {
995    fn command_name(&self) -> &'static str {
996        "blame"
997    }
998    fn human_readable(&self) -> String {
999        let mut out = format!("Blame for {}:\n", self.file);
1000        for line in &self.lines {
1001            out.push_str(&format!(
1002                "{} ({} {}) {}\n",
1003                &line.commit_hash[..8.min(line.commit_hash.len())],
1004                line.author,
1005                line.line_number,
1006                line.content
1007            ));
1008        }
1009        out
1010    }
1011}
1012
1013#[derive(Debug, Serialize, Deserialize)]
1014pub struct BisectResponse {
1015    pub action: String,
1016    pub current: Option<String>,
1017    pub remaining: usize,
1018    pub steps: usize,
1019    pub message: String,
1020}
1021
1022impl CommandResponse for BisectResponse {
1023    fn command_name(&self) -> &'static str {
1024        "bisect"
1025    }
1026    fn human_readable(&self) -> String {
1027        let mut out = format!("{}\n", self.message);
1028        if let Some(ref commit) = self.current {
1029            out.push_str(&format!("Current: {}\n", commit));
1030        }
1031        if self.remaining > 0 {
1032            out.push_str(&format!("~{} steps remaining\n", self.steps));
1033        }
1034        out
1035    }
1036}
1037
1038#[derive(Debug, Serialize, Deserialize)]
1039pub struct ReflogEntry {
1040    pub index: usize,
1041    pub old_hash: String,
1042    pub new_hash: String,
1043    pub action: String,
1044    pub message: String,
1045    pub timestamp: i64,
1046}
1047
1048#[derive(Debug, Serialize, Deserialize)]
1049pub struct ReflogResponse {
1050    pub ref_name: String,
1051    pub entries: Vec<ReflogEntry>,
1052}
1053
1054impl CommandResponse for ReflogResponse {
1055    fn command_name(&self) -> &'static str {
1056        "reflog"
1057    }
1058    fn human_readable(&self) -> String {
1059        let mut out = format!("Reflog for {}:\n", self.ref_name);
1060        for entry in &self.entries {
1061            out.push_str(&format!(
1062                "{}@{{{}}} {} -> {} {}: {}\n",
1063                self.ref_name,
1064                entry.index,
1065                &entry.old_hash[..8.min(entry.old_hash.len())],
1066                &entry.new_hash[..8.min(entry.new_hash.len())],
1067                entry.action,
1068                entry.message
1069            ));
1070        }
1071        out
1072    }
1073}
1074
1075// ============================================================================
1076// Phase 2 Response Types
1077// ============================================================================
1078
1079#[derive(Debug, Serialize, Deserialize)]
1080pub struct BatchOperationResult {
1081    pub index: usize,
1082    pub command: String,
1083    pub status: String,
1084    pub result: Option<serde_json::Value>,
1085    pub error: Option<String>,
1086}
1087
1088#[derive(Debug, Serialize, Deserialize)]
1089pub struct BatchResponse {
1090    pub total: usize,
1091    pub succeeded: usize,
1092    pub failed: usize,
1093    pub atomic: bool,
1094    pub dry_run: bool,
1095    pub results: Vec<BatchOperationResult>,
1096}
1097
1098impl CommandResponse for BatchResponse {
1099    fn command_name(&self) -> &'static str {
1100        "batch"
1101    }
1102    fn human_readable(&self) -> String {
1103        format!(
1104            "Batch complete: {}/{} succeeded, {} failed{}{}",
1105            self.succeeded,
1106            self.total,
1107            self.failed,
1108            if self.atomic { " (atomic)" } else { "" },
1109            if self.dry_run { " (dry-run)" } else { "" },
1110        )
1111    }
1112}
1113
1114#[derive(Debug, Serialize, Deserialize)]
1115pub struct TransactionResponse {
1116    pub action: String,
1117    pub tx_id: Option<String>,
1118    pub message: String,
1119}
1120
1121impl CommandResponse for TransactionResponse {
1122    fn command_name(&self) -> &'static str {
1123        "transaction"
1124    }
1125    fn human_readable(&self) -> String {
1126        if let Some(ref id) = self.tx_id {
1127            format!(
1128                "Transaction {}: {} [{}]",
1129                self.action,
1130                self.message,
1131                &id[..8.min(id.len())]
1132            )
1133        } else {
1134            format!("Transaction {}: {}", self.action, self.message)
1135        }
1136    }
1137}
1138
1139#[derive(Debug, Serialize, Deserialize)]
1140pub struct SnapshotResponse {
1141    pub hash: String,
1142    pub short_hash: String,
1143    pub tree: String,
1144    pub parent: Option<String>,
1145    pub author: String,
1146    pub message: String,
1147    pub timestamp: i64,
1148    pub files_added: usize,
1149}
1150
1151impl CommandResponse for SnapshotResponse {
1152    fn command_name(&self) -> &'static str {
1153        "snapshot"
1154    }
1155    fn human_readable(&self) -> String {
1156        format!(
1157            "[{}] Snapshot: {}\n  {} file(s) captured\n  Author: {}",
1158            self.short_hash, self.message, self.files_added, self.author,
1159        )
1160    }
1161}
1162
1163#[derive(Debug, Serialize, Deserialize)]
1164pub struct SearchMatch {
1165    pub file: String,
1166    pub line_number: usize,
1167    pub content: String,
1168    pub commit: Option<String>,
1169    pub match_type: String,
1170}
1171
1172#[derive(Debug, Serialize, Deserialize)]
1173pub struct SearchResponse {
1174    pub query: String,
1175    pub match_type: String,
1176    pub matches: Vec<SearchMatch>,
1177    pub total: usize,
1178}
1179
1180impl CommandResponse for SearchResponse {
1181    fn command_name(&self) -> &'static str {
1182        "search"
1183    }
1184    fn human_readable(&self) -> String {
1185        let mut out = format!("Search '{}': {} result(s)\n", self.query, self.total);
1186        for m in &self.matches {
1187            match m.match_type.as_str() {
1188                "content" => {
1189                    out.push_str(&format!(
1190                        "  {}:{}: {}\n",
1191                        m.file,
1192                        m.line_number,
1193                        m.content.trim()
1194                    ));
1195                }
1196                "message" => {
1197                    out.push_str(&format!(
1198                        "  commit {}: {}\n",
1199                        m.commit.as_deref().unwrap_or("?"),
1200                        m.content.trim()
1201                    ));
1202                }
1203                _ => {
1204                    out.push_str(&format!("  {}\n", m.content.trim()));
1205                }
1206            }
1207        }
1208        out
1209    }
1210}
1211
1212#[derive(Debug, Serialize, Deserialize)]
1213pub struct WatchEvent {
1214    pub event_type: String,
1215    pub path: String,
1216    pub timestamp: i64,
1217}
1218
1219#[derive(Debug, Serialize, Deserialize)]
1220pub struct WatchResponse {
1221    pub events_emitted: usize,
1222    pub message: String,
1223}
1224
1225impl CommandResponse for WatchResponse {
1226    fn command_name(&self) -> &'static str {
1227        "watch"
1228    }
1229    fn human_readable(&self) -> String {
1230        self.message.clone()
1231    }
1232}
1233
1234#[derive(Debug, Serialize, Deserialize)]
1235pub struct VerifyResult {
1236    pub check: String,
1237    pub status: String,
1238    pub details: Option<String>,
1239}
1240
1241#[derive(Debug, Serialize, Deserialize)]
1242pub struct VerifyResponse {
1243    pub valid: bool,
1244    pub checks: Vec<VerifyResult>,
1245    pub objects_checked: usize,
1246    pub refs_checked: usize,
1247    pub message: String,
1248}
1249
1250impl CommandResponse for VerifyResponse {
1251    fn command_name(&self) -> &'static str {
1252        "verify"
1253    }
1254    fn human_readable(&self) -> String {
1255        let mut out = format!("{}\n", self.message);
1256        for check in &self.checks {
1257            let icon = if check.status == "ok" { "+" } else { "!" };
1258            out.push_str(&format!("  [{}] {}", icon, check.check));
1259            if let Some(ref details) = check.details {
1260                out.push_str(&format!(": {}", details));
1261            }
1262            out.push('\n');
1263        }
1264        out.push_str(&format!(
1265            "  {} objects, {} refs checked\n",
1266            self.objects_checked, self.refs_checked
1267        ));
1268        out
1269    }
1270}
1271
1272// ============================================================================
1273// Phase 3 Response Types
1274// ============================================================================
1275
1276#[derive(Debug, Serialize, Deserialize)]
1277pub struct ServeResponse {
1278    pub message: String,
1279}
1280
1281impl CommandResponse for ServeResponse {
1282    fn command_name(&self) -> &'static str {
1283        "serve"
1284    }
1285    fn human_readable(&self) -> String {
1286        self.message.clone()
1287    }
1288}
1289
1290#[derive(Debug, Serialize, Deserialize)]
1291pub struct McpServeResponse {
1292    pub transport: String,
1293    pub message: String,
1294}
1295
1296impl CommandResponse for McpServeResponse {
1297    fn command_name(&self) -> &'static str {
1298        "mcp-serve"
1299    }
1300    fn human_readable(&self) -> String {
1301        format!("[{}] {}", self.transport, self.message)
1302    }
1303}
1304
1305#[derive(Debug, Serialize, Deserialize)]
1306pub struct SwarmResponse {
1307    pub action: String,
1308    pub agent_id: Option<String>,
1309    pub message: String,
1310    pub details: Option<serde_json::Value>,
1311}
1312
1313impl CommandResponse for SwarmResponse {
1314    fn command_name(&self) -> &'static str {
1315        "swarm"
1316    }
1317    fn human_readable(&self) -> String {
1318        let mut out = format!("Swarm {}: {}\n", self.action, self.message);
1319        if let Some(ref details) = self.details {
1320            out.push_str(&serde_json::to_string_pretty(details).unwrap_or_default());
1321        }
1322        out
1323    }
1324}
1325
1326#[derive(Debug, Serialize, Deserialize)]
1327pub struct OntologyResponse {
1328    pub ontology: serde_json::Value,
1329}
1330
1331impl CommandResponse for OntologyResponse {
1332    fn command_name(&self) -> &'static str {
1333        "ontology"
1334    }
1335    fn human_readable(&self) -> String {
1336        serde_json::to_string_pretty(&self.ontology).unwrap_or_else(|_| "{}".to_string())
1337    }
1338}
1339
1340#[derive(Debug, Serialize, Deserialize)]
1341pub struct SchemaResponse {
1342    pub schema: serde_json::Value,
1343}
1344
1345impl CommandResponse for SchemaResponse {
1346    fn command_name(&self) -> &'static str {
1347        "schema"
1348    }
1349    fn human_readable(&self) -> String {
1350        serde_json::to_string_pretty(&self.schema).unwrap_or_else(|_| "{}".to_string())
1351    }
1352}
1353
1354// ============================================================================
1355// Phase 4 Response Types (Git Interop)
1356// ============================================================================
1357
1358#[derive(Debug, Serialize, Deserialize)]
1359pub struct ImportGitResponse {
1360    pub source: String,
1361    pub objects_imported: u64,
1362    pub refs_imported: u64,
1363    pub hash_mapping_count: usize,
1364    pub message: String,
1365}
1366
1367impl CommandResponse for ImportGitResponse {
1368    fn command_name(&self) -> &'static str {
1369        "import-git"
1370    }
1371    fn human_readable(&self) -> String {
1372        format!(
1373            "{}\n  Objects imported: {}\n  Refs imported: {}\n  Hash mappings: {}",
1374            self.message, self.objects_imported, self.refs_imported, self.hash_mapping_count
1375        )
1376    }
1377}
1378
1379#[derive(Debug, Serialize, Deserialize)]
1380pub struct ExportGitResponse {
1381    pub destination: String,
1382    pub objects_exported: u64,
1383    pub refs_exported: u64,
1384    pub message: String,
1385}
1386
1387impl CommandResponse for ExportGitResponse {
1388    fn command_name(&self) -> &'static str {
1389        "export-git"
1390    }
1391    fn human_readable(&self) -> String {
1392        format!(
1393            "{}\n  Objects exported: {}\n  Refs exported: {}",
1394            self.message, self.objects_exported, self.refs_exported
1395        )
1396    }
1397}
1398
1399// ============================================================================
1400// Phase 5 Response Types (Performance)
1401// ============================================================================
1402
1403#[derive(Debug, Serialize, Deserialize)]
1404pub struct GcResponse {
1405    pub objects_packed: u64,
1406    pub packs_created: u64,
1407    pub loose_removed: u64,
1408    pub bytes_saved: u64,
1409    pub message: String,
1410}
1411
1412impl CommandResponse for GcResponse {
1413    fn command_name(&self) -> &'static str {
1414        "gc"
1415    }
1416    fn human_readable(&self) -> String {
1417        format!(
1418            "{}\n  Objects packed: {}\n  Packs created: {}\n  Loose removed: {}\n  Bytes saved: {}",
1419            self.message,
1420            self.objects_packed,
1421            self.packs_created,
1422            self.loose_removed,
1423            self.bytes_saved
1424        )
1425    }
1426}
1427
1428#[derive(Debug, Serialize, Deserialize)]
1429pub struct LfsTrackResponse {
1430    pub patterns: Vec<String>,
1431    pub message: String,
1432}
1433
1434impl CommandResponse for LfsTrackResponse {
1435    fn command_name(&self) -> &'static str {
1436        "lfs-track"
1437    }
1438    fn human_readable(&self) -> String {
1439        let mut out = format!("{}\n  Tracked patterns:\n", self.message);
1440        for pat in &self.patterns {
1441            out.push_str(&format!("    {}\n", pat));
1442        }
1443        out
1444    }
1445}
1446
1447#[derive(Debug, Serialize, Deserialize)]
1448pub struct LfsMigrateResponse {
1449    pub files_migrated: u64,
1450    pub bytes_saved: u64,
1451    pub message: String,
1452}
1453
1454impl CommandResponse for LfsMigrateResponse {
1455    fn command_name(&self) -> &'static str {
1456        "lfs-migrate"
1457    }
1458    fn human_readable(&self) -> String {
1459        format!(
1460            "{}\n  Files migrated: {}\n  Bytes saved: {}",
1461            self.message, self.files_migrated, self.bytes_saved
1462        )
1463    }
1464}
1465
1466// ============================================================================
1467// Sandbox Response
1468// ============================================================================
1469
1470#[derive(Debug, Serialize, Deserialize)]
1471pub struct SandboxResponse {
1472    pub action: String,
1473    pub name: String,
1474    pub path: String,
1475    pub message: String,
1476    pub output: Option<String>,
1477    pub exit_code: Option<i32>,
1478}
1479
1480impl CommandResponse for SandboxResponse {
1481    fn command_name(&self) -> &'static str {
1482        "sandbox"
1483    }
1484    fn human_readable(&self) -> String {
1485        let mut out = format!("{}\n", self.message);
1486        if let Some(ref text) = self.output {
1487            if !text.is_empty() {
1488                out.push_str(text);
1489                if !text.ends_with('\n') {
1490                    out.push('\n');
1491                }
1492            }
1493        }
1494        out
1495    }
1496}
1497
1498// ============================================================================
1499// Phase 6 Response Types (Decentralized Features)
1500// ============================================================================
1501
1502#[derive(Debug, Serialize, Deserialize)]
1503pub struct DidResponse {
1504    pub action: String,
1505    pub did: Option<String>,
1506    pub message: String,
1507    pub details: Option<serde_json::Value>,
1508}
1509
1510impl CommandResponse for DidResponse {
1511    fn command_name(&self) -> &'static str {
1512        "did"
1513    }
1514    fn human_readable(&self) -> String {
1515        let mut out = format!("{}\n", self.message);
1516        if let Some(ref did) = self.did {
1517            out.push_str(&format!("  DID: {}\n", did));
1518        }
1519        if let Some(ref d) = self.details {
1520            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1521            out.push('\n');
1522        }
1523        out
1524    }
1525}
1526
1527#[derive(Debug, Serialize, Deserialize)]
1528pub struct TrustResponse {
1529    pub action: String,
1530    pub did: Option<String>,
1531    pub score: Option<f64>,
1532    pub level: Option<String>,
1533    pub message: String,
1534    pub details: Option<serde_json::Value>,
1535}
1536
1537impl CommandResponse for TrustResponse {
1538    fn command_name(&self) -> &'static str {
1539        "trust"
1540    }
1541    fn human_readable(&self) -> String {
1542        let mut out = format!("{}\n", self.message);
1543        if let Some(ref did) = self.did {
1544            out.push_str(&format!("  Agent: {}\n", did));
1545        }
1546        if let Some(score) = self.score {
1547            out.push_str(&format!("  Score: {:.1}\n", score));
1548        }
1549        if let Some(ref level) = self.level {
1550            out.push_str(&format!("  Level: {}\n", level));
1551        }
1552        if let Some(ref d) = self.details {
1553            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1554            out.push('\n');
1555        }
1556        out
1557    }
1558}
1559
1560#[derive(Debug, Serialize, Deserialize)]
1561pub struct IssueResponse {
1562    pub action: String,
1563    pub id: Option<u64>,
1564    pub message: String,
1565    pub details: Option<serde_json::Value>,
1566}
1567
1568impl CommandResponse for IssueResponse {
1569    fn command_name(&self) -> &'static str {
1570        "issue"
1571    }
1572    fn human_readable(&self) -> String {
1573        let mut out = format!("{}\n", self.message);
1574        if let Some(id) = self.id {
1575            out.push_str(&format!("  Issue #{}\n", id));
1576        }
1577        if let Some(ref d) = self.details {
1578            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1579            out.push('\n');
1580        }
1581        out
1582    }
1583}
1584
1585#[derive(Debug, Serialize, Deserialize)]
1586pub struct PrResponse {
1587    pub action: String,
1588    pub id: Option<u64>,
1589    pub message: String,
1590    pub details: Option<serde_json::Value>,
1591}
1592
1593impl CommandResponse for PrResponse {
1594    fn command_name(&self) -> &'static str {
1595        "pr"
1596    }
1597    fn human_readable(&self) -> String {
1598        let mut out = format!("{}\n", self.message);
1599        if let Some(id) = self.id {
1600            out.push_str(&format!("  PR #{}\n", id));
1601        }
1602        if let Some(ref d) = self.details {
1603            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1604            out.push('\n');
1605        }
1606        out
1607    }
1608}
1609
1610#[derive(Debug, Serialize, Deserialize)]
1611pub struct SubscribeResponse {
1612    pub action: String,
1613    pub subscription_id: Option<String>,
1614    pub message: String,
1615    pub details: Option<serde_json::Value>,
1616}
1617
1618impl CommandResponse for SubscribeResponse {
1619    fn command_name(&self) -> &'static str {
1620        "subscribe"
1621    }
1622    fn human_readable(&self) -> String {
1623        let mut out = format!("{}\n", self.message);
1624        if let Some(ref id) = self.subscription_id {
1625            out.push_str(&format!("  Subscription: {}\n", id));
1626        }
1627        if let Some(ref d) = self.details {
1628            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1629            out.push('\n');
1630        }
1631        out
1632    }
1633}
1634
1635#[derive(Debug, Serialize, Deserialize)]
1636pub struct DelegateResponse {
1637    pub action: String,
1638    pub task_id: Option<String>,
1639    pub message: String,
1640    pub details: Option<serde_json::Value>,
1641}
1642
1643impl CommandResponse for DelegateResponse {
1644    fn command_name(&self) -> &'static str {
1645        "delegate"
1646    }
1647    fn human_readable(&self) -> String {
1648        let mut out = format!("{}\n", self.message);
1649        if let Some(ref id) = self.task_id {
1650            out.push_str(&format!("  Task: {}\n", id));
1651        }
1652        if let Some(ref d) = self.details {
1653            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1654            out.push('\n');
1655        }
1656        out
1657    }
1658}
1659
1660#[derive(Debug, Serialize, Deserialize)]
1661pub struct FederationResponse {
1662    pub action: String,
1663    pub message: String,
1664    pub details: Option<serde_json::Value>,
1665}
1666
1667impl CommandResponse for FederationResponse {
1668    fn command_name(&self) -> &'static str {
1669        "federation"
1670    }
1671    fn human_readable(&self) -> String {
1672        let mut out = format!("{}\n", self.message);
1673        if let Some(ref d) = self.details {
1674            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1675            out.push('\n');
1676        }
1677        out
1678    }
1679}
1680
1681#[derive(Debug, Serialize, Deserialize)]
1682pub struct UcanResponse {
1683    pub action: String,
1684    pub token_cid: Option<String>,
1685    pub message: String,
1686    pub details: Option<serde_json::Value>,
1687}
1688
1689impl CommandResponse for UcanResponse {
1690    fn command_name(&self) -> &'static str {
1691        "ucan"
1692    }
1693    fn human_readable(&self) -> String {
1694        let mut out = format!("{}\n", self.message);
1695        if let Some(ref cid) = self.token_cid {
1696            out.push_str(&format!("  Token CID: {}\n", cid));
1697        }
1698        if let Some(ref d) = self.details {
1699            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1700            out.push('\n');
1701        }
1702        out
1703    }
1704}
1705
1706// ── Intent / Converge response types ────────────────────────────────────────
1707
1708#[derive(Debug, Serialize, Deserialize)]
1709pub struct IntentResponse {
1710    pub action: String,
1711    pub intent_id: Option<String>,
1712    pub message: String,
1713    pub details: Option<serde_json::Value>,
1714}
1715
1716impl CommandResponse for IntentResponse {
1717    fn command_name(&self) -> &'static str {
1718        "intent"
1719    }
1720    fn human_readable(&self) -> String {
1721        let mut out = format!("{}\n", self.message);
1722        if let Some(ref id) = self.intent_id {
1723            out.push_str(&format!("  Intent: {}\n", id));
1724        }
1725        if let Some(ref d) = self.details {
1726            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1727            out.push('\n');
1728        }
1729        out
1730    }
1731}
1732
1733#[derive(Debug, Serialize, Deserialize)]
1734pub struct ConvergeResponse {
1735    pub converged: bool,
1736    pub strategy: String,
1737    pub intent_id: String,
1738    pub intent_title: String,
1739    pub commit_hash: Option<String>,
1740    pub commits_converged: usize,
1741    pub fast_forward: bool,
1742    pub message: String,
1743    pub details: Option<serde_json::Value>,
1744}
1745
1746impl CommandResponse for ConvergeResponse {
1747    fn command_name(&self) -> &'static str {
1748        "converge"
1749    }
1750    fn human_readable(&self) -> String {
1751        let mut out = format!("{}\n", self.message);
1752        if let Some(ref h) = self.commit_hash {
1753            out.push_str(&format!("  Commit: {}\n", h));
1754        }
1755        out.push_str(&format!("  Strategy: {}\n", self.strategy));
1756        out.push_str(&format!("  Fast-forward: {}\n", self.fast_forward));
1757        if let Some(ref d) = self.details {
1758            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1759            out.push('\n');
1760        }
1761        out
1762    }
1763}
1764
1765// ── Content Type Response ───────────────────────────────────────────────────
1766
1767#[derive(Debug, Serialize, Deserialize)]
1768pub struct ContentTypeResponse {
1769    pub action: String,
1770    pub content_type_id: Option<String>,
1771    pub message: String,
1772    pub details: Option<serde_json::Value>,
1773}
1774
1775impl CommandResponse for ContentTypeResponse {
1776    fn command_name(&self) -> &'static str {
1777        "content-type"
1778    }
1779    fn human_readable(&self) -> String {
1780        let mut out = format!("{}\n", self.message);
1781        if let Some(ref id) = self.content_type_id {
1782            out.push_str(&format!("  Content-Type: {}\n", id));
1783        }
1784        if let Some(ref d) = self.details {
1785            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1786            out.push('\n');
1787        }
1788        out
1789    }
1790}
1791
1792// ── Datacenter Response ─────────────────────────────────────────────────────
1793
1794#[derive(Debug, Serialize, Deserialize)]
1795pub struct DatacenterResponse {
1796    pub action: String,
1797    pub message: String,
1798    pub details: Option<serde_json::Value>,
1799}
1800
1801impl CommandResponse for DatacenterResponse {
1802    fn command_name(&self) -> &'static str {
1803        "datacenter"
1804    }
1805    fn human_readable(&self) -> String {
1806        let mut out = format!("{}\n", self.message);
1807        if let Some(ref d) = self.details {
1808            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1809            out.push('\n');
1810        }
1811        out
1812    }
1813}
1814
1815// ── Agent Profile Response ──────────────────────────────────────────────────
1816
1817#[derive(Debug, Serialize, Deserialize)]
1818pub struct AgentProfileResponse {
1819    pub action: String,
1820    pub profile_id: Option<String>,
1821    pub message: String,
1822    pub details: Option<serde_json::Value>,
1823}
1824
1825impl CommandResponse for AgentProfileResponse {
1826    fn command_name(&self) -> &'static str {
1827        "agent-profile"
1828    }
1829    fn human_readable(&self) -> String {
1830        let mut out = format!("{}\n", self.message);
1831        if let Some(ref id) = self.profile_id {
1832            out.push_str(&format!("  Profile: {}\n", id));
1833        }
1834        if let Some(ref d) = self.details {
1835            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1836            out.push('\n');
1837        }
1838        out
1839    }
1840}