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