1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum OutputFormat {
6 Json,
7 Human,
8}
9
10impl OutputFormat {
11 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 match std::env::var("LIT_OUTPUT").as_deref() {
21 Ok("human") => OutputFormat::Human,
22 _ => OutputFormat::Json, }
24 }
25}
26
27#[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
36pub trait CommandResponse: Serialize {
38 fn command_name(&self) -> &'static str;
40
41 fn human_readable(&self) -> String;
43
44 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 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
72pub 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
80pub 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#[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 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(); }
669
670 let mut out = String::new();
671
672 if self.stat_only {
673 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 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}