Restructure CLI: nest runs under arc run, promote models to top-level

- `arc run <pipeline>` → `arc run start <pipeline>`
- `arc runs list/prune` → `arc run list/prune`
- `arc llm models list` → `arc models list`
- Remove `arc llm models sync`
- Add `arc models test [--provider, --model]` for connectivity testing
- Clean up dead code (RunsArgs, RunsCommand, runs_command)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-01 22:04:32 -05:00
parent 4a0fc63d5a
commit b2b64dcc64
3 changed files with 97 additions and 81 deletions

View file

@ -17,32 +17,43 @@ struct Cli {
#[derive(Subcommand)]
enum Command {
/// LLM prompt and model operations
/// LLM prompt operations
Llm {
#[command(subcommand)]
command: LlmCommand,
},
/// Run an agentic coding session
Agent(arc_agent::cli::AgentArgs),
/// Launch a pipeline
Run(arc_workflows::cli::RunArgs),
/// Launch and manage pipeline runs
Run {
#[command(subcommand)]
command: RunCommand,
},
/// Validate a pipeline
Validate(arc_workflows::cli::ValidateArgs),
/// List and manage pipeline runs
Runs(arc_workflows::cli::runs::RunsArgs),
/// List and test LLM models
Models {
#[command(subcommand)]
command: Option<arc_llm::cli::ModelsCommand>,
},
/// Start the HTTP API server
Serve(arc_api::serve::ServeArgs),
}
#[derive(Subcommand)]
enum RunCommand {
/// Launch a pipeline from a .dot or .toml task file
Start(arc_workflows::cli::RunArgs),
/// List pipeline runs
List(arc_workflows::cli::runs::RunsListArgs),
/// Delete old pipeline runs
Prune(arc_workflows::cli::runs::RunsPruneArgs),
}
#[derive(Subcommand)]
enum LlmCommand {
/// Execute a prompt
Prompt(arc_llm::cli::PromptArgs),
/// Manage models
Models {
#[command(subcommand)]
command: Option<arc_llm::cli::ModelsCommand>,
},
}
#[tokio::main]
@ -59,9 +70,9 @@ async fn main() -> Result<()> {
let command_name = match &cli.command {
Command::Llm { .. } => "llm",
Command::Agent(_) => "agent",
Command::Run(_) => "run",
Command::Run { .. } => "run",
Command::Validate(_) => "validate",
Command::Runs(_) => "runs",
Command::Models { .. } => "models",
Command::Serve(_) => "serve",
};
debug!(command = %command_name, "CLI command started");
@ -69,21 +80,26 @@ async fn main() -> Result<()> {
match cli.command {
Command::Llm { command } => match command {
LlmCommand::Prompt(args) => arc_llm::cli::run_prompt(args).await?,
LlmCommand::Models { command } => arc_llm::cli::run_models(command).await?,
},
Command::Agent(args) => arc_agent::cli::run_with_args(args).await?,
Command::Run(args) => {
let styles: &'static arc_util::terminal::Styles =
Box::leak(Box::new(arc_util::terminal::Styles::detect_stderr()));
arc_workflows::cli::run::run_command(args, styles).await?;
}
Command::Run { command } => match command {
RunCommand::Start(args) => {
let styles: &'static arc_util::terminal::Styles =
Box::leak(Box::new(arc_util::terminal::Styles::detect_stderr()));
arc_workflows::cli::run::run_command(args, styles).await?;
}
RunCommand::List(args) => {
arc_workflows::cli::runs::list_command(&args)?;
}
RunCommand::Prune(args) => {
arc_workflows::cli::runs::prune_command(&args)?;
}
},
Command::Validate(args) => {
let styles = arc_util::terminal::Styles::detect_stderr();
arc_workflows::cli::validate::validate_command(&args, &styles)?;
}
Command::Runs(args) => {
arc_workflows::cli::runs::runs_command(args)?;
}
Command::Models { command } => arc_llm::cli::run_models(command).await?,
Command::Serve(args) => {
let styles: &'static arc_util::terminal::Styles =
Box::leak(Box::new(arc_util::terminal::Styles::detect_stderr()));

View file

@ -1,4 +1,5 @@
use std::io::{self, IsTerminal, Read};
use std::time::Duration;
use anyhow::{bail, Context, Result};
use clap::{Args, Subcommand};
@ -50,15 +51,15 @@ pub enum ModelsCommand {
query: Option<String>,
},
/// Download model metadata from OpenRouter
Sync {
/// URL to fetch models from
#[arg(long, default_value = "https://openrouter.ai/api/v1/models")]
url: String,
/// Test model availability by sending a simple prompt
Test {
/// Filter by provider
#[arg(short, long)]
provider: Option<String>,
/// Output file path
#[arg(short, long, default_value = "openrouter_models.json")]
output: String,
/// Test a specific model
#[arg(short, long)]
model: Option<String>,
},
}
@ -233,26 +234,6 @@ pub async fn run_prompt(args: PromptArgs) -> Result<()> {
Ok(())
}
async fn sync_models(url: &str, output: &str) -> Result<()> {
let body = reqwest::get(url)
.await
.context("failed to connect to models endpoint")?
.error_for_status()
.context("models endpoint returned an error")?
.text()
.await
.context("failed to read response body")?;
let json: serde_json::Value =
serde_json::from_str(&body).context("response is not valid JSON")?;
let pretty = serde_json::to_string_pretty(&json).context("failed to format JSON")?;
std::fs::write(output, &pretty).with_context(|| format!("failed to write {output}"))?;
eprintln!("Saved models to {output}");
Ok(())
}
pub async fn run_models(command: Option<ModelsCommand>) -> Result<()> {
let command = command.unwrap_or(ModelsCommand::List {
provider: None,
@ -276,10 +257,58 @@ pub async fn run_models(command: Option<ModelsCommand>) -> Result<()> {
print_models_table(&models);
}
ModelsCommand::Sync { url, output } => {
sync_models(&url, &output).await?;
ModelsCommand::Test { provider, model } => {
test_models(provider.as_deref(), model.as_deref()).await?;
}
}
Ok(())
}
async fn test_models(provider: Option<&str>, model: Option<&str>) -> Result<()> {
let models_to_test = if let Some(model_id) = model {
match catalog::get_model_info(model_id) {
Some(info) => vec![info],
None => bail!("Unknown model: {model_id}"),
}
} else {
catalog::list_models(provider)
};
if models_to_test.is_empty() {
bail!("No models found");
}
println!("{:<30} {:<12} RESULT", "MODEL", "PROVIDER");
let mut failures = 0u32;
for info in &models_to_test {
let params = GenerateParams::new(&info.id)
.provider(&info.provider)
.prompt("Say OK")
.max_tokens(5);
let result =
tokio::time::timeout(Duration::from_secs(10), generate::generate(params)).await;
let status = match result {
Ok(Ok(_)) => "ok".to_string(),
Ok(Err(e)) => {
failures += 1;
format!("error: {e}")
}
Err(_) => {
failures += 1;
"error: timeout (10s)".to_string()
}
};
println!("{:<30} {:<12} {status}", info.id, info.provider);
}
if failures > 0 {
bail!("{failures} model(s) failed");
}
Ok(())
}

View file

@ -2,25 +2,10 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::Result;
use clap::{Args, Subcommand};
use clap::Args;
use serde::Serialize;
use tracing::{debug, info};
/// Arguments for the `arc runs` command.
#[derive(Args)]
pub struct RunsArgs {
#[command(subcommand)]
pub command: Option<RunsCommand>,
}
#[derive(Subcommand)]
pub enum RunsCommand {
/// List pipeline runs
List(RunsListArgs),
/// Delete old pipeline runs
Prune(RunsPruneArgs),
}
#[derive(Args)]
pub struct RunsListArgs {
/// Only show runs started before this date (YYYY-MM-DD prefix match)
@ -336,20 +321,6 @@ pub fn prune_from(args: &RunsPruneArgs, base: &Path) -> Result<()> {
Ok(())
}
pub fn runs_command(args: RunsArgs) -> Result<()> {
match args.command {
None => list_command(&RunsListArgs {
before: None,
pipeline: None,
label: Vec::new(),
orphans: false,
json: false,
}),
Some(RunsCommand::List(args)) => list_command(&args),
Some(RunsCommand::Prune(args)) => prune_command(&args),
}
}
#[cfg(test)]
mod tests {
use super::*;