1pub mod cli;
2pub mod collect;
3pub mod queue;
4pub mod queue_path;
5
6use clap::{builder::StyledStr, Arg, ArgAction, Args, Command, CommandFactory, Parser, Subcommand};
7use std::path::PathBuf;
8
9#[derive(Parser)]
10#[command(
11 name = "sq",
12 version,
13 about = "Lightweight task-list CLI with structured sources",
14 long_about = "sq is a lightweight task-list CLI with structured sources.\n\nIt manages tasks in a JSONL file. You can use it directly from the shell or instruct agents to manage them for you."
15)]
16pub struct Cli {
17 #[arg(
19 short = 'q',
20 long = "queue",
21 value_name = "PATH",
22 global = true,
23 display_order = 900
24 )]
25 pub queue: Option<PathBuf>,
26
27 #[command(subcommand)]
28 pub command: Commands,
29}
30
31pub fn build_cli() -> Command {
32 let mut cmd = Cli::command().propagate_version(true).disable_version_flag(true).arg(
33 Arg::new("version")
34 .short('v')
35 .long("version")
36 .help("Print version")
37 .action(ArgAction::Version)
38 .global(true),
39 );
40 let styles = cmd.get_styles();
41 let header = styles.get_header();
42 let literal = styles.get_literal();
43 let root_help = StyledStr::from(format!(
44 "{header}Task file:{header:#}\n By default, {literal}sq{literal:#} uses {literal}.sift/issues.jsonl{literal:#}\n Override with {literal}-q, --queue <PATH>{literal:#} or {literal}SQ_QUEUE_PATH=<PATH>{literal:#}\n\n{header}Examples:{header:#}\n {literal}sq add --title \"Investigate checkout exception\" --description \"Review the pasted error report and identify the failing code path\" --priority 1 --text \"Sentry alert: NoMethodError in Checkout::ApplyDiscount at app/services/checkout/apply_discount.rb:42\"{literal:#}\n {literal}rg --json -n -C2 'OldApi.call' | sq collect --by-file --title-template \"migrate: {{{{filepath}}}}\" --description \"Migrate OldApi.call to NewApi.call\" --priority 2{literal:#}\n {literal}sq list --ready{literal:#}"
45 ));
46 cmd = cmd.after_help(root_help);
47
48 cmd.mut_subcommand("collect", |subcmd| {
49 let styles = subcmd.get_styles();
50 let header = styles.get_header();
51 let literal = styles.get_literal();
52 let help = StyledStr::from(format!(
53 "{header}Examples:{header:#}\n {literal}rg --json PATTERN | sq collect --by-file --title-template \"review: {{{{filepath}}}}\" --description \"Review ripgrep matches\"{literal:#}\n {literal}rg --json -n -C2 PATTERN | sq collect --by-file --title-template \"migrate: {{{{filepath}}}}\" --description \"Migrate OldApi.call to NewApi.call\"{literal:#}\n\nPlain-text {literal}rg{literal:#} output is not supported. Pass ripgrep context flags like {literal}-n{literal:#}, {literal}-C2{literal:#}, {literal}-A2{literal:#}, or {literal}-B2{literal:#} to include line numbers and surrounding context in each created text source.\n\n{header}Templates:{header:#}\n {literal}{{{{filepath}}}}{literal:#} Full file path for the grouped result\n {literal}{{{{filename}}}}{literal:#} Basename of {literal}{{{{filepath}}}}{literal:#}\n {literal}{{{{match_count}}}}{literal:#} Number of rg match events collected for the file\n\n Default title template: {literal}{{{{match_count}}}}:{{{{filepath}}}}{literal:#}"
54 ));
55
56 subcmd.after_help(help)
57 })
58}
59
60#[derive(Subcommand)]
61pub enum Commands {
62 Add(AddArgs),
64 Collect(CollectArgs),
66 List(ListArgs),
68 Show(ShowArgs),
70 Edit(EditArgs),
72 Close(StatusArgs),
74 Rm(RmArgs),
76 Prime(PrimeArgs),
78}
79
80#[derive(Parser)]
81pub struct AddArgs {
82 #[arg(long = "title", value_name = "TITLE", display_order = 1)]
84 pub title: Option<String>,
85
86 #[arg(long = "description", value_name = "TEXT", display_order = 2)]
88 pub description: Option<String>,
89
90 #[arg(long = "priority", value_name = "PRIORITY", display_order = 3)]
92 pub priority: Option<String>,
93
94 #[arg(long = "diff", value_name = "PATH", display_order = 10)]
96 pub diff: Vec<String>,
97
98 #[arg(long = "file", value_name = "PATH", display_order = 11)]
100 pub file: Vec<String>,
101
102 #[arg(long = "text", value_name = "STRING", display_order = 12)]
104 pub text: Vec<String>,
105
106 #[arg(long = "directory", value_name = "PATH", display_order = 13)]
108 pub directory: Vec<String>,
109
110 #[arg(long = "stdin", value_name = "TYPE", display_order = 14)]
112 pub stdin: Option<String>,
113
114 #[arg(long = "metadata", value_name = "JSON", display_order = 15)]
116 pub metadata: Option<String>,
117
118 #[arg(long = "blocked-by", value_name = "IDS", display_order = 16)]
120 pub blocked_by: Option<String>,
121
122 #[arg(long = "json", display_order = 17)]
124 pub json: bool,
125}
126
127#[derive(Args)]
128#[command(about = "Collect tasks from stdin")]
129pub struct CollectArgs {
130 #[arg(long = "title", value_name = "TITLE", display_order = 1)]
132 pub title: Option<String>,
133
134 #[arg(long = "description", value_name = "TEXT", display_order = 2)]
136 pub description: Option<String>,
137
138 #[arg(long = "priority", value_name = "PRIORITY", display_order = 3)]
140 pub priority: Option<String>,
141
142 #[arg(long = "by-file", display_order = 10)]
144 pub by_file: bool,
145
146 #[arg(long = "stdin-format", value_name = "FORMAT", display_order = 11)]
148 pub stdin_format: Option<String>,
149
150 #[arg(long = "title-template", value_name = "TEMPLATE", display_order = 12)]
152 pub title_template: Option<String>,
153
154 #[arg(long = "metadata", value_name = "JSON", display_order = 13)]
156 pub metadata: Option<String>,
157
158 #[arg(long = "blocked-by", value_name = "IDS", display_order = 14)]
160 pub blocked_by: Option<String>,
161
162 #[arg(long = "json", display_order = 15)]
164 pub json: bool,
165}
166
167#[derive(Parser)]
168pub struct ListArgs {
169 #[arg(long = "status", value_name = "STATUS")]
171 pub status: Option<String>,
172
173 #[arg(long = "all")]
175 pub all: bool,
176
177 #[arg(long = "json")]
179 pub json: bool,
180
181 #[arg(long = "filter", value_name = "EXPR")]
183 pub filter: Option<String>,
184
185 #[arg(long = "sort", value_name = "PATH")]
187 pub sort: Option<String>,
188
189 #[arg(long = "reverse")]
191 pub reverse: bool,
192
193 #[arg(long = "ready")]
195 pub ready: bool,
196}
197
198#[derive(Parser)]
199pub struct ShowArgs {
200 pub id: Option<String>,
202
203 #[arg(long = "json")]
205 pub json: bool,
206}
207
208#[derive(Parser)]
209pub struct EditArgs {
210 pub id: Option<String>,
212
213 #[arg(long = "set-title", value_name = "TITLE", display_order = 1)]
215 pub set_title: Option<String>,
216
217 #[arg(long = "set-description", value_name = "TEXT", display_order = 2)]
219 pub set_description: Option<String>,
220
221 #[arg(long = "set-status", value_name = "STATUS", display_order = 3)]
223 pub set_status: Option<String>,
224
225 #[arg(long = "set-priority", value_name = "PRIORITY", display_order = 4)]
227 pub set_priority: Option<String>,
228
229 #[arg(long = "clear-priority", display_order = 5)]
231 pub clear_priority: bool,
232
233 #[arg(long = "add-diff", value_name = "PATH", display_order = 10)]
235 pub add_diff: Vec<String>,
236
237 #[arg(long = "add-file", value_name = "PATH", display_order = 11)]
239 pub add_file: Vec<String>,
240
241 #[arg(long = "add-text", value_name = "STRING", display_order = 12)]
243 pub add_text: Vec<String>,
244
245 #[arg(long = "add-directory", value_name = "PATH", display_order = 13)]
247 pub add_directory: Vec<String>,
248
249 #[arg(long = "add-transcript", value_name = "PATH", display_order = 14)]
251 pub add_transcript: Vec<String>,
252
253 #[arg(long = "rm-source", value_name = "INDEX", display_order = 15)]
255 pub rm_source: Vec<usize>,
256
257 #[arg(long = "set-metadata", value_name = "JSON", display_order = 16)]
259 pub set_metadata: Option<String>,
260
261 #[arg(long = "merge-metadata", value_name = "JSON", display_order = 17)]
263 pub merge_metadata: Option<String>,
264
265 #[arg(long = "set-blocked-by", value_name = "IDS", display_order = 18)]
267 pub set_blocked_by: Option<String>,
268
269 #[arg(long = "json", display_order = 19)]
271 pub json: bool,
272}
273
274#[derive(Parser)]
275pub struct StatusArgs {
276 pub id: Option<String>,
278
279 #[arg(long = "json")]
281 pub json: bool,
282}
283
284#[derive(Parser)]
285pub struct RmArgs {
286 pub id: Option<String>,
288
289 #[arg(long = "json")]
291 pub json: bool,
292}
293
294#[derive(Parser)]
295pub struct PrimeArgs {
296 #[arg(long = "full")]
298 pub full: bool,
299}