use std::fs;
use std::path::Path;
use spec_spine_core::scaffold_init_with;
use spec_spine_types::Error;
use crate::load_repo_config;
pub fn run(repo: &Path, force: bool, with_kit: bool) -> Result<u8, Error> {
let cfg = load_repo_config(repo)?;
let scaffold = scaffold_init_with(&cfg, with_kit)?;
let specs_dir = cfg.layout.specs_dir.trim_end_matches('/').to_string();
let mut written = 0usize;
let mut skipped = 0usize;
for file in &scaffold.files {
let abs = repo.join(&file.rel_path);
if let Some((dir, ordinal)) = scaffolded_spec_ordinal(&specs_dir, &file.rel_path)
&& let Some(existing) = sibling_with_ordinal(repo, &specs_dir, dir, ordinal)
{
outln!(
" skip (ordinal {ordinal} already used by {specs_dir}/{existing}): {}",
file.rel_path
);
skipped += 1;
continue;
}
if abs.exists() && !force && !file.overwrite {
if file.append {
match append_block(&abs, file)? {
true => {
outln!(" append: {}", file.rel_path);
written += 1;
}
false => {
outln!(" skip (already present): {}", file.rel_path);
skipped += 1;
}
}
continue;
}
outln!(" skip (exists): {}", file.rel_path);
skipped += 1;
continue;
}
if let Some(parent) = abs.parent() {
fs::create_dir_all(parent)
.map_err(|e| Error::Io(format!("create {}: {e}", parent.display())))?;
}
fs::write(&abs, &file.contents)
.map_err(|e| Error::Io(format!("write {}: {e}", abs.display())))?;
if file.executable {
set_executable(&abs)?;
}
outln!(" write: {}", file.rel_path);
written += 1;
}
let bootstrap = format!("{specs_dir}/000-bootstrap/spec.md");
let next = if repo.join(&bootstrap).exists() {
format!("Next: customize {bootstrap}, then run `spec-spine compile`.")
} else {
"Next: run `spec-spine compile`.".to_string()
};
outln!(
"spec-spine init: {written} file(s) written, {skipped} skipped{}. {next}",
if force { " (--force)" } else { "" }
);
Ok(0)
}
fn scaffolded_spec_ordinal<'a>(specs_dir: &str, rel_path: &'a str) -> Option<(&'a str, &'a str)> {
let rest = rel_path.strip_prefix(specs_dir)?.strip_prefix('/')?;
let (dir, tail) = rest.split_once('/')?;
if tail != "spec.md" {
return None;
}
Some((dir, ordinal(dir)?))
}
fn ordinal(id: &str) -> Option<&str> {
let end = id.find(|c: char| !c.is_ascii_digit()).unwrap_or(id.len());
(end > 0).then(|| &id[..end])
}
fn sibling_with_ordinal(
repo: &Path,
specs_dir: &str,
scaffolded: &str,
ordinal: &str,
) -> Option<String> {
let entries = fs::read_dir(repo.join(specs_dir)).ok()?;
let mut found: Vec<String> = entries
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|name| name != scaffolded && crate::cmd_init::ordinal(name) == Some(ordinal))
.collect();
found.sort();
found.into_iter().next()
}
fn append_block(abs: &Path, file: &spec_spine_core::scaffold::ScaffoldFile) -> Result<bool, Error> {
let existing =
fs::read_to_string(abs).map_err(|e| Error::Io(format!("read {}: {e}", abs.display())))?;
let marker = file.append_marker.as_deref().unwrap_or(&file.contents);
if existing.contains(marker) {
return Ok(false);
}
let mut next = existing;
if !next.is_empty() && !next.ends_with('\n') {
next.push('\n');
}
if !next.is_empty() {
next.push('\n');
}
next.push_str(&file.contents);
fs::write(abs, next).map_err(|e| Error::Io(format!("append {}: {e}", abs.display())))?;
Ok(true)
}
#[cfg(unix)]
fn set_executable(abs: &Path) -> Result<(), Error> {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(abs)
.map_err(|e| Error::Io(format!("stat {}: {e}", abs.display())))?
.permissions();
let mode = perms.mode();
perms.set_mode(mode | ((mode & 0o444) >> 2));
fs::set_permissions(abs, perms).map_err(|e| Error::Io(format!("chmod {}: {e}", abs.display())))
}
#[cfg(not(unix))]
fn set_executable(_abs: &Path) -> Result<(), Error> {
Ok(())
}