mod auth;
mod bootstrap;
mod cli;
mod commands;
mod config;
mod setup;
use vta_sdk::client::VtaClient;
use vta_cli_common::render::{DIM, RESET};
use crate::cli::{
Cli, Commands, VtaCommands, install_force_exit_handler, is_online_template_cmd, print_banner,
requires_auth, retired_mediator_redirect,
};
use clap::Parser;
#[tokio::main]
async fn main() {
install_force_exit_handler();
if let Some(replacement) = retired_mediator_redirect(std::env::args()) {
eprintln!("`pnm mediator …` was retired in this release.");
eprintln!();
eprintln!("Run instead:");
eprintln!(" {replacement}");
eprintln!();
eprintln!("See `pnm services --help` for the full surface, or");
eprintln!("docs/03-integrating/runtime-service-management.md.");
std::process::exit(2);
}
let cli = Cli::parse();
vta_cli_common::render::set_full_display(cli.full_display);
if cli.json {
vta_cli_common::render::set_output_format(vta_cli_common::render::OutputFormat::Json);
}
vta_cli_common::render::set_bin_name("pnm");
let filter = if cli.verbose {
tracing_subscriber::EnvFilter::new("pnm_cli=debug")
} else {
tracing_subscriber::EnvFilter::from_default_env()
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.without_time()
.with_writer(std::io::stderr)
.init();
#[cfg(feature = "keyring")]
if let Err(e) = vta_sdk::keyring_init::install_default_store() {
eprintln!("warning: OS keyring unavailable: {e}");
}
print_banner();
let mut pnm_config = match config::load_config() {
Ok(c) => c,
Err(e) => {
eprintln!("Warning: could not load config: {e}");
config::PnmConfig::default()
}
};
let url_override = cli.url;
let vta_override = cli.vta;
let mut command = cli.command;
let needs_auth = requires_auth(&command);
match command {
Commands::Setup {
command: setup_cmd,
name,
overwrite,
} => {
let result = commands::setup::run(&mut pnm_config, setup_cmd, name, overwrite).await;
if let Err(e) = result {
vta_cli_common::render::print_cli_error(e.as_ref());
std::process::exit(1);
}
return;
}
Commands::Bootstrap { command: bs_cmd } => {
match commands::bootstrap::run_offline(&bs_cmd, &mut pnm_config).await {
Some(Ok(())) => return,
Some(Err(e)) => {
vta_cli_common::render::print_cli_error(e.as_ref());
std::process::exit(1);
}
None => {
command = Commands::Bootstrap { command: bs_cmd };
}
}
}
Commands::DidTemplates { command: dt_cmd } => {
if is_online_template_cmd(&dt_cmd) {
command = Commands::DidTemplates { command: dt_cmd };
} else {
if let Err(e) = commands::did_templates::run_offline(&dt_cmd) {
vta_cli_common::render::print_cli_error(e.as_ref());
std::process::exit(1);
}
return;
}
}
Commands::Vta { command: vta_cmd } => {
if commands::vta::run_offline(&mut pnm_config, vta_override.as_deref(), &vta_cmd).await
{
return;
}
command = Commands::Vta { command: vta_cmd };
}
other => {
command = other;
}
}
let (slug, vta_config) = match config::resolve_vta(vta_override.as_deref(), &pnm_config) {
Ok(v) => v,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
let keyring_key = config::vta_keyring_key(&slug);
eprintln!(" {DIM}VTA: {slug}{RESET}");
if let Some(ref did) = vta_config.vta_did {
eprintln!(" {DIM}DID: {did}{RESET}");
}
eprintln!();
let effective_url_override = url_override.as_deref().or(vta_config.url.as_deref());
let client = if needs_auth {
match auth::connect(effective_url_override, &keyring_key).await {
Ok(c) => c,
Err(e) => {
vta_cli_common::render::print_cli_error(e.as_ref());
std::process::exit(1);
}
}
} else {
let url = effective_url_override.unwrap_or_default().to_string();
VtaClient::new(&url)
};
let result = match command {
Commands::Setup { .. } => unreachable!("Setup handled in pre-auth dispatch"),
Commands::Bootstrap { command } => commands::bootstrap::run_authed(&client, command).await,
Commands::DidTemplates { command } => {
commands::did_templates::run_online(&client, command).await
}
Commands::Vta {
command: VtaCommands::Restart,
} => commands::vta::run_restart(&client).await,
Commands::Vta { .. } => unreachable!("VTA non-restart handled in pre-auth dispatch"),
Commands::Health => commands::health::run(effective_url_override, &keyring_key).await,
Commands::Auth { command } => commands::auth::run(&keyring_key, command),
Commands::Config { command } => commands::config::run(&client, command).await,
Commands::Services { command } => commands::services::run(&client, command).await,
Commands::Contexts { command } => commands::contexts::run(&client, command).await,
Commands::Acl { command } => commands::acl::run(&client, command).await,
Commands::AuthCredential { command } => {
commands::auth_credential::run(&client, command).await
}
Commands::Webvh { command } => commands::webvh::run(&client, command).await,
Commands::Audit { command } => commands::audit::run(&client, command).await,
Commands::Backup { command } => commands::backup::run(&client, command).await,
Commands::Keys { command } => commands::keys::run(&client, command).await,
};
client.shutdown().await;
if let Err(e) = result {
vta_cli_common::render::print_cli_error(e.as_ref());
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::{
AclCommands, AuthCommands, ConfigCommands, ContextCommands, KeyCommands, SetupCommands,
};
#[test]
fn test_requires_auth_health_false() {
assert!(!requires_auth(&Commands::Health));
}
#[test]
fn test_requires_auth_auth_status_false() {
let cmd = Commands::Auth {
command: AuthCommands::Status,
};
assert!(!requires_auth(&cmd));
}
#[test]
fn test_requires_auth_setup_false() {
let cmd = Commands::Setup {
command: None,
name: None,
overwrite: false,
};
assert!(!requires_auth(&cmd));
}
fn parse(args: &[&str]) -> Cli {
Cli::try_parse_from(args).expect("should parse")
}
#[test]
fn setup_bare_parses_all_none() {
let cli = parse(&["pnm", "setup"]);
match cli.command {
Commands::Setup {
command,
name,
overwrite,
} => {
assert!(command.is_none());
assert!(name.is_none());
assert!(!overwrite);
}
_ => panic!("expected Setup"),
}
}
#[test]
fn setup_with_name_parses_as_non_interactive_phase1() {
let cli = parse(&["pnm", "setup", "--name", "My VTA"]);
match cli.command {
Commands::Setup {
command,
name,
overwrite,
} => {
assert!(command.is_none());
assert_eq!(name.as_deref(), Some("My VTA"));
assert!(!overwrite);
}
_ => panic!("expected Setup"),
}
}
#[test]
fn setup_with_name_and_overwrite_parses() {
let cli = parse(&["pnm", "setup", "--name", "foo", "--overwrite"]);
match cli.command {
Commands::Setup {
name, overwrite, ..
} => {
assert_eq!(name.as_deref(), Some("foo"));
assert!(overwrite);
}
_ => panic!("expected Setup"),
}
}
#[test]
fn setup_continue_parses_interactive() {
let cli = parse(&["pnm", "setup", "continue", "my-vta"]);
match cli.command {
Commands::Setup {
command,
name,
overwrite,
} => {
assert!(matches!(
command,
Some(SetupCommands::Continue { vta_did: None, .. })
));
assert!(name.is_none());
assert!(!overwrite);
if let Some(SetupCommands::Continue { slug, .. }) = command {
assert_eq!(slug, "my-vta");
}
}
_ => panic!("expected Setup"),
}
}
#[test]
fn setup_continue_parses_non_interactive() {
let cli = parse(&[
"pnm",
"setup",
"continue",
"my-vta",
"--vta-did",
"did:webvh:abc:vta.example.com:primary",
]);
match cli.command {
Commands::Setup {
command: Some(SetupCommands::Continue { slug, vta_did, .. }),
..
} => {
assert_eq!(slug, "my-vta");
assert_eq!(
vta_did.as_deref(),
Some("did:webvh:abc:vta.example.com:primary")
);
}
_ => panic!("expected Setup+Continue"),
}
}
#[test]
fn setup_name_with_continue_subcommand_parses() {
let cli = parse(&["pnm", "setup", "--name", "foo", "continue", "bar"]);
match cli.command {
Commands::Setup { command, name, .. } => {
assert_eq!(name.as_deref(), Some("foo"));
assert!(matches!(command, Some(SetupCommands::Continue { .. })));
}
_ => panic!("expected Setup"),
}
}
#[test]
fn test_requires_auth_keys_true() {
let cmd = Commands::Keys {
command: KeyCommands::List {
limit: 50,
offset: 0,
status: None,
context: None,
},
};
assert!(requires_auth(&cmd));
}
#[test]
fn test_requires_auth_config_true() {
let cmd = Commands::Config {
command: ConfigCommands::Get,
};
assert!(requires_auth(&cmd));
}
#[test]
fn test_requires_auth_acl_true() {
let cmd = Commands::Acl {
command: AclCommands::List { context: None },
};
assert!(requires_auth(&cmd));
}
#[test]
fn test_requires_auth_contexts_true() {
let cmd = Commands::Contexts {
command: ContextCommands::List,
};
assert!(requires_auth(&cmd));
}
#[test]
fn test_requires_auth_vta_false() {
let cmd = Commands::Vta {
command: VtaCommands::List,
};
assert!(!requires_auth(&cmd));
}
}