use anyhow::Result;
use clap::{Args, Subcommand, ValueEnum};
use std::path::PathBuf;
use crate::commands::context as context_cmd;
use crate::config;
pub fn handle_context(args: ContextArgs) -> Result<()> {
let resolved = config::resolve_from_cwd()?;
match args.command {
ContextCommand::Init(init_args) => {
let report = context_cmd::run_context_init(
&resolved,
context_cmd::ContextInitOptions {
force: init_args.force,
project_type_hint: init_args.project_type,
output_path: init_args
.output
.unwrap_or_else(|| resolved.repo_root.join("AGENTS.md")),
interactive: init_args.interactive,
},
)?;
match report.status {
context_cmd::FileInitStatus::Created => {
log::info!(
"AGENTS.md created for {} project ({})",
format!("{:?}", report.detected_project_type).to_lowercase(),
report.output_path.display()
);
}
context_cmd::FileInitStatus::Valid => {
log::info!(
"AGENTS.md already exists ({}). Use --force to overwrite.",
report.output_path.display()
);
}
}
Ok(())
}
ContextCommand::Update(update_args) => {
let report = context_cmd::run_context_update(
&resolved,
context_cmd::ContextUpdateOptions {
sections: update_args.section,
file: update_args.file,
interactive: update_args.interactive,
dry_run: update_args.dry_run,
output_path: update_args
.output
.unwrap_or_else(|| resolved.repo_root.join("AGENTS.md")),
},
)?;
if report.dry_run {
log::info!("Dry run - no changes written");
} else {
log::info!(
"AGENTS.md updated: {} sections modified",
report.sections_updated.len()
);
}
Ok(())
}
ContextCommand::Validate(validate_args) => {
let report = context_cmd::run_context_validate(
&resolved,
context_cmd::ContextValidateOptions {
strict: validate_args.strict,
path: validate_args
.path
.unwrap_or_else(|| resolved.repo_root.join("AGENTS.md")),
},
)?;
if report.valid {
log::info!("AGENTS.md is valid and up to date");
} else {
log::warn!("AGENTS.md has issues:");
if !report.missing_sections.is_empty() {
log::warn!(" Missing sections: {:?}", report.missing_sections);
}
if !report.outdated_sections.is_empty() {
log::warn!(" Outdated sections: {:?}", report.outdated_sections);
}
anyhow::bail!("Validation failed");
}
Ok(())
}
}
}
#[derive(Args)]
#[command(
about = "Manage project context (AGENTS.md) for AI agents",
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"
)]
pub struct ContextArgs {
#[command(subcommand)]
pub command: ContextCommand,
}
#[derive(Subcommand)]
pub enum ContextCommand {
#[command(
after_long_help = "Examples:\n ralph context init\n ralph context init --force\n ralph context init --project-type python --output docs/AGENTS.md"
)]
Init(ContextInitArgs),
#[command(
after_long_help = "Examples:\n ralph context update --section troubleshooting\n ralph context update --file new_learnings.md\n ralph context update --interactive"
)]
Update(ContextUpdateArgs),
#[command(
after_long_help = "Examples:\n ralph context validate\n ralph context validate --strict"
)]
Validate(ContextValidateArgs),
}
#[derive(Args)]
pub struct ContextInitArgs {
#[arg(long)]
pub force: bool,
#[arg(long, value_enum)]
pub project_type: Option<ProjectTypeHint>,
#[arg(long, short)]
pub output: Option<PathBuf>,
#[arg(long, short)]
pub interactive: bool,
}
#[derive(Args)]
pub struct ContextUpdateArgs {
#[arg(long, short)]
pub section: Vec<String>,
#[arg(long, short)]
pub file: Option<PathBuf>,
#[arg(long, short)]
pub interactive: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long, short)]
pub output: Option<PathBuf>,
}
#[derive(Args)]
pub struct ContextValidateArgs {
#[arg(long)]
pub strict: bool,
#[arg(long, short)]
pub path: Option<PathBuf>,
}
#[derive(Clone, Copy, Debug, PartialEq, ValueEnum)]
pub enum ProjectTypeHint {
Rust,
Python,
TypeScript,
Go,
Generic,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn cli_parses_context_init() {
let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "init"]).expect("parse");
match cli.command {
crate::cli::Command::Context(args) => match args.command {
ContextCommand::Init(init_args) => {
assert!(!init_args.force);
assert!(init_args.project_type.is_none());
assert!(init_args.output.is_none());
assert!(!init_args.interactive);
}
_ => panic!("expected context init command"),
},
_ => panic!("expected context command"),
}
}
#[test]
fn cli_parses_context_init_with_force() {
let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "init", "--force"])
.expect("parse");
match cli.command {
crate::cli::Command::Context(args) => match args.command {
ContextCommand::Init(init_args) => {
assert!(init_args.force);
}
_ => panic!("expected context init command"),
},
_ => panic!("expected context command"),
}
}
#[test]
fn cli_parses_context_init_with_project_type() {
let cli =
crate::cli::Cli::try_parse_from(["ralph", "context", "init", "--project-type", "rust"])
.expect("parse");
match cli.command {
crate::cli::Command::Context(args) => match args.command {
ContextCommand::Init(init_args) => {
assert_eq!(init_args.project_type, Some(ProjectTypeHint::Rust));
}
_ => panic!("expected context init command"),
},
_ => panic!("expected context command"),
}
}
#[test]
fn cli_parses_context_init_with_output() {
let cli = crate::cli::Cli::try_parse_from([
"ralph",
"context",
"init",
"--output",
"docs/AGENTS.md",
])
.expect("parse");
match cli.command {
crate::cli::Command::Context(args) => match args.command {
ContextCommand::Init(init_args) => {
assert_eq!(init_args.output, Some(PathBuf::from("docs/AGENTS.md")));
}
_ => panic!("expected context init command"),
},
_ => panic!("expected context command"),
}
}
#[test]
fn cli_parses_context_update_with_section() {
let cli = crate::cli::Cli::try_parse_from([
"ralph",
"context",
"update",
"--section",
"troubleshooting",
])
.expect("parse");
match cli.command {
crate::cli::Command::Context(args) => match args.command {
ContextCommand::Update(update_args) => {
assert_eq!(update_args.section, vec!["troubleshooting"]);
assert!(!update_args.dry_run);
}
_ => panic!("expected context update command"),
},
_ => panic!("expected context command"),
}
}
#[test]
fn cli_parses_context_update_with_multiple_sections() {
let cli = crate::cli::Cli::try_parse_from([
"ralph",
"context",
"update",
"--section",
"troubleshooting",
"--section",
"git-hygiene",
])
.expect("parse");
match cli.command {
crate::cli::Command::Context(args) => match args.command {
ContextCommand::Update(update_args) => {
assert_eq!(update_args.section, vec!["troubleshooting", "git-hygiene"]);
}
_ => panic!("expected context update command"),
},
_ => panic!("expected context command"),
}
}
#[test]
fn cli_parses_context_update_with_dry_run() {
let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "update", "--dry-run"])
.expect("parse");
match cli.command {
crate::cli::Command::Context(args) => match args.command {
ContextCommand::Update(update_args) => {
assert!(update_args.dry_run);
}
_ => panic!("expected context update command"),
},
_ => panic!("expected context command"),
}
}
#[test]
fn cli_parses_context_validate() {
let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "validate"]).expect("parse");
match cli.command {
crate::cli::Command::Context(args) => match args.command {
ContextCommand::Validate(validate_args) => {
assert!(!validate_args.strict);
assert!(validate_args.path.is_none());
}
_ => panic!("expected context validate command"),
},
_ => panic!("expected context command"),
}
}
#[test]
fn cli_parses_context_validate_with_strict() {
let cli = crate::cli::Cli::try_parse_from(["ralph", "context", "validate", "--strict"])
.expect("parse");
match cli.command {
crate::cli::Command::Context(args) => match args.command {
ContextCommand::Validate(validate_args) => {
assert!(validate_args.strict);
}
_ => panic!("expected context validate command"),
},
_ => panic!("expected context command"),
}
}
}