1use std::io::IsTerminal;
9use std::path::PathBuf;
10
11use anyhow::Result;
12use console::style;
13use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect};
14
15use crate::config::Config;
16use crate::scan::scan_project;
17use crate::sync::{SyncExecutor, Tool as SyncTool};
18
19#[derive(Debug, Clone, Default)]
21pub struct InitOptions {
22 pub force: bool,
24 pub include: Vec<String>,
26 pub exclude: Vec<String>,
28 pub output: Option<PathBuf>,
30 pub cache_path: Option<PathBuf>,
32 pub vars_path: Option<PathBuf>,
34 pub workers: Option<usize>,
36 pub yes: bool,
38 pub no_bootstrap: bool,
40}
41
42pub fn execute_init(options: InitOptions) -> Result<()> {
44 let config_path = options
45 .output
46 .clone()
47 .unwrap_or_else(|| PathBuf::from(".acp.config.json"));
48
49 if config_path.exists() && !options.force {
50 eprintln!(
51 "{} Config file already exists. Use --force to overwrite.",
52 style("✗").red()
53 );
54 std::process::exit(1);
55 }
56
57 let mut config = Config::default();
58
59 let interactive = !options.yes
61 && std::io::stdin().is_terminal()
62 && options.include.is_empty()
63 && options.exclude.is_empty()
64 && options.output.is_none()
65 && options.cache_path.is_none()
66 && options.vars_path.is_none()
67 && options.workers.is_none();
68
69 if interactive {
70 run_interactive_init(&mut config)?;
71 } else {
72 apply_cli_options(&mut config, &options);
73 }
74
75 let acp_dir = PathBuf::from(".acp");
77 if !acp_dir.exists() {
78 std::fs::create_dir(&acp_dir)?;
79 println!("{} Created .acp/ directory", style("✓").green());
80 }
81
82 config.save(&config_path)?;
84 println!("{} Created {}", style("✓").green(), config_path.display());
85
86 if !options.no_bootstrap {
88 bootstrap_ai_tools(interactive)?;
89 }
90
91 println!("\n{}", style("Next steps:").bold());
93 println!(
94 " 1. Run {} to index your codebase",
95 style("acp index").cyan()
96 );
97 println!(" 2. AI tools will read context from generated files");
98
99 Ok(())
100}
101
102fn run_interactive_init(config: &mut Config) -> Result<()> {
103 println!("{} ACP Project Setup\n", style("→").cyan());
104
105 println!("{} Scanning project...", style("→").dim());
107 let scan = scan_project(".");
108
109 if scan.languages.is_empty() {
110 println!("{} No supported languages detected\n", style("⚠").yellow());
111 } else {
112 println!("{} Detected languages:", style("✓").green());
113 for lang in &scan.languages {
114 println!(
115 " {} ({} files)",
116 style(lang.name).cyan(),
117 lang.file_count
118 );
119 }
120 println!();
121
122 let mut include_patterns: Vec<String> = vec![];
124 for lang in &scan.languages {
125 include_patterns.extend(lang.patterns.iter().map(|s| s.to_string()));
126 }
127 config.include = include_patterns;
128
129 let use_detected = Confirm::with_theme(&ColorfulTheme::default())
131 .with_prompt("Use detected languages?")
132 .default(true)
133 .interact()?;
134
135 if !use_detected {
136 select_languages_manually(config)?;
137 }
138 }
139
140 let add_excludes = Confirm::with_theme(&ColorfulTheme::default())
142 .with_prompt("Add custom exclude patterns? (node_modules, dist, etc. already excluded)")
143 .default(false)
144 .interact()?;
145
146 if add_excludes {
147 let custom: String = Input::with_theme(&ColorfulTheme::default())
148 .with_prompt("Enter patterns (comma-separated)")
149 .interact_text()?;
150 config
151 .exclude
152 .extend(custom.split(',').map(|s| s.trim().to_string()));
153 }
154
155 Ok(())
156}
157
158fn select_languages_manually(config: &mut Config) -> Result<()> {
159 let all_languages = [
160 ("TypeScript/TSX", vec!["**/*.ts", "**/*.tsx"]),
161 ("JavaScript/JSX", vec!["**/*.js", "**/*.jsx", "**/*.mjs"]),
162 ("Rust", vec!["**/*.rs"]),
163 ("Python", vec!["**/*.py"]),
164 ("Go", vec!["**/*.go"]),
165 ("Java", vec!["**/*.java"]),
166 ];
167
168 let items: Vec<&str> = all_languages.iter().map(|(name, _)| *name).collect();
169 let selections = MultiSelect::with_theme(&ColorfulTheme::default())
170 .with_prompt("Select languages to index")
171 .items(&items)
172 .interact()?;
173
174 config.include = selections
175 .iter()
176 .flat_map(|&idx| all_languages[idx].1.iter().map(|s| s.to_string()))
177 .collect();
178
179 Ok(())
180}
181
182fn apply_cli_options(config: &mut Config, options: &InitOptions) {
183 if !options.include.is_empty() {
184 config.include = options.include.clone();
185 }
186 if !options.exclude.is_empty() {
187 config.exclude.extend(options.exclude.iter().cloned());
188 }
189 }
192
193fn bootstrap_ai_tools(interactive: bool) -> Result<()> {
194 let sync = SyncExecutor::new();
195 let project_root = PathBuf::from(".");
196 let detected = sync.detect_tools(&project_root);
197
198 if !detected.is_empty() {
199 println!("\n{} Detected AI tools:", style("✓").green());
200 for tool in &detected {
201 println!(" {} ({})", style(tool.name()).cyan(), tool.output_path());
202 }
203
204 let should_bootstrap = if interactive {
206 Confirm::with_theme(&ColorfulTheme::default())
207 .with_prompt("Bootstrap detected tools with ACP context?")
208 .default(true)
209 .interact()?
210 } else {
211 true
212 };
213
214 if should_bootstrap {
215 println!();
216 for tool in detected {
217 match sync.bootstrap_tool(tool, &project_root) {
218 Ok(result) => {
219 let action = match result.action {
220 crate::sync::BootstrapAction::Created => "Created",
221 crate::sync::BootstrapAction::Merged => "Updated",
222 crate::sync::BootstrapAction::Skipped => "Skipped",
223 };
224 println!(
225 "{} {} {}",
226 style("✓").green(),
227 action,
228 result.output_path.display()
229 );
230 }
231 Err(e) => {
232 eprintln!("{} Failed {}: {}", style("✗").red(), tool.output_path(), e);
233 }
234 }
235 }
236 }
237 }
238
239 let agents_md = project_root.join("AGENTS.md");
241 if !agents_md.exists() {
242 match sync.bootstrap_tool(SyncTool::Generic, &project_root) {
243 Ok(result) => {
244 println!(
245 "{} Created {} (universal fallback)",
246 style("✓").green(),
247 result.output_path.display()
248 );
249 }
250 Err(e) => {
251 eprintln!("{} Failed to create AGENTS.md: {}", style("✗").red(), e);
252 }
253 }
254 }
255
256 Ok(())
257}