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
56    /// Unlock a vault with its password.
57    Unlock {
58        /// Target profiles (CSV: "default,work" or "org:vault,org:vault").
59        /// Falls back to SESAME_PROFILES env var, then "default".
60        #[arg(short, long)]
61        profile: Option<String>,
62    },
63
64    /// Lock a vault (zeroize cached key material).
65    Lock {
66        /// Target profile. Omit to lock all vaults.
67        #[arg(short, long)]
68        profile: Option<String>,
69    },
70
71    /// Profile management.
72    #[command(subcommand)]
73    Profile(ProfileCmd),
74
75    /// SSH agent key management for passwordless vault unlock.
76    #[command(subcommand)]
77    Ssh(SshCmd),
78
79    /// Secret management (profile-scoped).
80    #[command(subcommand)]
81    Secret(SecretCmd),
82
83    /// Audit log operations.
84    #[command(subcommand)]
85    Audit(AuditCmd),
86
87    /// Application launcher.
88    #[command(subcommand)]
89    Launch(LaunchCmd),
90
91    /// Window manager operations.
92    #[command(subcommand)]
93    Wm(WmCmd),
94
95    /// Clipboard operations.
96    #[command(subcommand)]
97    Clipboard(ClipboardCmd),
98
99    /// Input remapper operations.
100    #[command(subcommand)]
101    Input(InputCmd),
102
103    /// Snippet operations.
104    #[command(subcommand)]
105    Snippet(SnippetCmd),
106
107    /// Setup COSMIC keybindings for window switcher and launcher overlay.
108    ///
109    /// Configures Alt+Tab (switch), Alt+Shift+Tab (switch backward),
110    /// and a launcher key (default: alt+space) in COSMIC's shortcuts.ron.
111    ///
112    /// Usage: `sesame setup-keybinding [KEY_COMBO]`
113    #[cfg(all(target_os = "linux", feature = "desktop"))]
114    SetupKeybinding {
115        /// Launcher key combo (default: "alt+space"). Examples: "super+space", "alt+space".
116        #[arg(default_value = "alt+space")]
117        launcher_key: String,
118    },
119
120    /// Remove sesame keybindings from COSMIC configuration.
121    #[cfg(all(target_os = "linux", feature = "desktop"))]
122    RemoveKeybinding,
123
124    /// Show current sesame keybinding status in COSMIC.
125    #[cfg(all(target_os = "linux", feature = "desktop"))]
126    KeybindingStatus,
127
128    /// Run a command with profile-scoped secrets as environment variables.
129    ///
130    /// Each secret key is transformed to an env var: uppercase, hyphens become
131    /// underscores. Example: secret "api-key" becomes env var "API_KEY".
132    ///
133    /// Usage: sesame env -p work -- aws s3 ls
134    Env {
135        /// Profiles to source secrets from (CSV: "default,work" or "org:vault").
136        /// Falls back to SESAME_PROFILES env var, then "default".
137        #[arg(short, long)]
138        profile: Option<String>,
139
140        /// Prefix for env var names (e.g., --prefix MYAPP: "api-key" becomes "MYAPP_API_KEY").
141        #[arg(long)]
142        prefix: Option<String>,
143
144        /// Command and arguments to execute.
145        #[arg(trailing_var_arg = true, required = true, allow_hyphen_values = true)]
146        command: Vec<String>,
147    },
148
149    /// Print profile secrets as shell/dotenv/json for eval or piping.
150    ///
151    /// Formats:
152    ///   shell  (default) — export KEY="value"  (eval in bash/zsh/direnv)
153    ///   dotenv           — KEY=value           (Docker, docker-compose, node)
154    ///   json             — {"KEY":"value",...}  (jq, CI/CD, programmatic)
155    ///
156    /// Usage:
157    ///   eval "$(sesame export -p work)"
158    ///   sesame export -p work --format dotenv > .env.secrets
159    ///   sesame export -p work --format json | jq .
160    Export {
161        /// Profiles to source secrets from (CSV: "default,work" or "org:vault").
162        /// Falls back to SESAME_PROFILES env var, then "default".
163        #[arg(short, long)]
164        profile: Option<String>,
165
166        /// Output format: shell, dotenv, json.
167        #[arg(short, long, default_value = "shell")]
168        format: ExportFormat,
169
170        /// Prefix for env var names (e.g., --prefix MYAPP: "api-key" becomes "MYAPP_API_KEY").
171        #[arg(long)]
172        prefix: Option<String>,
173    },
174
175    /// Workspace management (directory-scoped project environments).
176    #[command(subcommand, alias = "ws")]
177    Workspace(WorkspaceCmd),
178}
179
180/// Resolve the workspace root from `SESAME_WORKSPACE_ROOT` or fall back to `/workspace`.
181pub(crate) fn default_workspace_root() -> std::path::PathBuf {
182    std::env::var("SESAME_WORKSPACE_ROOT")
183        .map(std::path::PathBuf::from)
184        .unwrap_or_else(|_| std::path::PathBuf::from("/workspace"))
185}
186
187/// Resolve the workspace path argument, defaulting to the current directory.
188///
189/// Fails explicitly if the current directory cannot be determined — a security
190/// tool must never silently fall back to `"."`.
191pub(crate) fn resolve_workspace_path(
192    path: Option<std::path::PathBuf>,
193) -> anyhow::Result<std::path::PathBuf> {
194    match path {
195        Some(p) => Ok(p),
196        None => std::env::current_dir().context("failed to determine current directory"),
197    }
198}
199
200#[derive(Subcommand)]
201pub(crate) enum WorkspaceCmd {
202    /// Create the workspace root and user directory.
203    Init {
204        /// Override the workspace root directory (default: $SESAME_WORKSPACE_ROOT or /workspace).
205        #[arg(long, default_value_os_t = default_workspace_root())]
206        root: std::path::PathBuf,
207
208        /// Override username detection.
209        #[arg(long)]
210        user: Option<String>,
211    },
212
213    /// Clone a repository to its canonical workspace path.
214    Clone {
215        /// Git remote URL (HTTPS or SSH).
216        url: String,
217
218        /// Shallow clone depth.
219        #[arg(long)]
220        depth: Option<u32>,
221
222        /// Link to a profile after cloning.
223        #[arg(short, long)]
224        profile: Option<String>,
225
226        /// Adopt a pre-existing directory if it has the correct remote.
227        /// Enabled by default; use --no-adopt to require a fresh clone.
228        #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
229        adopt: bool,
230    },
231
232    /// List all discovered workspaces.
233    List {
234        /// Filter by git server hostname.
235        #[arg(long)]
236        server: Option<String>,
237
238        /// Filter by organization/user.
239        #[arg(long)]
240        org: Option<String>,
241
242        /// Filter by linked profile name.
243        #[arg(short, long)]
244        profile: Option<String>,
245
246        /// Output format.
247        #[arg(short, long, default_value = "table")]
248        format: WorkspaceListFormat,
249    },
250
251    /// Show workspace status and metadata.
252    Status {
253        /// Workspace path (default: current directory).
254        path: Option<std::path::PathBuf>,
255
256        /// Show detailed convention breakdown and disk usage.
257        #[arg(short, long)]
258        verbose: bool,
259    },
260
261    /// Associate a workspace directory with a sesame profile.
262    Link {
263        /// Profile to link.
264        #[arg(short, long)]
265        profile: String,
266
267        /// Workspace path (default: current directory).
268        path: Option<std::path::PathBuf>,
269    },
270
271    /// Remove a workspace-to-profile association.
272    Unlink {
273        /// Workspace path (default: current directory).
274        path: Option<std::path::PathBuf>,
275    },
276
277    /// Open an interactive shell with vault secrets injected.
278    ///
279    /// Secrets are injected as environment variables and are visible in
280    /// `/proc/<pid>/environ` to processes running as the same user. All
281    /// child processes inherit the secret environment.
282    Shell {
283        /// Override the linked profile.
284        #[arg(short, long)]
285        profile: Option<String>,
286
287        /// Workspace path (default: current directory).
288        path: Option<std::path::PathBuf>,
289
290        /// Shell binary (default: $SHELL).
291        #[arg(long)]
292        shell: Option<String>,
293
294        /// Prefix for env var names (e.g., --prefix MYAPP).
295        #[arg(long)]
296        prefix: Option<String>,
297
298        /// Command to run instead of interactive shell.
299        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
300        command: Vec<String>,
301    },
302
303    /// Show or inspect workspace configuration.
304    #[command(subcommand)]
305    Config(WorkspaceConfigCmd),
306}
307
308#[derive(Subcommand)]
309pub(crate) enum WorkspaceConfigCmd {
310    /// Show resolved configuration with provenance for the current workspace.
311    Show {
312        /// Workspace path (default: current directory).
313        path: Option<std::path::PathBuf>,
314    },
315}
316
317#[derive(Clone, ValueEnum)]
318pub(crate) enum WorkspaceListFormat {
319    /// Formatted table output.
320    Table,
321    /// JSON output.
322    Json,
323}
324
325#[derive(Clone, ValueEnum)]
326pub(crate) enum ExportFormat {
327    /// export KEY="value" — for eval in bash/zsh/direnv
328    Shell,
329    /// KEY=value — for Docker, docker-compose, node, python-dotenv
330    Dotenv,
331    /// {"KEY":"value",...} — for jq, CI/CD, programmatic consumers
332    Json,
333}
334
335#[derive(Subcommand)]
336pub(crate) enum ProfileCmd {
337    /// List configured profiles.
338    List,
339
340    /// Activate a profile scope (open vault, register namespace).
341    Activate {
342        /// Profile name.
343        name: String,
344    },
345
346    /// Deactivate a profile scope (flush cache, close vault).
347    Deactivate {
348        /// Profile name.
349        name: String,
350    },
351
352    /// Set the default profile.
353    Default {
354        /// Profile name.
355        name: String,
356    },
357
358    /// Show configuration for a named profile.
359    Show {
360        /// Profile name.
361        name: String,
362    },
363}
364
365#[derive(Subcommand)]
366pub(crate) enum SshCmd {
367    /// Enroll an SSH key for passwordless vault unlock.
368    ///
369    /// Requires the vault to be unlockable with a password (the master key
370    /// is derived via Argon2id, then wrapped under an SSH-derived KEK).
371    /// Only Ed25519 and RSA (PKCS#1 v1.5) keys are supported — their
372    /// signatures are deterministic, which is required for KEK derivation.
373    Enroll {
374        /// Target profiles (CSV: "default,work").
375        /// Falls back to SESAME_PROFILES env var, then "default".
376        #[arg(short, long)]
377        profile: Option<String>,
378
379        /// SSH key to enroll. Accepts a fingerprint (SHA256:...),
380        /// a public key file path (~/.ssh/id_ed25519.pub), or omit
381        /// to interactively select from the SSH agent.
382        #[arg(short = 'k', long = "ssh-key")]
383        ssh_key: Option<String>,
384    },
385
386    /// List SSH key enrollments for profiles.
387    List {
388        /// Target profiles (CSV: "default,work").
389        /// Falls back to SESAME_PROFILES env var, then "default".
390        #[arg(short, long)]
391        profile: Option<String>,
392    },
393
394    /// Revoke SSH key enrollment for a profile.
395    Revoke {
396        /// Target profiles (CSV: "default,work").
397        /// Falls back to SESAME_PROFILES env var, then "default".
398        #[arg(short, long)]
399        profile: Option<String>,
400    },
401}
402
403#[derive(Subcommand)]
404pub(crate) enum SecretCmd {
405    /// Store a secret (prompts for value).
406    Set {
407        /// Profile name.
408        #[arg(short, long)]
409        profile: String,
410
411        /// Secret key name.
412        key: String,
413    },
414
415    /// Retrieve a secret value.
416    Get {
417        /// Profile name.
418        #[arg(short, long)]
419        profile: String,
420
421        /// Secret key name.
422        key: String,
423    },
424
425    /// Delete a secret.
426    Delete {
427        /// Profile name.
428        #[arg(short, long)]
429        profile: String,
430
431        /// Secret key name.
432        key: String,
433
434        /// Skip confirmation prompt (for non-interactive/scripted use).
435        #[arg(long)]
436        yes: bool,
437    },
438
439    /// List secret keys (never values).
440    List {
441        /// Profile name.
442        #[arg(short, long)]
443        profile: String,
444    },
445}
446
447#[derive(Subcommand)]
448pub(crate) enum AuditCmd {
449    /// Verify audit log hash chain integrity.
450    Verify,
451
452    /// Show recent audit log entries.
453    Tail {
454        /// Number of entries to show.
455        #[arg(short = 'n', long, default_value = "20")]
456        count: usize,
457
458        /// Follow (stream) new entries as they are appended.
459        #[arg(short = 'f', long)]
460        follow: bool,
461    },
462}
463
464#[derive(Subcommand)]
465pub(crate) enum WmCmd {
466    /// List windows known to daemon-wm.
467    List,
468
469    /// Switch to next/previous window in MRU order.
470    Switch {
471        /// Switch backward (previous) instead of forward.
472        #[arg(long)]
473        backward: bool,
474    },
475
476    /// Activate a specific window by ID or app ID.
477    Focus {
478        /// Window ID or app ID string.
479        window_id: String,
480    },
481
482    /// Activate the window switcher overlay.
483    ///
484    /// Shows a visual overlay with hint keys for quick window selection.
485    /// Use --launcher to skip the border-only phase and show the full
486    /// overlay immediately.
487    Overlay {
488        /// Start in launcher mode (full overlay immediately, no border-only phase).
489        #[arg(long)]
490        launcher: bool,
491
492        /// Start with backward direction (previous window in MRU order).
493        #[arg(long)]
494        backward: bool,
495    },
496
497    /// Run as resident fast-path process for overlay activation.
498    ///
499    /// Holds an active IPC connection and listens on a Unix datagram socket
500    /// so subsequent overlay invocations can skip the Noise IK handshake.
501    /// Not intended for direct user invocation.
502    #[command(hide = true)]
503    OverlayResident,
504}
505
506#[derive(Subcommand)]
507pub(crate) enum LaunchCmd {
508    /// Search for applications by name (fuzzy match with frecency ranking).
509    Search {
510        /// Search query.
511        query: String,
512
513        /// Maximum results to return.
514        #[arg(short = 'n', long, default_value = "10")]
515        max_results: u32,
516
517        /// Profile context for scoped frecency ranking.
518        #[arg(short, long)]
519        profile: Option<String>,
520    },
521
522    /// Launch an application by its desktop entry ID.
523    ///
524    /// Use `sesame launch search <query>` to find entry IDs.
525    Run {
526        /// Desktop entry ID (e.g., "org.mozilla.firefox").
527        entry_id: String,
528
529        /// Profile context for secrets and frecency.
530        #[arg(short, long)]
531        profile: Option<String>,
532    },
533}
534
535#[derive(Subcommand)]
536pub(crate) enum ClipboardCmd {
537    /// Show clipboard history for a profile.
538    History {
539        /// Profile name.
540        #[arg(short, long)]
541        profile: String,
542
543        /// Maximum entries to show.
544        #[arg(short = 'n', long, default_value = "20")]
545        limit: u32,
546    },
547
548    /// Clear clipboard history for a profile.
549    Clear {
550        /// Profile name.
551        #[arg(short, long)]
552        profile: String,
553    },
554
555    /// Get a specific clipboard entry by ID.
556    Get {
557        /// Clipboard entry ID.
558        entry_id: String,
559    },
560}
561
562#[derive(Subcommand)]
563pub(crate) enum InputCmd {
564    /// List configured input layers.
565    Layers,
566
567    /// Show input daemon status (active layer, grabbed devices).
568    Status,
569}
570
571#[derive(Subcommand)]
572pub(crate) enum SnippetCmd {
573    /// List snippets for a profile.
574    List {
575        /// Profile name.
576        #[arg(short, long)]
577        profile: String,
578    },
579
580    /// Expand a snippet trigger.
581    Expand {
582        /// Profile name.
583        #[arg(short, long)]
584        profile: String,
585
586        /// Trigger string.
587        trigger: String,
588    },
589
590    /// Add a new snippet.
591    Add {
592        /// Profile name.
593        #[arg(short, long)]
594        profile: String,
595
596        /// Trigger string.
597        trigger: String,
598
599        /// Template body.
600        template: String,
601    },
602}