use std::path::Path;
use std::process::ExitCode;
use anyhow::{Context, Result};
use serde_json::Value;
use crate::commands;
use crate::extensions;
use crate::handoff::{self, BranchMode, CheckoutContinuityAdvisory};
use crate::output::{self, OutputFormat};
use crate::paths;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo;
use crate::state;
const START_EXTENSION_FIELDS: &[&str] = &["backlog", "extension_dispatch"];
pub(crate) fn preflight(args: crate::PreflightArgs, format: OutputFormat) -> Result<ExitCode> {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::preflight::run(&repo_path, args.profile.as_deref())?;
output::render_report(format, &report)
}
pub(crate) fn start(args: crate::StartArgs, format: OutputFormat) -> Result<ExitCode> {
if args.check && args.fields.is_some() {
anyhow::bail!(
"--fields is not supported with `start --check`; the check report already includes all readiness data without field filtering"
);
}
if args.check && args.memory_depth.is_some() {
anyhow::bail!("--memory-depth is not supported with `start --check`");
}
if args.check && args.activate {
anyhow::bail!("--activate is not supported with `start --check`");
}
if args.fields.is_some() && format != OutputFormat::Json {
anyhow::bail!("--fields requires --output json");
}
if args.memory_depth.is_some() && format != OutputFormat::Json {
anyhow::bail!("--memory-depth requires --output json");
}
let (kernel_fields, extension_fields) = match &args.fields {
Some(fields) => split_start_fields(fields)?,
None => (Vec::new(), Vec::new()),
};
let repo_path = paths::cli::resolve(&args.path)?;
if args.check {
let report = commands::start::run_check(&repo_path, args.profile.as_deref(), args.refresh)?;
return output::render_report(format, &report);
}
let use_metadata = args.memory_depth == Some(output::MemoryDepth::Metadata);
let build_start_report = || -> Result<_> {
if args.fields.is_some() {
if !output::needs_source_rendering(&kernel_fields) {
commands::start::run_compiled_only(
&repo_path,
args.profile.as_deref(),
args.refresh,
)
} else {
commands::start::run(&repo_path, args.profile.as_deref(), args.refresh)
}
} else {
commands::start::run(&repo_path, args.profile.as_deref(), args.refresh)
}
};
let mut report = build_start_report()?;
if args.activate {
let locality_id = repo::marker::load(&repo_path)
.ok()
.flatten()
.map(|marker| marker.locality_id);
let start_options = state::session::SessionStartOptions {
mode: args.mode.map(crate::SessionModeArg::into_state_mode),
lifecycle: state::session::SessionLifecycle::Interactive,
owner_kind: None,
actor_id: None,
supervisor_id: None,
lease_ttl_secs: None,
};
let activation = state::session::start(
&repo_path,
args.profile.as_deref(),
locality_id.as_deref(),
start_options,
)?;
report = build_start_report()?.with_activation(activation);
}
if format == OutputFormat::Json {
let mut value = serde_json::to_value(&report)?;
if use_metadata {
value = output::strip_memory_content(value);
}
if let Some(fields) = &args.fields {
value = attach_requested_start_extension_fields(
value,
&repo_path,
args.profile.as_deref(),
&extension_fields,
)?;
value = filter_requested_start_fields(value, fields)?;
} else {
value = output::apply_default_json_contract(value, output::DefaultJsonContract::Start)?;
}
println!("{}", serde_json::to_string_pretty(&value)?);
Ok(ExitCode::SUCCESS)
} else {
output::render_report(format, &report)
}
}
fn split_start_fields(fields: &[String]) -> Result<(Vec<String>, Vec<String>)> {
let mut kernel_fields = Vec::new();
let mut extension_fields = Vec::new();
for field in fields {
if START_EXTENSION_FIELDS.contains(&field.as_str()) {
extension_fields.push(field.clone());
} else {
kernel_fields.push(field.clone());
}
}
output::validate_start_fields(&kernel_fields)?;
Ok((kernel_fields, extension_fields))
}
fn attach_requested_start_extension_fields(
mut value: Value,
repo_path: &Path,
explicit_profile: Option<&str>,
extension_fields: &[String],
) -> Result<Value> {
if extension_fields.is_empty() {
return Ok(value);
}
let obj = value
.as_object_mut()
.context("start report is not a JSON object")?;
let profile = profile::resolve(explicit_profile)?;
let layout = StateLayout::resolve(repo_path, profile)?;
let locality_id = repo::marker::load(repo_path)?
.map(|marker| marker.locality_id)
.context("start extension fields require an attached project workspace")?;
let active_session_id = state::session::load_for_layout(&layout)?
.and_then(|session| session.session_id)
.filter(|session_id| !session_id.is_empty());
let git = handoff::read_git_state(repo_path, BranchMode::AllowDetachedHead).ok();
let branch = git.as_ref().map(|git| git.branch.as_str());
let landed_trunk = git.as_ref().and_then(|git| match handoff::checkout_continuity_advisory(
repo_path, git,
) {
Some(CheckoutContinuityAdvisory::LandedBranch { trunk }) => Some(trunk),
_ => None,
});
for field in extension_fields {
match field.as_str() {
"backlog" => {
obj.insert(
field.clone(),
serde_json::to_value(extensions::load_work_queue_view(&layout, 12)?)?,
);
}
"extension_dispatch" => {
obj.insert(
field.clone(),
extensions::build_startup_dispatch_compat(
&layout,
&locality_id,
active_session_id.as_deref(),
branch,
landed_trunk.as_deref(),
)?,
);
}
_ => {}
}
}
Ok(value)
}
fn filter_requested_start_fields(mut value: Value, fields: &[String]) -> Result<Value> {
let obj = value
.as_object_mut()
.context("start report is not a JSON object")?;
let keys_to_remove: Vec<String> = obj
.keys()
.filter(|key| !fields.iter().any(|field| field == *key))
.cloned()
.collect();
for key in keys_to_remove {
obj.remove(&key);
}
Ok(value)
}
pub(crate) fn status(args: crate::StatusArgs, format: OutputFormat) -> Result<ExitCode> {
if args.fields.is_some() && format != OutputFormat::Json {
anyhow::bail!("--fields requires --output json");
}
if let Some(fields) = &args.fields {
output::validate_fields(&output::STATUS_FIELD_FILTER_SPEC, fields)?;
}
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::status::run(&repo_path, args.profile.as_deref())?;
match &args.fields {
Some(fields) => {
output::render_report_with_fields(report, fields, &output::STATUS_FIELD_FILTER_SPEC)
}
None if format == OutputFormat::Json => {
let value = serde_json::to_value(&report)?;
let filtered =
output::apply_default_json_contract(value, output::DefaultJsonContract::Status)?;
println!("{}", serde_json::to_string_pretty(&filtered)?);
Ok(ExitCode::SUCCESS)
}
None => output::render_report(format, &report),
}
}
pub(crate) fn session(command: crate::SessionCommand, format: OutputFormat) -> Result<ExitCode> {
match command {
crate::SessionCommand::Open(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = commands::session_open::run(
&repo_path,
args.profile.as_deref(),
args.worktree.as_deref(),
args.branch.as_deref(),
args.from_ref.as_deref(),
args.pod.as_deref(),
)?;
output::render_report(format, &report)
}
}
}
pub(crate) fn session_state(
command: crate::SessionStateCommand,
format: OutputFormat,
) -> Result<ExitCode> {
match command {
crate::SessionStateCommand::Start(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let locality_id = repo::marker::load(&repo_path)
.ok()
.flatten()
.map(|marker| marker.locality_id);
let start_options = state::session::SessionStartOptions {
mode: args.mode.map(crate::SessionModeArg::into_state_mode),
lifecycle: args.lifecycle.into_state_lifecycle(),
owner_kind: args
.owner_kind
.map(crate::AutonomousOwnerKindArg::into_state_owner_kind),
actor_id: args.actor_id,
supervisor_id: args.supervisor_id,
lease_ttl_secs: args.lease_seconds,
};
let report = state::session::start(
&repo_path,
args.base.profile.as_deref(),
locality_id.as_deref(),
start_options,
)?;
output::render_report(format, &report)
}
crate::SessionStateCommand::Heartbeat(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session::heartbeat(
&repo_path,
args.base.profile.as_deref(),
state::session::SessionHeartbeatOptions {
actor_id: args.actor_id,
activity: args.activity,
},
)?;
output::render_report(format, &report)
}
crate::SessionStateCommand::Clear(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let locality_id = repo::marker::load(&repo_path)
.ok()
.flatten()
.map(|marker| marker.locality_id);
let report = state::session::clear(
&repo_path,
args.base.profile.as_deref(),
locality_id.as_deref(),
state::session::SessionClearOptions {
actor_id: args.actor_id,
reason: args.reason,
},
)?;
output::render_report(format, &report)
}
crate::SessionStateCommand::Takeover(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let locality_id = repo::marker::load(&repo_path)
.ok()
.flatten()
.map(|marker| marker.locality_id);
let report = state::session::takeover(
&repo_path,
args.base.profile.as_deref(),
locality_id.as_deref(),
state::session::SessionTakeoverOptions {
actor_id: args.actor_id,
supervisor_id: args.supervisor_id,
reason: args.reason,
},
)?;
output::render_report(format, &report)
}
crate::SessionStateCommand::Gates { command } => session_gates(command, format),
}
}
fn session_gates(command: crate::SessionGateCommand, format: OutputFormat) -> Result<ExitCode> {
match command {
crate::SessionGateCommand::List(args) => {
let repo_path = paths::cli::resolve(&args.path)?;
let report = state::session_gates::list(&repo_path, args.profile.as_deref())?;
output::render_report(format, &report)
}
crate::SessionGateCommand::Replace(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::replace(
&repo_path,
args.base.profile.as_deref(),
args.gates,
args.protected_write.into_state_options(),
)?;
output::render_report(format, &report)
}
crate::SessionGateCommand::Seed(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::seed(
&repo_path,
args.base.profile.as_deref(),
args.from.into_state_source(),
args.protected_write.into_state_options(),
)?;
output::render_report(format, &report)
}
crate::SessionGateCommand::SetStatus(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::set_status(
&repo_path,
args.base.profile.as_deref(),
args.index,
args.status.into_status(),
args.protected_write.into_state_options(),
)?;
output::render_report(format, &report)
}
crate::SessionGateCommand::Advance(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::advance(
&repo_path,
args.base.profile.as_deref(),
args.protected_write.into_state_options(),
)?;
output::render_report(format, &report)
}
crate::SessionGateCommand::Clear(args) => {
let repo_path = paths::cli::resolve(&args.base.path)?;
let report = state::session_gates::clear(
&repo_path,
args.base.profile.as_deref(),
args.protected_write.into_state_options(),
)?;
output::render_report(format, &report)
}
}
}