Skip to main content

memvid_cli/
error.rs

1//! CLI-specific error types and rendering helpers.
2
3use std::fmt;
4
5#[cfg(feature = "encryption")]
6use memvid_core::encryption::EncryptionError;
7use memvid_core::MemvidError;
8
9use crate::utils::format_bytes;
10
11/// Error indicating that the memory has reached its capacity limit
12#[derive(Debug)]
13pub struct CapacityExceededMessage {
14    pub current: u64,
15    pub limit: u64,
16    pub required: u64,
17}
18
19impl fmt::Display for CapacityExceededMessage {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        write!(
22            f,
23            "Storage capacity exceeded\n\n\
24             Current usage: {}\n\
25             Capacity limit: {}\n\
26             Required: {}\n\n\
27             To continue, either:\n\
28             1. Upgrade your plan: https://app.memvid.com/plan\n\
29             2. Sync tickets: memvid tickets sync <file> --memory-id <UUID>",
30            format_bytes(self.current),
31            format_bytes(self.limit),
32            format_bytes(self.required)
33        )
34    }
35}
36
37impl std::error::Error for CapacityExceededMessage {}
38
39/// Error indicating that an API key is required for large files
40#[derive(Debug)]
41pub struct ApiKeyRequiredMessage {
42    pub file_size: u64,
43    pub limit: u64,
44}
45
46impl fmt::Display for ApiKeyRequiredMessage {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(
49            f,
50            "API key required for files larger than {}\n\n\
51             File size: {}\n\
52             Free tier limit: {}\n\n\
53             To use this file:\n\
54             1. Get your API key from https://app.memvid.com/api\n\
55             2. Sync tickets: memvid tickets sync <file> --memory-id <UUID>",
56            format_bytes(self.limit),
57            format_bytes(self.file_size),
58            format_bytes(self.limit)
59        )
60    }
61}
62
63impl std::error::Error for ApiKeyRequiredMessage {}
64
65/// Error indicating that a memory is already bound to a different dashboard memory
66#[derive(Debug)]
67pub struct MemoryAlreadyBoundMessage {
68    pub existing_memory_id: String,
69    pub existing_memory_name: String,
70    pub bound_at: String,
71}
72
73impl fmt::Display for MemoryAlreadyBoundMessage {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(
76            f,
77            "This file is already bound to memory '{}' ({})\n\
78             Bound at: {}\n\n\
79             Each memory can only be bound to one file.\n\
80             To use more memories, upgrade your plan at https://memvid.com/dashboard/plan",
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    #[cfg(feature = "encryption")]
120    {
121        let enc = err
122            .chain()
123            .find_map(|cause| cause.downcast_ref::<EncryptionError>());
124        if let Some(enc_err) = enc {
125            let message = match enc_err {
126                EncryptionError::InvalidMagic { .. } => format!(
127                    "{enc_err}\nHint: is this an encrypted .mv2e capsule and not a plain .mv2 file?"
128                ),
129                _ => enc_err.to_string(),
130            };
131            return (5, message);
132        }
133    }
134
135    // Bubble up core errors even when wrapped in anyhow context.
136    let core = err
137        .chain()
138        .find_map(|cause| cause.downcast_ref::<MemvidError>());
139    if let Some(core_err) = core {
140        match core_err {
141            MemvidError::CapacityExceeded {
142                current,
143                limit,
144                required,
145            } => {
146                let msg = CapacityExceededMessage {
147                    current: *current,
148                    limit: *limit,
149                    required: *required,
150                }
151                .to_string();
152                return (2, msg);
153            }
154            MemvidError::ApiKeyRequired { file_size, limit } => {
155                let msg = ApiKeyRequiredMessage {
156                    file_size: *file_size,
157                    limit: *limit,
158                }
159                .to_string();
160                return (2, msg);
161            }
162            MemvidError::Lock(reason) => {
163                return (3, format!("File lock error: {reason}\nHint: check the active writer with `memvid who <file>` or request release with `memvid nudge <file>`"));
164            }
165            MemvidError::Locked(boxed_err) => {
166                let message = &boxed_err.message;
167                return (3, format!("File lock error: {message}\nHint: check the active writer with `memvid who <file>` or request release with `memvid nudge <file>`"));
168            }
169            MemvidError::InvalidHeader { reason } => {
170                return (4, format!("{core_err}\nHint: run `memvid doctor <file>` to rebuild indexes and repair the footer.\nDetails: {reason}"));
171            }
172            MemvidError::EncryptedFile { .. } => {
173                return (5, core_err.to_string());
174            }
175            MemvidError::InvalidToc { reason } => {
176                return (4, format!("{core_err}\nHint: run `memvid doctor <file>` to rebuild indexes and repair the footer.\nDetails: {reason}"));
177            }
178            MemvidError::WalCorruption { reason, .. } => {
179                return (4, format!("{core_err}\nHint: run `memvid doctor <file>` to rebuild indexes and repair the footer.\nDetails: {reason}"));
180            }
181            MemvidError::ManifestWalCorrupted { reason, .. } => {
182                return (4, format!("{core_err}\nHint: run `memvid doctor <file>` to rebuild indexes and repair the footer.\nDetails: {reason}"));
183            }
184            MemvidError::TicketRequired { tier } => {
185                return (2, format!("ticket required for tier {tier:?}. Apply a ticket before mutating this memory."));
186            }
187            _ => {
188                return (1, core_err.to_string());
189            }
190        }
191    }
192
193    // Fallback: generic error text, non-zero exit.
194    (1, err.to_string())
195}