memvid_cli/commands/
plan.rs

1//! Plan management commands (show, sync)
2//!
3//! Commands for viewing and syncing plan/subscription information.
4
5use anyhow::Result;
6use clap::{Args, Subcommand};
7use serde_json::json;
8
9use crate::config::CliConfig;
10use crate::org_ticket_cache;
11use crate::utils::{format_bytes, FREE_TIER_MAX_FILE_SIZE};
12
13/// Plan management commands
14#[derive(Subcommand)]
15pub enum PlanCommand {
16    /// Show current plan and capacity
17    Show(PlanShowArgs),
18    /// Sync plan ticket from the dashboard
19    Sync(PlanSyncArgs),
20    /// Clear cached plan ticket
21    Clear(PlanClearArgs),
22}
23
24/// Arguments for the `plan` command
25#[derive(Args)]
26pub struct PlanArgs {
27    #[command(subcommand)]
28    pub command: PlanCommand,
29}
30
31/// Arguments for the `plan show` subcommand
32#[derive(Args)]
33pub struct PlanShowArgs {
34    /// Output as JSON
35    #[arg(long)]
36    pub json: bool,
37}
38
39/// Arguments for the `plan sync` subcommand
40#[derive(Args)]
41pub struct PlanSyncArgs {
42    /// Output as JSON
43    #[arg(long)]
44    pub json: bool,
45}
46
47/// Arguments for the `plan clear` subcommand
48#[derive(Args)]
49pub struct PlanClearArgs {
50    /// Output as JSON
51    #[arg(long)]
52    pub json: bool,
53}
54
55pub fn handle_plan(config: &CliConfig, args: PlanArgs) -> Result<()> {
56    match args.command {
57        PlanCommand::Show(show) => handle_plan_show(config, show),
58        PlanCommand::Sync(sync) => handle_plan_sync(config, sync),
59        PlanCommand::Clear(clear) => handle_plan_clear(config, clear),
60    }
61}
62
63fn handle_plan_show(config: &CliConfig, args: PlanShowArgs) -> Result<()> {
64    if config.api_key.is_none() {
65        if args.json {
66            let output = json!({
67                "plan": "free",
68                "capacity_bytes": FREE_TIER_MAX_FILE_SIZE,
69                "capacity": format_bytes(FREE_TIER_MAX_FILE_SIZE),
70                "features": ["core", "temporal_track", "clip", "whisper", "temporal_enrich"],
71                "authenticated": false,
72            });
73            println!("{}", serde_json::to_string_pretty(&output)?);
74        } else {
75            println!("Plan: Free (not authenticated)");
76            println!("Capacity: {}", format_bytes(FREE_TIER_MAX_FILE_SIZE));
77            println!();
78            println!("To unlock more capacity, set your API key:");
79            println!("  export MEMVID_API_KEY=your_api_key");
80            println!();
81            println!("Get your API key at: https://memvid.com/dashboard/api-keys");
82        }
83        return Ok(());
84    }
85
86    // Try to get cached ticket or show info to sync
87    match org_ticket_cache::get_or_refresh(config) {
88        Ok(cached) => {
89            if args.json {
90                let output = json!({
91                    "plan": cached.plan_id,
92                    "plan_name": cached.plan_name,
93                    "capacity_bytes": cached.capacity_bytes(),
94                    "capacity": format_bytes(cached.capacity_bytes()),
95                    "features": cached.ticket.features,
96                    "org_id": cached.org_id,
97                    "org_name": cached.org_name,
98                    "total_storage_bytes": cached.total_storage_bytes,
99                    "total_storage": format_bytes(cached.total_storage_bytes),
100                    "subscription_status": cached.subscription_status,
101                    "expires_at": cached.ticket.expires_at,
102                    "expires_in_secs": cached.ticket.expires_in_secs(),
103                    "authenticated": true,
104                });
105                println!("{}", serde_json::to_string_pretty(&output)?);
106            } else {
107                println!("Plan: {}", cached.plan_name);
108                println!("Organisation: {}", cached.org_name);
109                println!("Subscription: {}", cached.subscription_status);
110                println!();
111                println!("Capacity: {}", format_bytes(cached.capacity_bytes()));
112                println!("Storage Used: {}", format_bytes(cached.total_storage_bytes));
113                println!();
114                println!("Features: {}", cached.ticket.features.join(", "));
115                println!();
116                let expires_in = cached.ticket.expires_in_secs();
117                if expires_in > 0 {
118                    let hours = expires_in / 3600;
119                    let mins = (expires_in % 3600) / 60;
120                    println!("Ticket expires in: {}h {}m", hours, mins);
121                } else {
122                    println!("Ticket expired (run `memvid plan sync` to refresh)");
123                }
124            }
125        }
126        Err(err) => {
127            if args.json {
128                let output = json!({
129                    "error": err.to_string(),
130                    "plan": "unknown",
131                    "authenticated": true,
132                });
133                println!("{}", serde_json::to_string_pretty(&output)?);
134            } else {
135                println!("Failed to get plan information: {}", err);
136                println!();
137                println!("Try syncing your plan ticket:");
138                println!("  memvid plan sync");
139            }
140        }
141    }
142
143    Ok(())
144}
145
146fn handle_plan_sync(config: &CliConfig, args: PlanSyncArgs) -> Result<()> {
147    if config.api_key.is_none() {
148        anyhow::bail!(
149            "API key required. Set it with:\n  export MEMVID_API_KEY=your_api_key\n\n\
150             Get your API key at: https://memvid.com/dashboard/api-keys"
151        );
152    }
153
154    let cached = org_ticket_cache::refresh(config)?;
155
156    if args.json {
157        let output = json!({
158            "success": true,
159            "plan": cached.plan_id,
160            "plan_name": cached.plan_name,
161            "capacity_bytes": cached.capacity_bytes(),
162            "capacity": format_bytes(cached.capacity_bytes()),
163            "features": cached.ticket.features,
164            "org_id": cached.org_id,
165            "org_name": cached.org_name,
166            "subscription_status": cached.subscription_status,
167            "expires_at": cached.ticket.expires_at,
168        });
169        println!("{}", serde_json::to_string_pretty(&output)?);
170    } else {
171        println!("✓ Plan synced successfully");
172        println!();
173        println!("Plan: {}", cached.plan_name);
174        println!("Organisation: {}", cached.org_name);
175        println!("Capacity: {}", format_bytes(cached.capacity_bytes()));
176        println!("Subscription: {}", cached.subscription_status);
177        println!();
178        let expires_in = cached.ticket.expires_in_secs();
179        let hours = expires_in / 3600;
180        let mins = (expires_in % 3600) / 60;
181        println!("Ticket valid for: {}h {}m", hours, mins);
182    }
183
184    Ok(())
185}
186
187fn handle_plan_clear(config: &CliConfig, args: PlanClearArgs) -> Result<()> {
188    org_ticket_cache::clear(config)?;
189
190    if args.json {
191        let output = json!({
192            "success": true,
193            "message": "Plan ticket cache cleared",
194        });
195        println!("{}", serde_json::to_string_pretty(&output)?);
196    } else {
197        println!("✓ Plan ticket cache cleared");
198    }
199
200    Ok(())
201}