memvid_cli/
error.rs

1//! CLI-specific error types and rendering helpers.
2
3use std::fmt;
4
5use memvid_core::error::LockedError;
6use memvid_core::MemvidError;
7
8use crate::utils::format_bytes;
9
10/// Error indicating that the memory has reached its capacity limit
11#[derive(Debug)]
12pub struct CapacityExceededMessage {
13    pub current: u64,
14    pub limit: u64,
15    pub required: u64,
16}
17
18impl fmt::Display for CapacityExceededMessage {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        write!(
21            f,
22            "Storage capacity exceeded\n\n\
23             Current usage: {}\n\
24             Capacity limit: {}\n\
25             Required: {}\n\n\
26             To continue, either:\n\
27             1. Upgrade your plan: https://app.memvid.com/plan\n\
28             2. Sync tickets: memvid tickets sync <file> --memory-id <UUID>",
29            format_bytes(self.current),
30            format_bytes(self.limit),
31            format_bytes(self.required)
32        )
33    }
34}
35
36impl std::error::Error for CapacityExceededMessage {}
37
38/// Error indicating that an API key is required for large files
39#[derive(Debug)]
40pub struct ApiKeyRequiredMessage {
41    pub file_size: u64,
42    pub limit: u64,
43}
44
45impl fmt::Display for ApiKeyRequiredMessage {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(
48            f,
49            "API key required for files larger than {}\n\n\
50             File size: {}\n\
51             Free tier limit: {}\n\n\
52             To use this file:\n\
53             1. Get your API key from https://app.memvid.com/api\n\
54             2. Sync tickets: memvid tickets sync <file> --memory-id <UUID>",
55            format_bytes(self.limit),
56            format_bytes(self.file_size),
57            format_bytes(self.limit)
58        )
59    }
60}
61
62impl std::error::Error for ApiKeyRequiredMessage {}
63
64/// Error indicating that a memory is already bound to a different dashboard memory
65#[derive(Debug)]
66pub struct MemoryAlreadyBoundMessage {
67    pub existing_memory_id: String,
68    pub existing_memory_name: String,
69    pub bound_at: String,
70}
71
72impl fmt::Display for MemoryAlreadyBoundMessage {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(
75            f,
76            "This file is already bound to memory '{}' ({})\n\
77             Bound at: {}\n\n\
78             To rebind to a different memory:\n\
79             memvid unbind <file>\n\
80             memvid tickets sync <file> --memory-id <NEW_UUID>",
81            self.existing_memory_name, self.existing_memory_id, self.bound_at
82        )
83    }
84}
85
86impl std::error::Error for MemoryAlreadyBoundMessage {}
87
88/// Error indicating that a frame with the same URI already exists
89#[derive(Debug)]
90pub struct DuplicateUriError {
91    uri: String,
92}
93
94impl DuplicateUriError {
95    pub fn new<S: Into<String>>(uri: S) -> Self {
96        Self { uri: uri.into() }
97    }
98}
99
100impl fmt::Display for DuplicateUriError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(
103            f,
104            "frame with URI '{}' already exists. Use --update-existing to replace it or --allow-duplicate to keep both entries.",
105            self.uri
106        )
107    }
108}
109
110impl std::error::Error for DuplicateUriError {}
111
112/// Render a fallible CLI error into a stable exit code + human message.
113pub fn render_error(err: &anyhow::Error) -> (i32, String) {
114    // Prefer richer, user-friendly wrappers when present.
115    if let Some(cap) = err.downcast_ref::<CapacityExceededMessage>() {
116        return (2, cap.to_string());
117    }
118
119    // Bubble up core errors even when wrapped in anyhow context.
120    let core = err
121        .chain()
122        .find_map(|cause| cause.downcast_ref::<MemvidError>());
123    if let Some(core_err) = core {
124        match core_err {
125            MemvidError::CapacityExceeded {
126                current,
127                limit,
128                required,
129            } => {
130                let msg = CapacityExceededMessage {
131                    current: *current,
132                    limit: *limit,
133                    required: *required,
134                }
135                .to_string();
136                return (2, msg);
137            }
138            MemvidError::ApiKeyRequired { file_size, limit } => {
139                let msg = ApiKeyRequiredMessage {
140                    file_size: *file_size,
141                    limit: *limit,
142                }
143                .to_string();
144                return (2, msg);
145            }
146            MemvidError::Lock(reason) => {
147                return (3, format!("File lock error: {reason}\nHint: check the active writer with `memvid who <file>` or request release with `memvid nudge <file>`"));
148            }
149            MemvidError::Locked(LockedError { message, .. }) => {
150                return (3, format!("File lock error: {message}\nHint: check the active writer with `memvid who <file>` or request release with `memvid nudge <file>`"));
151            }
152            MemvidError::InvalidHeader { reason } => {
153                return (4, format!("{core_err}\nHint: run `memvid doctor <file>` to rebuild indexes and repair the footer.\nDetails: {reason}"));
154            }
155            MemvidError::InvalidToc { reason } => {
156                return (4, format!("{core_err}\nHint: run `memvid doctor <file>` to rebuild indexes and repair the footer.\nDetails: {reason}"));
157            }
158            MemvidError::WalCorruption { reason, .. } => {
159                return (4, format!("{core_err}\nHint: run `memvid doctor <file>` to rebuild indexes and repair the footer.\nDetails: {reason}"));
160            }
161            MemvidError::ManifestWalCorrupted { reason, .. } => {
162                return (4, format!("{core_err}\nHint: run `memvid doctor <file>` to rebuild indexes and repair the footer.\nDetails: {reason}"));
163            }
164            MemvidError::TicketRequired { tier } => {
165                return (2, format!("ticket required for tier {tier:?}. Apply a ticket before mutating this memory."));
166            }
167            _ => {
168                return (1, core_err.to_string());
169            }
170        }
171    }
172
173    // Fallback: generic error text, non-zero exit.
174    (1, err.to_string())
175}