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}