use anyhow::{bail, Context, Result};
use std::env;
use std::fs;
use std::io::{self, Read as _, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
use patina::adapters::launch as adapters;
use patina::git;
use patina::paths;
use patina::project;
use patina::workspace;
use super::LaunchOptions;
pub fn launch(options: LaunchOptions) -> Result<()> {
if workspace::is_first_run() {
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!(" Welcome to Patina!");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
workspace::setup()?;
println!();
}
let project_path = resolve_project_path(options.path.as_deref())?;
let explicit_adapter: Option<String> = options.adapter.clone();
if let Some(ref name) = explicit_adapter {
let adapter_info = adapters::get(name)?;
if !adapter_info.detected {
bail!(
"Adapter '{}' ({}) is not installed.\n\
Install it and try again, or use a different adapter.",
name,
adapter_info.display
);
}
}
if options.auto_start_mother {
ensure_mother_running()?;
}
let patina_dir = project_path.join(".patina");
let adapter_name: String;
if !patina_dir.exists() {
if options.auto_init {
match prompt_are_you_lost(&project_path, explicit_adapter.as_deref())? {
Some(selected) => {
adapter_name = selected;
}
None => {
return Ok(());
}
}
} else {
bail!(
"Not a patina project (no .patina/ directory).\n\
Run `patina init .` first."
);
}
} else {
let project_config = project::load_with_migration(&project_path)?;
adapter_name = explicit_adapter.unwrap_or_else(|| {
if !project_config.adapters.default.is_empty() {
project_config.adapters.default.clone()
} else {
adapters::default_name().unwrap_or_else(|_| "claude".to_string())
}
});
let adapter_info = adapters::get(&adapter_name)?;
if !adapter_info.detected {
bail!(
"Adapter '{}' ({}) is not installed.\n\
Install it and try again, or use a different adapter.",
adapter_name,
adapter_info.display
);
}
println!(
"🚀 Launching {} in {}",
adapter_info.display,
project_path.display()
);
}
match ensure_on_patina_branch()? {
BranchAction::AlreadyOnPatina => {
}
BranchAction::Switched { .. } | BranchAction::StashedAndSwitched { .. } => {
}
BranchAction::Rebased { .. } => {
}
BranchAction::RebaseConflicts => {
bail!("Please resolve rebase conflicts before launching.");
}
BranchAction::NotGitRepo => {
println!("⚠️ Not a git repository (patina branch model disabled)");
}
BranchAction::NoPatinaExists => {
println!("⚠️ No 'patina' branch found (working on current branch)");
}
}
let project_config = project::load_with_migration(&project_path)?;
if !project_config.adapters.allowed.contains(&adapter_name) {
bail!(
"Adapter '{}' is not in allowed adapters for this project.\n\
Allowed: {:?}\n\n\
To add it, run: patina adapter add {}",
adapter_name,
project_config.adapters.allowed,
adapter_name
);
}
if !adapters::is_mcp_configured(&adapter_name).unwrap_or(true) {
let _ = adapters::configure_mcp(&adapter_name);
}
let bootstrap_file = match adapter_name.as_str() {
"claude" => "CLAUDE.md",
"gemini" => "GEMINI.md",
"opencode" => "OPENCODE.md",
_ => "CLAUDE.md",
};
let bootstrap_path = project_path.join(bootstrap_file);
if !bootstrap_path.exists() {
println!(" ✓ Generating {} bootstrap", bootstrap_file);
adapters::generate_bootstrap(&adapter_name, &project_path)?;
}
launch_adapter_cli(&adapter_name, &project_path)?;
Ok(())
}
fn resolve_project_path(path_opt: Option<&str>) -> Result<PathBuf> {
let path = match path_opt {
Some(p) => PathBuf::from(shellexpand::tilde(p).as_ref()),
None => env::current_dir().context("Failed to get current directory")?,
};
let canonical = fs::canonicalize(&path)
.with_context(|| format!("Project path does not exist: {}", path.display()))?;
Ok(canonical)
}
pub fn check_mother_health() -> bool {
let sock_path = paths::serve::socket_path();
let mut stream = match std::os::unix::net::UnixStream::connect(&sock_path) {
Ok(s) => s,
Err(_) => return false,
};
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
let request = "GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n";
if stream.write_all(request.as_bytes()).is_err() {
return false;
}
let mut buf = vec![0u8; 1024];
match stream.read(&mut buf) {
Ok(n) if n > 0 => {
let response = String::from_utf8_lossy(&buf[..n]);
response.contains("200")
}
_ => false,
}
}
fn ensure_mother_running() -> Result<()> {
if check_mother_health() {
println!(" ✓ Mother running");
return Ok(());
}
println!(" ⏳ Starting mother...");
start_mother_daemon()?;
for _ in 0..10 {
thread::sleep(Duration::from_millis(500));
if check_mother_health() {
println!(" ✓ Mother started");
return Ok(());
}
}
bail!("Failed to start mother daemon")
}
pub fn start_mother_daemon() -> Result<()> {
let patina_bin = env::current_exe().context("getting current executable path")?;
Command::new(&patina_bin)
.args(["mother", "start"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.context("spawning mother daemon")?;
Ok(())
}
fn prompt_are_you_lost(
project_path: &Path,
explicit_adapter: Option<&str>,
) -> Result<Option<String>> {
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!(" Are you lost?");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
println!("This is not a patina project.\n");
println!("📁 Path: {}", project_path.display());
if git::is_git_repo().unwrap_or(false) {
let branch = git::current_branch().unwrap_or_else(|_| "unknown".to_string());
let clean = git::is_clean().unwrap_or(true);
let status = if clean {
"clean".to_string()
} else {
let count = git::status_count().unwrap_or(0);
format!("{} files modified", count)
};
println!("🔀 Git: {} ({})", branch, status);
if let Ok(url) = git::remote_url("origin") {
let display_url = format_remote_url(&url);
println!("🌐 Remote: {}", display_url);
}
} else {
println!("🔀 Git: not a git repository");
}
println!();
print!("Initialize as patina project? [y/N]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let should_init = input.trim().to_lowercase() == "y";
if !should_init {
return Ok(None);
}
let adapter_name = if let Some(explicit) = explicit_adapter {
explicit.to_string()
} else {
let all_adapters = adapters::list()?;
let available: Vec<_> = all_adapters.into_iter().filter(|a| a.detected).collect();
let preference = adapters::default_name().ok();
adapters::select_adapter(&available, preference.as_deref())?
};
println!();
if initialize_project(project_path, &adapter_name)? {
Ok(Some(adapter_name))
} else {
Ok(None)
}
}
fn format_remote_url(url: &str) -> String {
url.trim()
.strip_prefix("git@")
.or_else(|| url.strip_prefix("https://"))
.unwrap_or(url)
.replace(":", "/")
.strip_suffix(".git")
.unwrap_or(url)
.to_string()
}
#[derive(Debug)]
pub enum BranchAction {
AlreadyOnPatina,
Switched { _from: String },
StashedAndSwitched { _from: String, _stash_name: String },
Rebased { _commits: usize },
RebaseConflicts,
NotGitRepo,
NoPatinaExists,
}
fn ensure_on_patina_branch() -> Result<BranchAction> {
if !git::is_git_repo()? {
return Ok(BranchAction::NotGitRepo);
}
let current = git::current_branch()?;
if !git::branch_exists("patina")? {
return Ok(BranchAction::NoPatinaExists);
}
if current == "patina" {
let _ = git::fetch("origin");
let behind = git::commits_behind("patina", "origin/patina").unwrap_or(0);
if behind > 0 {
println!(
"\n📥 Patina branch is {} commits behind origin/patina",
behind
);
println!(" Rebasing onto origin/patina...");
if git::rebase("origin/patina")? {
println!(" ✓ Rebased ({} commits)", behind);
return Ok(BranchAction::Rebased { _commits: behind });
} else {
println!(" ✗ Rebase failed (conflicts)");
println!();
println!(" To resolve:");
println!(" 1. Fix conflicts");
println!(" 2. git add <files>");
println!(" 3. git rebase --continue");
println!();
println!(" Or abort: git rebase --abort");
return Ok(BranchAction::RebaseConflicts);
}
}
return Ok(BranchAction::AlreadyOnPatina);
}
let clean = git::is_clean()?;
if clean {
println!("\n🔀 Switching to patina branch...");
git::checkout("patina")?;
println!(" ✓ Switched to patina");
return Ok(BranchAction::Switched { _from: current });
}
let timestamp = git::timestamp();
let stash_name = format!("patina-autostash-{}", timestamp);
println!("\n📦 Stashing changes on '{}'...", current);
git::stash_push(&stash_name)?;
println!(" ✓ Stashed: \"{}\"", stash_name);
println!("🔀 Switching to patina branch...");
git::checkout("patina")?;
println!(" ✓ Switched to patina");
println!();
println!("────────────────────────────────────────────────");
println!("💡 Your changes on '{}' are stashed.", current);
println!(" To restore: git checkout {} && git stash pop", current);
println!("────────────────────────────────────────────────");
Ok(BranchAction::StashedAndSwitched {
_from: current,
_stash_name: stash_name,
})
}
fn initialize_project(project_path: &Path, adapter_name: &str) -> Result<bool> {
let original_dir = env::current_dir()?;
env::set_current_dir(project_path)?;
let init_result = crate::commands::init::execute(
".".to_string(), false, true, false, );
if let Err(e) = init_result {
env::set_current_dir(original_dir)?;
eprintln!("\n❌ Failed to initialize: {}", e);
return Ok(false);
}
let adapter_result =
crate::commands::adapter::execute(Some(crate::commands::adapter::AdapterCommands::Add {
name: adapter_name.to_string(),
no_commit: false, }));
if let Err(e) = adapter_result {
env::set_current_dir(original_dir)?;
eprintln!("\n❌ Failed to add adapter: {}", e);
eprintln!(
" Run 'patina adapter add {}' to add it manually",
adapter_name
);
return Ok(false);
}
let mut config = project::load_with_migration(project_path)?;
config.adapters.default = adapter_name.to_string();
project::save(project_path, &config)?;
env::set_current_dir(original_dir)?;
println!(
"\n✓ Initialized as patina project with {} adapter",
adapter_name
);
Ok(true) }
fn launch_adapter_cli(adapter_name: &str, project_path: &Path) -> Result<()> {
println!("\nLaunching {}...\n", adapter_name);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = Command::new(adapter_name).current_dir(project_path).exec();
bail!("Failed to exec {}: {}", adapter_name, err);
}
#[cfg(not(unix))]
{
let status = Command::new(adapter_name)
.current_dir(project_path)
.status()
.with_context(|| format!("Failed to run {}", adapter_name))?;
if !status.success() {
bail!("{} exited with status: {}", adapter_name, status);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_current_dir() {
let path = resolve_project_path(None);
assert!(path.is_ok());
assert!(path.unwrap().is_absolute());
}
#[test]
fn test_resolve_tilde_path() {
let path = resolve_project_path(Some("~"));
if let Ok(p) = path {
assert!(p.is_absolute());
}
}
}