use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{Context, Result, bail};
use serde::Serialize;
use super::link::{self, LinkRequest};
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::paths::substrate::SubstrateKind;
use crate::profile;
use crate::repo::marker;
use crate::repo::registry::{self as repo_registry, RepoAliases, RepoRegistryEntry};
#[derive(Serialize)]
pub struct AttachReport {
command: &'static str,
ok: bool,
path: String,
profile: String,
locality_id: String,
action: &'static str,
resolution: &'static str,
profile_root: String,
clone_profile_root: String,
marker_path: String,
registry_path: String,
overlay_path: String,
created_profile: bool,
created_clone_profile: bool,
created_kernel_files: Vec<String>,
warnings: Vec<String>,
next_steps: Vec<String>,
}
impl CommandReport for AttachReport {
fn exit_code(&self) -> ExitCode {
ExitCode::SUCCESS
}
fn render_text(&self) {
if self.action == "already_attached" {
println!(
"Already attached to profile {} (project ID {}; locality_id compatibility). No changes needed.",
self.profile, self.locality_id
);
return;
}
println!(
"Initialized CCD state for profile {} with project ID {} ({}, {}).",
self.profile, self.locality_id, self.action, self.resolution
);
println!("Profile root: {}", self.profile_root);
println!("Workspace state: {}", self.clone_profile_root);
println!("Marker: {}", self.marker_path);
println!("Registry: {}", self.registry_path);
println!("Project overlay: {}", self.overlay_path);
for warning in &self.warnings {
println!("Warning: {warning}");
}
if self.next_steps.is_empty() {
return;
}
println!();
println!("Next steps:");
for (index, step) in self.next_steps.iter().enumerate() {
println!("{}. {step}", index + 1);
}
}
}
pub fn run(
target_dir: &Path,
explicit_profile: Option<&str>,
explicit_locality_id: Option<&str>,
display_name: Option<&str>,
) -> Result<AttachReport> {
validate_target(target_dir)?;
let profile_name = profile::resolve(explicit_profile)?;
let layout = StateLayout::resolve_for_attach(target_dir, profile_name.clone())?;
let existing_marker = marker::load(target_dir)?;
if explicit_profile.is_none() && explicit_locality_id.is_none() && display_name.is_none() {
if let Some(existing_marker) = existing_marker.as_ref() {
if layout
.repo_registry_dir(&existing_marker.locality_id)?
.is_dir()
&& (layout.resolved_substrate().kind() != SubstrateKind::Directory
|| layout.resolved_substrate().workspace_binding_exists())
{
return already_attached_report(
target_dir,
&profile_name,
&layout,
existing_marker,
);
}
}
}
match layout.resolved_substrate().kind() {
SubstrateKind::Git => run_git_attach(
target_dir,
explicit_profile,
explicit_locality_id,
display_name,
),
SubstrateKind::Directory => run_directory_attach(
target_dir,
profile_name,
layout,
existing_marker,
explicit_locality_id,
display_name,
),
}
}
fn already_attached_report(
target_dir: &Path,
profile_name: &profile::ProfileName,
layout: &StateLayout,
existing_marker: &marker::RepoMarker,
) -> Result<AttachReport> {
Ok(AttachReport {
command: "attach",
ok: true,
path: target_dir.display().to_string(),
profile: profile_name.to_string(),
locality_id: existing_marker.locality_id.clone(),
action: "already_attached",
resolution: "no_change",
profile_root: layout.profile_root().display().to_string(),
clone_profile_root: layout.clone_profile_root().display().to_string(),
marker_path: target_dir.join(marker::MARKER_FILE).display().to_string(),
registry_path: layout
.repo_metadata_path(&existing_marker.locality_id)?
.display()
.to_string(),
overlay_path: layout
.repo_overlay_root(&existing_marker.locality_id)?
.display()
.to_string(),
created_profile: false,
created_clone_profile: false,
created_kernel_files: vec![],
warnings: vec![],
next_steps: vec![],
})
}
fn run_git_attach(
target_dir: &Path,
explicit_profile: Option<&str>,
explicit_locality_id: Option<&str>,
display_name: Option<&str>,
) -> Result<AttachReport> {
let prepared = link::prepare_link(
target_dir,
LinkRequest {
explicit_profile,
explicit_locality_id,
display_name,
},
)?;
let created_profile = !prepared.layout.profile_root().is_dir();
let clone_profile_root = prepared.layout.clone_profile_root();
let created_clone_profile = !clone_profile_root.is_dir();
let profile_root = profile::ensure_profile_dir(prepared.layout.ccd_root(), &prepared.profile)?;
let created_kernel_files = ensure_profile_kernel_files(&prepared.layout)?;
fs::create_dir_all(&clone_profile_root).with_context(|| {
format!(
"failed to create directory {}",
clone_profile_root.display()
)
})?;
let link_result = link::finalize_link(prepared)?;
Ok(AttachReport {
command: "attach",
ok: true,
path: target_dir.display().to_string(),
profile: link_result.profile.to_string(),
locality_id: link_result.locality_id.clone(),
action: link_result.action,
resolution: link_result.resolution,
profile_root: profile_root.display().to_string(),
clone_profile_root: clone_profile_root.display().to_string(),
marker_path: link_result.marker_path.display().to_string(),
registry_path: link_result.registry_path.display().to_string(),
overlay_path: link_result.overlay_path.display().to_string(),
created_profile,
created_clone_profile,
created_kernel_files: created_kernel_files
.iter()
.map(|path| path.display().to_string())
.collect(),
warnings: link_result.warnings,
next_steps: vec![
format!(
"Run `ccd start --activate --path {} --profile {}` when you begin work in this workspace.",
target_dir.display(),
link_result.profile
),
format!(
"Use `ccd link --path <workspace> --project-id {}` to attach another workspace to the same project overlay.",
link_result.locality_id
),
],
})
}
fn run_directory_attach(
target_dir: &Path,
profile_name: profile::ProfileName,
layout: StateLayout,
existing_marker: Option<marker::RepoMarker>,
explicit_locality_id: Option<&str>,
display_name: Option<&str>,
) -> Result<AttachReport> {
let mut warnings = Vec::new();
let chosen_display_name = link::choose_display_name(
display_name,
existing_marker
.as_ref()
.and_then(|marker| marker.display_name.as_deref()),
target_dir.file_name().and_then(|name| name.to_str()),
);
let (locality_id, action, resolution, previous_locality_id) = if let Some(locality_id) =
explicit_locality_id
{
if let Some(existing_marker) = existing_marker.as_ref() {
if existing_marker.locality_id != locality_id {
warnings.push(format!(
"relinked directory clone from locality_id `{}` to explicit locality_id `{locality_id}`",
existing_marker.locality_id
));
}
}
(
locality_id.to_owned(),
"attached",
"explicit_locality_id",
existing_marker
.as_ref()
.map(|marker| marker.locality_id.clone())
.filter(|previous| previous != locality_id),
)
} else if let Some(existing_marker) = existing_marker.as_ref() {
if !layout.resolved_substrate().workspace_binding_exists() {
warnings.push(
"directory workspace state was not initialized for this canonical path; creating fresh local workspace state"
.to_owned(),
);
}
(
existing_marker.locality_id.clone(),
"refreshed",
"local_marker",
None,
)
} else {
(
link::generate_locality_id(target_dir, &layout)?,
"created",
"new_assignment",
None,
)
};
let marker = match existing_marker.as_ref() {
Some(existing_marker) => existing_marker
.rewrite_with_locality_id(locality_id.clone(), chosen_display_name.clone())?,
None => {
marker::RepoMarker::new_directory(locality_id.clone(), chosen_display_name.clone())?
}
};
let current_clone_root = repo_registry::normalize_clone_root(target_dir)?;
let registry_path = layout.repo_metadata_path(&locality_id)?;
let existing_entry = repo_registry::load(®istry_path)?;
let mut known_clone_roots = existing_entry
.as_ref()
.map(|entry| entry.known_clone_roots.clone())
.unwrap_or_default();
if !known_clone_roots
.iter()
.any(|known| known == ¤t_clone_root)
{
known_clone_roots.push(current_clone_root.clone());
known_clone_roots.sort();
}
let registry_entry = RepoRegistryEntry::new_with_known_clone_roots(
link::choose_display_name(
display_name,
existing_entry
.as_ref()
.and_then(|entry| entry.display_name.as_deref())
.or(chosen_display_name.as_deref()),
target_dir.file_name().and_then(|name| name.to_str()),
),
directory_aliases(target_dir),
known_clone_roots,
)?;
let created_profile = !layout.profile_root().is_dir();
let clone_profile_root = layout.clone_profile_root();
let created_clone_profile = !clone_profile_root.is_dir();
let profile_root = profile::ensure_profile_dir(layout.ccd_root(), &profile_name)?;
let created_kernel_files = ensure_profile_kernel_files(&layout)?;
layout.resolved_substrate().ensure_workspace_binding()?;
fs::create_dir_all(&clone_profile_root).with_context(|| {
format!(
"failed to create directory {}",
clone_profile_root.display()
)
})?;
let marker_path = marker::write(target_dir, &marker)?;
let registry_path = repo_registry::write(®istry_path, ®istry_entry)?;
if let Some(previous_locality_id) = previous_locality_id.as_deref() {
if let Err(error) =
link::remove_known_clone_root(&layout, previous_locality_id, ¤t_clone_root)
{
warnings.push(format!(
"failed to prune clone root {current_clone_root} from previous locality_id `{previous_locality_id}`: {error:#}",
));
}
}
let overlay_path = layout.repo_overlay_root(&locality_id)?;
fs::create_dir_all(&overlay_path)
.with_context(|| format!("failed to create directory {}", overlay_path.display()))?;
Ok(AttachReport {
command: "attach",
ok: true,
path: target_dir.display().to_string(),
profile: profile_name.to_string(),
locality_id: locality_id.clone(),
action,
resolution,
profile_root: profile_root.display().to_string(),
clone_profile_root: clone_profile_root.display().to_string(),
marker_path: marker_path.display().to_string(),
registry_path: registry_path.display().to_string(),
overlay_path: overlay_path.display().to_string(),
created_profile,
created_clone_profile,
created_kernel_files: created_kernel_files
.iter()
.map(|path| path.display().to_string())
.collect(),
warnings,
next_steps: vec![
format!(
"Run `ccd start --activate --path {} --profile {}` when you begin work in this directory.",
target_dir.display(),
profile_name
),
format!(
"Use `ccd attach --path <directory> --project-id {locality_id}` to attach another directory workspace to the same project overlay.",
),
],
})
}
fn directory_aliases(target_dir: &Path) -> RepoAliases {
RepoAliases {
remote_urls: Vec::new(),
root_commits: Vec::new(),
repo_basenames: target_dir
.file_name()
.and_then(|name| name.to_str())
.map(|name| vec![name.to_owned()])
.unwrap_or_default(),
}
}
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(())
}
fn ensure_profile_kernel_files(layout: &StateLayout) -> Result<Vec<PathBuf>> {
let files = vec![
layout.profile_config_path(),
layout.profile_policy_path(),
layout.profile_memory_path(),
];
let mut created = Vec::new();
for path in files {
if path.exists() {
continue;
}
fs::write(&path, "").with_context(|| format!("failed to write {}", path.display()))?;
created.push(path);
}
Ok(created)
}