Skip to main content

chub_cli/commands/
agent_config.rs

1use clap::{Args, Subcommand};
2use owo_colors::OwoColorize;
3
4use chub_core::team::agent_config;
5
6use crate::output;
7
8#[derive(Args)]
9pub struct AgentConfigArgs {
10    #[command(subcommand)]
11    command: AgentConfigCommand,
12}
13
14#[derive(Subcommand)]
15pub enum AgentConfigCommand {
16    /// Generate all target files (CLAUDE.md, .cursorrules, GEMINI.md, etc.)
17    Generate,
18    /// Update targets only if source changed (idempotent)
19    Sync,
20    /// Show what would change without writing
21    Diff,
22}
23
24pub fn run(args: AgentConfigArgs, json: bool) {
25    match args.command {
26        AgentConfigCommand::Generate | AgentConfigCommand::Sync => run_sync(json),
27        AgentConfigCommand::Diff => run_diff(json),
28    }
29}
30
31fn run_sync(json: bool) {
32    match agent_config::sync_configs() {
33        Ok(results) => {
34            if json {
35                let items: Vec<serde_json::Value> = results
36                    .iter()
37                    .map(|r| {
38                        serde_json::json!({
39                            "target": r.target,
40                            "filename": r.filename,
41                            "action": format!("{:?}", r.action).to_lowercase(),
42                        })
43                    })
44                    .collect();
45                println!(
46                    "{}",
47                    serde_json::to_string_pretty(&serde_json::json!({
48                        "results": items,
49                    }))
50                    .unwrap_or_default()
51                );
52            } else {
53                if results.is_empty() {
54                    eprintln!(
55                        "{}",
56                        "No targets configured in agent_rules.targets.".yellow()
57                    );
58                    return;
59                }
60                for r in &results {
61                    let action = match r.action {
62                        agent_config::SyncAction::Created => "created".green().to_string(),
63                        agent_config::SyncAction::Updated => "updated".yellow().to_string(),
64                        agent_config::SyncAction::Unchanged => "unchanged".dimmed().to_string(),
65                        agent_config::SyncAction::Unknown => {
66                            format!(
67                                "{}  (known: {})",
68                                "unknown target".red(),
69                                agent_config::Target::all_target_names().join(", ")
70                            )
71                        }
72                    };
73                    eprintln!("  {} {}", r.filename.bold(), action);
74                }
75            }
76        }
77        Err(e) => {
78            output::error(&e.to_string(), json);
79            std::process::exit(1);
80        }
81    }
82}
83
84fn run_diff(json: bool) {
85    match agent_config::diff_configs() {
86        Ok(diffs) => {
87            if json {
88                let items: Vec<serde_json::Value> = diffs
89                    .iter()
90                    .map(|(filename, new_content, existing)| {
91                        serde_json::json!({
92                            "filename": filename,
93                            "exists": existing.is_some(),
94                            "would_change": existing.as_ref().map(|e| e != new_content).unwrap_or(true),
95                        })
96                    })
97                    .collect();
98                println!(
99                    "{}",
100                    serde_json::to_string_pretty(&serde_json::json!({ "diffs": items }))
101                        .unwrap_or_default()
102                );
103            } else {
104                if diffs.is_empty() {
105                    eprintln!("{}", "No targets configured.".yellow());
106                    return;
107                }
108                for (filename, new_content, existing) in &diffs {
109                    match existing {
110                        None => {
111                            eprintln!("  {} {}", filename.bold(), "(new file)".green());
112                            for line in new_content.lines().take(10) {
113                                eprintln!("    + {}", line.green());
114                            }
115                            if new_content.lines().count() > 10 {
116                                eprintln!("    {} more lines...", "...".dimmed());
117                            }
118                        }
119                        Some(old) if old != new_content => {
120                            eprintln!("  {} {}", filename.bold(), "(changed)".yellow());
121                        }
122                        Some(_) => {
123                            eprintln!("  {} {}", filename.bold(), "(unchanged)".dimmed());
124                        }
125                    }
126                }
127            }
128        }
129        Err(e) => {
130            output::error(&e.to_string(), json);
131            std::process::exit(1);
132        }
133    }
134}