mod assembly;
mod commands;
mod report;
mod serve;
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
pub mod exit {
pub const PASS: i32 = 0;
pub const FAIL: i32 = 1;
pub const UNKNOWN: i32 = 2;
pub const SUSPENDED: i32 = 3;
pub const NOT_IN_M0: i32 = 64;
pub const INTERNAL: i32 = 70;
}
pub struct Failure {
pub code: i32,
pub message: String,
}
impl Failure {
pub fn new(code: i32, message: impl Into<String>) -> Self {
Failure {
code,
message: message.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Text,
Json,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SuperviseArg {
Mutating,
All,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum VisionArg {
Off,
Anthropic,
OpenaiCompat,
}
#[derive(Parser)]
#[command(
name = "pointlock",
version,
about = "Pointlock assembly-layer CLI (spine A.2: the seven commands)"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Lock(LockArgs),
Compile(CompileArgs),
Run(RunArgs),
Resume(ResumeArgs),
Inspect(InspectArgs),
Locate(LocateArgs),
Report(ReportArgs),
}
#[derive(clap::Args)]
struct LockArgs {
#[arg(long, default_value = assembly::FAKE_REGISTRATION)]
provider: String,
#[arg(long)]
out: PathBuf,
#[arg(long, value_name = "PATH")]
daemon_cmd: Option<PathBuf>,
#[arg(long = "daemon-env", value_name = "KEY=VALUE")]
daemon_env: Vec<String>,
#[arg(long)]
device: Option<String>,
}
#[derive(clap::Args)]
struct CompileArgs {
flow: Option<PathBuf>,
#[arg(long)]
emit_authoring_schema: bool,
#[arg(long)]
lockfile: Option<PathBuf>,
#[arg(long, default_value = assembly::FAKE_REGISTRATION)]
provider: String,
#[arg(long)]
out: PathBuf,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
}
#[derive(clap::Args)]
struct RunArgs {
flow_ir: PathBuf,
#[arg(long)]
store: PathBuf,
#[arg(long, default_value = assembly::FAKE_REGISTRATION)]
provider: String,
#[arg(long = "param", value_name = "KEY=VALUE")]
params: Vec<String>,
#[arg(long)]
device: Option<String>,
#[arg(long)]
lockfile: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
daemon_cmd: Option<PathBuf>,
#[arg(long = "daemon-env", value_name = "KEY=VALUE")]
daemon_env: Vec<String>,
#[arg(long)]
run_id: Option<String>,
#[arg(long, value_name = "STEP_ID")]
stop_after: Option<String>,
#[arg(long, value_enum)]
supervise: Option<SuperviseArg>,
#[arg(long, value_enum, default_value_t = VisionArg::Off)]
vision: VisionArg,
#[arg(long)]
interactive: bool,
#[arg(long, value_name = "URL")]
webhook_url: Option<String>,
}
#[derive(clap::Args)]
struct ResumeArgs {
flow_ir: PathBuf,
#[arg(long)]
store: PathBuf,
#[arg(long)]
run: String,
#[arg(long, default_value = assembly::FAKE_REGISTRATION)]
provider: String,
#[arg(long)]
lockfile: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
daemon_cmd: Option<PathBuf>,
#[arg(long = "daemon-env", value_name = "KEY=VALUE")]
daemon_env: Vec<String>,
#[arg(long, value_name = "FLOW_IR")]
old_ir: Option<PathBuf>,
#[arg(long, value_enum)]
supervise: Option<SuperviseArg>,
#[arg(long, value_enum, default_value_t = VisionArg::Off)]
vision: VisionArg,
#[arg(long = "allow-mutating-reexec", value_name = "STEP_ID")]
allow_mutating_reexec: Vec<String>,
#[arg(long = "force-reexecute", value_name = "STEP_ID")]
force_reexecute: Vec<String>,
#[arg(long)]
interactive: bool,
#[arg(long, value_name = "URL")]
webhook_url: Option<String>,
#[arg(long)]
preview: bool,
}
#[derive(clap::Args)]
struct InspectArgs {
#[arg(long)]
store: PathBuf,
#[arg(long)]
run: Option<String>,
#[arg(long)]
rebuild_checkpoint: bool,
#[arg(long)]
serve: bool,
#[arg(long)]
artifacts: Option<PathBuf>,
#[arg(long)]
ui: Option<PathBuf>,
#[arg(long)]
lockfile: Option<PathBuf>,
#[arg(long, default_value_t = 0)]
port: u16,
#[arg(long, value_enum, default_value_t = VisionArg::Off)]
vision: VisionArg,
}
#[derive(clap::Args)]
struct LocateArgs {
#[arg(long)]
store: PathBuf,
#[arg(long)]
run: String,
#[arg(long)]
step: String,
#[arg(long)]
flow_ir: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
}
#[derive(clap::Args)]
struct ReportArgs {
#[arg(long)]
store: PathBuf,
#[arg(long)]
run: String,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
}
fn main() {
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(err) => {
use clap::error::ErrorKind;
let code = match err.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => exit::PASS,
_ => exit::NOT_IN_M0,
};
let _ = err.print();
std::process::exit(code);
}
};
let result = match cli.command {
Command::Lock(args) => commands::lock(&args),
Command::Compile(args) => match (args.emit_authoring_schema, &args.flow) {
(true, None) => commands::emit_authoring_schema(&args.out),
(true, Some(_)) => Err(Failure::new(
exit::NOT_IN_M0,
"--emit-authoring-schema takes no flow source; drop the positional argument"
.to_owned(),
)),
(false, Some(flow)) => commands::compile(
flow,
args.lockfile.as_deref(),
&args.provider,
&args.out,
args.format,
),
(false, None) => Err(Failure::new(
exit::NOT_IN_M0,
"a *.flow.yaml source is required (or use --emit-authoring-schema)".to_owned(),
)),
},
Command::Run(args) => commands::run(&args),
Command::Resume(args) => commands::resume(&args),
Command::Inspect(args) => {
if args.serve {
if args.run.is_some() || args.rebuild_checkpoint {
Err(Failure::new(
exit::NOT_IN_M0,
"--serve is a mode of its own; drop --run/--rebuild-checkpoint".to_owned(),
))
} else {
serve::serve(serve::ServeConfig {
store_dir: args.store,
artifacts_dir: args.artifacts,
port: args.port,
ui_dir: args.ui,
lockfile_path: args.lockfile,
vision: args.vision,
})
}
} else if args.artifacts.is_some()
|| args.port != 0
|| args.ui.is_some()
|| args.lockfile.is_some()
|| args.vision != VisionArg::Off
{
Err(Failure::new(
exit::NOT_IN_M0,
"--artifacts/--ui/--port/--lockfile/--vision only apply to --serve".to_owned(),
))
} else {
match args.run {
Some(run) => commands::inspect(&args.store, &run, args.rebuild_checkpoint),
None => Err(Failure::new(
exit::NOT_IN_M0,
"--run is required (or use --serve)".to_owned(),
)),
}
}
}
Command::Locate(args) => commands::locate(
&args.store,
&args.run,
&args.step,
args.flow_ir.as_deref(),
args.format,
),
Command::Report(args) => report::report(&args.store, &args.run, args.format),
};
match result {
Ok(code) => std::process::exit(code),
Err(failure) => {
eprintln!("pointlock: {}", failure.message);
std::process::exit(failure.code);
}
}
}
pub(crate) use {LockArgs as LockCliArgs, ResumeArgs as ResumeCliArgs, RunArgs as RunCliArgs};