use super::CommandExecutor;
use crate::client::http_client::{
ManagedAdoptRequest, ManagedDecommissionOutcome, ManagedSessionSummary, ManagedSpawnRequest,
};
use crate::client::resolver::resolve_target;
use crate::client::result::{CommandResult, ManagedSessionView, ProjectFleetView};
impl CommandExecutor {
async fn resolve_managed(&self, target: &str) -> Result<ManagedSessionSummary, CommandResult> {
let sessions = self
.client()
.list_managed_sessions()
.await
.map_err(|e| CommandResult::Error(format!("daemon unreachable: {e}")))?;
match resolve_target(&sessions, target) {
Some(found) => Ok(found.clone()),
None => Err(CommandResult::Error(format!(
"managed session {target} not found"
))),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn managed_new(
&self,
repo_url: String,
git_ref: String,
task: String,
name_hint: Option<String>,
runtime: Option<String>,
inject_task: Option<bool>,
deliverable_id: Option<String>,
force_new: bool,
) -> CommandResult {
if let Err(e) = crate::core::local_repo_url::require_local_repo_url(&repo_url) {
return CommandResult::Error(format!("spawn failed: {e}"));
}
let req = ManagedSpawnRequest {
repo_url,
git_ref,
task,
name_hint,
runtime,
inject_task,
deliverable_id,
force_new,
};
match self.client().spawn_managed_session(&req).await {
Ok(resp) => CommandResult::ManagedSpawned {
id: resp.id,
name: resp.name,
state: resp.state,
runtime: resp.runtime,
attach_cmd: resp.attach_cmd,
},
Err(e) => CommandResult::Error(format!("spawn failed: {e}")),
}
}
pub(super) async fn managed_adopt(
&self,
tmux_name: String,
cwd: String,
task: Option<String>,
runtime: Option<String>,
) -> CommandResult {
if tmux_name.trim().is_empty() {
return CommandResult::Error("adopt: tmux_name is required".to_string());
}
if cwd.trim().is_empty() {
return CommandResult::Error("adopt: cwd is required".to_string());
}
let req = ManagedAdoptRequest {
tmux_name,
cwd,
task,
runtime,
};
match self.client().adopt_managed_session(&req).await {
Ok(resp) => CommandResult::ManagedAdopted {
id: resp.id,
name: resp.name,
state: resp.state,
cwd: resp.cwd,
runtime: resp.runtime,
attach_cmd: resp.attach_cmd,
},
Err(e) => CommandResult::Error(format!("adopt failed: {e}")),
}
}
pub(super) async fn managed_list(&self) -> CommandResult {
match self.client().list_managed_sessions().await {
Ok(sessions) => CommandResult::ManagedSessions(
sessions.into_iter().map(ManagedSessionView::from).collect(),
),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn managed_fleet(&self) -> CommandResult {
match self.client().fleet_managed_sessions().await {
Ok(groups) => {
let fleet: Vec<ProjectFleetView> = groups
.into_iter()
.map(|g| ProjectFleetView {
project_name: g.project_name,
repo_url: g.repo_url,
sessions: g
.sessions
.into_iter()
.map(ManagedSessionView::from)
.collect(),
})
.collect();
CommandResult::ManagedFleet(fleet)
}
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn managed_get(&self, target: &str) -> CommandResult {
let id = if is_session_id(target) {
target.to_string()
} else {
match self.resolve_managed(target).await {
Ok(s) => s.id,
Err(err) => return err,
}
};
match self.client().get_managed_session(&id).await {
Ok(summary) => CommandResult::ManagedSession(ManagedSessionView::from(summary)),
Err(e) => CommandResult::Error(format!("get failed: {e}")),
}
}
pub(super) async fn managed_send(&self, target: &str, text: &str) -> CommandResult {
if text.trim().is_empty() {
return CommandResult::Error("send: text is required".to_string());
}
let id = match self.resolve_managed(target).await {
Ok(s) => s.id,
Err(err) => return err,
};
match self.client().send_to_managed_session(&id, text).await {
Ok(resp) => CommandResult::ManagedSent {
id,
tmux_name: resp.tmux_name,
},
Err(e) => CommandResult::Error(format!("send failed: {e}")),
}
}
pub(super) async fn managed_answer(&self, target: &str, answer: &str) -> CommandResult {
if answer.trim().is_empty() {
return CommandResult::Error("answer: text is required".to_string());
}
let id = match self.resolve_managed(target).await {
Ok(s) => s.id,
Err(err) => return err,
};
match self.client().answer_managed_session(&id, answer).await {
Ok(resp) => CommandResult::ManagedAnswered {
id,
answer: answer.to_string(),
tmux_name: resp.tmux_name,
},
Err(e) => CommandResult::Error(format!("answer failed: {e}")),
}
}
pub(super) async fn managed_activity(&self, target: &str) -> CommandResult {
let id = match self.resolve_managed(target).await {
Ok(s) => s.id,
Err(err) => return err,
};
match self.client().managed_session_activity(&id).await {
Ok(act) => CommandResult::ManagedActivity {
id,
state: act.state,
summary: act.summary,
raw_pane: act.raw_pane,
runtime_active: act.runtime_active,
pending_decision: act.pending_decision,
},
Err(e) => CommandResult::Error(format!("activity failed: {e}")),
}
}
pub(super) async fn managed_attach_cmd(&self, target: &str) -> CommandResult {
let id = match self.resolve_managed(target).await {
Ok(s) => s.id,
Err(err) => return err,
};
match self.client().managed_session_attach_cmd(&id).await {
Ok(resp) => CommandResult::ManagedAttachCmd {
id,
attach_cmd: resp.attach_cmd,
},
Err(e) => CommandResult::Error(format!("attach-cmd failed: {e}")),
}
}
pub(super) async fn managed_runtime_stop(&self, target: &str) -> CommandResult {
let id = match self.resolve_managed(target).await {
Ok(s) => s.id,
Err(err) => return err,
};
match self.client().runtime_stop_managed_session(&id).await {
Ok(summary) => lifecycle(summary, "stopped", None),
Err(e) => CommandResult::Error(format!("runtime-stop failed: {e}")),
}
}
pub(super) async fn managed_resume(&self, target: &str) -> CommandResult {
let id = match self.resolve_managed(target).await {
Ok(s) => s.id,
Err(err) => return err,
};
match self.client().resume_managed_session(&id).await {
Ok(summary) => lifecycle(summary, "resumed", None),
Err(e) => CommandResult::Error(format!("resume failed: {e}")),
}
}
pub async fn decommission_managed_id(
&self,
id: &str,
) -> anyhow::Result<ManagedDecommissionOutcome> {
self.decommission_managed_id_with(id, false).await
}
pub async fn decommission_managed_target(
&self,
target: &str,
force: bool,
) -> anyhow::Result<ManagedDecommissionOutcome> {
let id = match self.resolve_managed(target).await {
Ok(s) => s.id,
Err(CommandResult::Error(msg)) => anyhow::bail!("{msg}"),
Err(other) => anyhow::bail!("could not resolve {target}: {other:?}"),
};
self.decommission_managed_id_with(&id, force).await
}
pub async fn decommission_managed_id_with(
&self,
id: &str,
force: bool,
) -> anyhow::Result<ManagedDecommissionOutcome> {
let outcome = self
.client()
.decommission_managed_session_with(id, force)
.await?;
if !outcome.unrecognized.is_empty() {
tracing::debug!(
fields = ?outcome.unrecognized.keys().collect::<Vec<_>>(),
"decommission response carried fields this client does not model"
);
}
if let Some(dir) = worktree_prune_dir(&outcome) {
let out = std::process::Command::new("git")
.args(["-C", &dir.to_string_lossy(), "worktree", "prune"])
.output();
if let Err(e) = out {
tracing::debug!(dir = %dir.display(), "git worktree prune failed: {e}");
}
}
Ok(outcome)
}
pub(super) async fn managed_decommission(&self, target: &str) -> CommandResult {
let id = match self.resolve_managed(target).await {
Ok(s) => s.id,
Err(err) => return err,
};
match self.decommission_managed_id(&id).await {
Ok(outcome) => lifecycle(outcome.summary, "decommissioned", outcome.workspace_removed),
Err(e) => CommandResult::Error(format!("decommission failed: {e}")),
}
}
}
pub(super) fn worktree_prune_dir(
outcome: &ManagedDecommissionOutcome,
) -> Option<std::path::PathBuf> {
if outcome.workspace_removed != Some(true) {
return None;
}
let was = std::path::Path::new(outcome.workspace_path_was.as_deref()?);
Some(was.parent().unwrap_or(was).to_path_buf())
}
fn lifecycle(
summary: ManagedSessionSummary,
action: &str,
workspace_removed: Option<bool>,
) -> CommandResult {
CommandResult::ManagedLifecycle {
id: summary.id,
name: summary.name,
state: summary.state,
action: action.to_string(),
workspace_removed,
}
}
pub(super) fn is_session_id(target: &str) -> bool {
uuid::Uuid::parse_str(target).is_ok()
}