use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand};
use tonin::commands;
#[derive(Parser)]
#[command(
name = "tonin",
version,
about = "Build gRPC microservices for Kubernetes — the tonin CLI",
long_about = "The tonin CLI scaffolds gRPC microservices and manages their lifecycle.
COMMON WORKFLOWS
Scaffold and run locally:
tonin service new greeter # creates ./greeter/ with proto + impl
cd greeter
cargo run -p greeter # boots gRPC + MCP locally
Render Helm chart and deploy (built-in, no separate install needed):
tonin helm generate # writes ./chart/ from tonin.toml
tonin helm diff --env prod # show what would change
tonin helm upgrade --env prod # helm upgrade --install
PLUGINS
tonin dispatches any unknown subcommand to a `tonin-<name>` binary on
$PATH. Install a plugin with `cargo install tonin-<name>`, then use it
as a first-class subcommand:
tonin myplugin <args> # delegates to tonin-myplugin on PATH
List installed plugins:
tonin plugin list
tonin plugin list --verbose # includes per-plugin descriptions
INTROSPECTION FOR CODING AGENTS
Every subcommand supports `--help`. For a machine-readable manifest of
every command + arg + prerequisite, use:
tonin describe # text
tonin describe --format json # JSON
ENVIRONMENT
TONIN_ENV Default overlay env for Helm rendering (overlaid by --env).
TONIN_TELEMETRY Set to 'off' to disable OTLP exports in scaffolded svcs.
OTEL_EXPORTER_OTLP_ENDPOINT Where scaffolded services send traces.
SEE ALSO
docs/00-overview.md — landing page for capability docs.
https://github.com/Rushit/tonin — source, issues, releases."
)]
struct Cli {
#[command(subcommand)]
cmd: TopCmd,
}
#[derive(Subcommand)]
enum TopCmd {
Proto {
#[command(subcommand)]
cmd: commands::proto::ProtoCmd,
},
Service {
#[command(subcommand)]
cmd: commands::service::ServiceCmd,
},
Describe(commands::describe::DescribeArgs),
Plugin {
#[command(subcommand)]
cmd: commands::plugin::PluginCmd,
},
Upgrade(commands::upgrade::UpgradeArgs),
Doctor(commands::doctor::DoctorArgs),
Helm {
#[command(subcommand)]
cmd: commands::helm::HelmCmd,
},
Affected(commands::affected::AffectedArgs),
Deploy(commands::deploy::DeployArgs),
Build(commands::build::BuildArgs),
Status(commands::status::StatusArgs),
Release(commands::release::ReleaseArgs),
Run(commands::run::RunArgs),
Test(commands::run_tests::TestArgs),
Observe(commands::observe::ObserveArgs),
GrpcUi(commands::grpc_ui::GrpcUiArgs),
Platform(commands::platform::PlatformArgs),
#[command(hide = true)]
K8s {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
_args: Vec<String>,
},
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_target(false)
.init();
match Cli::try_parse() {
Ok(cli) => dispatch(cli),
Err(err) => {
if err.kind() == clap::error::ErrorKind::InvalidSubcommand
&& let Some(name) = first_positional_arg()
{
return try_exec_plugin(&name);
}
err.exit()
}
}
}
fn dispatch(cli: Cli) -> Result<()> {
match cli.cmd {
TopCmd::Proto { cmd } => commands::proto::run(cmd),
TopCmd::Service { cmd } => commands::service::run(cmd),
TopCmd::Describe(args) => commands::describe::run(args, &Cli::command()),
TopCmd::Plugin { cmd } => commands::plugin::run(cmd),
TopCmd::Upgrade(args) => commands::upgrade::run(args),
TopCmd::Doctor(args) => commands::doctor::run(args),
TopCmd::Helm { cmd } => commands::helm::run(cmd),
TopCmd::Affected(args) => commands::affected::run(args),
TopCmd::Deploy(args) => commands::deploy::run(args),
TopCmd::Build(args) => commands::build::run(args),
TopCmd::Status(args) => commands::status::run(args),
TopCmd::Release(args) => commands::release::run(args),
TopCmd::Run(args) => commands::run::run(args),
TopCmd::Test(args) => commands::run_tests::run(args),
TopCmd::Observe(args) => commands::observe::run(args),
TopCmd::GrpcUi(args) => commands::grpc_ui::run(args),
TopCmd::Platform(args) => commands::platform::run(args),
TopCmd::K8s { .. } => {
eprintln!("error: `tonin k8s` has been removed.");
eprintln!();
eprintln!(" Kubernetes manifest generation is now `tonin helm` (built-in):");
eprintln!(" tonin helm generate # was: tonin k8s generate");
eprintln!(" tonin helm diff --env prod # was: tonin k8s diff");
eprintln!(" tonin helm upgrade --env prod # was: tonin k8s apply");
std::process::exit(1);
}
}
}
fn first_positional_arg() -> Option<String> {
std::env::args().skip(1).find(|a| !a.starts_with('-'))
}
fn try_exec_plugin(name: &str) -> Result<()> {
match commands::plugin::find_plugin(name) {
Some(bin) => exec_plugin(bin, name),
None => {
eprintln!("error: unknown subcommand '{name}'");
eprintln!(" hint: install `tonin-{name}` to use it as a plugin, e.g.:");
eprintln!(" cargo install tonin-{name}");
std::process::exit(1);
}
}
}
#[cfg(unix)]
fn exec_plugin(bin: std::path::PathBuf, name: &str) -> Result<()> {
use std::os::unix::process::CommandExt;
let plugin_args: Vec<std::ffi::OsString> = std::env::args_os().skip(2).collect();
let cwd = std::env::current_dir()?;
let err = std::process::Command::new(&bin)
.args(&plugin_args)
.env("TONIN_SERVICE_DIR", &cwd)
.env("TONIN_CLI_VERSION", env!("CARGO_PKG_VERSION"))
.exec();
anyhow::bail!("failed to exec tonin-{name} ({}): {err}", bin.display())
}
#[cfg(not(unix))]
fn exec_plugin(bin: std::path::PathBuf, name: &str) -> Result<()> {
let plugin_args: Vec<std::ffi::OsString> = std::env::args_os().skip(2).collect();
let cwd = std::env::current_dir()?;
let status = std::process::Command::new(&bin)
.args(&plugin_args)
.env("TONIN_SERVICE_DIR", &cwd)
.env("TONIN_CLI_VERSION", env!("CARGO_PKG_VERSION"))
.status()
.map_err(|e| anyhow::anyhow!("failed to spawn tonin-{name} ({}): {e}", bin.display()))?;
std::process::exit(status.code().unwrap_or(1));
}