use anyhow::{Context, Result};
use crossterm::style::Stylize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use xdg::BaseDirectories;
use octorus::config::find_project_root;
use crate::migrate::{
detect_version_from_hash, read_manifest, write_manifest, FileRecord, FileRecordStatus,
VersionManifest,
};
pub(crate) const DEFAULT_CONFIG: &str = r#"# Editor for writing review body.
# Resolved in order: this value → $VISUAL → $EDITOR → vi
# Supports arguments: editor = "code --wait"
# editor = "vim"
[diff]
theme = "base16-ocean.dark"
# Number of spaces per tab character in diff view (minimum: 1)
tab_width = 4
[layout]
# Left panel width percentage in split view (10-90, right panel fills the rest)
# left_panel_width = 35
# Hide header/footer for focused editing (default: false)
# zen_mode = false
[keybindings]
approve = 'a'
request_changes = 'r'
comment = 'c'
suggestion = 's'
[ai]
reviewer = "claude"
reviewee = "claude"
max_iterations = 10
timeout_secs = 600
# prompt_dir = "/custom/path/to/prompts" # Optional: custom prompt directory
# Additional tools for reviewer agent (Claude only)
# Specify in Claude Code --allowedTools format
# reviewer_additional_tools = ["Skill", "WebSearch"]
# Additional tools for reviewee agent (Claude only)
# NOTE: git push is disabled by default for safety.
# To enable automatic push, add "Bash(git push:*)" to this list.
# reviewee_additional_tools = ["Skill", "Bash(git push:*)"]
"#;
pub(crate) const DEFAULT_REVIEWER_PROMPT: &str = include_str!("ai/defaults/reviewer.md");
pub(crate) const DEFAULT_REVIEWEE_PROMPT: &str = include_str!("ai/defaults/reviewee.md");
pub(crate) const DEFAULT_REREVIEW_PROMPT: &str = include_str!("ai/defaults/rereview.md");
pub(crate) const AGENT_SKILL_CONTENT: &str = include_str!("ai/defaults/skill.md");
pub(crate) const AGENT_SKILL_REF_HEADLESS: &str =
include_str!("ai/defaults/references/headless-output.md");
pub(crate) const AGENT_SKILL_REF_CONFIG: &str =
include_str!("ai/defaults/references/config-reference.md");
pub(crate) const DEFAULT_LOCAL_CONFIG: &str = r#"# Project-local octorus configuration.
# Values here override the global config (~/.config/octorus/config.toml).
# Only specify values you want to override.
# [diff]
# theme = "base16-ocean.dark"
# tab_width = 4
# [layout]
# left_panel_width = 35
# zen_mode = false
# [ai]
# reviewer = "claude"
# reviewee = "claude"
# max_iterations = 10
# timeout_secs = 600
"#;
pub fn run_init(force: bool, local: bool) -> Result<()> {
if local {
let project_root = find_project_root();
return run_init_local(&project_root, force);
}
let base_dirs =
BaseDirectories::with_prefix("octorus").context("Failed to get config directory")?;
let config_home = base_dirs.get_config_home();
if !config_home.exists() {
println!(
"Creating configuration directory: {}",
config_home.display()
);
fs::create_dir_all(&config_home).context("Failed to create config directory")?;
}
let mut written_files: HashMap<String, bool> = HashMap::new();
let config_path = config_home.join("config.toml");
written_files.insert(
"config.toml".to_string(),
write_file_if_needed(&config_path, DEFAULT_CONFIG, force, "config.toml")?,
);
let prompts_dir = config_home.join("prompts");
if !prompts_dir.exists() {
println!("Creating prompts directory: {}", prompts_dir.display());
fs::create_dir_all(&prompts_dir).context("Failed to create prompts directory")?;
}
for (name, content) in &[
("reviewer.md", DEFAULT_REVIEWER_PROMPT),
("reviewee.md", DEFAULT_REVIEWEE_PROMPT),
("rereview.md", DEFAULT_REREVIEW_PROMPT),
] {
written_files.insert(
name.to_string(),
write_file_if_needed(&prompts_dir.join(name), content, force, name)?,
);
}
if let Err(e) = generate_agent_skill(force, &mut written_files) {
eprintln!("Warning: Failed to generate agent skill: {}", e);
}
write_init_manifest(&config_home, false, &written_files)?;
println!();
println!("Initialization complete!");
println!();
println!(
"You can customize prompts by editing files in {}",
prompts_dir.display()
);
println!("Available template variables: {{{{repo}}}}, {{{{pr_number}}}}, {{{{pr_title}}}}, {{{{diff}}}}, etc.");
Ok(())
}
fn write_file_if_needed(path: &PathBuf, content: &str, force: bool, name: &str) -> Result<bool> {
if path.exists() && !force {
println!(
"Skipping {} (already exists, use --force to overwrite)",
name
);
return Ok(false);
}
println!("Writing {}...", name);
fs::write(path, content).with_context(|| format!("Failed to write {}", name))?;
Ok(true)
}
fn generate_agent_skill_in(
claude_dir: &Path,
force: bool,
written_files: &mut HashMap<String, bool>,
) -> Result<()> {
let skill_dir = claude_dir.join("skills").join("octorus");
fs::create_dir_all(&skill_dir).context("Failed to create agent skill directory")?;
let refs_dir = skill_dir.join("references");
fs::create_dir_all(&refs_dir).context("Failed to create agent skill references directory")?;
written_files.insert(
"SKILL.md".to_string(),
write_file_if_needed(
&skill_dir.join("SKILL.md"),
AGENT_SKILL_CONTENT,
force,
"SKILL.md (agent skill)",
)?,
);
written_files.insert(
"headless-output.md".to_string(),
write_file_if_needed(
&refs_dir.join("headless-output.md"),
AGENT_SKILL_REF_HEADLESS,
force,
"headless-output.md (agent skill reference)",
)?,
);
written_files.insert(
"config-reference.md".to_string(),
write_file_if_needed(
&refs_dir.join("config-reference.md"),
AGENT_SKILL_REF_CONFIG,
force,
"config-reference.md (agent skill reference)",
)?,
);
Ok(())
}
fn generate_agent_skill(force: bool, written_files: &mut HashMap<String, bool>) -> Result<()> {
let claude_dir = match std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".claude"))
{
Some(dir) if dir.is_dir() => dir,
_ => return Ok(()),
};
generate_agent_skill_in(&claude_dir, force, written_files)
}
fn run_init_local(project_root: &Path, force: bool) -> Result<()> {
let octorus_dir = project_root.join(".octorus");
if !octorus_dir.exists() {
println!("Creating local config directory: {}", octorus_dir.display());
fs::create_dir_all(&octorus_dir).context("Failed to create .octorus directory")?;
}
let mut written_files: HashMap<String, bool> = HashMap::new();
let config_path = octorus_dir.join("config.toml");
written_files.insert(
"config.toml".to_string(),
write_file_if_needed(&config_path, DEFAULT_LOCAL_CONFIG, force, "config.toml")?,
);
let prompts_dir = octorus_dir.join("prompts");
if !prompts_dir.exists() {
println!("Creating prompts directory: {}", prompts_dir.display());
fs::create_dir_all(&prompts_dir).context("Failed to create prompts directory")?;
}
for (name, content) in &[
("reviewer.md", DEFAULT_REVIEWER_PROMPT),
("reviewee.md", DEFAULT_REVIEWEE_PROMPT),
("rereview.md", DEFAULT_REREVIEW_PROMPT),
] {
written_files.insert(
name.to_string(),
write_file_if_needed(&prompts_dir.join(name), content, force, name)?,
);
}
write_init_manifest(&octorus_dir, true, &written_files)?;
println!();
println!("Local initialization complete!");
println!("Project-local config: {}", config_path.display());
println!("Project-local prompts: {}", prompts_dir.display());
println!();
println!(
"{} Commit .octorus/ to share project-specific settings with your team.",
"Tip:".cyan()
);
println!(" Or add .octorus/ to .gitignore for personal-only configuration.");
println!();
println!("{} .octorus/config.toml can override {} settings including editor,", "Warning:".yellow(), "ALL".bold());
println!(" AI tool permissions, and auto_post. If you commit .octorus/ to a");
println!(" public repository, cloners will inherit these settings when running {}.", "or".bold());
println!(" Review the config carefully before committing.");
Ok(())
}
fn write_init_manifest(
config_dir: &Path,
is_local: bool,
written_files: &HashMap<String, bool>,
) -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let now = chrono::Utc::now().to_rfc3339();
let manifest_path = config_dir.join(".version");
let existing_manifest = read_manifest(&manifest_path);
let mut files = HashMap::new();
for (name, was_written) in written_files {
if *was_written {
files.insert(
name.clone(),
FileRecord {
version: version.to_string(),
status: FileRecordStatus::Created,
},
);
} else {
let preserved_version = existing_manifest
.as_ref()
.and_then(|m| m.files.get(name))
.map(|r| r.version.clone())
.or_else(|| {
let file_path = resolve_file_path(config_dir, name, is_local);
fs::read_to_string(&file_path)
.ok()
.and_then(|content| detect_version_from_hash(&content, name, is_local))
})
.unwrap_or_else(|| "0.0.0".to_string());
files.insert(
name.clone(),
FileRecord {
version: preserved_version,
status: FileRecordStatus::CustomizedSkipped,
},
);
}
}
let manifest = VersionManifest {
binary_version: version.to_string(),
initialized_at: existing_manifest
.as_ref()
.map(|m| m.initialized_at.clone())
.unwrap_or_else(|| now.clone()),
last_migrated_at: None,
files,
};
write_manifest(&manifest_path, &manifest).context("Failed to write .version manifest")?;
Ok(())
}
fn resolve_file_path(config_dir: &Path, name: &str, _is_local: bool) -> PathBuf {
match name {
"config.toml" => config_dir.join("config.toml"),
"SKILL.md" => {
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".claude/skills/octorus/SKILL.md"))
.unwrap_or_else(|| config_dir.join("SKILL.md"))
}
"headless-output.md" | "config-reference.md" => {
std::env::var("HOME")
.ok()
.map(|h| {
PathBuf::from(h)
.join(".claude/skills/octorus/references")
.join(name)
})
.unwrap_or_else(|| config_dir.join(name))
}
_ => config_dir.join("prompts").join(name),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn run_init_in_temp_dir(temp_dir: &TempDir, force: bool) -> Result<()> {
let config_home = temp_dir.path().join("octorus");
if !config_home.exists() {
println!(
"Creating configuration directory: {}",
config_home.display()
);
fs::create_dir_all(&config_home)?;
}
let config_path = config_home.join("config.toml");
write_file_if_needed(&config_path, DEFAULT_CONFIG, force, "config.toml")?;
let prompts_dir = config_home.join("prompts");
if !prompts_dir.exists() {
println!("Creating prompts directory: {}", prompts_dir.display());
fs::create_dir_all(&prompts_dir)?;
}
write_file_if_needed(
&prompts_dir.join("reviewer.md"),
DEFAULT_REVIEWER_PROMPT,
force,
"reviewer.md",
)?;
write_file_if_needed(
&prompts_dir.join("reviewee.md"),
DEFAULT_REVIEWEE_PROMPT,
force,
"reviewee.md",
)?;
write_file_if_needed(
&prompts_dir.join("rereview.md"),
DEFAULT_REREVIEW_PROMPT,
force,
"rereview.md",
)?;
Ok(())
}
#[test]
fn test_run_init_creates_files() {
let temp_dir = TempDir::new().unwrap();
run_init_in_temp_dir(&temp_dir, false).unwrap();
let config_path = temp_dir.path().join("octorus/config.toml");
let prompts_dir = temp_dir.path().join("octorus/prompts");
assert!(config_path.exists(), "config.toml should exist");
assert!(
prompts_dir.join("reviewer.md").exists(),
"reviewer.md should exist"
);
assert!(
prompts_dir.join("reviewee.md").exists(),
"reviewee.md should exist"
);
assert!(
prompts_dir.join("rereview.md").exists(),
"rereview.md should exist"
);
let config_content = fs::read_to_string(&config_path).unwrap();
assert!(config_content.contains("# editor = \"vim\""));
assert!(config_content.contains("[ai]"));
}
#[test]
fn test_run_init_skips_existing() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path().join("octorus");
fs::create_dir_all(&config_dir).unwrap();
let config_path = config_dir.join("config.toml");
fs::write(&config_path, "custom = true").unwrap();
run_init_in_temp_dir(&temp_dir, false).unwrap();
let content = fs::read_to_string(&config_path).unwrap();
assert_eq!(content, "custom = true");
}
#[test]
fn test_run_init_force_overwrites() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path().join("octorus");
fs::create_dir_all(&config_dir).unwrap();
let config_path = config_dir.join("config.toml");
fs::write(&config_path, "custom = true").unwrap();
run_init_in_temp_dir(&temp_dir, true).unwrap();
let content = fs::read_to_string(&config_path).unwrap();
assert!(content.contains("# editor = \"vim\""));
assert!(!content.contains("custom = true"));
}
#[test]
fn test_run_init_local_creates_files() {
let temp_dir = TempDir::new().unwrap();
run_init_local(temp_dir.path(), false).unwrap();
let octorus_dir = temp_dir.path().join(".octorus");
let config_path = octorus_dir.join("config.toml");
let prompts_dir = octorus_dir.join("prompts");
assert!(config_path.exists(), "config.toml should exist");
assert!(
prompts_dir.join("reviewer.md").exists(),
"reviewer.md should exist"
);
assert!(
prompts_dir.join("reviewee.md").exists(),
"reviewee.md should exist"
);
assert!(
prompts_dir.join("rereview.md").exists(),
"rereview.md should exist"
);
let config_content = fs::read_to_string(&config_path).unwrap();
assert!(config_content.contains("Project-local octorus configuration"));
assert!(config_content.contains("# [ai]"));
}
#[test]
fn test_run_init_local_skips_existing() {
let temp_dir = TempDir::new().unwrap();
let octorus_dir = temp_dir.path().join(".octorus");
fs::create_dir_all(&octorus_dir).unwrap();
let config_path = octorus_dir.join("config.toml");
fs::write(&config_path, "custom = true").unwrap();
run_init_local(temp_dir.path(), false).unwrap();
let content = fs::read_to_string(&config_path).unwrap();
assert_eq!(content, "custom = true");
}
#[test]
fn test_run_init_local_force_overwrites() {
let temp_dir = TempDir::new().unwrap();
let octorus_dir = temp_dir.path().join(".octorus");
fs::create_dir_all(&octorus_dir).unwrap();
let config_path = octorus_dir.join("config.toml");
fs::write(&config_path, "custom = true").unwrap();
run_init_local(temp_dir.path(), true).unwrap();
let content = fs::read_to_string(&config_path).unwrap();
assert!(content.contains("Project-local octorus configuration"));
assert!(!content.contains("custom = true"));
}
#[test]
fn test_generate_agent_skill_creates_file() {
let temp_dir = TempDir::new().unwrap();
let claude_dir = temp_dir.path().join(".claude");
fs::create_dir_all(&claude_dir).unwrap();
let mut written = HashMap::new();
generate_agent_skill_in(&claude_dir, false, &mut written).unwrap();
let skill_path = claude_dir.join("skills/octorus/SKILL.md");
assert!(skill_path.exists(), "SKILL.md should exist");
let content = fs::read_to_string(&skill_path).unwrap();
assert!(content.contains("or"), "Should contain binary name 'or'");
assert!(
content.contains("--ai-rally"),
"Should contain --ai-rally flag"
);
let refs_dir = claude_dir.join("skills/octorus/references");
let headless_path = refs_dir.join("headless-output.md");
let config_ref_path = refs_dir.join("config-reference.md");
assert!(headless_path.exists(), "headless-output.md should exist");
assert!(config_ref_path.exists(), "config-reference.md should exist");
let headless_content = fs::read_to_string(&headless_path).unwrap();
assert!(
headless_content.contains("Exit Codes"),
"headless-output.md should contain Exit Codes section"
);
let config_content = fs::read_to_string(&config_ref_path).unwrap();
assert!(
config_content.contains("config.toml"),
"config-reference.md should contain config.toml reference"
);
assert_eq!(written.get("SKILL.md"), Some(&true));
assert_eq!(written.get("headless-output.md"), Some(&true));
assert_eq!(written.get("config-reference.md"), Some(&true));
}
#[test]
fn test_generate_agent_skill_respects_force() {
let temp_dir = TempDir::new().unwrap();
let claude_dir = temp_dir.path().join(".claude");
let skill_dir = claude_dir.join("skills/octorus");
let refs_dir = skill_dir.join("references");
fs::create_dir_all(&refs_dir).unwrap();
let skill_path = skill_dir.join("SKILL.md");
fs::write(&skill_path, "custom content").unwrap();
fs::write(refs_dir.join("headless-output.md"), "custom headless").unwrap();
fs::write(refs_dir.join("config-reference.md"), "custom config").unwrap();
let mut written = HashMap::new();
generate_agent_skill_in(&claude_dir, false, &mut written).unwrap();
let content = fs::read_to_string(&skill_path).unwrap();
assert_eq!(content, "custom content");
assert_eq!(
fs::read_to_string(refs_dir.join("headless-output.md")).unwrap(),
"custom headless"
);
assert_eq!(
fs::read_to_string(refs_dir.join("config-reference.md")).unwrap(),
"custom config"
);
let mut written = HashMap::new();
generate_agent_skill_in(&claude_dir, true, &mut written).unwrap();
let content = fs::read_to_string(&skill_path).unwrap();
assert!(content.contains("--ai-rally"));
assert!(!content.contains("custom content"));
assert!(fs::read_to_string(refs_dir.join("headless-output.md"))
.unwrap()
.contains("Exit Codes"));
assert!(fs::read_to_string(refs_dir.join("config-reference.md"))
.unwrap()
.contains("config.toml"));
}
#[test]
fn test_generate_agent_skill_skips_when_claude_dir_missing() {
let temp_dir = TempDir::new().unwrap();
let claude_dir = temp_dir.path().join(".claude");
assert!(!claude_dir.is_dir());
let mut written = HashMap::new();
if claude_dir.is_dir() {
generate_agent_skill_in(&claude_dir, false, &mut written).unwrap();
}
assert!(
written.is_empty(),
"written_files should be empty when .claude dir is missing"
);
let skill_path = claude_dir.join("skills/octorus/SKILL.md");
assert!(
!skill_path.exists(),
"SKILL.md should not be created when .claude dir is missing"
);
}
#[test]
fn test_generate_agent_skill_skips_when_claude_is_file() {
let temp_dir = TempDir::new().unwrap();
let claude_path = temp_dir.path().join(".claude");
fs::write(&claude_path, "not a directory").unwrap();
assert!(!claude_path.is_dir());
let mut written = HashMap::new();
if claude_path.is_dir() {
generate_agent_skill_in(&claude_path, false, &mut written).unwrap();
}
assert!(written.is_empty());
let skill_path = claude_path.join("skills/octorus/SKILL.md");
assert!(
!skill_path.exists(),
"SKILL.md should not be created when .claude is a file"
);
}
#[test]
fn test_generate_agent_skill_creates_intermediate_dirs() {
let temp_dir = TempDir::new().unwrap();
let claude_dir = temp_dir.path().join(".claude");
fs::create_dir_all(&claude_dir).unwrap();
let mut written = HashMap::new();
generate_agent_skill_in(&claude_dir, false, &mut written).unwrap();
let skill_path = claude_dir.join("skills/octorus/SKILL.md");
assert!(
skill_path.exists(),
"Should create intermediate directories and SKILL.md"
);
let refs_dir = claude_dir.join("skills/octorus/references");
assert!(
refs_dir.join("headless-output.md").exists(),
"Should create references/headless-output.md"
);
assert!(
refs_dir.join("config-reference.md").exists(),
"Should create references/config-reference.md"
);
}
#[test]
fn test_init_local_manifest_all_created() {
let temp_dir = TempDir::new().unwrap();
run_init_local(temp_dir.path(), false).unwrap();
let manifest_path = temp_dir.path().join(".octorus/.version");
let manifest = read_manifest(&manifest_path).expect("manifest should exist");
for name in &["config.toml", "reviewer.md", "reviewee.md", "rereview.md"] {
let record = manifest.files.get(*name).unwrap_or_else(|| {
panic!("{} should be in manifest", name);
});
assert_eq!(
record.status,
FileRecordStatus::Created,
"{} should be Created on fresh init",
name
);
}
assert!(
!manifest.files.contains_key("SKILL.md"),
"SKILL.md should not be in local manifest"
);
assert!(
!manifest.files.contains_key("headless-output.md"),
"headless-output.md should not be in local manifest"
);
assert!(
!manifest.files.contains_key("config-reference.md"),
"config-reference.md should not be in local manifest"
);
}
#[test]
fn test_init_local_manifest_skipped_files() {
let temp_dir = TempDir::new().unwrap();
let octorus_dir = temp_dir.path().join(".octorus");
fs::create_dir_all(&octorus_dir).unwrap();
fs::write(octorus_dir.join("config.toml"), "custom = true").unwrap();
let prompts_dir = octorus_dir.join("prompts");
fs::create_dir_all(&prompts_dir).unwrap();
fs::write(prompts_dir.join("reviewer.md"), "custom reviewer").unwrap();
run_init_local(temp_dir.path(), false).unwrap();
let manifest_path = octorus_dir.join(".version");
let manifest = read_manifest(&manifest_path).expect("manifest should exist");
assert_eq!(
manifest.files["config.toml"].status,
FileRecordStatus::CustomizedSkipped,
"pre-existing config.toml should be CustomizedSkipped"
);
assert_eq!(
manifest.files["reviewer.md"].status,
FileRecordStatus::CustomizedSkipped,
"pre-existing reviewer.md should be CustomizedSkipped"
);
assert_eq!(
manifest.files["reviewee.md"].status,
FileRecordStatus::Created,
"newly created reviewee.md should be Created"
);
assert_eq!(
manifest.files["rereview.md"].status,
FileRecordStatus::Created,
"newly created rereview.md should be Created"
);
}
#[test]
fn test_init_local_manifest_force_all_created() {
let temp_dir = TempDir::new().unwrap();
let octorus_dir = temp_dir.path().join(".octorus");
fs::create_dir_all(&octorus_dir).unwrap();
fs::write(octorus_dir.join("config.toml"), "custom = true").unwrap();
run_init_local(temp_dir.path(), true).unwrap();
let manifest_path = octorus_dir.join(".version");
let manifest = read_manifest(&manifest_path).expect("manifest should exist");
for name in &["config.toml", "reviewer.md", "reviewee.md", "rereview.md"] {
assert_eq!(
manifest.files[*name].status,
FileRecordStatus::Created,
"{} should be Created with --force",
name
);
}
}
#[test]
fn test_init_skipped_files_do_not_record_current_version() {
let temp_dir = TempDir::new().unwrap();
let octorus_dir = temp_dir.path().join(".octorus");
fs::create_dir_all(&octorus_dir).unwrap();
fs::write(octorus_dir.join("config.toml"), "custom = true").unwrap();
run_init_local(temp_dir.path(), false).unwrap();
let manifest_path = octorus_dir.join(".version");
let manifest = read_manifest(&manifest_path).expect("manifest should exist");
let current_version = env!("CARGO_PKG_VERSION");
assert_ne!(
manifest.files["config.toml"].version, current_version,
"skipped file should not record current binary version"
);
assert_eq!(
manifest.files["config.toml"].version, "0.0.0",
"unknown origin should fall back to 0.0.0"
);
assert_eq!(
manifest.files["reviewee.md"].version, current_version,
"newly created file should have current binary version"
);
}
#[test]
fn test_init_skipped_files_preserve_existing_manifest_version() {
let temp_dir = TempDir::new().unwrap();
let octorus_dir = temp_dir.path().join(".octorus");
fs::create_dir_all(&octorus_dir).unwrap();
fs::write(octorus_dir.join("config.toml"), "custom = true").unwrap();
let old_manifest = VersionManifest {
binary_version: "0.4.0".to_string(),
initialized_at: "2025-01-01T00:00:00Z".to_string(),
last_migrated_at: None,
files: {
let mut files = HashMap::new();
files.insert(
"config.toml".to_string(),
FileRecord {
version: "0.4.0".to_string(),
status: FileRecordStatus::Created,
},
);
files
},
};
let manifest_path = octorus_dir.join(".version");
write_manifest(&manifest_path, &old_manifest).unwrap();
let prompts_dir = octorus_dir.join("prompts");
fs::create_dir_all(&prompts_dir).unwrap();
run_init_local(temp_dir.path(), false).unwrap();
let manifest = read_manifest(&manifest_path).expect("manifest should exist");
assert_eq!(
manifest.files["config.toml"].version, "0.4.0",
"skipped file should preserve version from existing manifest"
);
assert_eq!(
manifest.files["config.toml"].status,
FileRecordStatus::CustomizedSkipped,
);
assert_eq!(
manifest.initialized_at, "2025-01-01T00:00:00Z",
"initialized_at should be preserved from existing manifest"
);
}
#[test]
fn test_init_skipped_files_detect_version_from_hash() {
use crate::init::DEFAULT_LOCAL_CONFIG;
let temp_dir = TempDir::new().unwrap();
let octorus_dir = temp_dir.path().join(".octorus");
fs::create_dir_all(&octorus_dir).unwrap();
fs::write(octorus_dir.join("config.toml"), DEFAULT_LOCAL_CONFIG).unwrap();
run_init_local(temp_dir.path(), false).unwrap();
let manifest_path = octorus_dir.join(".version");
let manifest = read_manifest(&manifest_path).expect("manifest should exist");
let config_version = &manifest.files["config.toml"].version;
assert!(
config_version == "0.5.8",
"skipped file should detect version 0.5.8 from hash, got {}",
config_version
);
}
#[test]
fn test_generate_agent_skill_partial_existing() {
let temp_dir = TempDir::new().unwrap();
let claude_dir = temp_dir.path().join(".claude");
let skill_dir = claude_dir.join("skills/octorus");
fs::create_dir_all(&skill_dir).unwrap();
fs::write(skill_dir.join("SKILL.md"), "custom skill").unwrap();
let mut written = HashMap::new();
generate_agent_skill_in(&claude_dir, false, &mut written).unwrap();
assert_eq!(written.get("SKILL.md"), Some(&false));
assert_eq!(written.get("headless-output.md"), Some(&true));
assert_eq!(written.get("config-reference.md"), Some(&true));
assert_eq!(
fs::read_to_string(skill_dir.join("SKILL.md")).unwrap(),
"custom skill"
);
let refs_dir = skill_dir.join("references");
assert!(refs_dir.join("headless-output.md").exists());
assert!(refs_dir.join("config-reference.md").exists());
}
#[test]
fn test_generate_agent_skill_claude_dir_missing_written_files_empty() {
let temp_dir = TempDir::new().unwrap();
let claude_dir = temp_dir.path().join(".claude");
let mut written = HashMap::new();
if claude_dir.is_dir() {
generate_agent_skill_in(&claude_dir, false, &mut written).unwrap();
}
assert!(
written.is_empty(),
"written_files should be empty when .claude dir is missing"
);
}
}