mod cache;
#[cfg(test)]
mod defaults_tests;
mod dispatch;
#[cfg(test)]
mod identity_tests;
mod interactive;
mod maintenance;
mod remote;
mod render;
mod run;
mod workflow;
use dispatch::Context;
use render::{outcome_code, print_json};
mod request;
mod server;
mod session;
use request::RequestCommand;
use server::ServerCommand;
use session::SessionCommand;
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Args, Parser, Subcommand, error::ErrorKind};
use serde_json::json;
use crate::{
config::ProfileKind,
project::selection::{ProfileSelection, Selection, read_selection_file},
types::{RequestId, ReuseKey, RunId},
};
const DEFAULT_RUN_LIST_LIMIT: u32 = 50;
#[derive(Debug, Parser)]
#[command(
name = "artifactize",
// Help names the command, not the file run; on Windows that is artifactize.exe.
bin_name = "artifactize",
version,
about = "The AI-native collaboration layer for one-of-a-kind teammates and their agents."
)]
pub struct Cli {
#[arg(long, global = true, value_name = "PATH")]
pub repo: Option<PathBuf>,
#[arg(long, global = true, value_name = "PATH")]
pub state_dir: Option<PathBuf>,
#[arg(long, global = true)]
pub json: bool,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Tools {
#[command(subcommand)]
command: ToolsCommand,
},
Login {
#[command(subcommand)]
provider: AuthProvider,
},
Logout {
#[command(subcommand)]
provider: AuthProvider,
},
Remote {
#[command(subcommand)]
command: remote::RemoteCommand,
},
Doctor,
Prune {
#[arg(long, value_name = "DURATION", value_parser = crate::store::prune::parse_duration)]
older_than: Option<std::time::Duration>,
#[arg(long)]
dry_run: bool,
},
Models {
#[command(subcommand)]
provider: ModelProvider,
},
#[command(group(clap::ArgGroup::new("required_selection")
.args(["artifact", "eval", "evals", "artifacts", "evals_file", "artifacts_file", "all"])
.required(true)))]
Verify {
#[command(flatten)]
selection: SelectionArgs,
#[command(flatten)]
policy: PolicyArgs,
#[arg(long, value_name = "N", default_value_t = crate::project::DEFAULT_JOBS as u32, value_parser = clap::value_parser!(u32).range(1..))]
jobs: u32,
#[arg(long, value_name = "N")]
max_executions: Option<u64>,
#[arg(long, value_name = "MS", value_parser = clap::value_parser!(u32).range(1..=crate::config::validation::MAX_TIMEOUT_MS as i64))]
timeout_ms: Option<u32>,
#[arg(long, value_name = "KINDS", value_enum, value_delimiter = ',')]
reuse_only: Vec<ProfileKind>,
},
Status {
#[command(flatten)]
selection: SelectionArgs,
#[command(flatten)]
policy: PolicyArgs,
},
#[command(hide = true, disable_help_flag = true)]
Graph {
#[arg(trailing_var_arg = true, allow_hyphen_values = true, hide = true)]
args: Vec<String>,
},
Run {
#[command(subcommand)]
command: RunCommand,
},
Request {
#[command(subcommand)]
command: RequestCommand,
},
Session {
#[command(subcommand)]
command: SessionCommand,
},
Cache {
#[command(subcommand)]
command: CacheCommand,
},
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Monitor {
#[arg(long)]
all: bool,
},
Review {
#[arg(value_name = "REQUEST_ID")]
request: Option<RequestId>,
#[arg(long)]
all: bool,
#[arg(long, value_name = "NAME")]
reviewer: Option<String>,
},
Server {
#[command(subcommand)]
command: ServerCommand,
},
}
#[derive(Debug, Args)]
#[group(multiple = false)]
pub struct SelectionArgs {
artifact: Option<String>,
#[arg(long, value_name = "ID")]
eval: Option<String>,
#[arg(long, value_name = "CSV")]
evals: Option<String>,
#[arg(long, value_name = "CSV")]
artifacts: Option<String>,
#[arg(long, value_name = "PATH")]
evals_file: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
artifacts_file: Option<PathBuf>,
#[arg(long)]
all: bool,
}
impl SelectionArgs {
fn resolve(self) -> Result<Selection, String> {
if let Some(artifact_id) = self.artifact {
Ok(Selection::Artifact { artifact_id })
} else if let Some(eval_id) = self.eval {
Ok(Selection::Eval { eval_id })
} else if let Some(ids) = self.evals {
Ok(Selection::Evals {
eval_ids: ids.split(',').map(str::to_owned).collect(),
})
} else if let Some(ids) = self.artifacts {
Ok(Selection::Artifacts {
artifact_ids: ids.split(',').map(str::to_owned).collect(),
})
} else if let Some(path) = self.evals_file {
Ok(Selection::Evals {
eval_ids: read_selection_file(&path)?,
})
} else if let Some(path) = self.artifacts_file {
Ok(Selection::Artifacts {
artifact_ids: read_selection_file(&path)?,
})
} else {
Ok(Selection::All)
}
}
}
#[derive(Debug, Args)]
pub struct PolicyArgs {
#[arg(long, value_name = "NAME")]
profile: Option<String>,
#[arg(long)]
recursive: bool,
#[arg(long)]
force: bool,
#[arg(long)]
ignore_gates: bool,
#[arg(long, value_name = "N", value_parser = clap::value_parser!(u32).range(1..))]
fingerprint_jobs: Option<u32>,
}
impl PolicyArgs {
fn options(self) -> crate::project::VerifyOptions {
crate::project::VerifyOptions {
profile: self.profile.map(ProfileSelection::Named),
recursive: self.recursive,
force: self.force,
ignore_gates: self.ignore_gates.then_some(true),
fingerprint_jobs: self.fingerprint_jobs.map(|jobs| jobs as usize),
..Default::default()
}
}
}
#[derive(Debug, Subcommand)]
pub enum AuthProvider {
Codex,
}
#[derive(Debug, Subcommand)]
pub enum ModelProvider {
Openai,
Anthropic,
Codex,
}
impl ModelProvider {
fn backend(self) -> crate::config::Backend {
match self {
Self::Openai => crate::config::Backend::Openai,
Self::Anthropic => crate::config::Backend::Anthropic,
Self::Codex => crate::config::Backend::Codex,
}
}
}
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
Check,
Graph {
artifact: Option<String>,
},
}
#[derive(Debug, Subcommand)]
pub enum RunCommand {
List {
#[arg(long, conflicts_with = "all")]
repo_only: bool,
#[arg(long)]
all: bool,
#[arg(long, value_name = "N", default_value_t = DEFAULT_RUN_LIST_LIMIT)]
limit: u32,
#[arg(long, value_name = "N", default_value = "0")]
offset: u32,
},
Show {
run_id: RunId,
#[arg(long)]
wait: bool,
#[arg(long, value_name = "MS", requires = "wait", value_parser = clap::value_parser!(u32).range(1..=crate::config::validation::MAX_TIMEOUT_MS as i64))]
timeout_ms: Option<u32>,
},
}
#[derive(Debug, Subcommand)]
pub enum CacheCommand {
List {
#[arg(long)]
history: bool,
},
Show {
key: ReuseKey,
#[arg(long)]
history: bool,
},
Rm { key: ReuseKey },
}
#[derive(Debug, Subcommand)]
pub enum ToolsCommand {
Check(crate::diagnostics::ToolCheckOptions),
}
fn cancellation_listener() -> Result<
(
tokio_util::sync::CancellationToken,
tokio::task::JoinHandle<()>,
),
String,
> {
let cancellation = tokio_util::sync::CancellationToken::new();
let stop = crate::platform::stop_requested().map_err(|e| e.to_string())?;
let token = cancellation.clone();
let listener = tokio::spawn(async move {
stop.await;
token.cancel();
});
Ok((cancellation, listener))
}
fn failure(message: &str, json: bool) -> ExitCode {
if json {
let _ = writeln!(io::stdout().lock(), "{}", json!({ "error": message }));
} else {
let _ = writeln!(io::stderr().lock(), "{message}");
}
ExitCode::from(2)
}
fn repeated_global(options: &[&str]) -> Option<&'static str> {
["--repo", "--state-dir", "--json"]
.into_iter()
.find(|name| {
options
.iter()
.filter(|arg| {
arg.strip_prefix(name)
.is_some_and(|rest| rest.is_empty() || rest.starts_with('='))
})
.count()
> 1
})
}
pub fn run() -> ExitCode {
let args: Vec<_> = std::env::args_os().collect();
let options: Vec<_> = args
.iter()
.skip(1)
.take_while(|arg| *arg != "--")
.filter_map(|arg| arg.to_str())
.collect();
let json = options.contains(&"--json");
if let Some(name) = repeated_global(&options) {
return failure(&format!("{name} cannot be used multiple times."), json);
}
let cli = match Cli::try_parse_from(args) {
Ok(cli) => cli,
Err(error) if json && error.use_stderr() => {
let message = error.to_string();
let message = if error.kind() == ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand {
"A subcommand is required; see --help."
} else {
let usage = message.split("\n\n").next().unwrap_or_default();
usage.trim_start_matches("error: ")
};
return failure(message, true);
}
Err(error) => {
return if error.print().is_ok() && !error.use_stderr() {
ExitCode::SUCCESS
} else {
ExitCode::from(2)
};
}
};
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(error) => return failure(&error.to_string(), json),
};
match runtime.block_on(async {
let result = dispatch::execute(cli).await;
crate::changes::drain().await;
result
}) {
Ok(code) => ExitCode::from(code),
Err(error) => failure(&error, json),
}
}