Skip to main content

acp_cli/cli/
init.rs

1use std::io::{self, BufRead, Write};
2
3use crate::config::AcpCliConfig;
4
5/// Run the interactive init flow.
6pub fn run_init() -> crate::error::Result<()> {
7    println!("acp-cli init\n");
8
9    // 1. Check Claude Code installation
10    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    // 2. Check npx (needed for claude-agent-acp)
20    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    // 3. Detect existing auth token
30    println!("\nšŸ” Checking auth token...");
31
32    let mut detected_token: Option<String> = None;
33    let mut token_source = "";
34
35    // env var
36    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    // existing config
49    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    // ~/.claude.json
61    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    // macOS Keychain
73    #[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    // 4. Ask user what to do
90    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    // 5. Write config
111    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    // Load existing config to preserve other fields
122    let mut config = AcpCliConfig::load();
123
124    if let Some(token) = final_token {
125        config.auth_token = Some(token);
126    }
127
128    // Set default agent if not already set
129    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}