use clap::Parser;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use ccswarm::cli::{Cli, CliRunner};
#[tokio::main]
async fn main() {
let exit_code = run_main().await;
shutdown_otel();
if exit_code != 0 {
std::process::exit(exit_code);
}
}
async fn run_main() -> i32 {
let args: Vec<String> = std::env::args().collect();
let is_direct_task = args.len() >= 2
&& !args[1].starts_with('-')
&& !is_known_subcommand(&args[1])
&& !looks_like_subcommand(&args[1]);
if args.len() == 1 {
init_logging(false, "text");
if let Err(e) = run_interactive().await {
display_error(&e, false);
return 1;
}
return 0;
}
if is_direct_task {
init_logging(false, "text");
let task = args[1..].join(" ");
if let Err(e) = run_direct_task(&task).await {
display_error(&e, false);
return 1;
}
return 0;
}
let cli = Cli::parse();
init_logging(cli.verbose, &cli.log_format);
if let Err(e) = run_cli(&cli).await {
display_error(&e, cli.verbose);
return 1;
}
0
}
fn is_known_subcommand(arg: &str) -> bool {
matches!(
arg,
"doctor"
| "help"
| "init"
| "task"
| "agents"
| "agent-gen"
| "worktree"
| "logs"
| "config"
| "interactive"
| "pipeline"
| "health"
| "quickstart"
| "flow"
| "repertoire"
| "lab"
| "harness"
| "approve"
| "session"
| "run"
| "facets"
| "queue"
| "auto"
| "undo"
| "replay"
| "cost"
| "tail"
| "scaffold"
)
}
fn looks_like_subcommand(arg: &str) -> bool {
if arg.contains(' ') {
return false;
}
!arg.is_empty()
&& arg
.chars()
.all(|c| c.is_ascii_lowercase() || c == '-' || c == '_')
}
fn init_logging(verbose: bool, format: &str) {
let log_level = if verbose {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
let filter_layer =
tracing_subscriber::EnvFilter::from_default_env().add_directive(log_level.into());
match format {
"ndjson" | "json" => {
let json_layer = tracing_subscriber::fmt::layer()
.json()
.with_writer(std::io::stderr)
.with_target(true)
.with_thread_ids(false)
.with_file(false)
.with_line_number(false);
tracing_subscriber::registry()
.with(filter_layer)
.with(json_layer)
.with(otel_layer())
.init();
}
_ => {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_target(false)
.compact();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(otel_layer())
.init();
}
}
}
#[cfg(feature = "otel")]
static OTEL_PROVIDER: std::sync::OnceLock<opentelemetry_sdk::trace::SdkTracerProvider> =
std::sync::OnceLock::new();
#[cfg(feature = "otel")]
fn otel_layer<S>() -> Option<impl tracing_subscriber::Layer<S>>
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
use opentelemetry::trace::TracerProvider as _;
if std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_err() {
return None;
}
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.build()
.map_err(|e| eprintln!("otel: failed to build OTLP exporter: {e}"))
.ok()?;
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.build();
let tracer = provider.tracer("ccswarm");
opentelemetry::global::set_tracer_provider(provider.clone());
let _ = OTEL_PROVIDER.set(provider);
Some(tracing_opentelemetry::layer().with_tracer(tracer))
}
#[cfg(feature = "otel")]
fn shutdown_otel() {
if let Some(provider) = OTEL_PROVIDER.get()
&& let Err(e) = provider.shutdown()
{
eprintln!("otel: failed to flush spans on shutdown: {e}");
}
}
#[cfg(not(feature = "otel"))]
fn otel_layer() -> Option<tracing_subscriber::layer::Identity> {
None
}
#[cfg(not(feature = "otel"))]
fn shutdown_otel() {}
async fn run_interactive() -> anyhow::Result<()> {
use colored::Colorize;
eprintln!("{}", "ccswarm".bright_cyan().bold());
eprintln!();
eprint!("What do you want to build? > ");
let _ = std::io::Write::flush(&mut std::io::stderr());
let mut task = String::new();
std::io::stdin().read_line(&mut task)?;
let task = task.trim();
if task.is_empty() {
eprintln!("No task specified. Exiting.");
return Ok(());
}
run_direct_task(task).await
}
async fn run_direct_task(task: &str) -> anyhow::Result<()> {
use ccswarm::cli::CliRunner;
use std::path::PathBuf;
let repo = PathBuf::from(".");
let is_new_project = !repo.join("package.json").exists()
&& !repo.join("Cargo.toml").exists()
&& !repo.join("go.mod").exists()
&& !repo.join("pyproject.toml").exists()
&& !repo.join("index.html").exists();
if is_new_project {
eprintln!(
"{}",
colored::Colorize::bright_cyan(colored::Colorize::bold("ccswarm"))
);
eprintln!();
if !repo.join(".git").exists() {
let _ = tokio::process::Command::new("git")
.arg("init")
.output()
.await;
let _ = tokio::process::Command::new("git")
.args(["config", "user.email", "ccswarm@local"])
.output()
.await;
let _ = tokio::process::Command::new("git")
.args(["config", "user.name", "ccswarm"])
.output()
.await;
}
tokio::fs::write("package.json", "{}\n").await?;
let _ = tokio::fs::create_dir_all("public").await;
let _ = tokio::fs::create_dir_all("e2e").await;
let _ = tokio::process::Command::new("git")
.args(["add", "-A"])
.output()
.await;
let _ = tokio::process::Command::new("git")
.args(["commit", "-m", "init"])
.output()
.await;
eprintln!(
" {} Project initialized",
colored::Colorize::bright_green("\u{2713}")
);
}
let cli = Cli::parse_from([
"ccswarm",
"--repo",
".",
"pipeline",
"--task",
task,
"--flow",
"default",
"--timeout",
"600",
]);
let runner = CliRunner::new(&cli).await?;
runner
.handle_pipeline(
task, "default", "text", 600, false, None, false, None, None, None, false, false, None, )
.await
}
async fn run_cli(cli: &Cli) -> anyhow::Result<()> {
let runner = CliRunner::new(cli).await?;
runner.run(&cli.command).await
}
fn display_error(error: &anyhow::Error, verbose: bool) {
if let Some(ccswarm_err) = error.downcast_ref::<ccswarm::error::CCSwarmError>() {
ccswarm_err.to_user_error().display();
if verbose {
let mut source = std::error::Error::source(ccswarm_err);
if source.is_some() {
eprintln!(" {}:", "Caused by".dimmed());
}
while let Some(cause) = source {
eprintln!(" - {}", cause);
source = std::error::Error::source(cause);
}
}
} else {
use colored::Colorize;
eprintln!("\n{} {}", "Error:".red().bold(), error);
if verbose {
for cause in error.chain().skip(1) {
eprintln!(" {} {}", "Caused by:".yellow(), cause);
}
}
eprintln!(
"\n {} {}",
"Tip:".cyan().bold(),
"Run with '--verbose' for more details, or 'ccswarm doctor' for diagnostics.".dimmed()
);
}
}
use colored::Colorize;