microsandbox_cli/args/
msb.rs

1use std::{error::Error, path::PathBuf};
2
3use crate::styles;
4use clap::Parser;
5use microsandbox_core::oci::Reference;
6use typed_path::Utf8UnixPathBuf;
7
8//-------------------------------------------------------------------------------------------------
9// Types
10//-------------------------------------------------------------------------------------------------
11
12/// msb (microsandbox) is a tool for managing lightweight sandboxes and images
13#[derive(Debug, Parser)]
14#[command(name = "msb", author, styles=styles::styles())]
15pub struct MicrosandboxArgs {
16    /// The subcommand to run
17    #[command(subcommand)]
18    pub subcommand: Option<MicrosandboxSubcommand>,
19
20    /// Show version
21    #[arg(short = 'V', long, global = true)]
22    pub version: bool,
23
24    /// Show logs with error level
25    #[arg(long, global = true)]
26    pub error: bool,
27
28    /// Show logs with warn level
29    #[arg(long, global = true)]
30    pub warn: bool,
31
32    /// Show logs with info level
33    #[arg(long, global = true)]
34    pub info: bool,
35
36    /// Show logs with debug level
37    #[arg(long, global = true)]
38    pub debug: bool,
39
40    /// Show logs with trace level
41    #[arg(long, global = true)]
42    pub trace: bool,
43}
44
45/// Available subcommands for managing services
46#[derive(Debug, Parser)]
47pub enum MicrosandboxSubcommand {
48    /// Initialize a new microsandbox project
49    #[command(name = "init")]
50    Init {
51        /// Path to the sandbox file or the project directory
52        #[arg(short, long)]
53        file: Option<PathBuf>,
54    },
55
56    /// Add a new sandbox to the project
57    #[command(name = "add")]
58    Add {
59        /// Whether command should apply to a sandbox
60        #[arg(short, long)]
61        sandbox: bool,
62
63        /// Whether command should apply to a build sandbox
64        #[arg(short, long)]
65        build: bool,
66
67        /// Whether command should apply to a group
68        #[arg(short, long)]
69        group: bool,
70
71        /// Names of components to add
72        #[arg(required = true)]
73        names: Vec<String>,
74
75        /// Image to use
76        #[arg(short, long)]
77        image: String,
78
79        /// Memory in MiB
80        #[arg(long)]
81        memory: Option<u32>,
82
83        /// Number of CPUs
84        #[arg(long, alias = "cpu")]
85        cpus: Option<u32>,
86
87        /// Volume mappings, format: <host_path>:<container_path>
88        #[arg(short, long = "volume", name = "VOLUME")]
89        volumes: Vec<String>,
90
91        /// Port mappings, format: <host_port>:<container_port>
92        #[arg(short, long = "port", name = "PORT")]
93        ports: Vec<String>,
94
95        /// Environment variables, format: <key>=<value>
96        #[arg(long = "env", name = "ENV")]
97        envs: Vec<String>,
98
99        /// Environment file
100        #[arg(long)]
101        env_file: Option<Utf8UnixPathBuf>,
102
103        /// Dependencies
104        #[arg(long)]
105        depends_on: Vec<String>,
106
107        /// Working directory
108        #[arg(long)]
109        workdir: Option<Utf8UnixPathBuf>,
110
111        /// Shell to use
112        #[arg(long)]
113        shell: Option<String>,
114
115        /// Scripts to add
116        #[arg(long = "script", name = "SCRIPT", value_parser = parse_key_val::<String, String>)]
117        scripts: Vec<(String, String)>,
118
119        /// Start script
120        #[arg(long)]
121        start: Option<String>,
122
123        /// Files to import, format: <name>=<path>
124        #[arg(long = "import", name = "IMPORT", value_parser = parse_key_val::<String, String>)]
125        imports: Vec<(String, String)>,
126
127        /// Files to export, format: <name>=<path>
128        #[arg(long = "export", name = "EXPORT", value_parser = parse_key_val::<String, String>)]
129        exports: Vec<(String, String)>,
130
131        /// Network scope, options: local, public, any, none
132        #[arg(long)]
133        scope: Option<String>,
134
135        /// Path to the sandbox file or the project directory
136        #[arg(short, long)]
137        file: Option<PathBuf>,
138    },
139
140    /// Remove a sandbox from the project
141    #[command(name = "remove", alias = "rm")]
142    Remove {
143        /// Whether command should apply to a sandbox
144        #[arg(short, long)]
145        sandbox: bool,
146
147        /// Whether command should apply to a build sandbox
148        #[arg(short, long)]
149        build: bool,
150
151        /// Whether command should apply to a group
152        #[arg(short, long)]
153        group: bool,
154
155        /// Names of components to remove
156        #[arg(required = true)]
157        names: Vec<String>,
158
159        /// Path to the sandbox file or the project directory
160        #[arg(short, long)]
161        file: Option<PathBuf>,
162    },
163
164    /// List sandboxes defined in the project
165    #[command(name = "list")]
166    List {
167        /// Whether command should apply to a sandbox
168        #[arg(short, long)]
169        sandbox: bool,
170
171        /// Whether command should apply to a build sandbox
172        #[arg(short, long)]
173        build: bool,
174
175        /// Whether command should apply to a group
176        #[arg(short, long)]
177        group: bool,
178
179        /// Path to the sandbox file or the project directory
180        #[arg(short, long)]
181        file: Option<PathBuf>,
182    },
183
184    /// Show logs of a build, sandbox, or group
185    #[command(name = "log")]
186    Log {
187        /// Whether command should apply to a sandbox
188        #[arg(short, long)]
189        sandbox: bool,
190
191        /// Whether command should apply to a build sandbox
192        #[arg(short, long)]
193        build: bool,
194
195        /// Whether command should apply to a group
196        #[arg(short, long)]
197        group: bool,
198
199        /// Name of the component
200        #[arg(required = true)]
201        name: String,
202
203        /// Path to the sandbox file or the project directory
204        #[arg(short, long)]
205        file: Option<PathBuf>,
206
207        /// Follow the logs
208        #[arg(short, long)]
209        follow: bool,
210
211        /// Number of lines to show from the end
212        #[arg(short, long)]
213        tail: Option<usize>,
214    },
215
216    /// Show tree of layers that make up a sandbox
217    #[command(name = "tree")]
218    Tree {
219        /// Whether command should apply to a sandbox
220        #[arg(short, long)]
221        sandbox: bool,
222
223        /// Whether command should apply to a build sandbox
224        #[arg(short, long)]
225        build: bool,
226
227        /// Whether command should apply to a group
228        #[arg(short, long)]
229        group: bool,
230
231        /// Names of components to show
232        #[arg(required = true)]
233        names: Vec<String>,
234
235        /// Maximum depth level
236        #[arg(short = 'L')]
237        level: Option<usize>,
238    },
239
240    /// Run a sandbox defined in the project
241    #[command(name = "run", alias = "r")]
242    Run {
243        /// Whether command should apply to a sandbox
244        #[arg(short, long)]
245        sandbox: bool,
246
247        /// Whether command should apply to a build sandbox
248        #[arg(short, long)]
249        build: bool,
250
251        /// Name of the component
252        #[arg(required = true, name = "NAME[~SCRIPT]")]
253        name: String,
254
255        /// Path to the sandbox file or the project directory
256        #[arg(short, long)]
257        file: Option<PathBuf>,
258
259        /// Run sandbox in the background
260        #[arg(short, long)]
261        detach: bool,
262
263        /// Execute a command within the sandbox
264        #[arg(short, long, short_alias = 'x')]
265        exec: Option<String>,
266
267        /// Additional arguments after `--`. Passed to the script or exec.
268        #[arg(last = true)]
269        args: Vec<String>,
270    },
271
272    /// Open a shell in a sandbox
273    #[command(name = "shell")]
274    Shell {
275        /// Whether command should apply to a sandbox
276        #[arg(short, long)]
277        sandbox: bool,
278
279        /// Whether command should apply to a build sandbox
280        #[arg(short, long)]
281        build: bool,
282
283        /// Name of the component
284        #[arg(required = true)]
285        name: String,
286
287        /// Path to the sandbox file or the project directory
288        #[arg(short, long)]
289        file: Option<PathBuf>,
290
291        /// Run sandbox in the background
292        #[arg(short, long)]
293        detach: bool,
294
295        /// Additional arguments after `--`. Passed to the shell.
296        #[arg(last = true)]
297        args: Vec<String>,
298    },
299
300    /// Run a temporary sandbox
301    #[command(name = "exe", alias = "x")]
302    Exe {
303        /// Whether command should apply to a sandbox
304        #[arg(short, long)]
305        image: bool,
306
307        /// Name of the image
308        #[arg(required = true, name = "NAME[~SCRIPT]")]
309        name: String,
310
311        /// Number of CPUs
312        #[arg(long, alias = "cpu")]
313        cpus: Option<u8>,
314
315        /// Memory in MB
316        #[arg(long)]
317        memory: Option<u32>,
318
319        /// Volume mappings, format: <host_path>:<container_path>
320        #[arg(short, long = "volume", name = "VOLUME")]
321        volumes: Vec<String>,
322
323        /// Port mappings, format: <host_port>:<container_port>
324        #[arg(short, long = "port", name = "PORT")]
325        ports: Vec<String>,
326
327        /// Environment variables, format: <key>=<value>
328        #[arg(long = "env", name = "ENV")]
329        envs: Vec<String>,
330
331        /// Working directory
332        #[arg(long)]
333        workdir: Option<Utf8UnixPathBuf>,
334
335        /// Network scope, options: local, public, any, none
336        #[arg(long)]
337        scope: Option<String>,
338
339        /// Execute a command within the sandbox
340        #[arg(short, long, short_alias = 'x')]
341        exec: Option<String>,
342
343        /// Additional arguments after `--`. Passed to the script or exec.
344        #[arg(last = true)]
345        args: Vec<String>,
346    },
347
348    /// Install a script from an image
349    #[command(name = "install", alias = "i")]
350    Install {
351        /// Whether command should apply to a sandbox
352        #[arg(short, long)]
353        image: bool,
354
355        /// Name of the image
356        #[arg(required = true, name = "NAME[~SCRIPT]")]
357        name: String,
358
359        /// Alias for the script
360        #[arg()]
361        alias: Option<String>,
362
363        /// Number of CPUs
364        #[arg(long, alias = "cpu")]
365        cpus: Option<u8>,
366
367        /// Memory in MB
368        #[arg(long)]
369        memory: Option<u32>,
370
371        /// Volume mappings, format: <host_path>:<container_path>
372        #[arg(short, long = "volume", name = "VOLUME")]
373        volumes: Vec<String>,
374
375        /// Port mappings, format: <host_port>:<container_port>
376        #[arg(short, long = "port", name = "PORT")]
377        ports: Vec<String>,
378
379        /// Environment variables, format: <key>=<value>
380        #[arg(long = "env", name = "ENV")]
381        envs: Vec<String>,
382
383        /// Working directory
384        #[arg(long)]
385        workdir: Option<Utf8UnixPathBuf>,
386
387        /// Network scope, options: local, public, any, none
388        #[arg(long)]
389        scope: Option<String>,
390
391        /// Execute a command within the sandbox
392        #[arg(short, long, short_alias = 'x')]
393        exec: Option<String>,
394
395        /// Additional arguments after `--`. Passed to the script or exec.
396        #[arg(last = true)]
397        args: Vec<String>,
398    },
399
400    /// Uninstall a script
401    #[command(name = "uninstall")]
402    Uninstall {
403        /// Script to uninstall
404        script: Option<String>,
405    },
406
407    /// Start or stop project sandboxes based on configuration
408    #[command(name = "apply")]
409    Apply {
410        /// Path to the sandbox file or the project directory
411        #[arg(short, long)]
412        file: Option<PathBuf>,
413
414        /// Run sandboxes in the background
415        #[arg(short, long)]
416        detach: bool,
417    },
418
419    /// Run a project's sandboxes
420    #[command(name = "up")]
421    Up {
422        /// Whether command should apply to a sandbox
423        #[arg(short, long)]
424        sandbox: bool,
425
426        /// Whether command should apply to a build sandbox
427        #[arg(short, long)]
428        build: bool,
429
430        /// Whether command should apply to a group
431        #[arg(short, long)]
432        group: bool,
433
434        /// Names of components to start. If omitted, starts all sandboxes defined in the configuration.
435        names: Vec<String>,
436
437        /// Path to the sandbox file or the project directory
438        #[arg(short, long)]
439        file: Option<PathBuf>,
440
441        /// Run sandboxes in the background
442        #[arg(short, long)]
443        detach: bool,
444    },
445
446    /// Stop a project's sandboxes
447    #[command(name = "down")]
448    Down {
449        /// Whether command should apply to a sandbox
450        #[arg(short, long)]
451        sandbox: bool,
452
453        /// Whether command should apply to a build sandbox
454        #[arg(short, long)]
455        build: bool,
456
457        /// Whether command should apply to a group
458        #[arg(short, long)]
459        group: bool,
460
461        /// Names of components to stop. If omitted, stops all sandboxes defined in the configuration.
462        names: Vec<String>,
463
464        /// Path to the sandbox file or the project directory
465        #[arg(short, long)]
466        file: Option<PathBuf>,
467    },
468
469    /// Show statuses of a project's running sandboxes
470    #[command(name = "status", alias = "ps", alias = "stat")]
471    Status {
472        /// Whether command should apply to a sandbox
473        #[arg(short, long)]
474        sandbox: bool,
475
476        /// Whether command should apply to a build sandbox
477        #[arg(short, long)]
478        build: bool,
479
480        /// Whether command should apply to a group
481        #[arg(short, long)]
482        group: bool,
483
484        /// Names of components to show status for
485        #[arg()]
486        names: Vec<String>,
487
488        /// Path to the sandbox file or the project directory
489        #[arg(short, long)]
490        file: Option<PathBuf>,
491    },
492
493    /// Clean cached sandbox layers, metadata, etc.
494    #[command(name = "clean")]
495    Clean {
496        /// Whether command should apply to a sandbox
497        #[arg(short, long)]
498        sandbox: bool,
499
500        /// Name of the component
501        #[arg()]
502        name: Option<String>,
503
504        /// Clean user-level caches. This cleans $MICROSANDBOX_HOME
505        #[arg(short, long)]
506        user: bool,
507
508        /// Clean all
509        #[arg(short, long)]
510        all: bool,
511
512        /// Path to the sandbox file or the project directory
513        #[arg(short, long)]
514        file: Option<PathBuf>,
515
516        /// Force clean
517        #[arg(long)]
518        force: bool,
519    },
520
521    /// Build images
522    #[command(name = "build")]
523    Build {
524        /// Build from build definition
525        #[arg(short, long)]
526        build: bool,
527
528        /// Build from sandbox
529        #[arg(short, long)]
530        sandbox: bool,
531
532        /// Build from group
533        #[arg(short, long)]
534        group: bool,
535
536        /// Names of components to build
537        #[arg(required = true)]
538        names: Vec<String>,
539
540        /// Create a snapshot
541        #[arg(long)]
542        snapshot: bool,
543    },
544
545    /// Pull image from a registry
546    #[command(name = "pull")]
547    Pull {
548        /// Whether command should apply to an image
549        #[arg(short, long)]
550        image: bool,
551
552        /// Whether command should apply to an image group
553        #[arg(short = 'G', long)]
554        image_group: bool,
555
556        /// Name of the image or image group
557        #[arg(required = true)]
558        name: Reference,
559
560        /// Path to store the layer files
561        #[arg(short = 'L', long)]
562        layer_path: Option<PathBuf>,
563    },
564
565    /// Login to a registry
566    #[command(name = "login")]
567    Login,
568
569    /// Push image to a registry
570    #[command(name = "push")]
571    Push {
572        /// Whether command should apply to an image
573        #[arg(short, long)]
574        image: bool,
575
576        /// Whether command should apply to an image group
577        #[arg(short = 'G', long)]
578        image_group: bool,
579
580        /// Name of the image or image group
581        #[arg(required = true)]
582        name: String,
583    },
584
585    /// Manage microsandbox itself
586    #[command(name = "self")]
587    Self_ {
588        /// Action to perform
589        #[arg(value_enum)]
590        action: SelfAction,
591    },
592
593    /// Start a sandbox server for orchestrating and working with sandboxes
594    #[command(name = "server")]
595    Server {
596        /// The subcommand to run
597        #[command(subcommand)]
598        subcommand: ServerSubcommand,
599    },
600
601    /// Print version of microsandbox
602    #[command(name = "version")]
603    Version,
604}
605
606/// Subcommands for the server subcommand
607#[derive(Debug, Parser)]
608pub enum ServerSubcommand {
609    /// Start the sandbox server which is also an MCP server
610    Start {
611        /// Port to listen on
612        #[arg(long)]
613        port: Option<u16>,
614
615        /// Path to the namespace directory
616        #[arg(short = 'p', long = "path")]
617        namespace_dir: Option<PathBuf>,
618
619        /// Run server in development mode
620        #[arg(long = "dev")]
621        dev_mode: bool,
622
623        /// Set secret key for server. Automatically generated if not provided.
624        #[arg(short, long)]
625        key: Option<String>,
626
627        /// Run server in the background
628        #[arg(short, long)]
629        detach: bool,
630
631        /// Reset the server key
632        #[arg(short, long)]
633        reset_key: bool,
634    },
635
636    /// Stop the sandbox server
637    Stop,
638
639    /// Generate a new API key
640    #[command(name = "keygen")]
641    Keygen {
642        /// Token expiration duration. format: 1s, 2m, 3h, 4d, 5w, 6mo, 7y
643        #[arg(long)]
644        expire: Option<String>,
645
646        /// Namespace for the API key. If not specified, generates a key for all namespaces.
647        #[arg(short, long)]
648        namespace: Option<String>,
649    },
650
651    /// Show logs of a sandbox
652    #[command(name = "log")]
653    Log {
654        /// Whether command should apply to a sandbox
655        #[arg(short, long)]
656        sandbox: bool,
657
658        /// Name of the component
659        #[arg(required = true)]
660        name: String,
661
662        /// Namespace for the logs
663        #[arg(short, long)]
664        namespace: String,
665
666        /// Follow the logs
667        #[arg(short, long)]
668        follow: bool,
669
670        /// Number of lines to show from the end
671        #[arg(short, long)]
672        tail: Option<usize>,
673    },
674
675    /// List sandboxes in a namespace
676    #[command(name = "list")]
677    List {
678        /// Namespace to list sandboxes from. If not provided, lists sandboxes from all namespaces.
679        #[arg(short, long)]
680        namespace: Option<String>,
681    },
682
683    /// Show server status
684    #[command(name = "status")]
685    Status {
686        /// Whether command should apply to a sandbox
687        #[arg(short, long)]
688        sandbox: bool,
689
690        /// Name of the component
691        #[arg()]
692        names: Vec<String>,
693
694        /// Namespace to show status for. If not provided, shows status for all namespaces.
695        #[arg(short, long)]
696        namespace: Option<String>,
697    },
698
699    /// SSH into a sandbox
700    #[command(name = "ssh")]
701    Ssh {
702        /// Namespace for the SSH key
703        #[arg(short, long, required = true)]
704        namespace: String,
705
706        /// Whether to SSH into a sandbox
707        #[arg(short, long)]
708        sandbox: bool,
709
710        /// Name of the sandbox
711        #[arg(required = true)]
712        name: String,
713    },
714}
715
716/// Actions for the self subcommand
717#[derive(Debug, Clone, clap::ValueEnum)]
718pub enum SelfAction {
719    /// Upgrade microsandbox
720    Upgrade,
721
722    /// Uninstall microsandbox
723    Uninstall,
724}
725
726//-------------------------------------------------------------------------------------------------
727// Functions: Helpers
728//-------------------------------------------------------------------------------------------------
729
730fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
731where
732    T: std::str::FromStr,
733    T::Err: Error + Send + Sync + 'static,
734    U: std::str::FromStr,
735    U::Err: Error + Send + Sync + 'static,
736{
737    let pos = s
738        .find('=')
739        .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
740
741    Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
742}