1use std::io::{self, BufRead, Write};
2
3use crate::config::AcpCliConfig;
4
5pub fn run_init() -> crate::error::Result<()> {
7 println!("acp-cli init\n");
8
9 print!("š Checking Claude Code installation... ");
11 io::stdout().flush().ok();
12 let has_claude = which_command("claude");
13 if has_claude {
14 println!("ā
found");
15 } else {
16 println!("ā ļø not found (optional ā install from https://claude.ai/code)");
17 }
18
19 print!("š Checking npx... ");
21 io::stdout().flush().ok();
22 if which_command("npx") {
23 println!("ā
found");
24 } else {
25 println!("ā not found");
26 println!(" Hint: Install Node.js ā https://nodejs.org/");
27 }
28
29 println!("\nš Checking auth token...");
31
32 let mut detected_token: Option<String> = None;
33 let mut token_source = "";
34
35 if let Some(t) = std::env::var("ANTHROPIC_AUTH_TOKEN")
37 .ok()
38 .filter(|t| !t.is_empty())
39 {
40 println!(
41 " ANTHROPIC_AUTH_TOKEN env: ā
found ({}...)",
42 mask_token(&t)
43 );
44 detected_token = Some(t);
45 token_source = "env var";
46 }
47
48 if detected_token.is_none()
50 && let Some(t) = AcpCliConfig::load().auth_token.filter(|t| !t.is_empty())
51 {
52 println!(
53 " ~/.acp-cli/config.json: ā
found ({}...)",
54 mask_token(&t)
55 );
56 detected_token = Some(t);
57 token_source = "config";
58 }
59
60 if detected_token.is_none()
62 && let Some(t) = read_claude_json_token()
63 {
64 println!(
65 " ~/.claude.json: ā
found ({}...)",
66 mask_token(&t)
67 );
68 detected_token = Some(t);
69 token_source = "~/.claude.json";
70 }
71
72 #[cfg(target_os = "macos")]
74 if detected_token.is_none()
75 && let Some(t) = read_keychain_token()
76 {
77 println!(
78 " macOS Keychain: ā
found ({}...)",
79 mask_token(&t)
80 );
81 detected_token = Some(t);
82 token_source = "Keychain";
83 }
84
85 if detected_token.is_none() {
86 println!(" No token detected.");
87 }
88
89 let final_token = if let Some(ref token) = detected_token {
91 println!("\nDetected token from {token_source}.");
92 print!("Save to config? [Y/n] ");
93 io::stdout().flush().ok();
94 let answer = read_line_trim();
95 if answer.is_empty() || answer.to_lowercase().starts_with('y') {
96 Some(token.clone())
97 } else {
98 print!("\nEnter auth token (or press Enter to skip): ");
99 io::stdout().flush().ok();
100 let input = read_line_trim();
101 if input.is_empty() { None } else { Some(input) }
102 }
103 } else {
104 print!("\nEnter your Anthropic auth token (or press Enter to skip): ");
105 io::stdout().flush().ok();
106 let input = read_line_trim();
107 if input.is_empty() { None } else { Some(input) }
108 };
109
110 let config_dir = dirs::home_dir()
112 .ok_or_else(|| crate::error::AcpCliError::Usage("cannot find home directory".into()))?
113 .join(".acp-cli");
114
115 std::fs::create_dir_all(&config_dir).map_err(|e| {
116 crate::error::AcpCliError::Usage(format!("failed to create {}: {e}", config_dir.display()))
117 })?;
118
119 let config_path = config_dir.join("config.json");
120
121 let mut config = AcpCliConfig::load();
123
124 if let Some(token) = final_token {
125 config.auth_token = Some(token);
126 }
127
128 if config.default_agent.is_none() {
130 config.default_agent = Some("claude".to_string());
131 }
132
133 let json = serde_json::to_string_pretty(&config).map_err(|e| {
134 crate::error::AcpCliError::Usage(format!("failed to serialize config: {e}"))
135 })?;
136
137 std::fs::write(&config_path, &json).map_err(|e| {
138 crate::error::AcpCliError::Usage(format!("failed to write {}: {e}", config_path.display()))
139 })?;
140
141 println!("\nā
Config written to {}", config_path.display());
142
143 if config.auth_token.is_some() {
144 println!("ā
Auth token saved");
145 } else {
146 println!("ā ļø No auth token configured ā set ANTHROPIC_AUTH_TOKEN or re-run init");
147 }
148
149 println!(
150 "ā
Default agent: {}",
151 config.default_agent.as_deref().unwrap_or("claude")
152 );
153
154 Ok(())
155}
156
157fn read_line_trim() -> String {
158 let stdin = io::stdin();
159 let mut line = String::new();
160 stdin.lock().read_line(&mut line).ok();
161 line.trim().to_string()
162}
163
164fn mask_token(token: &str) -> String {
165 if token.len() <= 12 {
166 return "***".to_string();
167 }
168 format!("{}...{}", &token[..8], &token[token.len() - 4..])
169}
170
171fn which_command(cmd: &str) -> bool {
172 std::process::Command::new("which")
173 .arg(cmd)
174 .stdout(std::process::Stdio::null())
175 .stderr(std::process::Stdio::null())
176 .status()
177 .map(|s| s.success())
178 .unwrap_or(false)
179}
180
181fn read_claude_json_token() -> Option<String> {
182 let path = dirs::home_dir()?.join(".claude.json");
183 let content = std::fs::read_to_string(path).ok()?;
184 let json: serde_json::Value = serde_json::from_str(&content).ok()?;
185 json.pointer("/oauthAccount/accessToken")
186 .or_else(|| json.get("accessToken"))
187 .and_then(|v| v.as_str())
188 .filter(|s| !s.is_empty())
189 .map(|s| s.to_string())
190}
191
192#[cfg(target_os = "macos")]
193fn read_keychain_token() -> Option<String> {
194 for service in &["Claude Code", "claude.ai", "anthropic.claude"] {
195 let output = std::process::Command::new("security")
196 .args(["find-generic-password", "-s", service, "-w"])
197 .stderr(std::process::Stdio::null())
198 .output()
199 .ok()?;
200 if output.status.success() {
201 let token = String::from_utf8(output.stdout).ok()?.trim().to_string();
202 if !token.is_empty() {
203 return Some(token);
204 }
205 }
206 }
207 None
208}