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             Each memory can only be bound to one file.\n\
81             To use more memories, upgrade your plan at https://memvid.com/dashboard/plan",
82            self.existing_memory_name, self.existing_memory_id, self.bound_at
83        )
84    }
85}
86
87impl std::error::Error for MemoryAlreadyBoundMessage {}
88
89/// Error indicating that a frame with the same URI already exists
90#[derive(Debug)]
91pub struct DuplicateUriError {
92    uri: String,
93}
94
95impl DuplicateUriError {
96    pub fn new<S: Into<String>>(uri: S) -> Self {
97        Self { uri: uri.into() }
98    }
99}
100
101impl fmt::Display for DuplicateUriError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(
104            f,
105            "frame with URI '{}' already exists. Use --update-existing to replace it or --allow-duplicate to keep both entries.",
106            self.uri
107        )
108    }
109}
110
111impl std::error::Error for DuplicateUriError {}
112
113/// Render a fallible CLI error into a stable exit code + human message.
114pub fn render_error(err: &anyhow::Error) -> (i32, String) {
115    // Prefer richer, user-friendly wrappers when present.
116    if let Some(cap) = err.downcast_ref::<CapacityExceededMessage>() {
117        return (2, cap.to_string());
118    }
119
120    #[cfg(feature = "encryption")]
121    {
122        let enc = err
123            .chain()
124            .find_map(|cause| cause.downcast_ref::<EncryptionError>());
125        if let Some(enc_err) = enc {
126            let message = match enc_err {
127                EncryptionError::InvalidMagic { .. } => format!(
128                    "{enc_err}\nHint: is this an encrypted .mv2e capsule and not a plain .mv2 file?"
129                ),
130                _ => enc_err.to_string(),
131            };
132            return (5, message);
133        }
134    }
135
136    // Bubble up core errors even when wrapped in anyhow context.
137    let core = err
138        .chain()
139        .find_map(|cause| cause.downcast_ref::<MemvidError>());
140    if let Some(core_err) = core {
141        match core_err {
142            MemvidError::CapacityExceeded {
143                current,
144                limit,
145                required,
146            } => {
147                let msg = CapacityExceededMessage {
148                    current: *current,
149                    limit: *limit,
150                    required: *required,
151                }
152                .to_string();
153                return (2, msg);
154            }
155            MemvidError::ApiKeyRequired { file_size, limit } => {
156                let msg = ApiKeyRequiredMessage {
157                    file_size: *file_size,
158                    limit: *limit,
159                }
160                .to_string();
161                return (2, msg);
162            }
163            MemvidError::Lock(reason) => {
164                return (3, format!("File lock error: {reason}\nHint: check the active writer with `memvid who <file>` or request release with `memvid nudge <file>`"));
165            }
166            MemvidError::Locked(LockedError { 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}