use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{bail, Context, Result};
use serde::Serialize;
use serde_json::Value as JsonValue;
use super::{attach, doctor, start};
use crate::output::CommandReport;
use crate::paths::{git as git_paths, state::StateLayout, substrate::SubstrateKind};
use crate::profile::{self, ProfileName};
use crate::repo::marker as repo_marker;
use crate::repo::registry as repo_registry;
use crate::state::session as session_state;
#[derive(Serialize)]
pub struct SessionOpenReport {
command: &'static str,
ok: bool,
path: String,
profile: String,
worktree: WorktreeView,
bootstrap: BootstrapView,
doctor: DoctorView,
start: StartView,
session_state: SessionStateView,
warnings: Vec<String>,
}
#[derive(Serialize)]
struct WorktreeView {
status: &'static str,
path: String,
branch: Option<String>,
from: Option<String>,
}
#[derive(Serialize)]
struct BootstrapView {
status: &'static str,
action: Option<String>,
resolution: Option<String>,
}
#[derive(Serialize)]
struct DoctorView {
status: &'static str,
failures: u64,
warnings: u64,
}
#[derive(Serialize)]
struct StartView {
status: &'static str,
project_id: Option<String>,
locality_id: Option<String>,
handoff_path: Option<String>,
}
#[derive(Serialize)]
struct SessionStateView {
status: &'static str,
path: Option<String>,
start_count: Option<u64>,
}
impl CommandReport for SessionOpenReport {
fn exit_code(&self) -> ExitCode {
if self.ok {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
fn render_text(&self) {
let status = if self.ok { "opened" } else { "blocked" };
println!(
"Session open is {status} for profile `{}` at {}.",
self.profile, self.path
);
match self.worktree.status {
"created" => {
let branch = self.worktree.branch.as_deref().unwrap_or("unknown");
if let Some(from) = &self.worktree.from {
println!(
"Worktree: created {} on branch `{branch}` from `{from}`.",
self.worktree.path
);
} else {
println!(
"Worktree: created {} on branch `{branch}`.",
self.worktree.path
);
}
}
_ => println!(
"Worktree: using existing checkout at {}.",
self.worktree.path
),
}
match self.bootstrap.status {
"initialized" => {
let action = self.bootstrap.action.as_deref().unwrap_or("updated");
let resolution = self.bootstrap.resolution.as_deref().unwrap_or("resolved");
println!("Bootstrap: {action} profile state ({resolution}).");
}
_ => println!("Bootstrap: existing profile state was reused."),
}
println!(
"Doctor summary: {} failure(s), {} warning(s).",
self.doctor.failures, self.doctor.warnings
);
if !self.ok {
println!("Session start was skipped because doctor reported failures.");
return;
}
if let Some(project_id) = self
.start
.project_id
.as_ref()
.or(self.start.locality_id.as_ref())
{
println!("Project ID: {project_id}");
}
if let Some(handoff_path) = &self.start.handoff_path {
println!("Handoff: {handoff_path}");
}
if let Some(path) = &self.session_state.path {
let start_count = self.session_state.start_count.unwrap_or(0);
println!("Session state: {path} (start_count={start_count})");
}
for warning in &self.warnings {
println!("Warning: {warning}");
}
}
}
pub fn run(
repo_root: &Path,
explicit_profile: Option<&str>,
worktree: Option<&Path>,
branch: Option<&str>,
from: Option<&str>,
pod: Option<&str>,
) -> Result<SessionOpenReport> {
validate_args(worktree, branch, from, pod)?;
let profile = profile::resolve(explicit_profile)?;
let layout = StateLayout::resolve_for_attach(repo_root, profile.clone())?;
if layout.resolved_substrate().kind() != SubstrateKind::Git {
bail!(
"`ccd session open` is only supported for Git-backed projects; directory substrate workspaces have no non-Git session isolation yet"
);
}
if let Some(pod_name) = pod {
let source_profile = profile::resolve(None)?;
if let Ok(source_layout) = StateLayout::resolve(repo_root, source_profile) {
if let Ok(Some(marker)) = repo_marker::load(repo_root) {
if let Ok(Some(current)) = source_layout.focus_pod_name(&marker.locality_id) {
if current != pod_name {
bail!(
"source profile is already in pod `{current}` for this project. \
Use `ccd pod init {pod_name} --force --path .` to move it first."
);
}
}
}
}
}
let mut target_path = repo_root.to_path_buf();
let worktree_view = if let Some(worktree) = worktree {
let target = resolve_worktree_path(repo_root, worktree)?;
let branch = branch.expect("branch is validated");
git_paths::worktree_add(repo_root, &target, branch, from)?;
target_path = target.clone();
WorktreeView {
status: "created",
path: target.display().to_string(),
branch: Some(branch.to_owned()),
from: from.map(str::to_owned),
}
} else {
WorktreeView {
status: "existing",
path: target_path.display().to_string(),
branch: None,
from: None,
}
};
let bootstrap = if needs_bootstrap(&target_path, &profile)? {
let report = attach::run(&target_path, explicit_profile, None, None)?;
let json = serde_json::to_value(&report)?;
BootstrapView {
status: "initialized",
action: string_at(&json, &["action"]),
resolution: string_at(&json, &["resolution"]),
}
} else {
BootstrapView {
status: "existing",
action: None,
resolution: None,
}
};
if let Some(pod_name) = pod {
crate::commands::pod::init(&target_path, explicit_profile, pod_name, false)?;
crate::commands::pod::init(repo_root, None, pod_name, false)?;
}
let doctor_report = doctor::run(
&target_path,
explicit_profile,
doctor::RunOptions {
include_repo_native_checks: true,
},
)?;
let doctor_json = serde_json::to_value(&doctor_report)?;
let doctor_failures = u64_at(&doctor_json, &["failures"]).unwrap_or(0);
let doctor_warnings = u64_at(&doctor_json, &["warnings"]).unwrap_or(0);
if doctor_failures > 0 {
return Ok(SessionOpenReport {
command: "session-open",
ok: false,
path: target_path.display().to_string(),
profile: profile.to_string(),
worktree: worktree_view,
bootstrap,
doctor: DoctorView {
status: "failed",
failures: doctor_failures,
warnings: doctor_warnings,
},
start: StartView {
status: "skipped",
project_id: None,
locality_id: None,
handoff_path: None,
},
session_state: SessionStateView {
status: "skipped",
path: None,
start_count: None,
},
warnings: Vec::new(),
});
}
let start_report = start::run(&target_path, explicit_profile, false)?;
let start_json = serde_json::to_value(&start_report)?;
let locality_id = repo_marker::load(&target_path)
.ok()
.flatten()
.map(|m| m.locality_id);
let session_report = session_state::start(
&target_path,
explicit_profile,
locality_id.as_deref(),
session_state::SessionStartOptions::interactive(None),
)?;
let session_json = serde_json::to_value(&session_report)?;
Ok(SessionOpenReport {
command: "session-open",
ok: true,
path: target_path.display().to_string(),
profile: profile.to_string(),
worktree: worktree_view,
bootstrap,
doctor: DoctorView {
status: if doctor_warnings > 0 {
"warning"
} else {
"passed"
},
failures: doctor_failures,
warnings: doctor_warnings,
},
start: StartView {
status: "started",
project_id: string_at(&start_json, &["project_id"]),
locality_id: string_at(&start_json, &["locality_id"]),
handoff_path: string_at(&start_json, &["handoff", "path"]),
},
session_state: SessionStateView {
status: "recorded",
path: string_at(&session_json, &["path"]),
start_count: u64_at(&session_json, &["start_count"]),
},
warnings: string_array_at(&start_json, &["warnings"]),
})
}
fn validate_args(
worktree: Option<&Path>,
branch: Option<&str>,
from: Option<&str>,
pod: Option<&str>,
) -> Result<()> {
if pod.is_some() && worktree.is_none() {
bail!("`--pod` requires `--worktree`. Use `ccd pod init` for existing checkouts.");
}
if worktree.is_none() {
if branch.is_some() {
bail!("`--branch` requires `--worktree`");
}
if from.is_some() {
bail!("`--from` requires `--worktree`");
}
return Ok(());
}
let Some(branch) = branch else {
bail!("`--branch` is required when `--worktree` is set");
};
if branch.trim().is_empty() {
bail!("`--branch` cannot be empty");
}
if let Some(from) = from {
if from.trim().is_empty() {
bail!("`--from` cannot be empty");
}
}
Ok(())
}
fn needs_bootstrap(repo_root: &Path, profile: &ProfileName) -> Result<bool> {
let layout = StateLayout::resolve(repo_root, profile.clone())?;
if !layout.profile_root().is_dir() {
return Ok(true);
}
let Some(marker) = repo_marker::load(repo_root)? else {
return Ok(true);
};
let registry_path = layout.repo_metadata_path(&marker.locality_id)?;
if repo_registry::load(®istry_path)?.is_none() {
return Ok(true);
}
let overlay_path = layout.repo_overlay_root(&marker.locality_id)?;
Ok(!overlay_path.is_dir())
}
fn resolve_worktree_path(repo_root: &Path, worktree: &Path) -> Result<PathBuf> {
let raw = worktree
.to_str()
.ok_or_else(|| anyhow::anyhow!("unsafe --worktree value: paths must be valid UTF-8"))?;
if raw.chars().any(char::is_control) {
bail!("unsafe --worktree value: control characters are not allowed");
}
let anchored = if worktree.is_absolute() {
worktree.to_path_buf()
} else {
repo_root.join(worktree)
};
let file_name = anchored
.file_name()
.and_then(|value| value.to_str())
.filter(|value| !value.is_empty() && *value != "." && *value != "..")
.ok_or_else(|| {
anyhow::anyhow!("unsafe --worktree value: target directory name is required")
})?;
let parent = anchored.parent().unwrap_or(repo_root);
let parent = parent
.canonicalize()
.with_context(|| format!("failed to resolve worktree parent `{}`", parent.display()))?;
let target = parent.join(file_name);
if target.exists() {
bail!(
"worktree path already exists: {}; choose a new path or remove it first",
target.display()
);
}
if target.starts_with(repo_root) {
bail!(
"worktree path must be outside the source checkout: {}",
target.display()
);
}
Ok(target)
}
fn string_at(value: &JsonValue, path: &[&str]) -> Option<String> {
find_value(value, path)
.and_then(JsonValue::as_str)
.map(str::to_owned)
}
fn u64_at(value: &JsonValue, path: &[&str]) -> Option<u64> {
find_value(value, path).and_then(JsonValue::as_u64)
}
fn string_array_at(value: &JsonValue, path: &[&str]) -> Vec<String> {
find_value(value, path)
.and_then(JsonValue::as_array)
.map(|entries| {
entries
.iter()
.filter_map(JsonValue::as_str)
.map(str::to_owned)
.collect()
})
.unwrap_or_default()
}
fn find_value<'a>(value: &'a JsonValue, path: &[&str]) -> Option<&'a JsonValue> {
let mut current = value;
for segment in path {
current = current.get(*segment)?;
}
Some(current)
}