use crate::*;
use carryctx::adapter::unit_of_work::UnitOfWork;
use carryctx::application;
use carryctx::application::runtime::InvocationContext;
use carryctx::domain::dependency::DependencyKind;
use carryctx::domain::task::TransitionAction;
use carryctx::error::{CarryCtxError, ExitCode};
use clap::Parser;
#[derive(Parser, Debug)]
pub enum TaskCommand {
Create {
#[arg(long)]
title: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
priority: Option<String>,
#[arg(long)]
owner: Option<String>,
#[arg(long)]
status: Option<String>,
#[arg(long)]
depends_on: Vec<String>,
},
List {
#[arg(long)]
status: Option<String>,
#[arg(long)]
owner: Option<String>,
#[arg(long)]
mine: bool,
},
Show { task_ref: String },
Edit {
task_ref: String,
#[arg(long)]
title: Option<String>,
#[arg(long)]
priority: Option<String>,
},
Claim { task_ref: String },
Release { task_ref: String },
Start { task_ref: String },
Block {
task_ref: String,
#[arg(long)]
reason: String,
},
Unblock { task_ref: String },
Review { task_ref: String },
Complete { task_ref: String },
Cancel {
task_ref: String,
#[arg(long)]
reason: String,
},
Reopen { task_ref: String },
Depend {
task_ref: String,
#[arg(long)]
on: String,
#[arg(long)]
kind: Option<String>,
},
Undepend {
task_ref: String,
#[arg(long)]
on: String,
},
}
#[derive(Parser, Debug)]
pub struct TaskArgs {
#[command(subcommand)]
pub command: TaskCommand,
}
pub fn handle_task(
args: &TaskArgs,
ctx: &InvocationContext,
is_json: bool,
) -> Result<ExitCode, ExitCode> {
if let Some(result) = check_dry_run(ctx, &format!("task {:?}", args.command)) {
return result;
}
let mut runtime = try_open_runtime(ctx)?;
let project_id = &runtime.config.project.id;
let conn = runtime.database.connection_mut();
match &args.command {
TaskCommand::Create {
title,
description: _,
priority,
owner,
status,
depends_on,
} => {
let parsed_status = status
.as_deref()
.map(parse_task_status)
.transpose()
.map_err(|e| e.exit_code)?;
let parsed_priority = priority
.as_deref()
.map(parse_task_priority)
.transpose()
.map_err(|e| e.exit_code)?;
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::create_task(
project_id,
title,
Some(&runtime.config.project.task_prefix),
parsed_status,
parsed_priority,
owner.as_deref(),
depends_on,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|t| uow.commit().map(|_| t));
render_and_print("task.create", committed, is_json, ctx.quiet)
}
TaskCommand::List {
status,
owner,
mine,
} => {
let parsed_status = status
.as_deref()
.map(parse_task_status)
.transpose()
.map_err(|e| e.exit_code)?;
let filter = TaskFilter {
project_id: project_id.to_string(),
status: parsed_status,
owner_agent_id: owner.clone(),
ready: false,
blocked: false,
mine: if *mine { ctx.agent.clone() } else { None },
};
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::list_tasks(project_id, &filter, &uow);
if ctx.format == carryctx::application::runtime::OutputFormat::Markdown {
let md = match &result {
Ok(tasks) => {
let mut out = String::from("# Tasks\n\n");
out.push_str("| ID | Title | Status | Priority |\n");
out.push_str("|---|---|---|---|\n");
for t in tasks {
out.push_str(&format!(
"| {} | {} | {:?} | {:?} |\n",
t.display_id, t.title, t.status, t.priority
));
}
out
}
Err(e) => format!("Error: {e}"),
};
if !ctx.quiet {
print!("{md}");
}
return Ok(ExitCode::Success);
}
render_and_print("task.list", result, is_json, ctx.quiet)
}
TaskCommand::Show { task_ref } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::show_task(project_id, task_ref, &uow);
render_and_print("task.show", result, is_json, ctx.quiet)
}
TaskCommand::Edit {
task_ref,
title,
priority,
} => {
let parsed_priority = priority
.as_deref()
.map(parse_task_priority)
.transpose()
.map_err(|e| e.exit_code)?;
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::edit_task(
project_id,
task_ref,
title.as_deref(),
parsed_priority,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|t| uow.commit().map(|_| t));
render_and_print("task.edit", committed, is_json, ctx.quiet)
}
TaskCommand::Claim { task_ref } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let resolver = application::runtime::CurrentEntityResolver::new(project_id, &uow);
let agent = match resolver.resolve_agent(
ctx.agent.as_deref(),
None,
None,
runtime.config.agent.default_name.as_deref(),
runtime.config.agent.default_name.as_deref(),
) {
Ok(a) => a,
Err(e) => {
return render_and_print::<serde_json::Value>(
"task.claim",
Err(e),
is_json,
ctx.quiet,
);
}
};
let result = application::task::claim_task(project_id, task_ref, &agent.id, &uow);
let committed = result.and_then(|t| uow.commit().map(|_| t));
render_and_print("task.claim", committed, is_json, ctx.quiet)
}
TaskCommand::Release { task_ref } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::transition_task(
project_id,
task_ref,
TransitionAction::Release,
None,
runtime.config.task.strict_completion,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|(t, _w)| uow.commit().map(|_| t));
render_and_print("task.release", committed, is_json, ctx.quiet)
}
TaskCommand::Start { task_ref } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::transition_task(
project_id,
task_ref,
TransitionAction::Start,
None,
runtime.config.task.strict_completion,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|(t, _w)| uow.commit().map(|_| t));
render_and_print("task.start", committed, is_json, ctx.quiet)
}
TaskCommand::Block { task_ref, reason } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::transition_task(
project_id,
task_ref,
TransitionAction::Block,
Some(reason),
runtime.config.task.strict_completion,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|(t, _w)| uow.commit().map(|_| t));
render_and_print("task.block", committed, is_json, ctx.quiet)
}
TaskCommand::Unblock { task_ref } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::transition_task(
project_id,
task_ref,
TransitionAction::Unblock,
None,
runtime.config.task.strict_completion,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|(t, _w)| uow.commit().map(|_| t));
render_and_print("task.unblock", committed, is_json, ctx.quiet)
}
TaskCommand::Review { task_ref } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::transition_task(
project_id,
task_ref,
TransitionAction::Review,
None,
runtime.config.task.strict_completion,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|(t, _w)| uow.commit().map(|_| t));
render_and_print("task.review", committed, is_json, ctx.quiet)
}
TaskCommand::Complete { task_ref } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::transition_task(
project_id,
task_ref,
TransitionAction::Complete,
None,
runtime.config.task.strict_completion,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|(t, _w)| uow.commit().map(|_| t));
render_and_print("task.complete", committed, is_json, ctx.quiet)
}
TaskCommand::Cancel { task_ref, reason } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::transition_task(
project_id,
task_ref,
TransitionAction::Cancel,
Some(reason),
runtime.config.task.strict_completion,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|(t, _w)| uow.commit().map(|_| t));
render_and_print("task.cancel", committed, is_json, ctx.quiet)
}
TaskCommand::Reopen { task_ref } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::transition_task(
project_id,
task_ref,
TransitionAction::Reopen,
None,
runtime.config.task.strict_completion,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|(t, _w)| uow.commit().map(|_| t));
render_and_print("task.reopen", committed, is_json, ctx.quiet)
}
TaskCommand::Depend { task_ref, on, kind } => {
let dep_kind = kind
.as_deref()
.map(parse_dependency_kind)
.transpose()
.map_err(|e| e.exit_code)?
.unwrap_or(DependencyKind::Strong);
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::add_dependency(
project_id,
task_ref,
on,
dep_kind,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|t| uow.commit().map(|_| t));
render_and_print("task.depend", committed, is_json, ctx.quiet)
}
TaskCommand::Undepend { task_ref, on } => {
let tx = conn
.transaction()
.map_err(|e| CarryCtxError::database_error(format!("{e}")).exit_code)?;
let uow = UnitOfWork::new(tx);
let result = application::task::remove_dependency(
project_id,
task_ref,
on,
ctx.agent.as_deref(),
&uow,
);
let committed = result.and_then(|t| uow.commit().map(|_| t));
render_and_print("task.undepend", committed, is_json, ctx.quiet)
}
}
}