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}