sesame/cli.rs
1use anyhow::Context;
2use clap::ValueEnum;
3use clap::{Parser, Subcommand};
4
5/// Open Sesame — Platform Orchestration CLI.
6#[derive(Parser)]
7#[command(
8 name = "sesame",
9 about = "Open Sesame — platform orchestration CLI",
10 version
11)]
12pub(crate) struct Cli {
13 #[command(subcommand)]
14 pub command: Command,
15}
16
17#[derive(Subcommand)]
18pub(crate) enum Command {
19 /// Initialize Open Sesame: create config, start daemons, set master password.
20 Init {
21 /// Skip keybinding setup.
22 #[arg(long)]
23 no_keybinding: bool,
24
25 /// Destroy ALL Open Sesame data and reset to clean state. Requires typing "destroy all data" to confirm.
26 #[arg(long)]
27 wipe_reset_destroy_all_data: bool,
28
29 /// Organization domain for namespace scoping (e.g., "braincraft.io").
30 #[arg(long)]
31 org: Option<String>,
32
33 /// Enroll an SSH key for vault unlock.
34 /// Accepts a fingerprint (SHA256:...), a public key file path (~/.ssh/id_ed25519.pub),
35 /// or no value to interactively select from the SSH agent.
36 /// Without --password, creates an SSH-key-only vault.
37 /// With --password, creates a dual-factor vault.
38 #[arg(long, num_args = 0..=1, default_missing_value = "")]
39 ssh_key: Option<String>,
40
41 /// Enroll a password for vault unlock. Required with --ssh-key for dual-factor init.
42 /// Without --ssh-key, this is the default behavior.
43 #[arg(long)]
44 password: bool,
45
46 /// Auth policy for multi-factor vaults: "any" (either factor unlocks),
47 /// "all" (every factor required), or policy expression.
48 /// Default: "any" for dual-factor, ignored for single-factor.
49 #[arg(long, default_value = "any")]
50 auth_policy: String,
51 },
52
53 /// Show daemon status, active profiles, and lock state.
54 Status {
55 /// Run diagnostic checks across system health categories.
56 /// Accepts a comma-separated list of categories to check:
57 /// daemon, memory, sandbox, ipc, crypto, vault, platform, all.
58 /// Omit the value or pass "all" to run every category.
59 #[arg(long, value_name = "CATEGORIES", default_missing_value = "all", num_args = 0..=1)]
60 doctor: Option<String>,
61
62 /// Output format for --doctor results.
63 #[arg(long, default_value = "text", requires = "doctor")]
64 output: Option<String>,
65
66 /// Exit with code 0 if all checks pass, 1 if any fail, 2 if any warn.
67 /// Useful for systemd health checks and CI.
68 #[arg(long, requires = "doctor")]
69 exit_code: bool,
70
71 /// Suppress output, only set exit code. Implies --exit-code.
72 #[arg(long, requires = "doctor")]
73 quiet: bool,
74
75 /// Trigger immediate Noise IK key rotation across all daemons.
76 /// Phase 2 (finalize registry, remove old keys) runs automatically
77 /// after the 30-second grace period.
78 #[arg(long)]
79 rotate_keys: bool,
80 },
81
82 /// Clone a repository to its canonical workspace path.
83 ///
84 /// Automatically discovers and sets up the org-level workspace.git if one
85 /// exists on the server. Equivalent to `sesame workspace clone` with sane
86 /// defaults.
87 ///
88 /// Usage: sesame clone <https://github.com/org/repo>
89 #[command(alias = "cl")]
90 Clone {
91 /// Git remote URL (HTTPS or SSH).
92 url: String,
93
94 /// Shallow clone depth.
95 #[arg(long)]
96 depth: Option<u32>,
97
98 /// Link to a profile after cloning.
99 #[arg(short, long)]
100 profile: Option<String>,
101
102 /// Skip workspace.git auto-discovery for this clone.
103 #[arg(long)]
104 no_workspace: bool,
105
106 /// Clone all repositories in the org from the forge API.
107 /// URL identifies the server and org (repo component is ignored).
108 #[arg(long)]
109 project: bool,
110
111 /// Include forked repositories when using --project.
112 #[arg(long, requires = "project")]
113 include_forks: bool,
114
115 /// Include archived repositories when using --project.
116 #[arg(long, requires = "project")]
117 include_archived: bool,
118 },
119
120 /// Unlock a vault with its password.
121 Unlock {
122 /// Target profiles (CSV: "default,work" or "org:vault,org:vault").
123 /// Falls back to SESAME_PROFILES env var, then "default".
124 #[arg(short, long)]
125 profile: Option<String>,
126 },
127
128 /// Lock a vault (zeroize cached key material).
129 Lock {
130 /// Target profile. Omit to lock all vaults.
131 #[arg(short, long)]
132 profile: Option<String>,
133 },
134
135 /// Profile management.
136 #[command(subcommand)]
137 Profile(ProfileCmd),
138
139 /// SSH agent key management for passwordless vault unlock.
140 #[command(subcommand)]
141 Ssh(SshCmd),
142
143 /// Secret management (profile-scoped).
144 #[command(subcommand)]
145 Secret(SecretCmd),
146
147 /// Audit log operations.
148 #[command(subcommand)]
149 Audit(AuditCmd),
150
151 /// Application launcher.
152 #[command(subcommand)]
153 Launch(LaunchCmd),
154
155 /// Window manager operations.
156 #[command(subcommand)]
157 Wm(WmCmd),
158
159 /// Clipboard operations.
160 #[command(subcommand)]
161 Clipboard(ClipboardCmd),
162
163 /// Input remapper operations.
164 #[command(subcommand)]
165 Input(InputCmd),
166
167 /// Snippet operations.
168 #[command(subcommand)]
169 Snippet(SnippetCmd),
170
171 /// Setup COSMIC keybindings for window switcher and launcher overlay.
172 ///
173 /// Configures Alt+Tab (switch), Alt+Shift+Tab (switch backward),
174 /// and a launcher key (default: alt+space) in COSMIC's shortcuts.ron.
175 ///
176 /// Usage: `sesame setup-keybinding [KEY_COMBO]`
177 #[cfg(all(target_os = "linux", feature = "desktop"))]
178 SetupKeybinding {
179 /// Launcher key combo (default: "alt+space"). Examples: "super+space", "alt+space".
180 #[arg(default_value = "alt+space")]
181 launcher_key: String,
182 },
183
184 /// Remove sesame keybindings from COSMIC configuration.
185 #[cfg(all(target_os = "linux", feature = "desktop"))]
186 RemoveKeybinding,
187
188 /// Show current sesame keybinding status in COSMIC.
189 #[cfg(all(target_os = "linux", feature = "desktop"))]
190 KeybindingStatus,
191
192 /// Run a command with profile-scoped secrets as environment variables.
193 ///
194 /// Each secret key is transformed to an env var: uppercase, hyphens become
195 /// underscores. Example: secret "api-key" becomes env var "API_KEY".
196 ///
197 /// Usage: sesame env -p work -- aws s3 ls
198 Env {
199 /// Profiles to source secrets from (CSV: "default,work" or "org:vault").
200 /// Falls back to SESAME_PROFILES env var, then "default".
201 #[arg(short, long)]
202 profile: Option<String>,
203
204 /// Prefix for env var names (e.g., --prefix MYAPP: "api-key" becomes "MYAPP_API_KEY").
205 #[arg(long)]
206 prefix: Option<String>,
207
208 /// Command and arguments to execute.
209 #[arg(trailing_var_arg = true, required = true, allow_hyphen_values = true)]
210 command: Vec<String>,
211 },
212
213 /// Print profile secrets as shell/dotenv/json for eval or piping.
214 ///
215 /// Formats:
216 /// shell (default) — export KEY="value" (eval in bash/zsh/direnv)
217 /// dotenv — KEY=value (Docker, docker-compose, node)
218 /// json — {"KEY":"value",...} (jq, CI/CD, programmatic)
219 ///
220 /// Usage:
221 /// eval "$(sesame export -p work)"
222 /// sesame export -p work --format dotenv > .env.secrets
223 /// sesame export -p work --format json | jq .
224 Export {
225 /// Profiles to source secrets from (CSV: "default,work" or "org:vault").
226 /// Falls back to SESAME_PROFILES env var, then "default".
227 #[arg(short, long)]
228 profile: Option<String>,
229
230 /// Output format: shell, dotenv, json.
231 #[arg(short, long, default_value = "shell")]
232 format: ExportFormat,
233
234 /// Prefix for env var names (e.g., --prefix MYAPP: "api-key" becomes "MYAPP_API_KEY").
235 #[arg(long)]
236 prefix: Option<String>,
237 },
238
239 /// Workspace management (directory-scoped project environments).
240 #[command(subcommand, alias = "ws")]
241 Workspace(WorkspaceCmd),
242}
243
244/// Resolve the workspace root from `SESAME_WORKSPACE_ROOT` or fall back to `/workspace`.
245pub(crate) fn default_workspace_root() -> std::path::PathBuf {
246 std::env::var("SESAME_WORKSPACE_ROOT")
247 .map(std::path::PathBuf::from)
248 .unwrap_or_else(|_| std::path::PathBuf::from("/workspace"))
249}
250
251/// Resolve the workspace path argument, defaulting to the current directory.
252///
253/// Fails explicitly if the current directory cannot be determined — a security
254/// tool must never silently fall back to `"."`.
255pub(crate) fn resolve_workspace_path(
256 path: Option<std::path::PathBuf>,
257) -> anyhow::Result<std::path::PathBuf> {
258 match path {
259 Some(p) => Ok(p),
260 None => std::env::current_dir().context("failed to determine current directory"),
261 }
262}
263
264#[derive(Subcommand)]
265pub(crate) enum WorkspaceCmd {
266 /// Create the workspace root and user directory.
267 Init {
268 /// Override the workspace root directory (default: $SESAME_WORKSPACE_ROOT or /workspace).
269 #[arg(long, default_value_os_t = default_workspace_root())]
270 root: std::path::PathBuf,
271
272 /// Override username detection.
273 #[arg(long)]
274 user: Option<String>,
275 },
276
277 /// Clone a repository to its canonical workspace path.
278 Clone {
279 /// Git remote URL (HTTPS or SSH).
280 url: String,
281
282 /// Shallow clone depth.
283 #[arg(long)]
284 depth: Option<u32>,
285
286 /// Link to a profile after cloning.
287 #[arg(short, long)]
288 profile: Option<String>,
289
290 /// Adopt a pre-existing directory if it has the correct remote.
291 /// Enabled by default; use --no-adopt to require a fresh clone.
292 #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
293 adopt: bool,
294
295 /// Initialize the org-level workspace.git even if the org directory
296 /// already exists. Overrides the `workspace_auto` config setting.
297 /// If the org directory has existing files that would be overwritten,
298 /// requires `--force` to proceed.
299 #[arg(long)]
300 workspace_init: bool,
301
302 /// Pull workspace.git updates if behind remote.
303 /// Overrides the `workspace_auto` config setting.
304 #[arg(long)]
305 workspace_update: bool,
306
307 /// Skip all workspace.git auto-discovery for this clone.
308 /// Overrides the `workspace_auto` config setting.
309 #[arg(long)]
310 no_workspace: bool,
311
312 /// Allow destructive workspace operations: overwrite existing files
313 /// during `--workspace-init`, recover from broken partial init.
314 /// Without this flag, operations that would modify existing content
315 /// will print what would happen and refuse.
316 #[arg(long)]
317 force: bool,
318
319 /// Clone all repositories in the org from the forge API.
320 /// URL identifies the server and org (repo component is ignored).
321 #[arg(long)]
322 project: bool,
323
324 /// Include forked repositories when using --project.
325 #[arg(long, requires = "project")]
326 include_forks: bool,
327
328 /// Include archived repositories when using --project.
329 #[arg(long, requires = "project")]
330 include_archived: bool,
331 },
332
333 /// List all discovered workspaces.
334 List {
335 /// Filter by git server hostname.
336 #[arg(long)]
337 server: Option<String>,
338
339 /// Filter by organization/user.
340 #[arg(long)]
341 org: Option<String>,
342
343 /// Filter by linked profile name.
344 #[arg(short, long)]
345 profile: Option<String>,
346
347 /// Output format.
348 #[arg(short, long, default_value = "table")]
349 format: WorkspaceListFormat,
350 },
351
352 /// Show workspace status and metadata.
353 Status {
354 /// Workspace path (default: current directory).
355 path: Option<std::path::PathBuf>,
356
357 /// Show detailed convention breakdown and disk usage.
358 #[arg(short, long)]
359 verbose: bool,
360 },
361
362 /// Associate a workspace directory with a sesame profile.
363 Link {
364 /// Profile to link.
365 #[arg(short, long)]
366 profile: String,
367
368 /// Workspace path (default: current directory).
369 path: Option<std::path::PathBuf>,
370 },
371
372 /// Remove a workspace-to-profile association.
373 Unlink {
374 /// Workspace path (default: current directory).
375 path: Option<std::path::PathBuf>,
376 },
377
378 /// Open an interactive shell with vault secrets injected.
379 ///
380 /// Secrets are injected as environment variables and are visible in
381 /// `/proc/<pid>/environ` to processes running as the same user. All
382 /// child processes inherit the secret environment.
383 Shell {
384 /// Override the linked profile.
385 #[arg(short, long)]
386 profile: Option<String>,
387
388 /// Workspace path (default: current directory).
389 path: Option<std::path::PathBuf>,
390
391 /// Shell binary (default: $SHELL).
392 #[arg(long)]
393 shell: Option<String>,
394
395 /// Prefix for env var names (e.g., --prefix MYAPP).
396 #[arg(long)]
397 prefix: Option<String>,
398
399 /// Command to run instead of interactive shell.
400 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
401 command: Vec<String>,
402 },
403
404 /// Show or inspect workspace configuration.
405 #[command(subcommand)]
406 Config(WorkspaceConfigCmd),
407}
408
409#[derive(Subcommand)]
410pub(crate) enum WorkspaceConfigCmd {
411 /// Show resolved configuration with provenance for the current workspace.
412 Show {
413 /// Workspace path (default: current directory).
414 path: Option<std::path::PathBuf>,
415 },
416}
417
418#[derive(Clone, ValueEnum)]
419pub(crate) enum WorkspaceListFormat {
420 /// Formatted table output.
421 Table,
422 /// JSON output.
423 Json,
424}
425
426#[derive(Clone, ValueEnum)]
427pub(crate) enum ExportFormat {
428 /// export KEY="value" — for eval in bash/zsh/direnv
429 Shell,
430 /// KEY=value — for Docker, docker-compose, node, python-dotenv
431 Dotenv,
432 /// {"KEY":"value",...} — for jq, CI/CD, programmatic consumers
433 Json,
434}
435
436#[derive(Subcommand)]
437pub(crate) enum ProfileCmd {
438 /// List configured profiles.
439 List,
440
441 /// Activate a profile scope (open vault, register namespace).
442 Activate {
443 /// Profile name.
444 name: String,
445 },
446
447 /// Deactivate a profile scope (flush cache, close vault).
448 Deactivate {
449 /// Profile name.
450 name: String,
451 },
452
453 /// Set the default profile.
454 Default {
455 /// Profile name.
456 name: String,
457 },
458
459 /// Show configuration for a named profile.
460 Show {
461 /// Profile name.
462 name: String,
463 },
464}
465
466#[derive(Subcommand)]
467pub(crate) enum SshCmd {
468 /// Enroll an SSH key for passwordless vault unlock.
469 ///
470 /// Requires the vault to be unlockable with a password (the master key
471 /// is derived via Argon2id, then wrapped under an SSH-derived KEK).
472 /// Only Ed25519 and RSA (PKCS#1 v1.5) keys are supported — their
473 /// signatures are deterministic, which is required for KEK derivation.
474 Enroll {
475 /// Target profiles (CSV: "default,work").
476 /// Falls back to SESAME_PROFILES env var, then "default".
477 #[arg(short, long)]
478 profile: Option<String>,
479
480 /// SSH key to enroll. Accepts a fingerprint (SHA256:...),
481 /// a public key file path (~/.ssh/id_ed25519.pub), or omit
482 /// to interactively select from the SSH agent.
483 #[arg(short = 'k', long = "ssh-key")]
484 ssh_key: Option<String>,
485 },
486
487 /// List SSH key enrollments for profiles.
488 List {
489 /// Target profiles (CSV: "default,work").
490 /// Falls back to SESAME_PROFILES env var, then "default".
491 #[arg(short, long)]
492 profile: Option<String>,
493 },
494
495 /// Revoke SSH key enrollment for a profile.
496 Revoke {
497 /// Target profiles (CSV: "default,work").
498 /// Falls back to SESAME_PROFILES env var, then "default".
499 #[arg(short, long)]
500 profile: Option<String>,
501 },
502}
503
504#[derive(Subcommand)]
505pub(crate) enum SecretCmd {
506 /// Store a secret (prompts for value).
507 Set {
508 /// Profile name.
509 #[arg(short, long)]
510 profile: String,
511
512 /// Secret key name.
513 key: String,
514 },
515
516 /// Retrieve a secret value.
517 Get {
518 /// Profile name.
519 #[arg(short, long)]
520 profile: String,
521
522 /// Secret key name.
523 key: String,
524 },
525
526 /// Delete a secret.
527 Delete {
528 /// Profile name.
529 #[arg(short, long)]
530 profile: String,
531
532 /// Secret key name.
533 key: String,
534
535 /// Skip confirmation prompt (for non-interactive/scripted use).
536 #[arg(long)]
537 yes: bool,
538 },
539
540 /// List secret keys (never values).
541 List {
542 /// Profile name.
543 #[arg(short, long)]
544 profile: String,
545 },
546}
547
548#[derive(Subcommand)]
549pub(crate) enum AuditCmd {
550 /// Verify audit log hash chain integrity.
551 Verify,
552
553 /// Show recent audit log entries.
554 Tail {
555 /// Number of entries to show.
556 #[arg(short = 'n', long, default_value = "20")]
557 count: usize,
558
559 /// Follow (stream) new entries as they are appended.
560 #[arg(short = 'f', long)]
561 follow: bool,
562 },
563}
564
565#[derive(Subcommand)]
566pub(crate) enum WmCmd {
567 /// List windows known to daemon-wm.
568 List,
569
570 /// Switch to next/previous window in MRU order.
571 Switch {
572 /// Switch backward (previous) instead of forward.
573 #[arg(long)]
574 backward: bool,
575 },
576
577 /// Activate a specific window by ID or app ID.
578 Focus {
579 /// Window ID or app ID string.
580 window_id: String,
581 },
582
583 /// Activate the window switcher overlay.
584 ///
585 /// Shows a visual overlay with hint keys for quick window selection.
586 /// Use --launcher to skip the border-only phase and show the full
587 /// overlay immediately.
588 Overlay {
589 /// Start in launcher mode (full overlay immediately, no border-only phase).
590 #[arg(long)]
591 launcher: bool,
592
593 /// Start with backward direction (previous window in MRU order).
594 #[arg(long)]
595 backward: bool,
596 },
597
598 /// Run as resident fast-path process for overlay activation.
599 ///
600 /// Holds an active IPC connection and listens on a Unix datagram socket
601 /// so subsequent overlay invocations can skip the Noise IK handshake.
602 /// Not intended for direct user invocation.
603 #[command(hide = true)]
604 OverlayResident,
605}
606
607#[derive(Subcommand)]
608pub(crate) enum LaunchCmd {
609 /// Search for applications by name (fuzzy match with frecency ranking).
610 Search {
611 /// Search query.
612 query: String,
613
614 /// Maximum results to return.
615 #[arg(short = 'n', long, default_value = "10")]
616 max_results: u32,
617
618 /// Profile context for scoped frecency ranking.
619 #[arg(short, long)]
620 profile: Option<String>,
621 },
622
623 /// Launch an application by its desktop entry ID.
624 ///
625 /// Use `sesame launch search <query>` to find entry IDs.
626 Run {
627 /// Desktop entry ID (e.g., "org.mozilla.firefox").
628 entry_id: String,
629
630 /// Profile context for secrets and frecency.
631 #[arg(short, long)]
632 profile: Option<String>,
633 },
634}
635
636#[derive(Subcommand)]
637pub(crate) enum ClipboardCmd {
638 /// Show clipboard history for a profile.
639 History {
640 /// Profile name.
641 #[arg(short, long)]
642 profile: String,
643
644 /// Maximum entries to show.
645 #[arg(short = 'n', long, default_value = "20")]
646 limit: u32,
647 },
648
649 /// Clear clipboard history for a profile.
650 Clear {
651 /// Profile name.
652 #[arg(short, long)]
653 profile: String,
654 },
655
656 /// Get a specific clipboard entry by ID.
657 Get {
658 /// Clipboard entry ID.
659 entry_id: String,
660 },
661}
662
663#[derive(Subcommand)]
664pub(crate) enum InputCmd {
665 /// List configured input layers.
666 Layers,
667
668 /// Show input daemon status (active layer, grabbed devices).
669 Status,
670}
671
672#[derive(Subcommand)]
673pub(crate) enum SnippetCmd {
674 /// List snippets for a profile.
675 List {
676 /// Profile name.
677 #[arg(short, long)]
678 profile: String,
679 },
680
681 /// Expand a snippet trigger.
682 Expand {
683 /// Profile name.
684 #[arg(short, long)]
685 profile: String,
686
687 /// Trigger string.
688 trigger: String,
689 },
690
691 /// Add a new snippet.
692 Add {
693 /// Profile name.
694 #[arg(short, long)]
695 profile: String,
696
697 /// Trigger string.
698 trigger: String,
699
700 /// Template body.
701 template: String,
702 },
703}