use std::io;
use chrono::Utc;
use owo_colors::OwoColorize;
use crate::cli::InitArgs;
use crate::error::ShipItError;
use crate::settings::Settings;
const AGENT_GUIDE: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/AI.md"));
const CLAUDE_MD_START: &str = "<!-- shipit:start -->";
const CLAUDE_MD_END: &str = "<!-- shipit:end -->";
fn prompt_line(label: &str) -> Result<String, ShipItError> {
crate::output::print_token_prompt(label);
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|e| ShipItError::Error(format!("Failed to read input: {}", e)))?;
Ok(input.trim().to_string())
}
fn write_claude_md(dir: &std::path::Path) -> Result<std::path::PathBuf, ShipItError> {
let claude_md_path = dir.join("CLAUDE.md");
let section = format!(
"{}\n<!-- Generated by shipit v{} — do not edit this section manually -->\n\n{}\n{}",
CLAUDE_MD_START,
env!("CARGO_PKG_VERSION"),
AGENT_GUIDE.trim_end(),
CLAUDE_MD_END,
);
let existing = if claude_md_path.exists() {
std::fs::read_to_string(&claude_md_path)
.map_err(|e| ShipItError::Error(format!("Failed to read CLAUDE.md: {}", e)))?
} else {
String::new()
};
let updated = if existing.contains(CLAUDE_MD_START) {
let before = &existing[..existing.find(CLAUDE_MD_START).unwrap()];
let after_marker = &existing[existing.find(CLAUDE_MD_END).unwrap() + CLAUDE_MD_END.len()..];
format!("{}\n\n{}{}", before.trim_end_matches('\n'), section, after_marker)
} else if existing.is_empty() {
section
} else {
format!("{}\n\n{}", existing.trim_end(), section)
};
std::fs::write(&claude_md_path, updated)
.map_err(|e| ShipItError::Error(format!("Failed to write CLAUDE.md: {}", e)))?;
Ok(claude_md_path)
}
pub fn init(args: InitArgs) -> Result<(), ShipItError> {
let dir = args
.dir
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().expect("Failed to get current directory"));
let claude_md_path = write_claude_md(&dir)?;
crate::output::print_success(&format!("Agent guide written to: {}", claude_md_path.display().bold()));
let path = dir.join("shipit.toml");
if path.exists() {
eprintln!(
"{} Config already exists at: {}",
"!".yellow().bold(),
path.display().bold()
);
return Ok(());
}
let mut settings = Settings::default();
eprintln!();
eprintln!("{}", "Platform Domain".bold().cyan());
eprintln!(" A platform is a remote Git repository service (e.g. GitHub or GitLab).");
eprintln!(" Only {} and {} are currently supported.", "github.com".bold(), "gitlab.com".bold());
let domain = if let Some(domain) = args.platform_domain {
domain
} else {
eprintln!();
prompt_line("Platform domain (e.g. github.com):")?
};
if !domain.trim().is_empty() {
settings.platform.domain = domain.trim().to_string();
crate::output::print_success("Platform domain saved.");
} else {
crate::output::print_skipped("Platform domain skipped.");
}
eprintln!();
eprintln!("{}", "Platform Personal Access Token".bold().cyan());
let token = if let Some(token) = args.platform_token {
token
} else {
eprintln!();
prompt_line("Platform token (leave blank to skip):")?
};
if !token.trim().is_empty() {
settings.platform.token = token.trim().to_string();
crate::output::print_success("Platform token saved.");
} else {
crate::output::print_skipped("Platform token skipped.");
}
eprintln!();
confy::store_path(&path, &settings)
.map_err(|e| ShipItError::Error(format!("Failed to write config: {}", e)))?;
let existing = std::fs::read_to_string(&path)
.map_err(|e| ShipItError::Error(format!("Failed to read config: {}", e)))?;
let versioned = format!(
"# Generated by shipit v{} on {}\n{}",
env!("CARGO_PKG_VERSION"),
Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
existing
);
std::fs::write(&path, versioned)
.map_err(|e| ShipItError::Error(format!("Failed to write config: {}", e)))?;
crate::output::print_success(&format!("Config written to: {}", path.display().bold()));
let plans_dir = dir.join(".shipit").join("plans");
std::fs::create_dir_all(&plans_dir)
.map_err(|e| ShipItError::Error(format!("Failed to create .shipit/plans directory: {}", e)))?;
crate::output::print_success(&format!("Plans directory created at: {}", plans_dir.display().bold()));
update_gitignore(&dir)?;
Ok(())
}
fn update_gitignore(dir: &std::path::Path) -> Result<(), ShipItError> {
const ENTRIES: &[&str] = &["shipit.toml", ".shipit/"];
let gitignore_path = dir.join(".gitignore");
let existing = if gitignore_path.exists() {
std::fs::read_to_string(&gitignore_path)
.map_err(|e| ShipItError::Error(format!("Failed to read .gitignore: {}", e)))?
} else {
String::new()
};
let missing: Vec<&str> = ENTRIES
.iter()
.copied()
.filter(|entry| !existing.lines().any(|line| line.trim() == *entry))
.collect();
if missing.is_empty() {
return Ok(());
}
let block = format!(
"\n# shipit\n{}\n",
missing.join("\n")
);
let updated = format!("{}{}", existing.trim_end_matches('\n'), block);
std::fs::write(&gitignore_path, updated)
.map_err(|e| ShipItError::Error(format!("Failed to write .gitignore: {}", e)))?;
crate::output::print_success(&format!(
".gitignore updated with: {}",
missing.join(", ").bold()
));
Ok(())
}
#[cfg(test)]
mod tests {
use super::update_gitignore;
use tempfile::TempDir;
#[test]
fn test_creates_gitignore_when_absent() {
let dir = TempDir::new().unwrap();
update_gitignore(dir.path()).unwrap();
let content = std::fs::read_to_string(dir.path().join(".gitignore")).unwrap();
assert!(content.contains("shipit.toml"));
assert!(content.contains(".shipit/"));
assert!(content.contains("# shipit"));
}
#[test]
fn test_appends_to_existing_gitignore() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".gitignore"), "node_modules/\n.env\n").unwrap();
update_gitignore(dir.path()).unwrap();
let content = std::fs::read_to_string(dir.path().join(".gitignore")).unwrap();
assert!(content.contains("node_modules/"));
assert!(content.contains(".env"));
assert!(content.contains("shipit.toml"));
assert!(content.contains(".shipit/"));
}
#[test]
fn test_idempotent_when_entries_already_present() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".gitignore"), "# shipit\nshipit.toml\n.shipit/\n").unwrap();
update_gitignore(dir.path()).unwrap();
let content = std::fs::read_to_string(dir.path().join(".gitignore")).unwrap();
assert_eq!(content.matches("shipit.toml").count(), 1, "shipit.toml should not be duplicated");
assert_eq!(content.matches(".shipit/").count(), 1, ".shipit/ should not be duplicated");
}
#[test]
fn test_only_missing_entries_are_added() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".gitignore"), "shipit.toml\n").unwrap();
update_gitignore(dir.path()).unwrap();
let content = std::fs::read_to_string(dir.path().join(".gitignore")).unwrap();
assert_eq!(content.matches("shipit.toml").count(), 1, "shipit.toml should not be duplicated");
assert!(content.contains(".shipit/"), ".shipit/ should have been added");
}
}