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;
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) {
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 ¤t_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,
})
}