Skip to main content

ralph/cli/
prd.rs

1//! `ralph prd ...` command group: Clap types and handler.
2//!
3//! Responsibilities:
4//! - Define clap structures for PRD-related commands.
5//! - Route PRD subcommands to the implementation layer.
6//!
7//! Not handled here:
8//! - PRD parsing logic (see `crate::commands::prd`).
9//! - Queue persistence or lock management.
10//! - Task generation from PRD content.
11//!
12//! Invariants/assumptions:
13//! - Callers resolve configuration before executing commands.
14//! - PRD file paths are validated to exist and be readable.
15//! - Generated tasks follow the standard task schema.
16
17use anyhow::Result;
18use clap::{Args, Subcommand};
19
20use crate::commands::prd as prd_cmd;
21use crate::config;
22
23pub fn handle_prd(args: PrdArgs, force: bool) -> Result<()> {
24    let resolved = config::resolve_from_cwd()?;
25
26    match args.command {
27        PrdCommand::Create(args) => {
28            let opts = prd_cmd::CreateOptions {
29                path: args.path,
30                multi: args.multi,
31                dry_run: args.dry_run,
32                priority: args.priority.map(|p| p.into()),
33                tags: args.tag,
34                draft: args.draft,
35            };
36            prd_cmd::create_from_prd(&resolved, &opts, force)
37        }
38    }
39}
40
41#[derive(Args)]
42#[command(
43    about = "Convert PRD (Product Requirements Document) markdown to tasks",
44    after_long_help = "Examples:\n  ralph prd create docs/prd/new-feature.md\n  ralph prd create docs/prd/new-feature.md --multi\n  ralph prd create docs/prd/new-feature.md --dry-run\n  ralph prd create docs/prd/new-feature.md --priority high --tag feature\n  ralph prd create docs/prd/new-feature.md --draft"
45)]
46pub struct PrdArgs {
47    #[command(subcommand)]
48    pub command: PrdCommand,
49}
50
51#[derive(Subcommand)]
52pub enum PrdCommand {
53    /// Create task(s) from a PRD markdown file.
54    #[command(
55        after_long_help = "Converts a PRD markdown file into one or more Ralph tasks.\n\nBy default, creates a single consolidated task from the PRD.\nUse --multi to create one task per user story found in the PRD.\n\nPRD Format:\nThe PRD should contain standard markdown sections:\n- Title (first # heading)\n- Introduction/Overview (optional)\n- User Stories (### US-XXX: Title format)\n- Functional Requirements (optional)\n- Non-Goals (optional)\n\nExamples:\n  ralph prd create path/to/prd.md\n  ralph prd create path/to/prd.md --multi\n  ralph prd create path/to/prd.md --dry-run\n  ralph prd create path/to/prd.md --priority high --tag feature --tag v2.0\n  ralph prd create path/to/prd.md --draft\n  ralph prd create path/to/prd.md --multi --priority medium --tag user-story"
56    )]
57    Create(PrdCreateArgs),
58}
59
60#[derive(Args)]
61pub struct PrdCreateArgs {
62    /// Path to the PRD markdown file.
63    #[arg(value_name = "PATH")]
64    pub path: std::path::PathBuf,
65
66    /// Create multiple tasks (one per user story) instead of a single consolidated task.
67    #[arg(long)]
68    pub multi: bool,
69
70    /// Preview generated tasks without inserting into the queue.
71    #[arg(long)]
72    pub dry_run: bool,
73
74    /// Set priority for generated tasks (low, medium, high, critical).
75    #[arg(long, value_enum)]
76    pub priority: Option<PrdPriorityArg>,
77
78    /// Add tags to all generated tasks (repeatable).
79    #[arg(long = "tag")]
80    pub tag: Vec<String>,
81
82    /// Create tasks as draft status instead of todo.
83    #[arg(long)]
84    pub draft: bool,
85}
86
87#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
88#[clap(rename_all = "snake_case")]
89pub enum PrdPriorityArg {
90    Low,
91    Medium,
92    High,
93    Critical,
94}
95
96impl From<PrdPriorityArg> for crate::contracts::TaskPriority {
97    fn from(value: PrdPriorityArg) -> Self {
98        match value {
99            PrdPriorityArg::Low => crate::contracts::TaskPriority::Low,
100            PrdPriorityArg::Medium => crate::contracts::TaskPriority::Medium,
101            PrdPriorityArg::High => crate::contracts::TaskPriority::High,
102            PrdPriorityArg::Critical => crate::contracts::TaskPriority::Critical,
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use clap::Parser;
111
112    #[test]
113    fn cli_parses_prd_create_basic() {
114        let cli = crate::cli::Cli::try_parse_from(["ralph", "prd", "create", "docs/prd.md"])
115            .expect("parse");
116        match cli.command {
117            crate::cli::Command::Prd(args) => match args.command {
118                PrdCommand::Create(create_args) => {
119                    assert_eq!(create_args.path, std::path::PathBuf::from("docs/prd.md"));
120                    assert!(!create_args.multi);
121                    assert!(!create_args.dry_run);
122                    assert!(!create_args.draft);
123                }
124            },
125            _ => panic!("expected prd command"),
126        }
127    }
128
129    #[test]
130    fn cli_parses_prd_create_with_flags() {
131        let cli = crate::cli::Cli::try_parse_from([
132            "ralph",
133            "prd",
134            "create",
135            "docs/prd.md",
136            "--multi",
137            "--dry-run",
138            "--priority",
139            "high",
140            "--tag",
141            "feature",
142            "--tag",
143            "v2.0",
144            "--draft",
145        ])
146        .expect("parse");
147        match cli.command {
148            crate::cli::Command::Prd(args) => match args.command {
149                PrdCommand::Create(create_args) => {
150                    assert_eq!(create_args.path, std::path::PathBuf::from("docs/prd.md"));
151                    assert!(create_args.multi);
152                    assert!(create_args.dry_run);
153                    assert!(create_args.draft);
154                    assert_eq!(create_args.priority, Some(PrdPriorityArg::High));
155                    assert_eq!(create_args.tag, vec!["feature", "v2.0"]);
156                }
157            },
158            _ => panic!("expected prd command"),
159        }
160    }
161
162    #[test]
163    fn cli_parses_prd_create_priority_variants() {
164        for (arg, expected) in [
165            ("low", PrdPriorityArg::Low),
166            ("medium", PrdPriorityArg::Medium),
167            ("high", PrdPriorityArg::High),
168            ("critical", PrdPriorityArg::Critical),
169        ] {
170            let cli = crate::cli::Cli::try_parse_from([
171                "ralph",
172                "prd",
173                "create",
174                "docs/prd.md",
175                "--priority",
176                arg,
177            ])
178            .expect("parse");
179            match cli.command {
180                crate::cli::Command::Prd(args) => match args.command {
181                    PrdCommand::Create(create_args) => {
182                        assert_eq!(
183                            create_args.priority,
184                            Some(expected),
185                            "failed for priority: {arg}"
186                        );
187                    }
188                },
189                _ => panic!("expected prd command"),
190            }
191        }
192    }
193}