use std::path::Path;
use std::process::Command;
use anyhow::{Context, Result, bail};
use crate::claude::{build_claude_settings_json, create_symlink};
use crate::profile::{AppConfig, atomic_write, home_dir, profile_dir};
pub(crate) fn run(config: &AppConfig, name: &str, claude_args: &[String]) -> Result<()> {
let home = home_dir()?;
let claude_dir = home.join(".claude");
if !claude_dir.exists() {
bail!("~/.claude not found; install Claude Code first");
}
let profile = config.find(name).context("profile not found")?;
let tmp = tempfile::Builder::new()
.prefix("clauth-")
.tempdir()
.context("failed to create temp dir")?;
for entry in std::fs::read_dir(&claude_dir)
.with_context(|| format!("failed to read {}", claude_dir.display()))?
{
let entry = entry?;
let file_name = entry.file_name();
if file_name == "settings.json" || file_name == ".credentials.json" {
continue;
}
link_entry(&entry.path(), &tmp.path().join(&file_name))?;
}
let settings_src = claude_dir.join("settings.json");
let merged = build_claude_settings_json(&settings_src, profile, &[])?;
atomic_write(&tmp.path().join("settings.json"), merged)
.context("failed to write tempdir settings.json")?;
let creds = profile_dir(&profile.name)?.join("credentials.json");
if creds.exists() {
create_symlink(&creds, &tmp.path().join(".credentials.json"))?;
}
let claude_json = home.join(".claude.json");
if claude_json.exists() {
link_entry(&claude_json, &tmp.path().join(".claude.json"))?;
}
let status = Command::new("claude")
.env("CLAUDE_CONFIG_DIR", tmp.path())
.args(claude_args)
.status()
.context("failed to spawn claude")?;
if let Ok(target) = profile_dir(&profile.name).map(|d| d.join("credentials.json"))
&& sync_relogged_credentials(&tmp.path().join(".credentials.json"), &target)
{
eprintln!(
"clauth: re-login detected; updated credentials for profile '{}'",
profile.name
);
}
drop(tmp);
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
Ok(())
}
pub(crate) fn sync_relogged_credentials(tempdir_creds: &Path, target: &Path) -> bool {
let Ok(meta) = tempdir_creds.symlink_metadata() else {
return false;
};
if meta.file_type().is_symlink() {
return false;
}
let Ok(bytes) = std::fs::read(tempdir_creds) else {
return false;
};
if std::fs::read(target).ok().as_deref() == Some(bytes.as_slice()) {
return false;
}
atomic_write(target, &bytes).is_ok()
}
#[cfg(unix)]
fn link_entry(src: &Path, dst: &Path) -> Result<()> {
std::os::unix::fs::symlink(src, dst)
.with_context(|| format!("failed to symlink {} -> {}", dst.display(), src.display()))
}
#[cfg(windows)]
fn link_entry(src: &Path, dst: &Path) -> Result<()> {
let result = if src.is_dir() {
std::os::windows::fs::symlink_dir(src, dst)
} else {
std::os::windows::fs::symlink_file(src, dst)
};
result.with_context(|| {
format!(
"failed to symlink {} -> {} (enable developer mode or run as admin)",
dst.display(),
src.display()
)
})
}
#[cfg(not(any(unix, windows)))]
fn link_entry(_src: &Path, _dst: &Path) -> Result<()> {
bail!("clauth start requires symlink support");
}
#[cfg(test)]
#[path = "../tests/inline/start.rs"]
mod tests;