1pub mod cli;
2pub mod collect;
3pub mod queue;
4pub mod queue_path;
5
6use clap::{builder::StyledStr, 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();
33 let styles = cmd.get_styles();
34 let header = styles.get_header();
35 let literal = styles.get_literal();
36 let root_help = StyledStr::from(format!(
37 "{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\" --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\"{literal:#}\n {literal}sq list --ready{literal:#}"
38 ));
39 cmd = cmd.after_help(root_help);
40
41 cmd.mut_subcommand("collect", |subcmd| {
42 let styles = subcmd.get_styles();
43 let header = styles.get_header();
44 let literal = styles.get_literal();
45 let help = StyledStr::from(format!(
46 "{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:#}"
47 ));
48
49 subcmd.after_help(help)
50 })
51}
52
53#[derive(Subcommand)]
54pub enum Commands {
55 Add(AddArgs),
57 Collect(CollectArgs),
59 List(ListArgs),
61 Show(ShowArgs),
63 Edit(EditArgs),
65 Close(StatusArgs),
67 Rm(RmArgs),
69 Prime(PrimeArgs),
71}
72
73#[derive(Parser)]
74pub struct AddArgs {
75 #[arg(long = "title", value_name = "TITLE", display_order = 1)]
77 pub title: Option<String>,
78
79 #[arg(long = "description", value_name = "TEXT", display_order = 2)]
81 pub description: Option<String>,
82
83 #[arg(long = "diff", value_name = "PATH", display_order = 10)]
85 pub diff: Vec<String>,
86
87 #[arg(long = "file", value_name = "PATH", display_order = 11)]
89 pub file: Vec<String>,
90
91 #[arg(long = "text", value_name = "STRING", display_order = 12)]
93 pub text: Vec<String>,
94
95 #[arg(long = "directory", value_name = "PATH", display_order = 13)]
97 pub directory: Vec<String>,
98
99 #[arg(long = "stdin", value_name = "TYPE", display_order = 14)]
101 pub stdin: Option<String>,
102
103 #[arg(long = "metadata", value_name = "JSON", display_order = 15)]
105 pub metadata: Option<String>,
106
107 #[arg(long = "blocked-by", value_name = "IDS", display_order = 16)]
109 pub blocked_by: Option<String>,
110
111 #[arg(long = "json", display_order = 17)]
113 pub json: bool,
114}
115
116#[derive(Args)]
117#[command(about = "Collect tasks from stdin")]
118pub struct CollectArgs {
119 #[arg(long = "title", value_name = "TITLE", display_order = 1)]
121 pub title: Option<String>,
122
123 #[arg(long = "description", value_name = "TEXT", display_order = 2)]
125 pub description: Option<String>,
126
127 #[arg(long = "by-file", display_order = 10)]
129 pub by_file: bool,
130
131 #[arg(long = "stdin-format", value_name = "FORMAT", display_order = 11)]
133 pub stdin_format: Option<String>,
134
135 #[arg(long = "title-template", value_name = "TEMPLATE", display_order = 12)]
137 pub title_template: Option<String>,
138
139 #[arg(long = "metadata", value_name = "JSON", display_order = 13)]
141 pub metadata: Option<String>,
142
143 #[arg(long = "blocked-by", value_name = "IDS", display_order = 14)]
145 pub blocked_by: Option<String>,
146
147 #[arg(long = "json", display_order = 15)]
149 pub json: bool,
150}
151
152#[derive(Parser)]
153pub struct ListArgs {
154 #[arg(long = "status", value_name = "STATUS")]
156 pub status: Option<String>,
157
158 #[arg(long = "all")]
160 pub all: bool,
161
162 #[arg(long = "json")]
164 pub json: bool,
165
166 #[arg(long = "filter", value_name = "EXPR")]
168 pub filter: Option<String>,
169
170 #[arg(long = "sort", value_name = "PATH")]
172 pub sort: Option<String>,
173
174 #[arg(long = "reverse")]
176 pub reverse: bool,
177
178 #[arg(long = "ready")]
180 pub ready: bool,
181}
182
183#[derive(Parser)]
184pub struct ShowArgs {
185 pub id: Option<String>,
187
188 #[arg(long = "json")]
190 pub json: bool,
191}
192
193#[derive(Parser)]
194pub struct EditArgs {
195 pub id: Option<String>,
197
198 #[arg(long = "set-title", value_name = "TITLE", display_order = 1)]
200 pub set_title: Option<String>,
201
202 #[arg(long = "set-description", value_name = "TEXT", display_order = 2)]
204 pub set_description: Option<String>,
205
206 #[arg(long = "set-status", value_name = "STATUS", display_order = 3)]
208 pub set_status: Option<String>,
209
210 #[arg(long = "add-diff", value_name = "PATH", display_order = 10)]
212 pub add_diff: Vec<String>,
213
214 #[arg(long = "add-file", value_name = "PATH", display_order = 11)]
216 pub add_file: Vec<String>,
217
218 #[arg(long = "add-text", value_name = "STRING", display_order = 12)]
220 pub add_text: Vec<String>,
221
222 #[arg(long = "add-directory", value_name = "PATH", display_order = 13)]
224 pub add_directory: Vec<String>,
225
226 #[arg(long = "add-transcript", value_name = "PATH", display_order = 14)]
228 pub add_transcript: Vec<String>,
229
230 #[arg(long = "rm-source", value_name = "INDEX", display_order = 15)]
232 pub rm_source: Vec<usize>,
233
234 #[arg(long = "set-metadata", value_name = "JSON", display_order = 16)]
236 pub set_metadata: Option<String>,
237
238 #[arg(long = "merge-metadata", value_name = "JSON", display_order = 17)]
240 pub merge_metadata: Option<String>,
241
242 #[arg(long = "set-blocked-by", value_name = "IDS", display_order = 18)]
244 pub set_blocked_by: Option<String>,
245
246 #[arg(long = "json", display_order = 19)]
248 pub json: bool,
249}
250
251#[derive(Parser)]
252pub struct StatusArgs {
253 pub id: Option<String>,
255
256 #[arg(long = "json")]
258 pub json: bool,
259}
260
261#[derive(Parser)]
262pub struct RmArgs {
263 pub id: Option<String>,
265
266 #[arg(long = "json")]
268 pub json: bool,
269}
270
271#[derive(Parser)]
272pub struct PrimeArgs {
273 #[arg(long = "full")]
275 pub full: bool,
276}