use std::path::Path;
use clap::Parser;
use carryctx::adapter::config::ConfigLoader;
use carryctx::adapter::git::GitCli;
use carryctx::adapter::sqlite::ProjectDatabase;
use carryctx::adapter::sqlite_repos::*;
use carryctx::adapter::xdg::XdgPaths;
use carryctx::application::runtime::{InvocationContext, OutputFormat, ProjectRuntime};
use carryctx::domain::dependency::DependencyKind;
use carryctx::domain::task::{TaskPriority, TaskStatus};
use carryctx::error::{CarryCtxError, ExitCode};
use carryctx::output;
use carryctx::repository::*;
#[derive(Parser, Debug)]
#[command(name = "carryctx", version = env!("CARGO_PKG_VERSION"), about, long_about = None)]
pub struct Cli {
#[arg(long, global = true)]
pub project: Option<String>,
#[arg(long, global = true)]
pub config: Option<String>,
#[arg(long, global = true)]
pub profile: Option<String>,
#[arg(long, global = true, env = "CARRYCTX_AGENT")]
pub agent: Option<String>,
#[arg(long, global = true, env = "CARRYCTX_SESSION")]
pub session: Option<String>,
#[arg(long, global = true, env = "CARRYCTX_TASK")]
pub task: Option<String>,
#[arg(long, global = true, value_parser = ["text", "json", "markdown"])]
pub format: Option<String>,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true)]
pub no_color: bool,
#[arg(long, global = true)]
pub quiet: bool,
#[arg(long, global = true, conflicts_with = "quiet")]
pub verbose: bool,
#[arg(long, global = true)]
pub yes: bool,
#[arg(long, global = true)]
pub dry_run: bool,
#[arg(long, global = true)]
pub non_interactive: bool,
#[arg(long, global = true, value_parser = ["error", "warn"])]
pub config_compat: Option<String>,
#[command(subcommand)]
pub command: Option<Commands>,
}
pub mod commands;
pub use commands::*;
#[derive(Parser, Debug)]
pub enum Commands {
Init(InitArgs),
Status(StatusArgs),
Resume(ResumeArgs),
Context(ContextArgs),
Checkpoint(CheckpointArgs),
Doctor(DoctorArgs),
Agent(AgentArgs),
Task(TaskArgs),
Session(SessionArgs),
Progress(ProgressArgs),
Mcp(McpArgs),
Preset(PresetArgs),
Worktree(WorktreeArgs),
Event(EventArgs),
Config(ConfigArgs),
Project(ProjectArgs),
Decision(DecisionArgs),
Handoff(HandoffArgs),
Skill(SkillArgs),
Completions(CompletionsArgs),
Hooks(HooksArgs),
Sync(SyncArgs),
Stats(StatsArgs),
Graph(GraphArgs),
}
fn main() {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(true)
.try_init();
let cli = Cli::parse();
let result = run(cli);
match result {
Ok(exit_code) => std::process::exit(exit_code as i32),
Err(code) => std::process::exit(code as i32),
}
}
fn run(cli: Cli) -> Result<ExitCode, ExitCode> {
tracing::debug!(command = ?cli.command, "dispatching command");
let mut ctx = build_invocation_context(&cli)?;
let is_json = matches!(ctx.format, OutputFormat::Json);
if let Ok(runtime) = try_open_runtime(&ctx) {
if let Some(agent_ref) = &ctx.agent {
if !agent_ref.trim().is_empty() {
if let Ok(ulid) = resolve_agent_id(
&runtime.config.project.id,
agent_ref,
runtime.database.connection(),
) {
ctx.agent = Some(ulid);
}
}
}
}
match &cli.command {
Some(Commands::Init(args)) => handle_init(args, &ctx),
Some(Commands::Status(args)) => handle_status(args, &ctx, is_json),
Some(Commands::Resume(args)) => handle_resume(args, &ctx, is_json),
Some(Commands::Context(args)) => handle_context(args, &ctx, is_json),
Some(Commands::Checkpoint(args)) => handle_checkpoint(args, &ctx, is_json),
Some(Commands::Doctor(args)) => handle_doctor(args, &ctx, is_json),
Some(Commands::Agent(args)) => handle_agent(args, &ctx, is_json),
Some(Commands::Task(args)) => handle_task(args, &ctx, is_json),
Some(Commands::Session(args)) => handle_session(args, &ctx, is_json),
Some(Commands::Progress(args)) => handle_progress(args, &ctx, is_json),
Some(Commands::Mcp(args)) => handle_mcp(args, &ctx),
Some(Commands::Preset(args)) => handle_preset(args, &ctx, is_json),
Some(Commands::Worktree(args)) => handle_worktree(args, &ctx, is_json),
Some(Commands::Event(args)) => handle_event(args, &ctx, is_json),
Some(Commands::Config(args)) => handle_config(args, &ctx, is_json),
Some(Commands::Project(args)) => handle_project(args, &ctx, is_json),
Some(Commands::Decision(args)) => handle_decision(args, &ctx, is_json),
Some(Commands::Handoff(args)) => handle_handoff(args, &ctx, is_json),
Some(Commands::Skill(args)) => handle_skill(args, &ctx, is_json),
Some(Commands::Completions(args)) => handle_completions(args),
Some(Commands::Hooks(args)) => handle_hooks(args, &ctx, is_json),
Some(Commands::Sync(args)) => handle_sync(args, &ctx, is_json),
Some(Commands::Stats(args)) => handle_stats(args, &ctx, is_json),
Some(Commands::Graph(args)) => handle_graph(args, &ctx, is_json),
None => {
if !ctx.quiet {
println!(
"CarryCtx v{} - Persistent project context for coding agents",
env!("CARGO_PKG_VERSION")
);
println!("Use --help for usage information.");
}
Ok(ExitCode::Success)
}
}
}
pub fn build_invocation_context(cli: &Cli) -> Result<InvocationContext, ExitCode> {
let cwd = std::env::current_dir().map_err(|e| {
eprintln!("Failed to get current directory: {e}");
ExitCode::General
})?;
InvocationContext::new(
cwd,
cli.project.clone(),
cli.config.clone(),
cli.profile.clone(),
cli.agent.clone(),
cli.session.clone(),
cli.task.clone(),
cli.format.clone(),
cli.json,
cli.no_color,
cli.quiet,
cli.verbose,
cli.dry_run,
cli.yes,
!cli.non_interactive,
)
.map_err(|e| e.exit_code)
}
pub fn resolve_work_dir(ctx: &InvocationContext) -> &Path {
ctx.project.as_deref().map(Path::new).unwrap_or(&ctx.cwd)
}
pub fn try_open_runtime(ctx: &InvocationContext) -> Result<ProjectRuntime, ExitCode> {
let xdg = XdgPaths::new();
let cfg_loader = ConfigLoader::new(xdg.clone());
let work_dir = resolve_work_dir(ctx);
let mut config = cfg_loader.load(Some(work_dir)).map_err(|e| e.exit_code)?;
let git = GitCli::new();
let git_project = git.discover(work_dir).map_err(|e| e.exit_code)?;
let db_path = xdg.project_db(&git_project.git_common_dir);
let database = ProjectDatabase::open(&db_path).map_err(|e| e.exit_code)?;
if let Ok(mut stmt) = database
.connection()
.prepare("SELECT id, name, task_prefix FROM projects LIMIT 1")
{
if let Ok(row) = stmt.query_row([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
}) {
config.project.id = row.0;
if !row.1.is_empty() {
config.project.name = row.1;
}
if !row.2.is_empty() {
config.project.task_prefix = row.2;
}
}
}
Ok(ProjectRuntime {
git_project,
database,
config,
xdg,
db_path,
})
}
pub fn render_and_print<T: serde::Serialize>(
command: &str,
result: Result<T, CarryCtxError>,
is_json: bool,
quiet: bool,
) -> Result<ExitCode, ExitCode> {
let (output, sink, exit_code) = match &result {
Ok(data) => output::render_json(command, Ok(data), is_json),
Err(err) => output::render_json::<serde_json::Value>(command, Err(err), is_json),
};
if !quiet || matches!(sink, output::OutputSink::Stderr) {
match sink {
output::OutputSink::Stdout => println!("{output}"),
output::OutputSink::Stderr => eprintln!("{output}"),
}
}
match exit_code {
ExitCode::Success => Ok(ExitCode::Success),
other => Err(other),
}
}
pub fn not_implemented(command: &str) -> ExitCode {
eprintln!("{command}: not yet implemented");
ExitCode::Unsupported
}
pub fn check_dry_run(
ctx: &InvocationContext,
description: &str,
) -> Option<Result<ExitCode, ExitCode>> {
if ctx.dry_run {
eprintln!("[dry-run] Would {description}");
Some(Ok(ExitCode::Success))
} else {
None
}
}
pub fn resolve_agent_id(
project_id: &str,
agent_ref: &str,
conn: &rusqlite::Connection,
) -> Result<String, CarryCtxError> {
let repo = SqliteAgentRepository::new(conn);
if let Some(agent) = repo.find_by_name(project_id, agent_ref)? {
return Ok(agent.id);
}
if let Some(agent) = repo.find_by_id(project_id, agent_ref)? {
return Ok(agent.id);
}
Err(CarryCtxError::resource_not_found(format!(
"Agent '{agent_ref}' not found."
)))
}
pub fn resolve_task_id(
project_id: &str,
task_ref: &str,
conn: &rusqlite::Connection,
) -> Result<String, CarryCtxError> {
let repo = SqliteTaskRepository::new(conn);
if let Some(task) = repo.find_by_display_id(project_id, task_ref)? {
return Ok(task.id);
}
if let Some(task) = repo.find_by_id(project_id, task_ref)? {
return Ok(task.id);
}
Err(CarryCtxError::resource_not_found(format!(
"Task '{task_ref}' not found."
)))
}
pub fn parse_task_status(s: &str) -> Result<TaskStatus, CarryCtxError> {
match s {
"planned" => Ok(TaskStatus::Planned),
"ready" => Ok(TaskStatus::Ready),
"in_progress" => Ok(TaskStatus::InProgress),
"blocked" => Ok(TaskStatus::Blocked),
"review" => Ok(TaskStatus::Review),
"completed" => Ok(TaskStatus::Completed),
"cancelled" => Ok(TaskStatus::Cancelled),
other => Err(CarryCtxError::invalid_arguments(format!(
"Unknown status: {other}"
))),
}
}
pub fn parse_task_priority(s: &str) -> Result<TaskPriority, CarryCtxError> {
match s {
"low" => Ok(TaskPriority::Low),
"normal" => Ok(TaskPriority::Normal),
"high" => Ok(TaskPriority::High),
"urgent" => Ok(TaskPriority::Urgent),
other => Err(CarryCtxError::invalid_arguments(format!(
"Unknown priority: {other}"
))),
}
}
pub fn parse_dependency_kind(s: &str) -> Result<DependencyKind, CarryCtxError> {
match s {
"strong" => Ok(DependencyKind::Strong),
"informational" | "info" => Ok(DependencyKind::Informational),
other => Err(CarryCtxError::invalid_arguments(format!(
"Unknown dependency kind: {other}"
))),
}
}