use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use crate::channel::SurfaceKind;
pub const DAG_GRAPH_OFF: &str = "off";
pub const DEFAULT_HEARTBEAT_INTERVAL_SECONDS: u64 = 1_800;
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
#[command(name = "onepipeline", version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
#[command(rename_all = "kebab-case")]
pub enum Command {
Start(StartArgs),
#[command(subcommand)]
Plan(PlanCommand),
Adopt(AdoptArgs),
#[command(subcommand)]
Channel(ChannelCommand),
Next(ReadArgs),
Reply(ReplyArgs),
Surface(SurfaceArgs),
Attest(AttestArgs),
Stop(StopArgs),
Runs(RunsArgs),
Status(OptionalRunArgs),
Host,
Monitor(ReadArgs),
Watch(WatchArgs),
Unwatched(UnwatchedArgs),
Results(RunArgs),
Goals(OptionalRunArgs),
Transcript(TranscriptArgs),
Telemetry(TelemetryArgs),
#[command(hide = true, name = crate::engine::DRIVE_VERB)]
DriveRun(DriveRunArgs),
#[command(hide = true, name = crate::agentgraph::DRIVE_VERB)]
Drive(DriveArgs),
}
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
#[command(rename_all = "kebab-case")]
pub enum PlanCommand {
Check(PlanCheckArgs),
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct PlanCheckArgs {
pub project: String,
#[arg(long = "check", value_name = "PATH")]
pub checks: Vec<PathBuf>,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct StartArgs {
pub project: String,
#[arg(long, conflicts_with = "detach")]
pub attach: bool,
#[arg(long)]
pub detach: bool,
#[arg(long, value_name = "REF", default_value = DAG_GRAPH_OFF)]
pub dag_graph: String,
#[arg(long, value_name = "REF")]
pub pr_author_graph: Option<String>,
#[arg(long, value_name = "COMMAND")]
pub node_validator: Option<String>,
#[arg(long, value_name = "COMMAND")]
pub envelope_reviewer: Option<String>,
#[arg(long, value_name = "SECONDS", default_value_t = DEFAULT_HEARTBEAT_INTERVAL_SECONDS)]
pub heartbeat_interval: u64,
#[arg(long = "set", value_name = "PATH=VALUE")]
pub dag_sets: Vec<String>,
#[arg(long = "node-set", value_name = "PATH=VALUE")]
pub node_sets: Vec<String>,
#[arg(long)]
pub acknowledge_concurrent: bool,
#[arg(long, value_name = "FILE")]
pub launch_config: Option<PathBuf>,
#[arg(long, value_name = "SPEC")]
pub filter_agentgraph: Option<String>,
#[arg(long, value_name = "SPEC")]
pub filter_vcs: Option<String>,
#[arg(long = "filter-profile", value_name = "NAME=SPEC")]
pub filter_profiles: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct AdoptArgs {
pub run: String,
#[arg(long, conflicts_with = "detach")]
pub attach: bool,
#[arg(long)]
pub detach: bool,
}
pub(crate) const ADOPT_FLAG: &str = "adopt";
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct DriveRunArgs {
pub run: String,
#[arg(long = ADOPT_FLAG)]
pub adopt: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct ReadArgs {
pub run: String,
#[arg(long, value_name = "NAME|SPEC", conflicts_with = "all")]
pub filter: Option<String>,
#[arg(long)]
pub all: bool,
}
pub const DEFAULT_WATCH_TIMEOUT_SECONDS: u64 = 300;
pub const DEFAULT_WATCH_TICK_SECONDS: u64 = 30;
pub const WATCH_CURSOR_VERSION: &str = "1";
pub const WATCH_TIMEOUT_UNBOUNDED: &str = "none";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WatchTimeout {
Bounded(u64),
Unbounded,
}
impl std::fmt::Display for WatchTimeout {
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bounded(seconds) => write!(out, "{seconds}"),
Self::Unbounded => out.write_str(WATCH_TIMEOUT_UNBOUNDED),
}
}
}
impl std::str::FromStr for WatchTimeout {
type Err = String;
fn from_str(text: &str) -> std::result::Result<Self, Self::Err> {
if text == WATCH_TIMEOUT_UNBOUNDED {
return Ok(Self::Unbounded);
}
text.parse().map(Self::Bounded).map_err(|_| {
format!(
"'{text}' is not a wait this verb can take: a wait is a number of seconds, \
where `0` reads the run once and returns, or `{WATCH_TIMEOUT_UNBOUNDED}`, \
which does not bound the wait at all"
)
})
}
}
const CONDITIONS: [(&str, WatchUntil); 5] = [
("settled", WatchUntil::Settled),
("surface", WatchUntil::Surface),
("nothing-driving", WatchUntil::NothingDriving),
("node-settled", WatchUntil::NodeSettled),
(WATCH_NODE_CONDITION_SHAPE, WatchUntil::Node(String::new())),
];
pub const WATCH_NODE_CONDITION_SHAPE: &str = "node=<ID>";
const NODE_ID_PLACEHOLDER: &str = "<ID>";
pub fn watch_conditions() -> [&'static str; 5] {
CONDITIONS.map(|(spelling, _)| spelling)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum WatchUntil {
#[default]
Surface,
Settled,
NothingDriving,
NodeSettled,
Node(String),
}
impl std::fmt::Display for WatchUntil {
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Surface => out.write_str("surface"),
Self::Settled => out.write_str("settled"),
Self::NothingDriving => out.write_str("nothing-driving"),
Self::NodeSettled => out.write_str("node-settled"),
Self::Node(node) => write!(
out,
"{}{node}",
WATCH_NODE_CONDITION_SHAPE
.strip_suffix(NODE_ID_PLACEHOLDER)
.unwrap_or(WATCH_NODE_CONDITION_SHAPE)
),
}
}
}
impl std::str::FromStr for WatchUntil {
type Err = String;
fn from_str(text: &str) -> std::result::Result<Self, Self::Err> {
for (spelling, condition) in CONDITIONS {
match spelling.strip_suffix(NODE_ID_PLACEHOLDER) {
Some(prefix) => {
if let Some(node) = text.strip_prefix(prefix).filter(|node| !node.is_empty()) {
return Ok(Self::Node(node.to_string()));
}
}
None if text == spelling => return Ok(condition),
None => {}
}
}
Err(format!(
"'{text}' is not a condition this verb returns on; it returns on {}",
watch_conditions().join(", ")
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct WatchArgs {
#[command(flatten)]
pub read: ReadArgs,
#[arg(long, value_name = "SECONDS|none", default_value_t = WatchTimeout::Bounded(DEFAULT_WATCH_TIMEOUT_SECONDS))]
pub timeout: WatchTimeout,
#[arg(long, value_name = "SECONDS", default_value_t = DEFAULT_WATCH_TICK_SECONDS)]
pub tick_interval: u64,
#[arg(long, value_name = "CURSOR")]
pub cursor: Option<String>,
#[arg(long, value_name = "CONDITION", default_values_t = [WatchUntil::Surface])]
pub until: Vec<WatchUntil>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct UnwatchedArgs {
#[arg(long, value_name = "ID")]
pub session: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct DriveArgs {
pub graph: String,
#[arg(long, value_name = "TEXT")]
pub task: String,
#[arg(long, value_name = "DIR")]
pub dir: PathBuf,
#[arg(long = "label", value_name = "KEY=VALUE")]
pub labels: Vec<String>,
#[arg(long = "set", value_name = "PATH=VALUE")]
pub sets: Vec<String>,
#[arg(long, value_name = "SPEC")]
pub event_filter: Option<String>,
#[arg(long)]
pub await_ending: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
#[command(rename_all = "kebab-case")]
pub enum ChannelCommand {
Serve(RunArgs),
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct RunArgs {
pub run: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct OptionalRunArgs {
pub run: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct ReplyArgs {
pub run: String,
pub file: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct SurfaceArgs {
pub run: String,
#[arg(conflicts_with = "message")]
pub file: Option<PathBuf>,
#[arg(long, value_enum)]
pub kind: SurfaceKind,
#[arg(long, value_name = "TEXT")]
pub message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct AttestArgs {
pub run: String,
pub reference: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct StopArgs {
pub run: String,
#[arg(long)]
pub force: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct RunsArgs {
#[arg(long)]
pub mine: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct TranscriptArgs {
pub run: String,
pub node: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct TelemetryArgs {
pub run: Option<String>,
#[arg(long)]
pub breakdown: bool,
}