ccd-cli 1.0.0-beta.1

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::db;
use crate::handoff::{self, BranchMode};
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::state::runtime as runtime_state;
use crate::state::session as session_state;

/// Remove the legacy `handoff.md` export if it exists under the profile root.
///
/// Phase A2 of the artifact-consistency framework stopped emitting this file,
/// and Phase A4 requires `ccd start` to reap any stale copy left over from
/// pre-A2 profiles. The cleanup is narrow (only this exact path) and fails
/// closed on any error other than `NotFound`, so unexpected filesystem
/// conditions surface as a start-path failure rather than a silent partial
/// cleanup.
pub(crate) fn cleanup_legacy_markdown(layout: &StateLayout) -> Result<()> {
    let legacy_path = layout.handoff_path();
    match std::fs::remove_file(&legacy_path) {
        Ok(()) => {
            tracing::debug!(
                path = %legacy_path.display(),
                "removed stale handoff.md export"
            );
            Ok(())
        }
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(err) => Err(err).with_context(|| {
            format!(
                "failed to remove stale handoff export {}",
                legacy_path.display()
            )
        }),
    }
}

#[derive(Serialize)]
pub struct HandoffExportReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    native_state_path: String,
    revision: u64,
    body: String,
}

impl CommandReport for HandoffExportReport {
    fn exit_code(&self) -> ExitCode {
        ExitCode::SUCCESS
    }

    fn render_text(&self) {
        // Header on stderr so stdout stays a clean Markdown body that
        // callers can pipe or redirect without post-processing.
        eprintln!(
            "Handoff export from {} (revision {})",
            self.native_state_path, self.revision
        );
        print!("{}", self.body);
    }
}

fn ensure_repo_linked(repo_root: &Path) -> Result<String> {
    let Some(marker) = repo_marker::load(repo_root)? else {
        bail!(
            "repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
            repo_root.join(repo_marker::MARKER_FILE).display(),
            repo_root.display(),
            repo_root.display()
        )
    };

    Ok(marker.locality_id)
}

fn ensure_profile_exists(layout: &StateLayout, repo_root: &Path) -> Result<()> {
    let profile_root = layout.profile_root();
    if profile_root.is_dir() {
        return Ok(());
    }

    bail!(
        "profile `{}` does not exist at {}; bootstrap it with `ccd attach --path {}` before using `ccd handoff export`",
        layout.profile(),
        profile_root.display(),
        repo_root.display()
    )
}

pub fn run(repo_root: &Path, explicit_profile: Option<&str>) -> Result<HandoffExportReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    ensure_profile_exists(&layout, repo_root)?;
    let locality_id = ensure_repo_linked(repo_root)?;

    let runtime = runtime_state::load_runtime_state(repo_root, &layout, &locality_id)?;
    if runtime.sources.handoff.is_missing() {
        bail!(
            "canonical native handoff state is missing at {}; run `ccd start --path {}` first",
            layout.state_db_path().display(),
            repo_root.display()
        );
    }
    let db = db::StateDb::open_for_layout(&layout)?;
    let revision = db::handoff::current_revision(db.conn())?;
    let git = if layout.resolved_substrate().is_git() {
        Some(handoff::read_git_state(
            repo_root,
            BranchMode::AllowDetachedHead,
        )?)
    } else {
        None
    };
    let session_id = session_state::load_session_id(&layout)?;
    let current_system_state =
        handoff::current_system_state_lines(git.as_ref(), session_id.as_deref());
    let mut content = runtime_state::render_handoff_markdown(&runtime.state.handoff);
    if !current_system_state.is_empty() {
        if !content.is_empty() {
            content.push('\n');
        }
        content.push_str("## Current System State\n\n");
        for line in &current_system_state {
            content.push_str("- ");
            content.push_str(line);
            content.push('\n');
        }
    }

    Ok(HandoffExportReport {
        command: "handoff-export",
        ok: true,
        path: repo_root.display().to_string(),
        profile: profile.to_string(),
        native_state_path: layout.state_db_path().display().to_string(),
        revision,
        body: content,
    })
}