sparrow/cli/mod.rs
1use clap::{Parser, Subcommand};
2use std::path::PathBuf;
3
4#[derive(Parser)]
5#[command(name = "sparrow", about = "one cli · grows with you", version)]
6pub struct Cli {
7 #[command(subcommand)]
8 pub command: Option<Commands>,
9
10 /// Launch terminal TUI (native)
11 #[arg(long)]
12 pub tui: bool,
13
14 /// Launch webview console (HTTP + WebSocket)
15 #[arg(long)]
16 pub web: bool,
17
18 /// JSON output (NDJSON event stream)
19 #[arg(long)]
20 pub json: bool,
21
22 /// Override autonomy level
23 #[arg(long)]
24 pub autonomy: Option<String>,
25
26 /// Force a specific model
27 #[arg(long)]
28 pub model: Option<String>,
29
30 /// Prefer local/offline models
31 #[arg(long, global = true)]
32 pub local: bool,
33
34 /// Session budget cap (USD)
35 #[arg(long, global = true)]
36 pub budget: Option<f64>,
37
38 /// Hard stop on cumulative USD spent in this run (alias for --budget,
39 /// kept separately to match competitor tools' UX).
40 #[arg(long, global = true)]
41 pub max_cost_usd: Option<f64>,
42
43 /// Hard stop on wall-clock seconds elapsed in this run.
44 #[arg(long, global = true)]
45 pub max_wall_secs: Option<u64>,
46
47 /// Hard stop on total tokens consumed in this run.
48 #[arg(long, global = true)]
49 pub max_tokens: Option<u64>,
50
51 /// Bind address for daemon / cockpit servers (default 127.0.0.1).
52 /// Use 0.0.0.0 when running under WSL or in a container.
53 #[arg(long, global = true)]
54 pub bind: Option<String>,
55
56 /// Sandbox backend
57 #[arg(long, global = true)]
58 pub sandbox: Option<String>,
59
60 /// Profile name
61 #[arg(long, global = true)]
62 pub profile: Option<String>,
63
64 /// Disable checkpointing
65 #[arg(long, global = true)]
66 pub no_checkpoint: bool,
67
68 /// Run as a named agent
69 #[arg(long)]
70 pub agent: Option<String>,
71
72 /// Continue the most recent session (any surface) instead of this
73 /// directory's session
74 #[arg(long = "continue", global = true)]
75 pub continue_last: bool,
76
77 /// Start with a fresh context (ignore this directory's saved session)
78 #[arg(long, global = true)]
79 pub fresh: bool,
80
81 /// Skip the pre-run quote confirmation
82 #[arg(long, global = true)]
83 pub yes: bool,
84}
85
86#[derive(Subcommand)]
87pub enum Commands {
88 /// Run a single agentic task
89 Run {
90 /// Task description
91 task: String,
92
93 /// Emit NDJSON event stream (same as the global --json flag, but may
94 /// follow the task: `sparrow run "..." --json`)
95 #[arg(long)]
96 json: bool,
97
98 /// Show a read-only plan first; continue only with `--yes`.
99 #[arg(long)]
100 plan_first: bool,
101
102 /// Read-only dry run: propose actions/diffs, but deny mutating tools.
103 #[arg(long)]
104 dry_run: bool,
105
106 /// Patch mode: ask for a unified diff only and deny mutating tools.
107 #[arg(long)]
108 patch: bool,
109 },
110
111 /// Create a read-only execution plan for a task
112 Plan {
113 /// Task description
114 task: String,
115
116 /// Emit JSON instead of Markdown
117 #[arg(long)]
118 json: bool,
119 },
120
121 /// Audit the current repository: architecture map, stubs, TODO/FIXME, and
122 /// suspicious Rust files. Writes `./artifacts/audit-<timestamp>.md`.
123 Audit {
124 /// Emit JSON instead of Markdown path output
125 #[arg(long)]
126 json: bool,
127 },
128
129 /// Detect and run the project test suite (`cargo`, `npm`, or `pytest`).
130 Test {
131 /// If tests fail, hand the failure to Sparrow's repair loop.
132 #[arg(long)]
133 fix: bool,
134
135 /// Emit JSON instead of human-readable output.
136 #[arg(long)]
137 json: bool,
138 },
139
140 /// Adversarial review of the current local diff (uncommitted, staged,
141 /// and commits ahead of `--base`). Read-only — no edits, no commits, no
142 /// network beyond the model call. Findings are structured around
143 /// security, correctness, regressions, performance, and readability.
144 Review {
145 /// Base ref to diff against (defaults to `origin/main`, then `main`,
146 /// then `HEAD~1`).
147 #[arg(long)]
148 base: Option<String>,
149
150 /// Only review changes touching these path globs (repeatable).
151 #[arg(long)]
152 paths: Vec<String>,
153
154 /// Print the prompt the model will see and exit (no model call).
155 #[arg(long)]
156 dry_run: bool,
157 },
158
159 /// Interactive multi-turn chat
160 Chat,
161
162 /// Answer a reasoning-heavy task with inference-time scaling
163 /// (best-of-N + judge selection + self-refine) instead of one greedy pass.
164 Reason {
165 /// The task / question to reason about.
166 task: String,
167 },
168
169 /// Launch TUI
170 Tui,
171
172 /// Launch first-run setup, then the WebView cockpit
173 Launch {
174 /// TCP port for the WebView HTTP/WS server
175 #[arg(long, default_value = "9339")]
176 port: u16,
177
178 /// Launch the terminal TUI instead of the WebView cockpit
179 #[arg(long)]
180 tui: bool,
181
182 /// Use the older expert setup wizard before opening the surface
183 #[arg(long)]
184 pro: bool,
185 },
186
187 /// Create a clean git commit from staged changes after a secret scan.
188 Commit {
189 /// Commit message. If omitted, Sparrow generates a conservative one
190 /// from the staged diff stat.
191 #[arg(short, long)]
192 message: Option<String>,
193
194 /// Show what would be committed without running `git commit`.
195 #[arg(long)]
196 dry_run: bool,
197 },
198
199 /// Release workflow helpers.
200 Release {
201 #[command(subcommand)]
202 action: ReleaseAction,
203 },
204
205 /// Public release intelligence (opt-in network scan, local cache reports).
206 Intel {
207 #[command(subcommand)]
208 action: IntelAction,
209 },
210
211 /// Launch webview console (HTTP + WebSocket)
212 #[command(visible_aliases = ["montre", "show"])]
213 Console {
214 /// TCP port for the webview HTTP/WS server
215 #[arg(long, default_value = "9339")]
216 port: u16,
217
218 /// Fast start: skip boot animation, eager panel preloads, and boot-time
219 /// provider discovery. Panels still load lazily when opened.
220 #[arg(long)]
221 fast: bool,
222 },
223
224 /// Réparer un problème — décris ce qui ne va pas, Sparrow diagnostique
225 /// puis corrige (avec ton accord). « sparrow fix "message d'erreur" »,
226 /// ou sans argument pour scanner le dossier courant.
227 #[command(visible_aliases = ["repare", "répare"])]
228 Fix {
229 /// Le problème, avec tes mots, ou une erreur collée (entre guillemets
230 /// si elle contient des espaces). Optionnel : sans argument, Sparrow
231 /// inspecte le dossier courant.
232 problem: Vec<String>,
233 },
234
235 /// Dis ce que tu veux en langage naturel — Sparrow choisit la commande.
236 /// « sparrow do "corrige le build" » · « sparrow do "montre la console" ».
237 /// Pas besoin d'apprendre les commandes : décris, Sparrow comprend.
238 #[command(visible_aliases = ["fais"])]
239 Do {
240 /// Ta demande, avec tes mots.
241 request: Vec<String>,
242 /// Montre seulement la commande choisie, sans l'exécuter.
243 #[arg(long)]
244 dry_run: bool,
245 },
246
247 /// Expliquer un fichier, une erreur ou un concept en langage simple.
248 /// « sparrow explique src/main.rs » · « sparrow explique "borrow checker" »
249 #[command(visible_aliases = ["explain"])]
250 Explique {
251 /// Ce qu'il faut expliquer : un chemin de fichier, une erreur, ou un
252 /// mot (entre guillemets si plusieurs mots).
253 target: Vec<String>,
254 },
255
256 /// Annuler la dernière action de Sparrow — revient au dernier point de
257 /// sauvegarde, rien n'est perdu. « sparrow annule » · « sparrow annule
258 /// --tout » pour revenir au début de la session.
259 #[command(visible_aliases = ["undo"])]
260 Annule {
261 /// Point de sauvegarde précis (sinon : le tout dernier).
262 id: Option<String>,
263
264 /// Revenir au tout premier point de sauvegarde de la session.
265 #[arg(long, visible_alias = "all")]
266 tout: bool,
267 },
268
269 /// Dire bonjour — l'accueil chaleureux : Sparrow regarde ton dossier et
270 /// te propose quoi faire. Parfait pour un premier contact.
271 #[command(visible_aliases = ["hello", "salut"])]
272 Bonjour,
273
274 /// Voir ou changer le plafond de dépense par session. « sparrow budget »
275 /// affiche le réglage actuel ; « sparrow budget 2€ » le change.
276 Budget {
277 /// Le montant max par session (ex. « 2€ », « $0.50 », « 1.5 »).
278 /// Vide : affiche le réglage actuel.
279 amount: Option<String>,
280 },
281
282 /// Des idées de ce que tu peux faire avec Sparrow, classées par profil.
283 /// « sparrow idees » · « sparrow idees enseignant » · « sparrow idees
284 /// "factures" ».
285 #[command(visible_aliases = ["ideas", "idées"])]
286 Idees {
287 /// Filtre : un profil (enseignant, developpeur, …) ou un mot-clé.
288 filter: Vec<String>,
289 },
290
291 /// C'est quoi ce mot ? — définition instantanée d'un terme de Sparrow,
292 /// en deux phrases simples, sans appel modèle. « sparrow whatis token ».
293 #[command(name = "whatis", visible_aliases = ["c-est-quoi", "cest-quoi", "glossaire"])]
294 Whatis {
295 /// Le terme à définir (ex. checkpoint, token, swarm). Vide : liste les
296 /// mots connus.
297 term: Vec<String>,
298 },
299
300 /// Choisir comment Sparrow te parle : simple (langage clair), builder
301 /// (workflows build), pro (sortie technique complète) ou auto. Sans
302 /// argument : affiche le mode actuel.
303 Mode {
304 /// « simple », « builder », « pro » ou « auto ».
305 mode: Option<String>,
306 },
307
308 /// Run headless Sparrow runtime daemon
309 Daemon,
310
311 /// Manage persistent agents
312 Agent {
313 #[command(subcommand)]
314 action: AgentAction,
315 },
316
317 /// Run swarm: planner → coder → verifier
318 Swarm {
319 /// Task or plan file
320 task: String,
321 },
322
323 /// Schedule periodic jobs
324 Schedule {
325 /// Task description
326 task: String,
327
328 /// Cron expression
329 #[arg(long)]
330 cron: String,
331
332 /// Autonomy level for scheduled jobs
333 #[arg(long)]
334 autonomy: Option<String>,
335
336 /// Report to surfaces
337 #[arg(long)]
338 report: Vec<String>,
339 },
340
341 /// Manage model routing
342 Model {
343 /// Set active route
344 #[arg(long)]
345 set: Option<String>,
346
347 /// List available models
348 #[arg(long)]
349 list: bool,
350 },
351
352 /// Configure intelligent auto-routing provider
353 Route {
354 #[command(subcommand)]
355 action: RouteAction,
356 },
357
358 /// Manage provider credentials
359 Auth {
360 #[command(subcommand)]
361 action: AuthAction,
362 },
363
364 /// Manage skill library
365 Skills {
366 #[command(subcommand)]
367 action: SkillsAction,
368 },
369
370 /// Manage local Sparrow plugins
371 Plugins {
372 #[command(subcommand)]
373 action: PluginsAction,
374 },
375
376 /// Inspect and gate toolsets
377 Tools {
378 #[command(subcommand)]
379 action: ToolsAction,
380 },
381
382 /// Security audit of config, permissions, plugins, hooks, secrets
383 Security {
384 #[command(subcommand)]
385 action: SecurityAction,
386 },
387
388 /// GitHub Action / remote PR workflow
389 Github {
390 #[command(subcommand)]
391 action: GithubAction,
392 },
393
394 /// Compact context and write a durable handoff doc
395 Compact {
396 /// Task description (recorded in the handoff)
397 #[arg(long)]
398 task: Option<String>,
399 /// Output path (default: .sparrow/handoff/<timestamp>.md)
400 #[arg(long)]
401 out: Option<PathBuf>,
402 /// Emit JSON instead of Markdown to stdout (the file is always Markdown)
403 #[arg(long)]
404 json: bool,
405 },
406
407 /// Manage MCP connectors
408 Mcp {
409 #[command(subcommand)]
410 action: McpAction,
411 },
412
413 /// List checkpoints
414 Checkpoint {
415 #[command(subcommand)]
416 action: CheckpointAction,
417 },
418
419 /// Rewind to a checkpoint
420 Rewind {
421 /// Checkpoint ID or number
422 id: String,
423 },
424
425 /// Replay a transcript
426 Replay {
427 /// Run ID to replay
428 run_id: String,
429 /// Open an interactive TUI scrubber (←/→ to step through events)
430 #[arg(long)]
431 scrub: bool,
432 },
433
434 /// Start/stop gateway daemon
435 Gateway {
436 #[command(subcommand)]
437 action: GatewayAction,
438 },
439
440 /// Manage saved sessions
441 Sessions {
442 #[command(subcommand)]
443 action: SessionAction,
444 },
445
446 /// Interactive tutorial
447 Learn,
448
449 /// Initialize a project with .sparrow/ config
450 Init,
451
452 /// Show live status (active runs, budget, session)
453 Status,
454
455 /// Manage persistent memory
456 Memory {
457 #[command(subcommand)]
458 action: MemoryAction,
459 },
460
461 /// Inspect and update permission policy
462 Permissions {
463 #[command(subcommand)]
464 action: PermissionAction,
465 },
466
467 /// Profile management
468 Profile {
469 #[command(subcommand)]
470 action: ProfileAction,
471 },
472
473 /// Import config from another tool (claude-code, codex, opencode, openclaw)
474 Import {
475 #[command(subcommand)]
476 source: ImportSource,
477 },
478
479 /// Edit configuration
480 Config {
481 /// Open config.toml in editor
482 #[arg(short)]
483 edit: bool,
484 },
485
486 /// Self-update
487 Update,
488
489 /// Run diagnostics
490 Doctor,
491
492 /// (Re)run conversational setup
493 Setup,
494
495 /// Run a self-contained demo (snake game)
496 Demo,
497
498 /// Share latest session as GitHub Gist
499 Share,
500
501 /// Install or scan security pre-commit hooks
502 Hook {
503 #[command(subcommand)]
504 action: HookAction,
505 },
506
507 /// Voice commands (speak, transcribe, providers)
508 Voice {
509 #[command(subcommand)]
510 action: VoiceAction,
511 },
512
513 /// Test browser/vision (screenshot, navigate)
514 Browser {
515 /// URL to test
516 #[arg(default_value = "https://example.com")]
517 url: String,
518 },
519
520 /// Free-text front door: anything that isn't a known command is treated as
521 /// natural language and routed to the right command automatically. You never
522 /// have to learn a command — just say what you want.
523 /// e.g. `sparrow corrige le build` · `sparrow montre la console`.
524 #[command(external_subcommand)]
525 Natural(Vec<String>),
526}
527
528#[derive(Subcommand)]
529pub enum AgentAction {
530 Create { name: String },
531 List,
532 Edit { name: String },
533 Rm { name: String },
534 Run { name: String, task: String },
535 Mention { name: String, message: String },
536}
537
538#[derive(Subcommand)]
539pub enum AuthAction {
540 Add {
541 provider: String,
542 },
543 List,
544 Rm {
545 provider: String,
546 },
547 /// Authenticate a provider via OAuth device flow (github/google/microsoft).
548 Login {
549 provider: String,
550 /// OAuth client id (or set <PROVIDER>_CLIENT_ID env var)
551 #[arg(long)]
552 client_id: Option<String>,
553 },
554}
555
556#[derive(Subcommand)]
557pub enum SkillsAction {
558 List,
559 View {
560 name: String,
561 },
562 Create {
563 name: String,
564 },
565 /// Install a skill from GitHub (gh:user/repo[/path]), a git URL, or a
566 /// local path to a SKILL.md
567 Install {
568 source: String,
569 },
570 Update {
571 name: String,
572 },
573 Prune,
574 /// Remove a skill by name (e.g. to delete junk auto-learned skills)
575 Rm {
576 name: String,
577 },
578}
579
580#[derive(Subcommand)]
581pub enum PluginsAction {
582 List,
583 Install {
584 source: String,
585 #[arg(long)]
586 allow: bool,
587 },
588 Rm {
589 name: String,
590 },
591}
592
593#[derive(Subcommand)]
594pub enum GithubAction {
595 /// Review a pull request: fetch diff via `gh`, run a read-only review prompt
596 Review {
597 /// PR number
598 pr: u64,
599 /// Print the review plan without invoking the model or posting comments
600 #[arg(long)]
601 dry_run: bool,
602 /// Override the model id
603 #[arg(long)]
604 model: Option<String>,
605 /// Restrict tool allow-list (comma-separated). Empty = inherit config.
606 #[arg(long)]
607 allowed_tools: Option<String>,
608 },
609 /// Show CI status for the current branch (via `gh run list`)
610 Status,
611 /// Fetch CI logs for a workflow run id (via `gh run view --log`)
612 Logs { run_id: String },
613}
614
615#[derive(Subcommand)]
616pub enum ReleaseAction {
617 /// Prepare launch notes and migration notes from local artifacts.
618 Prep {
619 /// Show the target files without writing them.
620 #[arg(long)]
621 dry_run: bool,
622 },
623}
624
625#[derive(Subcommand)]
626pub enum IntelAction {
627 /// Fetch configured or explicit public sources into the local intel cache.
628 Scan {
629 /// TOML file containing [[source]] entries.
630 #[arg(long)]
631 config: Option<PathBuf>,
632
633 /// Explicit source as kind:name:url, e.g.
634 /// github_releases:Codex:https://github.com/openai/codex
635 #[arg(long)]
636 source: Vec<String>,
637
638 /// Max releases per GitHub source.
639 #[arg(long, default_value_t = 5)]
640 limit: usize,
641
642 /// Emit JSON instead of a human summary.
643 #[arg(long)]
644 json: bool,
645 },
646
647 /// Show cached release digests without network access.
648 Report {
649 #[arg(long, default_value_t = 20)]
650 limit: usize,
651 #[arg(long)]
652 json: bool,
653 },
654
655 /// Show cached scored backlog tickets without network access.
656 Backlog {
657 #[arg(long, default_value_t = 20)]
658 limit: usize,
659 #[arg(long)]
660 json: bool,
661 },
662
663 /// Repeated opt-in scan loop. Requires intel.enabled=true or explicit sources.
664 Watch {
665 #[arg(long, default_value_t = 3600)]
666 interval: u64,
667 #[arg(long)]
668 config: Option<PathBuf>,
669 #[arg(long)]
670 source: Vec<String>,
671 },
672}
673
674#[derive(Subcommand)]
675pub enum SecurityAction {
676 /// Run a full security audit
677 Audit {
678 /// Emit JSON instead of human-readable summary
679 #[arg(long)]
680 json: bool,
681 },
682}
683
684#[derive(Subcommand)]
685pub enum ToolsAction {
686 List {
687 #[arg(long)]
688 surface: Option<String>,
689 },
690 Enable {
691 tool: String,
692 },
693 Disable {
694 tool: String,
695 },
696}
697
698#[derive(Subcommand)]
699pub enum McpAction {
700 Add {
701 server: String,
702
703 /// Command to launch the MCP server
704 #[arg(long)]
705 command: Option<String>,
706
707 /// Command arguments, either repeated or space-delimited
708 #[arg(long, value_delimiter = ' ', allow_hyphen_values = true)]
709 args: Vec<String>,
710
711 /// Transport backend: stdio, sse, or url
712 #[arg(long)]
713 transport: Option<String>,
714 },
715 List,
716 Rm {
717 server: String,
718 },
719}
720
721#[derive(Subcommand)]
722pub enum CheckpointAction {
723 /// List all checkpoints
724 List,
725 /// Show diff between HEAD and a checkpoint
726 Diff {
727 /// Checkpoint ID
728 id: String,
729 },
730 /// Delete checkpoints older than N days (default: 30)
731 Prune {
732 /// Remove checkpoints older than this many days
733 #[arg(long, default_value = "30")]
734 older_than_days: u64,
735 },
736}
737
738#[derive(Subcommand)]
739pub enum GatewayAction {
740 Start,
741 Status,
742 Health,
743 Abort { run: String },
744 Stop,
745}
746
747#[derive(Subcommand)]
748pub enum SessionAction {
749 List,
750 Export {
751 id: String,
752 path: Option<PathBuf>,
753 },
754 Cleanup {
755 #[arg(long, default_value_t = 30)]
756 older_than_days: u64,
757 },
758 /// Full-text search across sessions
759 Search {
760 query: String,
761 #[arg(long, default_value_t = 10)]
762 limit: usize,
763 },
764}
765
766#[derive(Subcommand)]
767pub enum ProfileAction {
768 Create { name: String },
769 List,
770 Use { name: String },
771}
772
773#[derive(Subcommand)]
774pub enum ImportSource {
775 /// Import from Claude Code (~/.claude/)
776 ClaudeCode {
777 /// Path to project with .claude/ directory (defaults to cwd)
778 path: Option<PathBuf>,
779 },
780 /// Import from OpenAI Codex CLI (~/.codex/)
781 Codex {
782 /// Path to project with codex config (defaults to cwd)
783 path: Option<PathBuf>,
784 },
785 /// Import from OpenCode (~/.config/opencode/)
786 #[command(name = "opencode")]
787 OpenCode {
788 /// Path to project with opencode.json (defaults to cwd)
789 path: Option<PathBuf>,
790 },
791 /// Import from OpenClaw (~/.openclaw/)
792 Openclaw {
793 /// Path to the OpenClaw config directory (defaults to ~/.openclaw)
794 path: Option<PathBuf>,
795 },
796 /// Auto-detect installed tools and import each one
797 Auto,
798}
799
800#[derive(Subcommand)]
801pub enum MemoryAction {
802 List,
803 Forget {
804 id: String,
805 },
806 Add {
807 key: String,
808 value: String,
809 },
810 Replace {
811 id: String,
812 key: String,
813 value: String,
814 },
815 Recall {
816 query: String,
817 #[arg(long, default_value_t = 10)]
818 limit: usize,
819 },
820 Consolidate,
821 Docs,
822 Search {
823 query: String,
824 #[arg(long, default_value_t = 10)]
825 limit: usize,
826 },
827 Scroll {
828 session: String,
829 #[arg(long, default_value_t = 0)]
830 around: usize,
831 #[arg(long, default_value_t = 3)]
832 before: usize,
833 #[arg(long, default_value_t = 3)]
834 after: usize,
835 },
836 Graph {
837 #[command(subcommand)]
838 action: GraphAction,
839 },
840}
841
842#[derive(Subcommand)]
843pub enum GraphAction {
844 UpsertNode {
845 id: String,
846 label: String,
847 #[arg(long, default_value = "entity")]
848 kind: String,
849 #[arg(long, default_value = "{}")]
850 properties: String,
851 },
852 UpsertEdge {
853 from_id: String,
854 relation: String,
855 to_id: String,
856 #[arg(long)]
857 id: Option<String>,
858 #[arg(long, default_value_t = 1.0)]
859 weight: f64,
860 #[arg(long, default_value = "{}")]
861 properties: String,
862 },
863 Get {
864 id: String,
865 },
866 Neighbors {
867 id: String,
868 #[arg(long, default_value = "both")]
869 direction: String,
870 #[arg(long, default_value_t = 20)]
871 limit: usize,
872 },
873 Search {
874 query: String,
875 #[arg(long, default_value_t = 20)]
876 limit: usize,
877 },
878 Export,
879 DeleteNode {
880 id: String,
881 },
882 DeleteEdge {
883 id: String,
884 },
885 SyncNeo4j,
886}
887
888#[derive(Subcommand)]
889pub enum PermissionAction {
890 /// Show current permission mode and rules
891 List,
892 /// Set permission mode (read-only|plan|supervised|trusted|autonomous|emergency-stop)
893 Set { mode: String },
894 /// Add an explicitly allowed tool pattern
895 AllowTool { tool: String },
896 /// Add a tool pattern that always asks for approval
897 AskTool { tool: String },
898 /// Add an explicitly denied tool pattern
899 DenyTool { tool: String },
900 /// Add an allowed path boundary
901 AllowPath { path: PathBuf },
902 /// Add a denied path boundary
903 DenyPath { path: PathBuf },
904}
905
906#[derive(Subcommand)]
907pub enum RouteAction {
908 /// Pin routing to a specific provider or provider/model.
909 /// Examples: sparrow route set deepseek | sparrow route set deepseek/deepseek-v4-pro
910 Set {
911 /// Provider ID, or provider/model (e.g. \"deepseek/deepseek-v4-pro\")
912 provider: String,
913 },
914 /// Clear the pinned provider/model — let the multi-tier policy decide per task.
915 Clear,
916 /// Show the current routing config (preferred provider + per-tier policy).
917 Show,
918 /// Switch to manual mode — always use the chosen provider/model, never fall back.
919 Manual,
920 /// Switch to auto mode — tier-based policy + free_first fallback (default).
921 Auto,
922}
923
924#[derive(Subcommand)]
925pub enum HookAction {
926 /// Install pre-commit security hook
927 Install,
928 /// Scan staged files (or all files with --all) for secrets
929 Scan {
930 /// Scan entire working tree instead of just staged files
931 #[arg(long)]
932 all: bool,
933 },
934}
935
936#[derive(Subcommand)]
937pub enum VoiceAction {
938 /// Convert text to speech
939 Speak { text: String },
940 /// Transcribe audio file
941 Transcribe { file: String },
942 /// List available voice providers
943 Providers,
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949 use clap::Parser;
950
951 // v0.9 Pilier 1: the human front-door commands must collect their
952 // free-text argument WITHOUT swallowing global flags like --yes. A first
953 // implementation used `trailing_var_arg` and captured "--yes" into the
954 // problem text — the model then complained about the stray flags.
955 #[test]
956 fn explique_does_not_swallow_global_flags() {
957 let cli = Cli::parse_from(["sparrow", "explique", "borrow checker", "--yes"]);
958 assert!(cli.yes, "--yes must be parsed as a flag, not text");
959 match cli.command {
960 Some(Commands::Explique { target }) => {
961 assert_eq!(target, vec!["borrow checker".to_string()]);
962 }
963 _ => panic!("expected Explique"),
964 }
965 }
966
967 #[test]
968 fn fix_collects_words_and_respects_flags() {
969 let cli = Cli::parse_from(["sparrow", "fix", "le", "build", "casse", "--yes"]);
970 assert!(cli.yes);
971 match cli.command {
972 Some(Commands::Fix { problem }) => {
973 assert_eq!(problem, vec!["le", "build", "casse"]);
974 }
975 _ => panic!("expected Fix"),
976 }
977 }
978
979 #[test]
980 fn fix_accepts_no_argument() {
981 let cli = Cli::parse_from(["sparrow", "fix"]);
982 match cli.command {
983 Some(Commands::Fix { problem }) => assert!(problem.is_empty()),
984 _ => panic!("expected Fix"),
985 }
986 }
987
988 #[test]
989 fn human_aliases_resolve() {
990 // repare → Fix, explain → Explique, montre → Console, undo → Annule.
991 assert!(matches!(
992 Cli::parse_from(["sparrow", "repare", "x"]).command,
993 Some(Commands::Fix { .. })
994 ));
995 assert!(matches!(
996 Cli::parse_from(["sparrow", "explain", "x"]).command,
997 Some(Commands::Explique { .. })
998 ));
999 assert!(matches!(
1000 Cli::parse_from(["sparrow", "montre"]).command,
1001 Some(Commands::Console { .. })
1002 ));
1003 assert!(matches!(
1004 Cli::parse_from(["sparrow", "undo"]).command,
1005 Some(Commands::Annule { .. })
1006 ));
1007 }
1008
1009 #[test]
1010 fn console_fast_flag_parses() {
1011 match Cli::parse_from(["sparrow", "console", "--fast"]).command {
1012 Some(Commands::Console { port, fast }) => {
1013 assert_eq!(port, 9339);
1014 assert!(fast);
1015 }
1016 _ => panic!("expected Console"),
1017 }
1018 }
1019
1020 #[test]
1021 fn v092_audit_and_test_commands_parse() {
1022 assert!(matches!(
1023 Cli::parse_from(["sparrow", "audit", "--json"]).command,
1024 Some(Commands::Audit { json: true })
1025 ));
1026 assert!(matches!(
1027 Cli::parse_from(["sparrow", "test", "--fix"]).command,
1028 Some(Commands::Test {
1029 fix: true,
1030 json: false
1031 })
1032 ));
1033 assert!(matches!(
1034 Cli::parse_from(["sparrow", "commit", "--dry-run", "-m", "feat: x"]).command,
1035 Some(Commands::Commit {
1036 dry_run: true,
1037 message: Some(_)
1038 })
1039 ));
1040 assert!(matches!(
1041 Cli::parse_from(["sparrow", "release", "prep"]).command,
1042 Some(Commands::Release {
1043 action: ReleaseAction::Prep { dry_run: false }
1044 })
1045 ));
1046 assert!(matches!(
1047 Cli::parse_from([
1048 "sparrow",
1049 "intel",
1050 "scan",
1051 "--source",
1052 "github_releases:Codex:https://github.com/openai/codex",
1053 "--limit",
1054 "2"
1055 ])
1056 .command,
1057 Some(Commands::Intel {
1058 action: IntelAction::Scan { limit: 2, .. }
1059 })
1060 ));
1061 assert!(matches!(
1062 Cli::parse_from([
1063 "sparrow",
1064 "run",
1065 "fix it",
1066 "--plan-first",
1067 "--dry-run",
1068 "--patch"
1069 ])
1070 .command,
1071 Some(Commands::Run {
1072 plan_first: true,
1073 dry_run: true,
1074 patch: true,
1075 ..
1076 })
1077 ));
1078 }
1079
1080 #[test]
1081 fn v09_human_commands_parse() {
1082 assert!(matches!(
1083 Cli::parse_from(["sparrow", "idees", "enseignant"]).command,
1084 Some(Commands::Idees { .. })
1085 ));
1086 assert!(matches!(
1087 Cli::parse_from(["sparrow", "ideas"]).command,
1088 Some(Commands::Idees { .. })
1089 ));
1090 assert!(matches!(
1091 Cli::parse_from(["sparrow", "whatis", "token"]).command,
1092 Some(Commands::Whatis { .. })
1093 ));
1094 assert!(matches!(
1095 Cli::parse_from(["sparrow", "c-est-quoi", "checkpoint"]).command,
1096 Some(Commands::Whatis { .. })
1097 ));
1098 match Cli::parse_from(["sparrow", "budget", "2€"]).command {
1099 Some(Commands::Budget { amount }) => assert_eq!(amount.as_deref(), Some("2€")),
1100 _ => panic!("expected Budget"),
1101 }
1102 }
1103
1104 #[test]
1105 fn mode_command_parses_optional_argument() {
1106 match Cli::parse_from(["sparrow", "mode"]).command {
1107 Some(Commands::Mode { mode }) => assert!(mode.is_none()),
1108 _ => panic!("expected Mode"),
1109 }
1110 match Cli::parse_from(["sparrow", "mode", "pro"]).command {
1111 Some(Commands::Mode { mode }) => assert_eq!(mode.as_deref(), Some("pro")),
1112 _ => panic!("expected Mode"),
1113 }
1114 match Cli::parse_from(["sparrow", "mode", "builder"]).command {
1115 Some(Commands::Mode { mode }) => assert_eq!(mode.as_deref(), Some("builder")),
1116 _ => panic!("expected Mode"),
1117 }
1118 }
1119
1120 #[test]
1121 fn annule_defaults_and_flags() {
1122 // No id → latest (None); --tout → whole session.
1123 match Cli::parse_from(["sparrow", "annule"]).command {
1124 Some(Commands::Annule { id, tout }) => {
1125 assert!(id.is_none());
1126 assert!(!tout);
1127 }
1128 _ => panic!("expected Annule"),
1129 }
1130 match Cli::parse_from(["sparrow", "annule", "--tout"]).command {
1131 Some(Commands::Annule { id, tout }) => {
1132 assert!(id.is_none());
1133 assert!(tout);
1134 }
1135 _ => panic!("expected Annule"),
1136 }
1137 match Cli::parse_from(["sparrow", "annule", "cp-123"]).command {
1138 Some(Commands::Annule { id, .. }) => assert_eq!(id.as_deref(), Some("cp-123")),
1139 _ => panic!("expected Annule"),
1140 }
1141 }
1142}