1use 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 #[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 #[arg(value_name = "PATH")]
64 pub path: std::path::PathBuf,
65
66 #[arg(long)]
68 pub multi: bool,
69
70 #[arg(long)]
72 pub dry_run: bool,
73
74 #[arg(long, value_enum)]
76 pub priority: Option<PrdPriorityArg>,
77
78 #[arg(long = "tag")]
80 pub tag: Vec<String>,
81
82 #[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}