Skip to main content

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}