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 MigrateEncryptionResponse {
809    pub objects_encrypted: usize,
810    pub objects_unpacked: usize,
811    pub index_encrypted: bool,
812    pub packs_expanded: usize,
813    pub already_encrypted: usize,
814    pub message: String,
815}
816
817impl CommandResponse for MigrateEncryptionResponse {
818    fn command_name(&self) -> &'static str {
819        "migrate-encryption"
820    }
821    fn human_readable(&self) -> String {
822        format!("{}\n", self.message)
823    }
824}
825
826#[derive(Debug, Serialize, Deserialize)]
827pub struct RotateKeyResponse {
828    pub objects_rotated: usize,
829    pub refs_rotated: usize,
830}
831
832impl CommandResponse for RotateKeyResponse {
833    fn command_name(&self) -> &'static str {
834        "rotate-key"
835    }
836    fn human_readable(&self) -> String {
837        format!(
838            "Passphrase rotation complete!\n  {} objects re-encrypted\n  {} refs re-encrypted\n  Old passphrase is no longer valid.\n",
839            self.objects_rotated, self.refs_rotated
840        )
841    }
842}
843
844// ============================================================================
845// Phase 1.5-1.8 Response Types
846// ============================================================================
847
848#[derive(Debug, Serialize, Deserialize)]
849pub struct StashEntryInfo {
850    pub index: usize,
851    pub message: String,
852    pub branch: Option<String>,
853    pub timestamp: i64,
854}
855
856#[derive(Debug, Serialize, Deserialize)]
857#[serde(tag = "action")]
858pub enum StashResponse {
859    #[serde(rename = "push")]
860    Push { index: usize, message: String },
861    #[serde(rename = "pop")]
862    Pop { index: usize, message: String },
863    #[serde(rename = "apply")]
864    Apply { index: usize, message: String },
865    #[serde(rename = "list")]
866    List { entries: Vec<StashEntryInfo> },
867    #[serde(rename = "drop")]
868    Drop { index: usize, message: String },
869}
870
871impl CommandResponse for StashResponse {
872    fn command_name(&self) -> &'static str {
873        "stash"
874    }
875    fn human_readable(&self) -> String {
876        match self {
877            StashResponse::Push { index, message } => {
878                format!(
879                    "Saved working directory to stash@{{{}}}: {}",
880                    index, message
881                )
882            }
883            StashResponse::Pop { index, message } => {
884                format!("Applied and dropped stash@{{{}}}: {}", index, message)
885            }
886            StashResponse::Apply { index, message } => {
887                format!("Applied stash@{{{}}}: {}", index, message)
888            }
889            StashResponse::List { entries } => {
890                if entries.is_empty() {
891                    "No stash entries".to_string()
892                } else {
893                    entries
894                        .iter()
895                        .map(|e| format!("stash@{{{}}}: {}", e.index, e.message))
896                        .collect::<Vec<_>>()
897                        .join("\n")
898                }
899            }
900            StashResponse::Drop { index, message } => {
901                format!("Dropped stash@{{{}}}: {}", index, message)
902            }
903        }
904    }
905}
906
907#[derive(Debug, Serialize, Deserialize)]
908pub struct ResetResponse {
909    pub target: String,
910    pub mode: String,
911    pub message: String,
912}
913
914impl CommandResponse for ResetResponse {
915    fn command_name(&self) -> &'static str {
916        "reset"
917    }
918    fn human_readable(&self) -> String {
919        format!(
920            "HEAD is now at {} ({})\n{}",
921            self.target, self.mode, self.message
922        )
923    }
924}
925
926#[derive(Debug, Serialize, Deserialize)]
927pub struct RevertResponse {
928    pub reverted_commit: String,
929    pub new_commit: String,
930    pub files_changed: usize,
931    pub message: String,
932}
933
934impl CommandResponse for RevertResponse {
935    fn command_name(&self) -> &'static str {
936        "revert"
937    }
938    fn human_readable(&self) -> String {
939        format!(
940            "Reverted {}\nNew commit: {}\n{} file(s) changed\n{}",
941            self.reverted_commit, self.new_commit, self.files_changed, self.message
942        )
943    }
944}
945
946#[derive(Debug, Serialize, Deserialize)]
947pub struct CherryPickResponse {
948    pub source_commit: String,
949    pub new_commit: String,
950    pub files_changed: usize,
951    pub message: String,
952}
953
954impl CommandResponse for CherryPickResponse {
955    fn command_name(&self) -> &'static str {
956        "cherry-pick"
957    }
958    fn human_readable(&self) -> String {
959        format!(
960            "Cherry-picked {}\nNew commit: {}\n{} file(s) changed\n{}",
961            self.source_commit, self.new_commit, self.files_changed, self.message
962        )
963    }
964}
965
966#[derive(Debug, Serialize, Deserialize)]
967pub struct RebaseResponse {
968    pub rebased_commits: usize,
969    pub onto: String,
970    pub branch: String,
971    pub message: String,
972    #[serde(skip_serializing_if = "Option::is_none")]
973    pub todo: Option<serde_json::Value>,
974}
975
976impl CommandResponse for RebaseResponse {
977    fn command_name(&self) -> &'static str {
978        "rebase"
979    }
980    fn human_readable(&self) -> String {
981        let mut out = format!("{}\n", self.message);
982        if self.rebased_commits > 0 {
983            out.push_str(&format!(
984                "Rebased {} commit(s) onto {}\n",
985                self.rebased_commits, self.onto
986            ));
987        }
988        if let Some(ref todo) = self.todo {
989            out.push_str(&format!(
990                "Todo: {}\n",
991                serde_json::to_string_pretty(todo).unwrap_or_default()
992            ));
993        }
994        out
995    }
996}
997
998#[derive(Debug, Serialize, Deserialize)]
999pub struct BlameLineInfo {
1000    pub line_number: usize,
1001    pub content: String,
1002    pub commit_hash: String,
1003    pub author: String,
1004    pub timestamp: i64,
1005}
1006
1007#[derive(Debug, Serialize, Deserialize)]
1008pub struct BlameResponse {
1009    pub file: String,
1010    pub lines: Vec<BlameLineInfo>,
1011}
1012
1013impl CommandResponse for BlameResponse {
1014    fn command_name(&self) -> &'static str {
1015        "blame"
1016    }
1017    fn human_readable(&self) -> String {
1018        let mut out = format!("Blame for {}:\n", self.file);
1019        for line in &self.lines {
1020            out.push_str(&format!(
1021                "{} ({} {}) {}\n",
1022                &line.commit_hash[..8.min(line.commit_hash.len())],
1023                line.author,
1024                line.line_number,
1025                line.content
1026            ));
1027        }
1028        out
1029    }
1030}
1031
1032#[derive(Debug, Serialize, Deserialize)]
1033pub struct BisectResponse {
1034    pub action: String,
1035    pub current: Option<String>,
1036    pub remaining: usize,
1037    pub steps: usize,
1038    pub message: String,
1039}
1040
1041impl CommandResponse for BisectResponse {
1042    fn command_name(&self) -> &'static str {
1043        "bisect"
1044    }
1045    fn human_readable(&self) -> String {
1046        let mut out = format!("{}\n", self.message);
1047        if let Some(ref commit) = self.current {
1048            out.push_str(&format!("Current: {}\n", commit));
1049        }
1050        if self.remaining > 0 {
1051            out.push_str(&format!("~{} steps remaining\n", self.steps));
1052        }
1053        out
1054    }
1055}
1056
1057#[derive(Debug, Serialize, Deserialize)]
1058pub struct ReflogEntry {
1059    pub index: usize,
1060    pub old_hash: String,
1061    pub new_hash: String,
1062    pub action: String,
1063    pub message: String,
1064    pub timestamp: i64,
1065}
1066
1067#[derive(Debug, Serialize, Deserialize)]
1068pub struct ReflogResponse {
1069    pub ref_name: String,
1070    pub entries: Vec<ReflogEntry>,
1071}
1072
1073impl CommandResponse for ReflogResponse {
1074    fn command_name(&self) -> &'static str {
1075        "reflog"
1076    }
1077    fn human_readable(&self) -> String {
1078        let mut out = format!("Reflog for {}:\n", self.ref_name);
1079        for entry in &self.entries {
1080            out.push_str(&format!(
1081                "{}@{{{}}} {} -> {} {}: {}\n",
1082                self.ref_name,
1083                entry.index,
1084                &entry.old_hash[..8.min(entry.old_hash.len())],
1085                &entry.new_hash[..8.min(entry.new_hash.len())],
1086                entry.action,
1087                entry.message
1088            ));
1089        }
1090        out
1091    }
1092}
1093
1094// ============================================================================
1095// Phase 2 Response Types
1096// ============================================================================
1097
1098#[derive(Debug, Serialize, Deserialize)]
1099pub struct BatchOperationResult {
1100    pub index: usize,
1101    pub command: String,
1102    pub status: String,
1103    pub result: Option<serde_json::Value>,
1104    pub error: Option<String>,
1105}
1106
1107#[derive(Debug, Serialize, Deserialize)]
1108pub struct BatchResponse {
1109    pub total: usize,
1110    pub succeeded: usize,
1111    pub failed: usize,
1112    pub atomic: bool,
1113    pub dry_run: bool,
1114    pub results: Vec<BatchOperationResult>,
1115}
1116
1117impl CommandResponse for BatchResponse {
1118    fn command_name(&self) -> &'static str {
1119        "batch"
1120    }
1121    fn human_readable(&self) -> String {
1122        format!(
1123            "Batch complete: {}/{} succeeded, {} failed{}{}",
1124            self.succeeded,
1125            self.total,
1126            self.failed,
1127            if self.atomic { " (atomic)" } else { "" },
1128            if self.dry_run { " (dry-run)" } else { "" },
1129        )
1130    }
1131}
1132
1133#[derive(Debug, Serialize, Deserialize)]
1134pub struct TransactionResponse {
1135    pub action: String,
1136    pub tx_id: Option<String>,
1137    pub message: String,
1138}
1139
1140impl CommandResponse for TransactionResponse {
1141    fn command_name(&self) -> &'static str {
1142        "transaction"
1143    }
1144    fn human_readable(&self) -> String {
1145        if let Some(ref id) = self.tx_id {
1146            format!(
1147                "Transaction {}: {} [{}]",
1148                self.action,
1149                self.message,
1150                &id[..8.min(id.len())]
1151            )
1152        } else {
1153            format!("Transaction {}: {}", self.action, self.message)
1154        }
1155    }
1156}
1157
1158#[derive(Debug, Serialize, Deserialize)]
1159pub struct SnapshotResponse {
1160    pub hash: String,
1161    pub short_hash: String,
1162    pub tree: String,
1163    pub parent: Option<String>,
1164    pub author: String,
1165    pub message: String,
1166    pub timestamp: i64,
1167    pub files_added: usize,
1168}
1169
1170impl CommandResponse for SnapshotResponse {
1171    fn command_name(&self) -> &'static str {
1172        "snapshot"
1173    }
1174    fn human_readable(&self) -> String {
1175        format!(
1176            "[{}] Snapshot: {}\n  {} file(s) captured\n  Author: {}",
1177            self.short_hash, self.message, self.files_added, self.author,
1178        )
1179    }
1180}
1181
1182#[derive(Debug, Serialize, Deserialize)]
1183pub struct SearchMatch {
1184    pub file: String,
1185    pub line_number: usize,
1186    pub content: String,
1187    pub commit: Option<String>,
1188    pub match_type: String,
1189}
1190
1191#[derive(Debug, Serialize, Deserialize)]
1192pub struct SearchResponse {
1193    pub query: String,
1194    pub match_type: String,
1195    pub matches: Vec<SearchMatch>,
1196    pub total: usize,
1197}
1198
1199impl CommandResponse for SearchResponse {
1200    fn command_name(&self) -> &'static str {
1201        "search"
1202    }
1203    fn human_readable(&self) -> String {
1204        let mut out = format!("Search '{}': {} result(s)\n", self.query, self.total);
1205        for m in &self.matches {
1206            match m.match_type.as_str() {
1207                "content" => {
1208                    out.push_str(&format!(
1209                        "  {}:{}: {}\n",
1210                        m.file,
1211                        m.line_number,
1212                        m.content.trim()
1213                    ));
1214                }
1215                "message" => {
1216                    out.push_str(&format!(
1217                        "  commit {}: {}\n",
1218                        m.commit.as_deref().unwrap_or("?"),
1219                        m.content.trim()
1220                    ));
1221                }
1222                _ => {
1223                    out.push_str(&format!("  {}\n", m.content.trim()));
1224                }
1225            }
1226        }
1227        out
1228    }
1229}
1230
1231#[derive(Debug, Serialize, Deserialize)]
1232pub struct WatchEvent {
1233    pub event_type: String,
1234    pub path: String,
1235    pub timestamp: i64,
1236}
1237
1238#[derive(Debug, Serialize, Deserialize)]
1239pub struct WatchResponse {
1240    pub events_emitted: usize,
1241    pub message: String,
1242}
1243
1244impl CommandResponse for WatchResponse {
1245    fn command_name(&self) -> &'static str {
1246        "watch"
1247    }
1248    fn human_readable(&self) -> String {
1249        self.message.clone()
1250    }
1251}
1252
1253#[derive(Debug, Serialize, Deserialize)]
1254pub struct VerifyResult {
1255    pub check: String,
1256    pub status: String,
1257    pub details: Option<String>,
1258}
1259
1260#[derive(Debug, Serialize, Deserialize)]
1261pub struct VerifyResponse {
1262    pub valid: bool,
1263    pub checks: Vec<VerifyResult>,
1264    pub objects_checked: usize,
1265    pub refs_checked: usize,
1266    pub message: String,
1267}
1268
1269impl CommandResponse for VerifyResponse {
1270    fn command_name(&self) -> &'static str {
1271        "verify"
1272    }
1273    fn human_readable(&self) -> String {
1274        let mut out = format!("{}\n", self.message);
1275        for check in &self.checks {
1276            let icon = if check.status == "ok" { "+" } else { "!" };
1277            out.push_str(&format!("  [{}] {}", icon, check.check));
1278            if let Some(ref details) = check.details {
1279                out.push_str(&format!(": {}", details));
1280            }
1281            out.push('\n');
1282        }
1283        out.push_str(&format!(
1284            "  {} objects, {} refs checked\n",
1285            self.objects_checked, self.refs_checked
1286        ));
1287        out
1288    }
1289}
1290
1291// ============================================================================
1292// Phase 3 Response Types
1293// ============================================================================
1294
1295#[derive(Debug, Serialize, Deserialize)]
1296pub struct ServeResponse {
1297    pub message: String,
1298}
1299
1300impl CommandResponse for ServeResponse {
1301    fn command_name(&self) -> &'static str {
1302        "serve"
1303    }
1304    fn human_readable(&self) -> String {
1305        self.message.clone()
1306    }
1307}
1308
1309#[derive(Debug, Serialize, Deserialize)]
1310pub struct McpServeResponse {
1311    pub transport: String,
1312    pub message: String,
1313}
1314
1315impl CommandResponse for McpServeResponse {
1316    fn command_name(&self) -> &'static str {
1317        "mcp-serve"
1318    }
1319    fn human_readable(&self) -> String {
1320        format!("[{}] {}", self.transport, self.message)
1321    }
1322}
1323
1324#[derive(Debug, Serialize, Deserialize)]
1325pub struct SwarmResponse {
1326    pub action: String,
1327    pub agent_id: Option<String>,
1328    pub message: String,
1329    pub details: Option<serde_json::Value>,
1330}
1331
1332impl CommandResponse for SwarmResponse {
1333    fn command_name(&self) -> &'static str {
1334        "swarm"
1335    }
1336    fn human_readable(&self) -> String {
1337        let mut out = format!("Swarm {}: {}\n", self.action, self.message);
1338        if let Some(ref details) = self.details {
1339            out.push_str(&serde_json::to_string_pretty(details).unwrap_or_default());
1340        }
1341        out
1342    }
1343}
1344
1345#[derive(Debug, Serialize, Deserialize)]
1346pub struct OntologyResponse {
1347    pub ontology: serde_json::Value,
1348}
1349
1350impl CommandResponse for OntologyResponse {
1351    fn command_name(&self) -> &'static str {
1352        "ontology"
1353    }
1354    fn human_readable(&self) -> String {
1355        serde_json::to_string_pretty(&self.ontology).unwrap_or_else(|_| "{}".to_string())
1356    }
1357}
1358
1359#[derive(Debug, Serialize, Deserialize)]
1360pub struct SchemaResponse {
1361    pub schema: serde_json::Value,
1362}
1363
1364impl CommandResponse for SchemaResponse {
1365    fn command_name(&self) -> &'static str {
1366        "schema"
1367    }
1368    fn human_readable(&self) -> String {
1369        serde_json::to_string_pretty(&self.schema).unwrap_or_else(|_| "{}".to_string())
1370    }
1371}
1372
1373// ============================================================================
1374// Phase 4 Response Types (Git Interop)
1375// ============================================================================
1376
1377#[derive(Debug, Serialize, Deserialize)]
1378pub struct ImportGitResponse {
1379    pub source: String,
1380    pub objects_imported: u64,
1381    pub refs_imported: u64,
1382    pub hash_mapping_count: usize,
1383    pub message: String,
1384}
1385
1386impl CommandResponse for ImportGitResponse {
1387    fn command_name(&self) -> &'static str {
1388        "import-git"
1389    }
1390    fn human_readable(&self) -> String {
1391        format!(
1392            "{}\n  Objects imported: {}\n  Refs imported: {}\n  Hash mappings: {}",
1393            self.message, self.objects_imported, self.refs_imported, self.hash_mapping_count
1394        )
1395    }
1396}
1397
1398#[derive(Debug, Serialize, Deserialize)]
1399pub struct ExportGitResponse {
1400    pub destination: String,
1401    pub objects_exported: u64,
1402    pub refs_exported: u64,
1403    pub message: String,
1404}
1405
1406impl CommandResponse for ExportGitResponse {
1407    fn command_name(&self) -> &'static str {
1408        "export-git"
1409    }
1410    fn human_readable(&self) -> String {
1411        format!(
1412            "{}\n  Objects exported: {}\n  Refs exported: {}",
1413            self.message, self.objects_exported, self.refs_exported
1414        )
1415    }
1416}
1417
1418// ============================================================================
1419// Phase 5 Response Types (Performance)
1420// ============================================================================
1421
1422#[derive(Debug, Serialize, Deserialize)]
1423pub struct GcResponse {
1424    pub objects_packed: u64,
1425    pub packs_created: u64,
1426    pub loose_removed: u64,
1427    pub bytes_saved: u64,
1428    pub message: String,
1429}
1430
1431impl CommandResponse for GcResponse {
1432    fn command_name(&self) -> &'static str {
1433        "gc"
1434    }
1435    fn human_readable(&self) -> String {
1436        format!(
1437            "{}\n  Objects packed: {}\n  Packs created: {}\n  Loose removed: {}\n  Bytes saved: {}",
1438            self.message,
1439            self.objects_packed,
1440            self.packs_created,
1441            self.loose_removed,
1442            self.bytes_saved
1443        )
1444    }
1445}
1446
1447#[derive(Debug, Serialize, Deserialize)]
1448pub struct LfsTrackResponse {
1449    pub patterns: Vec<String>,
1450    pub message: String,
1451}
1452
1453impl CommandResponse for LfsTrackResponse {
1454    fn command_name(&self) -> &'static str {
1455        "lfs-track"
1456    }
1457    fn human_readable(&self) -> String {
1458        let mut out = format!("{}\n  Tracked patterns:\n", self.message);
1459        for pat in &self.patterns {
1460            out.push_str(&format!("    {}\n", pat));
1461        }
1462        out
1463    }
1464}
1465
1466#[derive(Debug, Serialize, Deserialize)]
1467pub struct LfsMigrateResponse {
1468    pub files_migrated: u64,
1469    pub bytes_saved: u64,
1470    pub message: String,
1471}
1472
1473impl CommandResponse for LfsMigrateResponse {
1474    fn command_name(&self) -> &'static str {
1475        "lfs-migrate"
1476    }
1477    fn human_readable(&self) -> String {
1478        format!(
1479            "{}\n  Files migrated: {}\n  Bytes saved: {}",
1480            self.message, self.files_migrated, self.bytes_saved
1481        )
1482    }
1483}
1484
1485// ============================================================================
1486// Sandbox Response
1487// ============================================================================
1488
1489#[derive(Debug, Serialize, Deserialize)]
1490pub struct SandboxResponse {
1491    pub action: String,
1492    pub name: String,
1493    pub path: String,
1494    pub message: String,
1495    pub output: Option<String>,
1496    pub exit_code: Option<i32>,
1497}
1498
1499impl CommandResponse for SandboxResponse {
1500    fn command_name(&self) -> &'static str {
1501        "sandbox"
1502    }
1503    fn human_readable(&self) -> String {
1504        let mut out = format!("{}\n", self.message);
1505        if let Some(ref text) = self.output {
1506            if !text.is_empty() {
1507                out.push_str(text);
1508                if !text.ends_with('\n') {
1509                    out.push('\n');
1510                }
1511            }
1512        }
1513        out
1514    }
1515}
1516
1517// ============================================================================
1518// Phase 6 Response Types (Decentralized Features)
1519// ============================================================================
1520
1521#[derive(Debug, Serialize, Deserialize)]
1522pub struct DidResponse {
1523    pub action: String,
1524    pub did: Option<String>,
1525    pub message: String,
1526    pub details: Option<serde_json::Value>,
1527}
1528
1529impl CommandResponse for DidResponse {
1530    fn command_name(&self) -> &'static str {
1531        "did"
1532    }
1533    fn human_readable(&self) -> String {
1534        let mut out = format!("{}\n", self.message);
1535        if let Some(ref did) = self.did {
1536            out.push_str(&format!("  DID: {}\n", did));
1537        }
1538        if let Some(ref d) = self.details {
1539            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1540            out.push('\n');
1541        }
1542        out
1543    }
1544}
1545
1546#[derive(Debug, Serialize, Deserialize)]
1547pub struct TrustResponse {
1548    pub action: String,
1549    pub did: Option<String>,
1550    pub score: Option<f64>,
1551    pub level: Option<String>,
1552    pub message: String,
1553    pub details: Option<serde_json::Value>,
1554}
1555
1556impl CommandResponse for TrustResponse {
1557    fn command_name(&self) -> &'static str {
1558        "trust"
1559    }
1560    fn human_readable(&self) -> String {
1561        let mut out = format!("{}\n", self.message);
1562        if let Some(ref did) = self.did {
1563            out.push_str(&format!("  Agent: {}\n", did));
1564        }
1565        if let Some(score) = self.score {
1566            out.push_str(&format!("  Score: {:.1}\n", score));
1567        }
1568        if let Some(ref level) = self.level {
1569            out.push_str(&format!("  Level: {}\n", level));
1570        }
1571        if let Some(ref d) = self.details {
1572            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1573            out.push('\n');
1574        }
1575        out
1576    }
1577}
1578
1579#[derive(Debug, Serialize, Deserialize)]
1580pub struct IssueResponse {
1581    pub action: String,
1582    pub id: Option<u64>,
1583    pub message: String,
1584    pub details: Option<serde_json::Value>,
1585}
1586
1587impl CommandResponse for IssueResponse {
1588    fn command_name(&self) -> &'static str {
1589        "issue"
1590    }
1591    fn human_readable(&self) -> String {
1592        let mut out = format!("{}\n", self.message);
1593        if let Some(id) = self.id {
1594            out.push_str(&format!("  Issue #{}\n", id));
1595        }
1596        if let Some(ref d) = self.details {
1597            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1598            out.push('\n');
1599        }
1600        out
1601    }
1602}
1603
1604#[derive(Debug, Serialize, Deserialize)]
1605pub struct PrResponse {
1606    pub action: String,
1607    pub id: Option<u64>,
1608    pub message: String,
1609    pub details: Option<serde_json::Value>,
1610}
1611
1612impl CommandResponse for PrResponse {
1613    fn command_name(&self) -> &'static str {
1614        "pr"
1615    }
1616    fn human_readable(&self) -> String {
1617        let mut out = format!("{}\n", self.message);
1618        if let Some(id) = self.id {
1619            out.push_str(&format!("  PR #{}\n", id));
1620        }
1621        if let Some(ref d) = self.details {
1622            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1623            out.push('\n');
1624        }
1625        out
1626    }
1627}
1628
1629#[derive(Debug, Serialize, Deserialize)]
1630pub struct SubscribeResponse {
1631    pub action: String,
1632    pub subscription_id: Option<String>,
1633    pub message: String,
1634    pub details: Option<serde_json::Value>,
1635}
1636
1637impl CommandResponse for SubscribeResponse {
1638    fn command_name(&self) -> &'static str {
1639        "subscribe"
1640    }
1641    fn human_readable(&self) -> String {
1642        let mut out = format!("{}\n", self.message);
1643        if let Some(ref id) = self.subscription_id {
1644            out.push_str(&format!("  Subscription: {}\n", id));
1645        }
1646        if let Some(ref d) = self.details {
1647            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1648            out.push('\n');
1649        }
1650        out
1651    }
1652}
1653
1654#[derive(Debug, Serialize, Deserialize)]
1655pub struct DelegateResponse {
1656    pub action: String,
1657    pub task_id: Option<String>,
1658    pub message: String,
1659    pub details: Option<serde_json::Value>,
1660}
1661
1662impl CommandResponse for DelegateResponse {
1663    fn command_name(&self) -> &'static str {
1664        "delegate"
1665    }
1666    fn human_readable(&self) -> String {
1667        let mut out = format!("{}\n", self.message);
1668        if let Some(ref id) = self.task_id {
1669            out.push_str(&format!("  Task: {}\n", id));
1670        }
1671        if let Some(ref d) = self.details {
1672            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1673            out.push('\n');
1674        }
1675        out
1676    }
1677}
1678
1679#[derive(Debug, Serialize, Deserialize)]
1680pub struct FederationResponse {
1681    pub action: String,
1682    pub message: String,
1683    pub details: Option<serde_json::Value>,
1684}
1685
1686impl CommandResponse for FederationResponse {
1687    fn command_name(&self) -> &'static str {
1688        "federation"
1689    }
1690    fn human_readable(&self) -> String {
1691        let mut out = format!("{}\n", self.message);
1692        if let Some(ref d) = self.details {
1693            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1694            out.push('\n');
1695        }
1696        out
1697    }
1698}
1699
1700#[derive(Debug, Serialize, Deserialize)]
1701pub struct UcanResponse {
1702    pub action: String,
1703    pub token_cid: Option<String>,
1704    pub message: String,
1705    pub details: Option<serde_json::Value>,
1706}
1707
1708impl CommandResponse for UcanResponse {
1709    fn command_name(&self) -> &'static str {
1710        "ucan"
1711    }
1712    fn human_readable(&self) -> String {
1713        let mut out = format!("{}\n", self.message);
1714        if let Some(ref cid) = self.token_cid {
1715            out.push_str(&format!("  Token CID: {}\n", cid));
1716        }
1717        if let Some(ref d) = self.details {
1718            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1719            out.push('\n');
1720        }
1721        out
1722    }
1723}
1724
1725// ── Intent / Converge response types ────────────────────────────────────────
1726
1727#[derive(Debug, Serialize, Deserialize)]
1728pub struct IntentResponse {
1729    pub action: String,
1730    pub intent_id: Option<String>,
1731    pub message: String,
1732    pub details: Option<serde_json::Value>,
1733}
1734
1735impl CommandResponse for IntentResponse {
1736    fn command_name(&self) -> &'static str {
1737        "intent"
1738    }
1739    fn human_readable(&self) -> String {
1740        let mut out = format!("{}\n", self.message);
1741        if let Some(ref id) = self.intent_id {
1742            out.push_str(&format!("  Intent: {}\n", id));
1743        }
1744        if let Some(ref d) = self.details {
1745            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1746            out.push('\n');
1747        }
1748        out
1749    }
1750}
1751
1752#[derive(Debug, Serialize, Deserialize)]
1753pub struct ConvergeResponse {
1754    pub converged: bool,
1755    pub strategy: String,
1756    pub intent_id: String,
1757    pub intent_title: String,
1758    pub commit_hash: Option<String>,
1759    pub commits_converged: usize,
1760    pub fast_forward: bool,
1761    pub message: String,
1762    pub details: Option<serde_json::Value>,
1763}
1764
1765impl CommandResponse for ConvergeResponse {
1766    fn command_name(&self) -> &'static str {
1767        "converge"
1768    }
1769    fn human_readable(&self) -> String {
1770        let mut out = format!("{}\n", self.message);
1771        if let Some(ref h) = self.commit_hash {
1772            out.push_str(&format!("  Commit: {}\n", h));
1773        }
1774        out.push_str(&format!("  Strategy: {}\n", self.strategy));
1775        out.push_str(&format!("  Fast-forward: {}\n", self.fast_forward));
1776        if let Some(ref d) = self.details {
1777            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1778            out.push('\n');
1779        }
1780        out
1781    }
1782}
1783
1784// ── Content Type Response ───────────────────────────────────────────────────
1785
1786#[derive(Debug, Serialize, Deserialize)]
1787pub struct ContentTypeResponse {
1788    pub action: String,
1789    pub content_type_id: Option<String>,
1790    pub message: String,
1791    pub details: Option<serde_json::Value>,
1792}
1793
1794impl CommandResponse for ContentTypeResponse {
1795    fn command_name(&self) -> &'static str {
1796        "content-type"
1797    }
1798    fn human_readable(&self) -> String {
1799        let mut out = format!("{}\n", self.message);
1800        if let Some(ref id) = self.content_type_id {
1801            out.push_str(&format!("  Content-Type: {}\n", id));
1802        }
1803        if let Some(ref d) = self.details {
1804            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1805            out.push('\n');
1806        }
1807        out
1808    }
1809}
1810
1811// ── Datacenter Response ─────────────────────────────────────────────────────
1812
1813#[derive(Debug, Serialize, Deserialize)]
1814pub struct DatacenterResponse {
1815    pub action: String,
1816    pub message: String,
1817    pub details: Option<serde_json::Value>,
1818}
1819
1820impl CommandResponse for DatacenterResponse {
1821    fn command_name(&self) -> &'static str {
1822        "datacenter"
1823    }
1824    fn human_readable(&self) -> String {
1825        let mut out = format!("{}\n", self.message);
1826        if let Some(ref d) = self.details {
1827            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1828            out.push('\n');
1829        }
1830        out
1831    }
1832}
1833
1834// ── Agent Profile Response ──────────────────────────────────────────────────
1835
1836#[derive(Debug, Serialize, Deserialize)]
1837pub struct AgentProfileResponse {
1838    pub action: String,
1839    pub profile_id: Option<String>,
1840    pub message: String,
1841    pub details: Option<serde_json::Value>,
1842}
1843
1844impl CommandResponse for AgentProfileResponse {
1845    fn command_name(&self) -> &'static str {
1846        "agent-profile"
1847    }
1848    fn human_readable(&self) -> String {
1849        let mut out = format!("{}\n", self.message);
1850        if let Some(ref id) = self.profile_id {
1851            out.push_str(&format!("  Profile: {}\n", id));
1852        }
1853        if let Some(ref d) = self.details {
1854            out.push_str(&serde_json::to_string_pretty(d).unwrap_or_default());
1855            out.push('\n');
1856        }
1857        out
1858    }
1859}