Skip to main content

rok_cli/
cli.rs

1use crate::error::RokError;
2use crate::schema::Payload;
3use clap::{Parser, Subcommand, ValueEnum};
4use std::fs;
5use std::io::{self, Read};
6
7#[derive(Parser, Debug)]
8#[command(name = "rok")]
9#[command(version = option_env!("CARGO_PKG_VERSION").unwrap_or("0.2.0"))]
10#[command(about = "Run One, Know All - Execute multi-step tasks from JSON")]
11#[command(long_about = "rok - AI Agent Task Runner
12
13A CLI tool that collapses multi-step operations into a single JSON invocation.
14
15EXAMPLES:
16  rok -f task.json                    Run from file
17  echo '{\"steps\":[{\"type\":\"bash\",\"cmd\":\"echo hello\"}]}' | rok    Run from stdin
18  rok templates                       List available templates
19  rok --help                          Show this help
20  rok --verbose -f task.json          Run with verbose output
21  rok -q -f task.json                 Run quietly (suppress output)
22
23SHELL COMPLETIONS:
24  Generate completions for your shell:
25  - Bash: cargo run --quiet --example completion bash > /etc/bash_completion.d/rok
26  - Zsh:  cargo run --quiet --example completion zsh  > ~/.zsh/_rok
27  - Fish: cargo run --quiet --example completion fish > ~/.config/fish/completions/rok.fish
28
29For more info, see: https://github.com/ateeq1999/rok")]
30pub struct Cli {
31    #[command(subcommand)]
32    pub command: Option<Commands>,
33
34    #[arg(
35        short = 'j',
36        long = "json",
37        conflicts_with = "file",
38        help = "JSON payload inline"
39    )]
40    pub json: Option<String>,
41
42    #[arg(
43        short = 'f',
44        long = "file",
45        conflicts_with = "json",
46        help = "Path to JSON file"
47    )]
48    pub file: Option<String>,
49
50    #[arg(
51        short = 'o',
52        long = "output",
53        default_value = "json",
54        help = "Output format: json, pretty, silent"
55    )]
56    pub output: OutputFormat,
57
58    #[arg(long = "dry-run", help = "Preview steps without executing")]
59    pub dry_run: bool,
60
61    #[arg(long = "verbose", short = 'v', help = "Enable verbose output")]
62    pub verbose: bool,
63
64    #[arg(long = "quiet", short = 'q', help = "Suppress output (except errors)")]
65    pub quiet: bool,
66}
67
68#[derive(Debug, Subcommand)]
69pub enum Commands {
70    #[command(about = "List available templates")]
71    Templates,
72
73    #[command(about = "Create a new template interactively")]
74    InitTemplate {
75        #[arg(help = "Template name")]
76        name: Option<String>,
77    },
78
79    #[command(about = "Validate a template schema")]
80    ValidateTemplate {
81        #[arg(help = "Path to template directory or .rok-template.json")]
82        path: Option<String>,
83    },
84
85    #[command(about = "Run a saved task")]
86    Run {
87        #[arg(help = "Task name")]
88        name: String,
89    },
90
91    #[command(about = "Save current payload as a named task")]
92    Save {
93        #[arg(help = "Task name")]
94        name: String,
95
96        #[arg(short = 'd', long = "description", help = "Task description")]
97        description: Option<String>,
98    },
99
100    #[command(about = "List saved tasks")]
101    List,
102
103    #[command(about = "Edit a saved task")]
104    Edit {
105        #[arg(help = "Task name")]
106        name: String,
107    },
108
109    #[command(about = "Watch files and re-run on changes")]
110    Watch {
111        #[arg(help = "Path to JSON file")]
112        file: Option<String>,
113
114        #[arg(short = 'w', long = "watch", help = "Files/dirs to watch")]
115        watch: Option<Vec<String>>,
116
117        #[arg(
118            short = 'i',
119            long = "interval",
120            default_value = "1000",
121            help = "Polling interval in ms"
122        )]
123        interval: u64,
124    },
125
126    #[command(about = "Show execution history")]
127    History {
128        #[arg(
129            short = 'n',
130            long = "count",
131            default_value = "10",
132            help = "Number of entries to show"
133        )]
134        count: usize,
135    },
136
137    #[command(about = "Replay a previous execution")]
138    Replay {
139        #[arg(help = "Run ID")]
140        run_id: Option<String>,
141    },
142
143    #[command(about = "Show cache statistics")]
144    Cache {
145        #[arg(short = 's', long = "stats", help = "Show cache statistics")]
146        stats: bool,
147
148        #[arg(long = "clear", help = "Clear cache")]
149        clear: bool,
150    },
151
152    #[command(about = "Manage checkpoints")]
153    Checkpoints {
154        #[arg(short = 'l', long = "list", help = "List all checkpoints")]
155        list: bool,
156
157        #[arg(long = "delete", help = "Delete a checkpoint by ID")]
158        delete: Option<String>,
159    },
160
161    #[command(about = "Serve documentation website")]
162    Serve {
163        #[arg(
164            short = 'p',
165            long = "port",
166            default_value = "8080",
167            help = "Port to serve on"
168        )]
169        port: String,
170    },
171}
172
173#[derive(Debug, Clone, ValueEnum)]
174pub enum OutputFormat {
175    Json,
176    Pretty,
177    Silent,
178}
179
180impl Cli {
181    pub fn parse_payload(&self) -> Result<Payload, RokError> {
182        let json_str = if let Some(ref json) = self.json {
183            json.clone()
184        } else if let Some(ref file) = self.file {
185            fs::read_to_string(file)
186                .map_err(|e| RokError::schema(format!("Failed to read file: {}", e)))?
187        } else {
188            let mut stdin_content = String::new();
189            io::stdin()
190                .read_to_string(&mut stdin_content)
191                .map_err(|e| RokError::schema(format!("Failed to read stdin: {}", e)))?;
192            if stdin_content.trim().is_empty() {
193                return Err(RokError::schema(
194                    "No input provided. Use --json, --file, or pipe JSON to stdin.",
195                ));
196            }
197            stdin_content
198        };
199
200        serde_json::from_str(&json_str)
201            .map_err(|e| RokError::schema(format!("Invalid JSON: {}", e)))
202    }
203}