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}