Skip to main content

sift_queue/
lib.rs

1pub mod cli;
2pub mod collect;
3pub mod queue;
4pub mod queue_path;
5
6use clap::{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    /// Path to task file
18    #[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        .propagate_version(true)
34        .disable_version_flag(true)
35        .arg(
36            Arg::new("version")
37                .short('v')
38                .long("version")
39                .help("Print version")
40                .action(ArgAction::Version)
41                .global(true),
42        );
43    let root_help = crate::cli::help::root_after_help(cmd.get_styles());
44    cmd = cmd.after_help(root_help);
45
46    cmd = cmd.mut_subcommand("collect", |subcmd| {
47        let help = crate::cli::commands::collect::after_help(subcmd.get_styles());
48        subcmd.after_help(help)
49    });
50
51    cmd = cmd.mut_subcommand("add", |subcmd| {
52        let help = crate::cli::commands::add::after_help(subcmd.get_styles());
53        subcmd.after_help(help)
54    });
55
56    cmd = cmd.mut_subcommand("edit", |subcmd| {
57        let help = crate::cli::commands::edit::after_help(subcmd.get_styles());
58        subcmd.after_help(help)
59    });
60
61    cmd = cmd.mut_subcommand("list", |subcmd| {
62        let help = crate::cli::commands::list::after_help(subcmd.get_styles());
63        subcmd.after_help(help)
64    });
65
66    cmd = cmd.mut_subcommand("rm", |subcmd| {
67        let help = crate::cli::commands::rm::after_help(subcmd.get_styles());
68        subcmd.after_help(help)
69    });
70
71    cmd = cmd.mut_subcommand("close", |subcmd| {
72        let help = crate::cli::commands::status::close_after_help(subcmd.get_styles());
73        subcmd.long_about("").after_help(help)
74    });
75
76    cmd
77}
78
79#[derive(Subcommand)]
80pub enum Commands {
81    /// Add a new task
82    Add(AddArgs),
83    /// Collect tasks from stdin
84    Collect(CollectArgs),
85    /// List tasks
86    List(ListArgs),
87    /// Show task details
88    Show(ShowArgs),
89    /// Edit an existing task
90    Edit(EditArgs),
91    /// Mark a task as closed
92    Close(StatusArgs),
93    /// Remove a task
94    Rm(RmArgs),
95    /// Output task workflow context for AI agents
96    Prime(PrimeArgs),
97}
98
99#[derive(Args)]
100#[command(about = "Output task workflow context for AI agents")]
101pub struct PrimeArgs {
102    /// Output only the prelude and skip the command reference
103    #[arg(long = "prelude", display_order = 1)]
104    pub prelude: bool,
105}
106
107#[derive(Parser)]
108pub struct AddArgs {
109    /// Title for the item
110    #[arg(long = "title", value_name = "TITLE", display_order = 1)]
111    pub title: Option<String>,
112
113    /// Description for the item
114    #[arg(long = "description", value_name = "TEXT", display_order = 2)]
115    pub description: Option<String>,
116
117    /// Priority (0-4, 0=highest)
118    #[arg(long = "priority", value_name = "PRIORITY", display_order = 3)]
119    pub priority: Option<String>,
120
121    /// Add diff source (repeatable)
122    #[arg(long = "diff", value_name = "PATH", display_order = 10)]
123    pub diff: Vec<String>,
124
125    /// Add file source (repeatable)
126    #[arg(long = "file", value_name = "PATH", display_order = 11)]
127    pub file: Vec<String>,
128
129    /// Add text source (repeatable)
130    #[arg(long = "text", value_name = "STRING", display_order = 12)]
131    pub text: Vec<String>,
132
133    /// Add directory source (repeatable)
134    #[arg(long = "directory", value_name = "PATH", display_order = 13)]
135    pub directory: Vec<String>,
136
137    /// Read source content from stdin (diff|file|text|directory)
138    #[arg(long = "stdin", value_name = "TYPE", display_order = 14)]
139    pub stdin: Option<String>,
140
141    /// Attach metadata as JSON
142    #[arg(long = "metadata", value_name = "JSON", display_order = 15)]
143    pub metadata: Option<String>,
144
145    /// Comma-separated blocker IDs
146    #[arg(long = "blocked-by", value_name = "IDS", display_order = 16)]
147    pub blocked_by: Option<String>,
148
149    /// Output as JSON
150    #[arg(long = "json", display_order = 17)]
151    pub json: bool,
152}
153
154#[derive(Args)]
155#[command(about = "Collect tasks from stdin")]
156pub struct CollectArgs {
157    /// Title for every created item
158    #[arg(long = "title", value_name = "TITLE", display_order = 1)]
159    pub title: Option<String>,
160
161    /// Description for every created item
162    #[arg(long = "description", value_name = "TEXT", display_order = 2)]
163    pub description: Option<String>,
164
165    /// Priority (0-4, 0=highest)
166    #[arg(long = "priority", value_name = "PRIORITY", display_order = 3)]
167    pub priority: Option<String>,
168
169    /// Split stdin into one item per file
170    #[arg(long = "by-file", display_order = 10)]
171    pub by_file: bool,
172
173    /// Input format: currently only rg-json is supported
174    #[arg(long = "stdin-format", value_name = "FORMAT", display_order = 11)]
175    pub stdin_format: Option<String>,
176
177    /// Template for each created item title
178    #[arg(long = "title-template", value_name = "TEMPLATE", display_order = 12)]
179    pub title_template: Option<String>,
180
181    /// Attach metadata as JSON
182    #[arg(long = "metadata", value_name = "JSON", display_order = 13)]
183    pub metadata: Option<String>,
184
185    /// Comma-separated blocker IDs
186    #[arg(long = "blocked-by", value_name = "IDS", display_order = 14)]
187    pub blocked_by: Option<String>,
188
189    /// Output as JSON
190    #[arg(long = "json", display_order = 15)]
191    pub json: bool,
192}
193
194#[derive(Parser)]
195pub struct ListArgs {
196    /// Filter by status (repeatable: pending|blocked|in_progress|closed)
197    #[arg(long = "status", value_name = "STATUS", display_order = 1)]
198    pub status: Vec<String>,
199
200    /// Include closed items when status is not explicitly filtered
201    #[arg(long = "all", display_order = 2)]
202    pub all: bool,
203
204    /// Filter by priority (repeatable: 0-4)
205    #[arg(long = "priority", value_name = "PRIORITY", display_order = 3)]
206    pub priority: Vec<String>,
207
208    /// Show only ready items (pending and unblocked)
209    #[arg(long = "ready", display_order = 4)]
210    pub ready: bool,
211
212    /// Output as JSON
213    #[arg(long = "json", display_order = 10)]
214    pub json: bool,
215
216    /// jq select expression
217    #[arg(long = "filter", value_name = "EXPR", display_order = 11)]
218    pub filter: Option<String>,
219
220    /// jq path expression to sort by
221    #[arg(long = "sort", value_name = "PATH", display_order = 12)]
222    pub sort: Option<String>,
223
224    /// Reverse sort order
225    #[arg(long = "reverse", display_order = 13)]
226    pub reverse: bool,
227}
228
229#[derive(Parser)]
230pub struct ShowArgs {
231    /// Item ID
232    pub id: Option<String>,
233
234    /// Output as JSON
235    #[arg(long = "json")]
236    pub json: bool,
237}
238
239#[derive(Parser)]
240pub struct EditArgs {
241    /// Item ID
242    pub id: Option<String>,
243
244    /// Set title for the item
245    #[arg(long = "set-title", value_name = "TITLE", display_order = 1)]
246    pub set_title: Option<String>,
247
248    /// Set description for the item
249    #[arg(long = "set-description", value_name = "TEXT", display_order = 2)]
250    pub set_description: Option<String>,
251
252    /// Change status (pending|in_progress|closed)
253    #[arg(long = "set-status", value_name = "STATUS", display_order = 3)]
254    pub set_status: Option<String>,
255
256    /// Set priority (0-4, 0=highest)
257    #[arg(long = "set-priority", value_name = "PRIORITY", display_order = 4)]
258    pub set_priority: Option<String>,
259
260    /// Clear priority
261    #[arg(long = "clear-priority", display_order = 5)]
262    pub clear_priority: bool,
263
264    /// Add diff source
265    #[arg(long = "add-diff", value_name = "PATH", display_order = 10)]
266    pub add_diff: Vec<String>,
267
268    /// Add file source
269    #[arg(long = "add-file", value_name = "PATH", display_order = 11)]
270    pub add_file: Vec<String>,
271
272    /// Add text source
273    #[arg(long = "add-text", value_name = "STRING", display_order = 12)]
274    pub add_text: Vec<String>,
275
276    /// Add directory source
277    #[arg(long = "add-directory", value_name = "PATH", display_order = 13)]
278    pub add_directory: Vec<String>,
279
280    /// Remove source by index (0-based, repeatable)
281    #[arg(long = "rm-source", value_name = "INDEX", display_order = 14)]
282    pub rm_source: Vec<usize>,
283
284    /// Set metadata as JSON (replaces full metadata object)
285    #[arg(long = "set-metadata", value_name = "JSON", display_order = 15)]
286    pub set_metadata: Option<String>,
287
288    /// Merge metadata object as JSON (deep object merge)
289    #[arg(long = "merge-metadata", value_name = "JSON", display_order = 16)]
290    pub merge_metadata: Option<String>,
291
292    /// Set blocker IDs (comma-separated, empty to clear)
293    #[arg(long = "set-blocked-by", value_name = "IDS", display_order = 17)]
294    pub set_blocked_by: Option<String>,
295
296    /// Output as JSON
297    #[arg(long = "json", display_order = 18)]
298    pub json: bool,
299}
300
301#[derive(Parser)]
302pub struct StatusArgs {
303    /// Item ID
304    pub id: Option<String>,
305
306    /// Output as JSON
307    #[arg(long = "json")]
308    pub json: bool,
309}
310
311#[derive(Parser)]
312pub struct RmArgs {
313    /// Item ID
314    pub id: Option<String>,
315
316    /// Output as JSON
317    #[arg(long = "json")]
318    pub json: bool,
319}