memvid_cli/
error.rs

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