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,
97}
98
99#[derive(Parser)]
100pub struct AddArgs {
101    /// Title for the item
102    #[arg(long = "title", value_name = "TITLE", display_order = 1)]
103    pub title: Option<String>,
104
105    /// Description for the item
106    #[arg(long = "description", value_name = "TEXT", display_order = 2)]
107    pub description: Option<String>,
108
109    /// Priority (0-4, 0=highest)
110    #[arg(long = "priority", value_name = "PRIORITY", display_order = 3)]
111    pub priority: Option<String>,
112
113    /// Add diff source (repeatable)
114    #[arg(long = "diff", value_name = "PATH", display_order = 10)]
115    pub diff: Vec<String>,
116
117    /// Add file source (repeatable)
118    #[arg(long = "file", value_name = "PATH", display_order = 11)]
119    pub file: Vec<String>,
120
121    /// Add text source (repeatable)
122    #[arg(long = "text", value_name = "STRING", display_order = 12)]
123    pub text: Vec<String>,
124
125    /// Add directory source (repeatable)
126    #[arg(long = "directory", value_name = "PATH", display_order = 13)]
127    pub directory: Vec<String>,
128
129    /// Read source content from stdin (diff|file|text|directory)
130    #[arg(long = "stdin", value_name = "TYPE", display_order = 14)]
131    pub stdin: Option<String>,
132
133    /// Attach metadata as JSON
134    #[arg(long = "metadata", value_name = "JSON", display_order = 15)]
135    pub metadata: Option<String>,
136
137    /// Comma-separated blocker IDs
138    #[arg(long = "blocked-by", value_name = "IDS", display_order = 16)]
139    pub blocked_by: Option<String>,
140
141    /// Output as JSON
142    #[arg(long = "json", display_order = 17)]
143    pub json: bool,
144}
145
146#[derive(Args)]
147#[command(about = "Collect tasks from stdin")]
148pub struct CollectArgs {
149    /// Title for every created item
150    #[arg(long = "title", value_name = "TITLE", display_order = 1)]
151    pub title: Option<String>,
152
153    /// Description for every created item
154    #[arg(long = "description", value_name = "TEXT", display_order = 2)]
155    pub description: Option<String>,
156
157    /// Priority (0-4, 0=highest)
158    #[arg(long = "priority", value_name = "PRIORITY", display_order = 3)]
159    pub priority: Option<String>,
160
161    /// Split stdin into one item per file
162    #[arg(long = "by-file", display_order = 10)]
163    pub by_file: bool,
164
165    /// Input format: currently only rg-json is supported
166    #[arg(long = "stdin-format", value_name = "FORMAT", display_order = 11)]
167    pub stdin_format: Option<String>,
168
169    /// Template for each created item title
170    #[arg(long = "title-template", value_name = "TEMPLATE", display_order = 12)]
171    pub title_template: Option<String>,
172
173    /// Attach metadata as JSON
174    #[arg(long = "metadata", value_name = "JSON", display_order = 13)]
175    pub metadata: Option<String>,
176
177    /// Comma-separated blocker IDs
178    #[arg(long = "blocked-by", value_name = "IDS", display_order = 14)]
179    pub blocked_by: Option<String>,
180
181    /// Output as JSON
182    #[arg(long = "json", display_order = 15)]
183    pub json: bool,
184}
185
186#[derive(Parser)]
187pub struct ListArgs {
188    /// Filter by status (pending|blocked|in_progress|closed)
189    #[arg(long = "status", value_name = "STATUS", display_order = 1)]
190    pub status: Option<String>,
191
192    /// Include closed items when status is not explicitly filtered
193    #[arg(long = "all", display_order = 2)]
194    pub all: bool,
195
196    /// Filter by priority (repeatable: 0-4)
197    #[arg(long = "priority", value_name = "PRIORITY", display_order = 3)]
198    pub priority: Vec<String>,
199
200    /// Show only ready items (pending and unblocked)
201    #[arg(long = "ready", display_order = 4)]
202    pub ready: bool,
203
204    /// Output as JSON
205    #[arg(long = "json", display_order = 10)]
206    pub json: bool,
207
208    /// jq select expression
209    #[arg(long = "filter", value_name = "EXPR", display_order = 11)]
210    pub filter: Option<String>,
211
212    /// jq path expression to sort by
213    #[arg(long = "sort", value_name = "PATH", display_order = 12)]
214    pub sort: Option<String>,
215
216    /// Reverse sort order
217    #[arg(long = "reverse", display_order = 13)]
218    pub reverse: bool,
219}
220
221#[derive(Parser)]
222pub struct ShowArgs {
223    /// Item ID
224    pub id: Option<String>,
225
226    /// Output as JSON
227    #[arg(long = "json")]
228    pub json: bool,
229}
230
231#[derive(Parser)]
232pub struct EditArgs {
233    /// Item ID
234    pub id: Option<String>,
235
236    /// Set title for the item
237    #[arg(long = "set-title", value_name = "TITLE", display_order = 1)]
238    pub set_title: Option<String>,
239
240    /// Set description for the item
241    #[arg(long = "set-description", value_name = "TEXT", display_order = 2)]
242    pub set_description: Option<String>,
243
244    /// Change status (pending|in_progress|closed)
245    #[arg(long = "set-status", value_name = "STATUS", display_order = 3)]
246    pub set_status: Option<String>,
247
248    /// Set priority (0-4, 0=highest)
249    #[arg(long = "set-priority", value_name = "PRIORITY", display_order = 4)]
250    pub set_priority: Option<String>,
251
252    /// Clear priority
253    #[arg(long = "clear-priority", display_order = 5)]
254    pub clear_priority: bool,
255
256    /// Add diff source
257    #[arg(long = "add-diff", value_name = "PATH", display_order = 10)]
258    pub add_diff: Vec<String>,
259
260    /// Add file source
261    #[arg(long = "add-file", value_name = "PATH", display_order = 11)]
262    pub add_file: Vec<String>,
263
264    /// Add text source
265    #[arg(long = "add-text", value_name = "STRING", display_order = 12)]
266    pub add_text: Vec<String>,
267
268    /// Add directory source
269    #[arg(long = "add-directory", value_name = "PATH", display_order = 13)]
270    pub add_directory: Vec<String>,
271
272    /// Remove source by index (0-based, repeatable)
273    #[arg(long = "rm-source", value_name = "INDEX", display_order = 14)]
274    pub rm_source: Vec<usize>,
275
276    /// Set metadata as JSON (replaces full metadata object)
277    #[arg(long = "set-metadata", value_name = "JSON", display_order = 15)]
278    pub set_metadata: Option<String>,
279
280    /// Merge metadata object as JSON (deep object merge)
281    #[arg(long = "merge-metadata", value_name = "JSON", display_order = 16)]
282    pub merge_metadata: Option<String>,
283
284    /// Set blocker IDs (comma-separated, empty to clear)
285    #[arg(long = "set-blocked-by", value_name = "IDS", display_order = 17)]
286    pub set_blocked_by: Option<String>,
287
288    /// Output as JSON
289    #[arg(long = "json", display_order = 18)]
290    pub json: bool,
291}
292
293#[derive(Parser)]
294pub struct StatusArgs {
295    /// Item ID
296    pub id: Option<String>,
297
298    /// Output as JSON
299    #[arg(long = "json")]
300    pub json: bool,
301}
302
303#[derive(Parser)]
304pub struct RmArgs {
305    /// Item ID
306    pub id: Option<String>,
307
308    /// Output as JSON
309    #[arg(long = "json")]
310    pub json: bool,
311}