ccd-cli 1.0.0-beta.2

Bootstrap and validate Continuous Context Development repositories
use std::path::Path;
use std::process::ExitCode;

use anyhow::{bail, Context, Result};
use serde::Serialize;

use crate::output::CommandReport;
use crate::paths::write;

struct StarterFile {
    relative_path: &'static str,
    contents: &'static str,
}

const STARTER_FILES: &[StarterFile] = &[StarterFile {
    relative_path: "AGENTS.md",
    contents: include_str!("../../templates/level-1/AGENTS.md"),
}];

#[derive(Serialize)]
pub struct ScaffoldReport {
    command: &'static str,
    ok: bool,
    action: &'static str,
    path: String,
    files: Vec<String>,
    conflicts: Vec<String>,
    message: String,
    next_steps: Vec<String>,
}

impl CommandReport for ScaffoldReport {
    fn exit_code(&self) -> ExitCode {
        if self.ok {
            ExitCode::SUCCESS
        } else {
            ExitCode::from(1)
        }
    }

    fn render_text(&self) {
        println!("{}", self.message);

        if self.ok {
            for file in &self.files {
                println!("Wrote {file}");
            }

            if self.next_steps.is_empty() {
                return;
            }

            println!();
            println!("Next steps:");
            for (index, step) in self.next_steps.iter().enumerate() {
                println!("{}. {step}", index + 1);
            }
            return;
        }

        for conflict in &self.conflicts {
            println!("Conflict: {conflict}");
        }
    }
}

pub fn run(target_dir: &Path, force: bool) -> Result<ScaffoldReport> {
    validate_target(target_dir)?;

    let conflicts = STARTER_FILES
        .iter()
        .map(|file| target_dir.join(file.relative_path))
        .filter(|path| path.exists())
        .map(|path| path.display().to_string())
        .collect::<Vec<_>>();

    if !force && !conflicts.is_empty() {
        return Ok(ScaffoldReport {
            command: "scaffold",
            ok: false,
            action: "refused",
            path: target_dir.display().to_string(),
            files: Vec::new(),
            conflicts,
            message: "Starter surfaces already exist; refusing to overwrite. Use `ccd scaffold --force` to replace them.".to_owned(),
            next_steps: Vec::new(),
        });
    }

    let mut files = Vec::new();
    for file in STARTER_FILES {
        let destination = target_dir.join(file.relative_path);
        if destination.is_dir() {
            bail!(
                "starter target is a directory, refusing to overwrite: {}",
                destination.display()
            );
        }

        let write_result = if force {
            write::replace_text(&destination, file.contents, None)
        } else {
            write::create_text(&destination, file.contents, None)
        };
        write_result.with_context(|| format!("failed to write {}", destination.display()))?;
        files.push(destination.display().to_string());
    }

    let action = if conflicts.is_empty() {
        "created"
    } else {
        "updated"
    };
    let message = match action {
        "created" => "Scaffolded project-truth starter surfaces.",
        "updated" => "Overwrote project-truth starter surfaces.",
        _ => unreachable!(),
    };

    let next_steps = vec![
        "Replace the starter placeholders in `AGENTS.md`.".to_owned(),
        "Run `ccd attach --path .` if this repo is not linked into CCD state yet.".to_owned(),
        "Run `ccd sync --path .` after curating `AGENTS.md` if you use generated policy mirrors."
            .to_owned(),
    ];

    Ok(ScaffoldReport {
        command: "scaffold",
        ok: true,
        action,
        path: target_dir.display().to_string(),
        files,
        conflicts,
        message: message.to_owned(),
        next_steps,
    })
}

fn validate_target(target_dir: &Path) -> Result<()> {
    if !target_dir.exists() {
        bail!("target directory does not exist: {}", target_dir.display());
    }

    if !target_dir.is_dir() {
        bail!("target path is not a directory: {}", target_dir.display());
    }

    Ok(())
}