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}