mirror of
https://github.com/katanemo/plano.git
synced 2026-05-11 08:42:48 +02:00
rewrite planoai CLI in Rust
Add crates/plano-cli/ as a full Rust rewrite of the Python CLI. Binary name: planoai. All subcommands ported: up, down, build, logs, cli-agent, trace, init. Config validation and Tera template rendering replace the Python config_generator. CI updated to use cargo test/build instead of Python test jobs.
This commit is contained in:
parent
406fa92802
commit
15b9e8b95c
37 changed files with 4658 additions and 91 deletions
105
crates/plano-cli/src/commands/build.rs
Normal file
105
crates/plano-cli/src/commands/build.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use std::process::Command;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::consts::plano_docker_image;
|
||||
use crate::utils::{find_repo_root, print_cli_header};
|
||||
|
||||
pub async fn run(docker: bool) -> Result<()> {
|
||||
let dim = console::Style::new().dim();
|
||||
let red = console::Style::new().red();
|
||||
let bold = console::Style::new().bold();
|
||||
|
||||
if !docker {
|
||||
print_cli_header();
|
||||
|
||||
let repo_root = find_repo_root().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Could not find repository root. Make sure you're inside the plano repository."
|
||||
)
|
||||
})?;
|
||||
|
||||
let crates_dir = repo_root.join("crates");
|
||||
|
||||
// Check cargo is available
|
||||
if which::which("cargo").is_err() {
|
||||
eprintln!(
|
||||
"{} {} not found. Install Rust: https://rustup.rs",
|
||||
red.apply_to("✗"),
|
||||
bold.apply_to("cargo")
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Build WASM plugins
|
||||
eprintln!(
|
||||
"{}",
|
||||
dim.apply_to("Building WASM plugins (wasm32-wasip1)...")
|
||||
);
|
||||
let status = Command::new("cargo")
|
||||
.args([
|
||||
"build",
|
||||
"--release",
|
||||
"--target",
|
||||
"wasm32-wasip1",
|
||||
"-p",
|
||||
"llm_gateway",
|
||||
"-p",
|
||||
"prompt_gateway",
|
||||
])
|
||||
.current_dir(&crates_dir)
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("WASM build failed");
|
||||
}
|
||||
|
||||
// Build brightstaff
|
||||
eprintln!("{}", dim.apply_to("Building brightstaff (native)..."));
|
||||
let status = Command::new("cargo")
|
||||
.args(["build", "--release", "-p", "brightstaff"])
|
||||
.current_dir(&crates_dir)
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("brightstaff build failed");
|
||||
}
|
||||
|
||||
let wasm_dir = crates_dir.join("target/wasm32-wasip1/release");
|
||||
let native_dir = crates_dir.join("target/release");
|
||||
|
||||
println!("\n{}:", bold.apply_to("Build artifacts"));
|
||||
println!(" {}", wasm_dir.join("prompt_gateway.wasm").display());
|
||||
println!(" {}", wasm_dir.join("llm_gateway.wasm").display());
|
||||
println!(" {}", native_dir.join("brightstaff").display());
|
||||
} else {
|
||||
let repo_root =
|
||||
find_repo_root().ok_or_else(|| anyhow::anyhow!("Could not find repository root."))?;
|
||||
|
||||
let dockerfile = repo_root.join("Dockerfile");
|
||||
if !dockerfile.exists() {
|
||||
bail!("Dockerfile not found at {}", dockerfile.display());
|
||||
}
|
||||
|
||||
println!("Building plano image from {}...", repo_root.display());
|
||||
let status = Command::new("docker")
|
||||
.args([
|
||||
"build",
|
||||
"-f",
|
||||
&dockerfile.to_string_lossy(),
|
||||
"-t",
|
||||
&plano_docker_image(),
|
||||
&repo_root.to_string_lossy(),
|
||||
"--add-host=host.docker.internal:host-gateway",
|
||||
])
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("Docker build failed");
|
||||
}
|
||||
|
||||
println!("plano image built successfully.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
103
crates/plano-cli/src/commands/cli_agent.rs
Normal file
103
crates/plano-cli/src/commands/cli_agent.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::consts::PLANO_DOCKER_NAME;
|
||||
use crate::utils::{find_config_file, is_native_plano_running};
|
||||
|
||||
pub async fn run(agent_type: &str, file: Option<String>, path: &str, settings: &str) -> Result<()> {
|
||||
let native_running = is_native_plano_running();
|
||||
let docker_running = if !native_running {
|
||||
crate::docker::container_status(PLANO_DOCKER_NAME).await? == "running"
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !native_running && !docker_running {
|
||||
bail!("Plano is not running. Start Plano first using 'plano up <config.yaml>' (native or --docker mode).");
|
||||
}
|
||||
|
||||
let plano_config_file = find_config_file(path, file.as_deref());
|
||||
if !plano_config_file.exists() {
|
||||
bail!("Config file not found: {}", plano_config_file.display());
|
||||
}
|
||||
|
||||
start_cli_agent(&plano_config_file, agent_type, settings)
|
||||
}
|
||||
|
||||
fn start_cli_agent(
|
||||
plano_config_path: &std::path::Path,
|
||||
agent_type: &str,
|
||||
_settings_json: &str,
|
||||
) -> Result<()> {
|
||||
let config_str = std::fs::read_to_string(plano_config_path)?;
|
||||
let config: serde_yaml::Value = serde_yaml::from_str(&config_str)?;
|
||||
|
||||
// Resolve CLI agent endpoint
|
||||
let (host, port) = resolve_cli_agent_endpoint(&config)?;
|
||||
let base_url = format!("http://{host}:{port}/v1");
|
||||
|
||||
let mut env: HashMap<String, String> = std::env::vars().collect();
|
||||
|
||||
match agent_type {
|
||||
"claude" => {
|
||||
env.insert("ANTHROPIC_BASE_URL".to_string(), base_url);
|
||||
|
||||
// Check for model alias
|
||||
if let Some(model) = config
|
||||
.get("model_aliases")
|
||||
.and_then(|a| a.get("arch"))
|
||||
.and_then(|a| a.get("claude"))
|
||||
.and_then(|a| a.get("code"))
|
||||
.and_then(|a| a.get("small"))
|
||||
.and_then(|a| a.get("fast"))
|
||||
.and_then(|a| a.get("target"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
env.insert("ANTHROPIC_MODEL".to_string(), model.to_string());
|
||||
}
|
||||
|
||||
let status = Command::new("claude").envs(&env).status()?;
|
||||
|
||||
if !status.success() {
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
}
|
||||
"codex" => {
|
||||
env.insert("OPENAI_BASE_URL".to_string(), base_url);
|
||||
|
||||
let status = Command::new("codex").envs(&env).status()?;
|
||||
|
||||
if !status.success() {
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
}
|
||||
_ => bail!("Unsupported agent type: {agent_type}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_cli_agent_endpoint(config: &serde_yaml::Value) -> Result<(String, u16)> {
|
||||
// Look for model listener (egress_traffic)
|
||||
if let Some(listeners) = config.get("listeners").and_then(|v| v.as_sequence()) {
|
||||
for listener in listeners {
|
||||
let listener_type = listener.get("type").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if listener_type == "model" {
|
||||
let host = listener
|
||||
.get("address")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0.0.0.0");
|
||||
let port = listener
|
||||
.get("port")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(12000) as u16;
|
||||
return Ok((host.to_string(), port));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default
|
||||
Ok(("0.0.0.0".to_string(), 12000))
|
||||
}
|
||||
15
crates/plano-cli/src/commands/down.rs
Normal file
15
crates/plano-cli/src/commands/down.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use anyhow::Result;
|
||||
|
||||
use crate::utils::print_cli_header;
|
||||
|
||||
pub async fn run(docker: bool) -> Result<()> {
|
||||
print_cli_header();
|
||||
|
||||
if !docker {
|
||||
crate::native::runner::stop_native()?;
|
||||
} else {
|
||||
crate::docker::stop_container().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
132
crates/plano-cli/src/commands/init.rs
Normal file
132
crates/plano-cli/src/commands/init.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
const TEMPLATES: &[(&str, &str, &str)] = &[
|
||||
(
|
||||
"sub_agent_orchestration",
|
||||
"Sub-agent Orchestration",
|
||||
include_str!("../../templates/sub_agent_orchestration.yaml"),
|
||||
),
|
||||
(
|
||||
"coding_agent_routing",
|
||||
"Coding Agent Routing",
|
||||
include_str!("../../templates/coding_agent_routing.yaml"),
|
||||
),
|
||||
(
|
||||
"preference_aware_routing",
|
||||
"Preference-Aware Routing",
|
||||
include_str!("../../templates/preference_aware_routing.yaml"),
|
||||
),
|
||||
(
|
||||
"filter_chain_guardrails",
|
||||
"Filter Chain Guardrails",
|
||||
include_str!("../../templates/filter_chain_guardrails.yaml"),
|
||||
),
|
||||
(
|
||||
"conversational_state",
|
||||
"Conversational State",
|
||||
include_str!("../../templates/conversational_state.yaml"),
|
||||
),
|
||||
];
|
||||
|
||||
pub async fn run(
|
||||
template: Option<String>,
|
||||
clean: bool,
|
||||
output: Option<String>,
|
||||
force: bool,
|
||||
list_templates: bool,
|
||||
) -> Result<()> {
|
||||
let bold = console::Style::new().bold();
|
||||
let dim = console::Style::new().dim();
|
||||
let green = console::Style::new().green();
|
||||
let cyan = console::Style::new().cyan();
|
||||
|
||||
if list_templates {
|
||||
println!("\n{}:", bold.apply_to("Available templates"));
|
||||
for (id, name, _) in TEMPLATES {
|
||||
println!(" {} - {}", cyan.apply_to(id), name);
|
||||
}
|
||||
println!();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output_path = output.unwrap_or_else(|| "plano_config.yaml".to_string());
|
||||
let output_path = Path::new(&output_path);
|
||||
|
||||
if output_path.exists() && !force {
|
||||
bail!(
|
||||
"File {} already exists. Use --force to overwrite.",
|
||||
output_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
if clean {
|
||||
let content = "version: v0.3.0\nlisteners:\n - type: model\n name: egress_traffic\n port: 12000\nmodel_providers: []\n";
|
||||
std::fs::write(output_path, content)?;
|
||||
println!(
|
||||
"{} Created clean config at {}",
|
||||
green.apply_to("✓"),
|
||||
output_path.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(template_id) = template {
|
||||
let tmpl = TEMPLATES
|
||||
.iter()
|
||||
.find(|(id, _, _)| *id == template_id)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Unknown template '{}'. Use --list-templates to see available templates.",
|
||||
template_id
|
||||
)
|
||||
})?;
|
||||
|
||||
std::fs::write(output_path, tmpl.2)?;
|
||||
println!(
|
||||
"{} Created config from template '{}' at {}",
|
||||
green.apply_to("✓"),
|
||||
tmpl.1,
|
||||
output_path.display()
|
||||
);
|
||||
|
||||
// Preview
|
||||
let lines: Vec<&str> = tmpl.2.lines().take(28).collect();
|
||||
println!("\n{}:", dim.apply_to("Preview"));
|
||||
for line in &lines {
|
||||
println!(" {}", dim.apply_to(line));
|
||||
}
|
||||
if tmpl.2.lines().count() > 28 {
|
||||
println!(" {}", dim.apply_to("..."));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Interactive mode using dialoguer
|
||||
if !atty::is(atty::Stream::Stdin) {
|
||||
bail!(
|
||||
"Interactive mode requires a TTY. Use --template or --clean for non-interactive mode."
|
||||
);
|
||||
}
|
||||
|
||||
let selections: Vec<&str> = TEMPLATES.iter().map(|(_, name, _)| *name).collect();
|
||||
|
||||
let selection = dialoguer::Select::new()
|
||||
.with_prompt("Choose a template")
|
||||
.items(&selections)
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
let tmpl = &TEMPLATES[selection];
|
||||
std::fs::write(output_path, tmpl.2)?;
|
||||
println!(
|
||||
"\n{} Created config from template '{}' at {}",
|
||||
green.apply_to("✓"),
|
||||
tmpl.1,
|
||||
output_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
10
crates/plano-cli/src/commands/logs.rs
Normal file
10
crates/plano-cli/src/commands/logs.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use anyhow::Result;
|
||||
|
||||
pub async fn run(debug: bool, follow: bool, docker: bool) -> Result<()> {
|
||||
if !docker {
|
||||
crate::native::runner::native_logs(debug, follow)?;
|
||||
} else {
|
||||
crate::docker::stream_logs(debug, follow).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
262
crates/plano-cli/src/commands/mod.rs
Normal file
262
crates/plano-cli/src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
pub mod build;
|
||||
pub mod cli_agent;
|
||||
pub mod down;
|
||||
pub mod init;
|
||||
pub mod logs;
|
||||
pub mod up;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::consts::PLANO_VERSION;
|
||||
|
||||
const LOGO: &str = r#"
|
||||
______ _
|
||||
| ___ \ |
|
||||
| |_/ / | __ _ _ __ ___
|
||||
| __/| |/ _` | '_ \ / _ \
|
||||
| | | | (_| | | | | (_) |
|
||||
\_| |_|\__,_|_| |_|\___/
|
||||
"#;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "planoai",
|
||||
about = "The Delivery Infrastructure for Agentic Apps"
|
||||
)]
|
||||
#[command(version = PLANO_VERSION)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Start Plano
|
||||
Up {
|
||||
/// Config file path (positional)
|
||||
file: Option<String>,
|
||||
|
||||
/// Path to the directory containing config.yaml
|
||||
#[arg(long, default_value = ".")]
|
||||
path: String,
|
||||
|
||||
/// Run Plano in the foreground
|
||||
#[arg(long)]
|
||||
foreground: bool,
|
||||
|
||||
/// Start a local OTLP trace collector
|
||||
#[arg(long)]
|
||||
with_tracing: bool,
|
||||
|
||||
/// Port for the OTLP trace collector
|
||||
#[arg(long, default_value_t = 4317)]
|
||||
tracing_port: u16,
|
||||
|
||||
/// Run Plano inside Docker instead of natively
|
||||
#[arg(long)]
|
||||
docker: bool,
|
||||
},
|
||||
|
||||
/// Stop Plano
|
||||
Down {
|
||||
/// Stop a Docker-based Plano instance
|
||||
#[arg(long)]
|
||||
docker: bool,
|
||||
},
|
||||
|
||||
/// Build Plano from source
|
||||
Build {
|
||||
/// Build the Docker image instead of native binaries
|
||||
#[arg(long)]
|
||||
docker: bool,
|
||||
},
|
||||
|
||||
/// Stream logs from Plano
|
||||
Logs {
|
||||
/// Show detailed debug logs
|
||||
#[arg(long)]
|
||||
debug: bool,
|
||||
|
||||
/// Follow the logs
|
||||
#[arg(long)]
|
||||
follow: bool,
|
||||
|
||||
/// Stream logs from a Docker-based Plano instance
|
||||
#[arg(long)]
|
||||
docker: bool,
|
||||
},
|
||||
|
||||
/// Start a CLI agent connected to Plano
|
||||
CliAgent {
|
||||
/// The type of CLI agent to start
|
||||
#[arg(value_parser = ["claude", "codex"])]
|
||||
agent_type: String,
|
||||
|
||||
/// Config file path (positional)
|
||||
file: Option<String>,
|
||||
|
||||
/// Path to the directory containing plano_config.yaml
|
||||
#[arg(long, default_value = ".")]
|
||||
path: String,
|
||||
|
||||
/// Additional settings as JSON string for the CLI agent
|
||||
#[arg(long, default_value = "{}")]
|
||||
settings: String,
|
||||
},
|
||||
|
||||
/// Manage distributed traces
|
||||
Trace {
|
||||
#[command(subcommand)]
|
||||
command: TraceCommand,
|
||||
},
|
||||
|
||||
/// Initialize a new Plano configuration
|
||||
Init {
|
||||
/// Use a built-in template
|
||||
#[arg(long)]
|
||||
template: Option<String>,
|
||||
|
||||
/// Create a clean empty config
|
||||
#[arg(long)]
|
||||
clean: bool,
|
||||
|
||||
/// Output file path
|
||||
#[arg(long, short)]
|
||||
output: Option<String>,
|
||||
|
||||
/// Overwrite existing files
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
|
||||
/// List available templates
|
||||
#[arg(long)]
|
||||
list_templates: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum TraceCommand {
|
||||
/// Start the OTLP trace listener
|
||||
Listen {
|
||||
/// Host to bind to
|
||||
#[arg(long, default_value = "0.0.0.0")]
|
||||
host: String,
|
||||
|
||||
/// Port to listen on
|
||||
#[arg(long, default_value_t = 4317)]
|
||||
port: u16,
|
||||
},
|
||||
|
||||
/// Stop the trace listener
|
||||
Down,
|
||||
|
||||
/// Show a specific trace
|
||||
Show {
|
||||
/// Trace ID to display
|
||||
trace_id: String,
|
||||
|
||||
/// Show verbose span details
|
||||
#[arg(long)]
|
||||
verbose: bool,
|
||||
},
|
||||
|
||||
/// Tail recent traces
|
||||
Tail {
|
||||
/// Include spans matching these patterns
|
||||
#[arg(long)]
|
||||
include_spans: Option<String>,
|
||||
|
||||
/// Exclude spans matching these patterns
|
||||
#[arg(long)]
|
||||
exclude_spans: Option<String>,
|
||||
|
||||
/// Filter by attribute key=value
|
||||
#[arg(long, name = "KEY=VALUE")]
|
||||
r#where: Vec<String>,
|
||||
|
||||
/// Show traces since (e.g. 10s, 5m, 1h)
|
||||
#[arg(long)]
|
||||
since: Option<String>,
|
||||
|
||||
/// Show verbose span details
|
||||
#[arg(long)]
|
||||
verbose: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
// Initialize logging
|
||||
let log_level = std::env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string());
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(&log_level)),
|
||||
)
|
||||
.init();
|
||||
|
||||
match cli.command {
|
||||
None => {
|
||||
print_logo();
|
||||
// Print help by re-parsing with --help
|
||||
let _ = Cli::parse_from(["planoai", "--help"]);
|
||||
Ok(())
|
||||
}
|
||||
Some(Command::Up {
|
||||
file,
|
||||
path,
|
||||
foreground,
|
||||
with_tracing,
|
||||
tracing_port,
|
||||
docker,
|
||||
}) => up::run(file, path, foreground, with_tracing, tracing_port, docker).await,
|
||||
Some(Command::Down { docker }) => down::run(docker).await,
|
||||
Some(Command::Build { docker }) => build::run(docker).await,
|
||||
Some(Command::Logs {
|
||||
debug,
|
||||
follow,
|
||||
docker,
|
||||
}) => logs::run(debug, follow, docker).await,
|
||||
Some(Command::CliAgent {
|
||||
agent_type,
|
||||
file,
|
||||
path,
|
||||
settings,
|
||||
}) => cli_agent::run(&agent_type, file, &path, &settings).await,
|
||||
Some(Command::Trace { command }) => match command {
|
||||
TraceCommand::Listen { host, port } => crate::trace::listen::run(&host, port).await,
|
||||
TraceCommand::Down => crate::trace::down::run().await,
|
||||
TraceCommand::Show { trace_id, verbose } => {
|
||||
crate::trace::show::run(&trace_id, verbose).await
|
||||
}
|
||||
TraceCommand::Tail {
|
||||
include_spans,
|
||||
exclude_spans,
|
||||
r#where,
|
||||
since,
|
||||
verbose,
|
||||
} => {
|
||||
crate::trace::tail::run(
|
||||
include_spans.as_deref(),
|
||||
exclude_spans.as_deref(),
|
||||
&r#where,
|
||||
since.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
Some(Command::Init {
|
||||
template,
|
||||
clean,
|
||||
output,
|
||||
force,
|
||||
list_templates,
|
||||
}) => init::run(template, clean, output, force, list_templates).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn print_logo() {
|
||||
let style = console::Style::new().bold().color256(141); // closest to #969FF4
|
||||
println!("{}", style.apply_to(LOGO));
|
||||
println!(" The Delivery Infrastructure for Agentic Apps\n");
|
||||
}
|
||||
174
crates/plano-cli/src/commands/up.rs
Normal file
174
crates/plano-cli/src/commands/up.rs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::consts::{
|
||||
DEFAULT_NATIVE_OTEL_TRACING_GRPC_ENDPOINT, DEFAULT_OTEL_TRACING_GRPC_ENDPOINT,
|
||||
};
|
||||
use crate::utils::{
|
||||
find_config_file, get_llm_provider_access_keys, is_port_in_use, load_env_file,
|
||||
print_cli_header, print_missing_keys,
|
||||
};
|
||||
|
||||
pub async fn run(
|
||||
file: Option<String>,
|
||||
path: String,
|
||||
foreground: bool,
|
||||
with_tracing: bool,
|
||||
tracing_port: u16,
|
||||
docker: bool,
|
||||
) -> Result<()> {
|
||||
let green = console::Style::new().green();
|
||||
let red = console::Style::new().red();
|
||||
let dim = console::Style::new().dim();
|
||||
let cyan = console::Style::new().cyan();
|
||||
|
||||
print_cli_header();
|
||||
|
||||
let plano_config_file = find_config_file(&path, file.as_deref());
|
||||
|
||||
if !plano_config_file.exists() {
|
||||
eprintln!(
|
||||
"{} Config file not found: {}",
|
||||
red.apply_to("✗"),
|
||||
dim.apply_to(plano_config_file.display().to_string())
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if !docker {
|
||||
eprint!("{}", dim.apply_to("Validating configuration..."));
|
||||
match crate::native::runner::validate_config(&plano_config_file) {
|
||||
Ok(()) => eprintln!(" {}", green.apply_to("✓")),
|
||||
Err(e) => {
|
||||
eprintln!("\n{} Validation failed", red.apply_to("✗"));
|
||||
eprintln!(" {}", dim.apply_to(format!("{e:#}")));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprint!("{}", dim.apply_to("Validating configuration (Docker)..."));
|
||||
match crate::docker::validate_config(&plano_config_file).await {
|
||||
Ok(()) => eprintln!(" {}", green.apply_to("✓")),
|
||||
Err(e) => {
|
||||
eprintln!("\n{} Validation failed", red.apply_to("✗"));
|
||||
eprintln!(" {}", dim.apply_to(format!("{e:#}")));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up environment
|
||||
let default_otel = if docker {
|
||||
DEFAULT_OTEL_TRACING_GRPC_ENDPOINT
|
||||
} else {
|
||||
DEFAULT_NATIVE_OTEL_TRACING_GRPC_ENDPOINT
|
||||
};
|
||||
|
||||
let mut env_stage: HashMap<String, String> = HashMap::new();
|
||||
env_stage.insert(
|
||||
"OTEL_TRACING_GRPC_ENDPOINT".to_string(),
|
||||
default_otel.to_string(),
|
||||
);
|
||||
|
||||
// Check access keys
|
||||
let access_keys = get_llm_provider_access_keys(&plano_config_file)?;
|
||||
let access_keys: Vec<String> = access_keys
|
||||
.into_iter()
|
||||
.map(|k| k.strip_prefix('$').unwrap_or(&k).to_string())
|
||||
.collect();
|
||||
let access_keys_set: std::collections::HashSet<_> = access_keys.into_iter().collect();
|
||||
|
||||
let mut missing_keys = Vec::new();
|
||||
if !access_keys_set.is_empty() {
|
||||
let app_env_file = if let Some(ref f) = file {
|
||||
Path::new(f).parent().unwrap_or(Path::new(".")).join(".env")
|
||||
} else {
|
||||
Path::new(&path).join(".env")
|
||||
};
|
||||
|
||||
if !app_env_file.exists() {
|
||||
for key in &access_keys_set {
|
||||
match std::env::var(key) {
|
||||
Ok(val) => {
|
||||
env_stage.insert(key.clone(), val);
|
||||
}
|
||||
Err(_) => missing_keys.push(key.clone()),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let env_dict = load_env_file(&app_env_file)?;
|
||||
for key in &access_keys_set {
|
||||
if let Some(val) = env_dict.get(key.as_str()) {
|
||||
env_stage.insert(key.clone(), val.clone());
|
||||
} else {
|
||||
missing_keys.push(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !missing_keys.is_empty() {
|
||||
print_missing_keys(&missing_keys);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
env_stage.insert(
|
||||
"LOG_LEVEL".to_string(),
|
||||
std::env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()),
|
||||
);
|
||||
|
||||
// Handle tracing
|
||||
if with_tracing {
|
||||
if is_port_in_use(tracing_port) {
|
||||
eprintln!(
|
||||
"{} Trace collector already running on port {}",
|
||||
green.apply_to("✓"),
|
||||
cyan.apply_to(tracing_port.to_string())
|
||||
);
|
||||
} else {
|
||||
match crate::trace::listen::start_background(tracing_port).await {
|
||||
Ok(()) => {
|
||||
eprintln!(
|
||||
"{} Trace collector listening on {}",
|
||||
green.apply_to("✓"),
|
||||
cyan.apply_to(format!("0.0.0.0:{tracing_port}"))
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} Failed to start trace collector on port {tracing_port}: {e}",
|
||||
red.apply_to("✗")
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tracing_host = if docker {
|
||||
"host.docker.internal"
|
||||
} else {
|
||||
"localhost"
|
||||
};
|
||||
env_stage.insert(
|
||||
"OTEL_TRACING_GRPC_ENDPOINT".to_string(),
|
||||
format!("http://{tracing_host}:{tracing_port}"),
|
||||
);
|
||||
}
|
||||
|
||||
// Build full env
|
||||
let mut env: HashMap<String, String> = std::env::vars().collect();
|
||||
env.remove("PATH");
|
||||
env.extend(env_stage);
|
||||
|
||||
if !docker {
|
||||
crate::native::runner::start_native(&plano_config_file, &env, foreground, with_tracing)
|
||||
.await?;
|
||||
} else {
|
||||
crate::docker::start_plano(&plano_config_file, &env, foreground).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue