1use anyhow::Result;
16use clap::{Args, Subcommand, ValueEnum};
17use std::path::PathBuf;
18
19use crate::commands::context as context_cmd;
20use crate::config;
21
22pub fn handle_context(args: ContextArgs) -> Result<()> {
24 let resolved = config::resolve_from_cwd()?;
25
26 match args.command {
27 ContextCommand::Init(init_args) => {
28 let report = context_cmd::run_context_init(
29 &resolved,
30 context_cmd::ContextInitOptions {
31 force: init_args.force,
32 project_type_hint: init_args.project_type,
33 output_path: init_args
34 .output
35 .unwrap_or_else(|| resolved.repo_root.join("AGENTS.md")),
36 interactive: init_args.interactive,
37 },
38 )?;
39
40 match report.status {
41 context_cmd::FileInitStatus::Created => {
42 log::info!(
43 "AGENTS.md created for {} project ({})",
44 format!("{:?}", report.detected_project_type).to_lowercase(),
45 report.output_path.display()
46 );
47 }
48 context_cmd::FileInitStatus::Valid => {
49 log::info!(
50 "AGENTS.md already exists ({}). Use --force to overwrite.",
51 report.output_path.display()
52 );
53 }
54 }
55 Ok(())
56 }
57 ContextCommand::Update(update_args) => {
58 let report = context_cmd::run_context_update(
59 &resolved,
60 context_cmd::ContextUpdateOptions {
61 sections: update_args.section,
62 file: update_args.file,
63 interactive: update_args.interactive,
64 dry_run: update_args.dry_run,
65 output_path: update_args
66 .output
67 .unwrap_or_else(|| resolved.repo_root.join("AGENTS.md")),
68 },
69 )?;
70
71 if report.dry_run {
72 log::info!("Dry run - no changes written");
73 } else {
74 log::info!(
75 "AGENTS.md updated: {} sections modified",
76 report.sections_updated.len()
77 );
78 }
79 Ok(())
80 }
81 ContextCommand::Validate(validate_args) => {
82 let report = context_cmd::run_context_validate(
83 &resolved,
84 context_cmd::ContextValidateOptions {
85 strict: validate_args.strict,
86 path: validate_args
87 .path
88 .unwrap_or_else(|| resolved.repo_root.join("AGENTS.md")),
89 },
90 )?;
91
92 if report.valid {
93 log::info!("AGENTS.md is valid and up to date");
94 } else {
95 log::warn!("AGENTS.md has issues:");
96 if !report.missing_sections.is_empty() {
97 log::warn!(" Missing sections: {:?}", report.missing_sections);
98 }
99 if !report.outdated_sections.is_empty() {
100 log::warn!(" Outdated sections: {:?}", report.outdated_sections);
101 }
102 anyhow::bail!("Validation failed");
103 }
104 Ok(())
105 }
106 }
107}
108
109#[derive(Args)]
110#[command(
111 about = "Manage project context (AGENTS.md) for AI agents",
112 after_long_help = "Examples:\n ralph context init\n ralph context init --project-type rust\n ralph context update --section troubleshooting\n ralph context validate\n ralph context update --dry-run"
113)]
114pub struct ContextArgs {
115 #[command(subcommand)]
116 pub command: ContextCommand,
117}
118
119#[derive(Subcommand)]
120pub enum ContextCommand {
121 #[command(
123 after_long_help = "Examples:\n ralph context init\n ralph context init --force\n ralph context init --project-type python --output docs/AGENTS.md"
124 )]
125 Init(ContextInitArgs),
126
127 #[command(
129 after_long_help = "Examples:\n ralph context update --section troubleshooting\n ralph context update --file new_learnings.md\n ralph context update --interactive"
130 )]
131 Update(ContextUpdateArgs),
132
133 #[command(
135 after_long_help = "Examples:\n ralph context validate\n ralph context validate --strict"
136 )]
137 Validate(ContextValidateArgs),
138}
139
140#[derive(Args)]
141pub struct ContextInitArgs {
142 #[arg(long)]
144 pub force: bool,
145
146 #[arg(long, value_enum)]
148 pub project_type: Option<ProjectTypeHint>,
149
150 #[arg(long, short)]
152 pub output: Option<PathBuf>,
153
154 #[arg(long, short)]
156 pub interactive: bool,
157}
158
159#[derive(Args)]
160pub struct ContextUpdateArgs {
161 #[arg(long, short)]
163 pub section: Vec<String>,
164
165 #[arg(long, short)]
167 pub file: Option<PathBuf>,
168
169 #[arg(long, short)]
171 pub interactive: bool,
172
173 #[arg(long)]
175 pub dry_run: bool,
176
177 #[arg(long, short)]
179 pub output: Option<PathBuf>,
180}
181
182#[derive(Args)]
183pub struct ContextValidateArgs {
184 #[arg(long)]
186 pub strict: bool,
187
188 #[arg(long, short)]
190 pub path: Option<PathBuf>,
191}
192
193#[derive(Clone, Copy, Debug, PartialEq, ValueEnum)]
194pub enum ProjectTypeHint {
195 Rust,
196 Python,
197 TypeScript,
198 Go,
199 Generic,
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use clap::Parser;
206
207 #[test]
208 fn cli_parses_context_init() {
209 let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "init"]).expect("parse");
210 match cli.command {
211 crate::cli::Command::Context(args) => match args.command {
212 ContextCommand::Init(init_args) => {
213 assert!(!init_args.force);
214 assert!(init_args.project_type.is_none());
215 assert!(init_args.output.is_none());
216 assert!(!init_args.interactive);
217 }
218 _ => panic!("expected context init command"),
219 },
220 _ => panic!("expected context command"),
221 }
222 }
223
224 #[test]
225 fn cli_parses_context_init_with_force() {
226 let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "init", "--force"])
227 .expect("parse");
228 match cli.command {
229 crate::cli::Command::Context(args) => match args.command {
230 ContextCommand::Init(init_args) => {
231 assert!(init_args.force);
232 }
233 _ => panic!("expected context init command"),
234 },
235 _ => panic!("expected context command"),
236 }
237 }
238
239 #[test]
240 fn cli_parses_context_init_with_project_type() {
241 let cli =
242 crate::cli::Cli::try_parse_from(["ralph", "context", "init", "--project-type", "rust"])
243 .expect("parse");
244 match cli.command {
245 crate::cli::Command::Context(args) => match args.command {
246 ContextCommand::Init(init_args) => {
247 assert_eq!(init_args.project_type, Some(ProjectTypeHint::Rust));
248 }
249 _ => panic!("expected context init command"),
250 },
251 _ => panic!("expected context command"),
252 }
253 }
254
255 #[test]
256 fn cli_parses_context_init_with_output() {
257 let cli = crate::cli::Cli::try_parse_from([
258 "ralph",
259 "context",
260 "init",
261 "--output",
262 "docs/AGENTS.md",
263 ])
264 .expect("parse");
265 match cli.command {
266 crate::cli::Command::Context(args) => match args.command {
267 ContextCommand::Init(init_args) => {
268 assert_eq!(init_args.output, Some(PathBuf::from("docs/AGENTS.md")));
269 }
270 _ => panic!("expected context init command"),
271 },
272 _ => panic!("expected context command"),
273 }
274 }
275
276 #[test]
277 fn cli_parses_context_update_with_section() {
278 let cli = crate::cli::Cli::try_parse_from([
279 "ralph",
280 "context",
281 "update",
282 "--section",
283 "troubleshooting",
284 ])
285 .expect("parse");
286 match cli.command {
287 crate::cli::Command::Context(args) => match args.command {
288 ContextCommand::Update(update_args) => {
289 assert_eq!(update_args.section, vec!["troubleshooting"]);
290 assert!(!update_args.dry_run);
291 }
292 _ => panic!("expected context update command"),
293 },
294 _ => panic!("expected context command"),
295 }
296 }
297
298 #[test]
299 fn cli_parses_context_update_with_multiple_sections() {
300 let cli = crate::cli::Cli::try_parse_from([
301 "ralph",
302 "context",
303 "update",
304 "--section",
305 "troubleshooting",
306 "--section",
307 "git-hygiene",
308 ])
309 .expect("parse");
310 match cli.command {
311 crate::cli::Command::Context(args) => match args.command {
312 ContextCommand::Update(update_args) => {
313 assert_eq!(update_args.section, vec!["troubleshooting", "git-hygiene"]);
314 }
315 _ => panic!("expected context update command"),
316 },
317 _ => panic!("expected context command"),
318 }
319 }
320
321 #[test]
322 fn cli_parses_context_update_with_dry_run() {
323 let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "update", "--dry-run"])
324 .expect("parse");
325 match cli.command {
326 crate::cli::Command::Context(args) => match args.command {
327 ContextCommand::Update(update_args) => {
328 assert!(update_args.dry_run);
329 }
330 _ => panic!("expected context update command"),
331 },
332 _ => panic!("expected context command"),
333 }
334 }
335
336 #[test]
337 fn cli_parses_context_validate() {
338 let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "validate"]).expect("parse");
339 match cli.command {
340 crate::cli::Command::Context(args) => match args.command {
341 ContextCommand::Validate(validate_args) => {
342 assert!(!validate_args.strict);
343 assert!(validate_args.path.is_none());
344 }
345 _ => panic!("expected context validate command"),
346 },
347 _ => panic!("expected context command"),
348 }
349 }
350
351 #[test]
352 fn cli_parses_context_validate_with_strict() {
353 let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "validate", "--strict"])
354 .expect("parse");
355 match cli.command {
356 crate::cli::Command::Context(args) => match args.command {
357 ContextCommand::Validate(validate_args) => {
358 assert!(validate_args.strict);
359 }
360 _ => panic!("expected context validate command"),
361 },
362 _ => panic!("expected context command"),
363 }
364 }
365}