use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "pnm-cli",
about = "CLI for managing a personal Verifiable Trust Agent"
)]
pub(crate) struct Cli {
#[arg(long, env = "VTA_URL")]
pub(crate) url: Option<String>,
#[arg(short, long, env = "PNM_VTA", global = true)]
pub(crate) vta: Option<String>,
#[arg(short = 'V', long, global = true)]
pub(crate) verbose: bool,
#[arg(long, global = true)]
pub(crate) full_display: bool,
#[arg(long, global = true)]
pub(crate) json: bool,
#[command(subcommand)]
pub(crate) command: Commands,
}
#[derive(Subcommand)]
pub(crate) enum Commands {
Setup {
#[command(subcommand)]
command: Option<SetupCommands>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
overwrite: bool,
},
Health,
Auth {
#[command(subcommand)]
command: AuthCommands,
},
Config {
#[command(subcommand)]
command: ConfigCommands,
},
Services {
#[command(subcommand)]
command: ServicesCommands,
},
Keys {
#[command(subcommand)]
command: KeyCommands,
},
Contexts {
#[command(subcommand)]
command: ContextCommands,
},
Acl {
#[command(subcommand)]
command: AclCommands,
},
StepUp {
#[command(subcommand)]
command: StepUpCommands,
},
Device {
#[command(subcommand)]
command: DeviceCommands,
},
Vault {
#[command(subcommand)]
command: VaultCommands,
},
AuthCredential {
#[command(subcommand)]
command: AuthCredentialCommands,
},
DidMgmt {
#[command(subcommand)]
command: DidMgmtCommands,
},
Audit {
#[command(subcommand)]
command: AuditCommands,
},
Backup {
#[command(subcommand)]
command: BackupCommands,
},
Vta {
#[command(subcommand)]
command: VtaCommands,
},
Bootstrap {
#[command(subcommand)]
command: BootstrapCommands,
},
#[command(name = "did-templates")]
DidTemplates {
#[command(subcommand)]
command: DidTemplateCommands,
},
}
#[derive(Subcommand)]
pub(crate) enum SetupCommands {
Continue {
slug: String,
#[arg(long)]
vta_did: Option<String>,
#[arg(long)]
vta_url: Option<String>,
#[arg(long)]
mediator_did: Option<String>,
},
}
#[derive(Subcommand)]
pub(crate) enum BootstrapCommands {
Request {
#[arg(long)]
out: std::path::PathBuf,
#[arg(long)]
label: Option<String>,
},
Open {
#[arg(long)]
bundle: std::path::PathBuf,
#[arg(long)]
expect_digest: Option<String>,
#[arg(long)]
no_verify_digest: bool,
#[arg(long)]
expect_vta_did: Option<String>,
},
Connect {
#[arg(long)]
vta_url: String,
#[arg(long)]
expect_digest: Option<String>,
#[arg(long)]
no_verify_digest: bool,
#[arg(long)]
expect_pcr0: Option<String>,
#[arg(long)]
expect_pcr8: Option<String>,
#[arg(long)]
slug: Option<String>,
},
ProvisionRequest {
#[arg(long)]
template: String,
#[arg(long = "var", value_name = "KEY=VALUE")]
vars: Vec<String>,
#[arg(long)]
context_hint: Option<String>,
#[arg(long)]
admin_template: Option<String>,
#[arg(long, value_name = "HOURS", default_value_t = 168.0)]
validity_hours: f64,
#[arg(long)]
label: Option<String>,
#[arg(long)]
out: std::path::PathBuf,
},
ProvisionIntegration {
#[arg(long)]
request: std::path::PathBuf,
#[arg(long)]
context: Option<String>,
#[arg(long, default_value = "did-signed")]
assertion: String,
#[arg(long, value_name = "SECONDS")]
vc_validity_seconds: Option<i64>,
#[arg(long)]
out: std::path::PathBuf,
#[arg(long)]
create_context: bool,
},
}
#[derive(Subcommand)]
pub(crate) enum DidTemplateCommands {
Validate {
file: std::path::PathBuf,
},
Init {
kind: String,
},
#[command(name = "list-builtins")]
ListBuiltins,
List {
#[arg(long)]
context: Option<String>,
},
Show {
name: String,
#[arg(long)]
context: Option<String>,
#[arg(long)]
rendered: bool,
#[arg(long = "var", value_parser = parse_key_value)]
vars: Vec<(String, String)>,
},
Create {
#[arg(long)]
file: std::path::PathBuf,
#[arg(long)]
context: Option<String>,
},
Update {
name: String,
#[arg(long)]
file: std::path::PathBuf,
#[arg(long)]
context: Option<String>,
},
Delete {
name: String,
#[arg(long)]
context: Option<String>,
},
Export {
name: String,
#[arg(long)]
context: Option<String>,
},
Diff {
name: String,
#[arg(long)]
file: std::path::PathBuf,
#[arg(long)]
context: Option<String>,
},
}
pub(crate) fn parse_key_value(s: &str) -> Result<(String, String), String> {
let (k, v) = s
.split_once('=')
.ok_or_else(|| format!("expected KEY=VALUE, got '{s}'"))?;
Ok((k.to_string(), v.to_string()))
}
#[derive(Subcommand)]
pub(crate) enum BackupCommands {
Export {
#[arg(long)]
include_audit: bool,
#[arg(short, long)]
output: Option<std::path::PathBuf>,
#[arg(long)]
use_trust_task: bool,
},
Import {
file: std::path::PathBuf,
#[arg(long)]
preview: bool,
#[arg(long)]
use_trust_task: bool,
},
}
#[derive(Subcommand)]
pub(crate) enum VtaCommands {
List,
Use { slug: String },
Remove { slug: String },
Info,
Restart,
}
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum WebvhCommands {
AddServer {
#[arg(long)]
id: String,
#[arg(long)]
did: String,
#[arg(long)]
label: Option<String>,
},
ListServers,
UpdateServer {
id: String,
#[arg(long)]
label: Option<String>,
},
RemoveServer {
id: String,
},
CreateDid {
#[arg(long)]
context: String,
#[arg(long)]
server: Option<String>,
#[arg(long)]
did_url: Option<String>,
#[arg(long)]
path: Option<String>,
#[arg(long)]
domain: Option<String>,
#[arg(long)]
label: Option<String>,
#[arg(long, default_value = "true")]
portable: bool,
#[arg(long)]
mediator_service: bool,
#[arg(long)]
services: Option<String>,
#[arg(long, default_value = "0")]
pre_rotation: u32,
#[arg(long)]
did_document: Option<String>,
#[arg(long)]
did_log: Option<String>,
#[arg(long)]
no_primary: bool,
#[arg(long)]
signing_key: Option<String>,
#[arg(long)]
ka_key: Option<String>,
#[arg(long)]
template: Option<String>,
#[arg(long)]
template_context: Option<String>,
#[arg(long = "var", value_parser = parse_key_value)]
vars: Vec<(String, String)>,
},
EditDid {
#[arg(long)]
did: String,
#[arg(long)]
document: Option<std::path::PathBuf>,
#[arg(long)]
options_file: Option<std::path::PathBuf>,
#[arg(long)]
pre_rotation: Option<u32>,
#[arg(long)]
ttl: Option<u32>,
#[arg(long = "watcher")]
watchers: Vec<String>,
#[arg(long)]
no_watchers: bool,
#[arg(long)]
label: Option<String>,
#[arg(long)]
no_confirm: bool,
},
RegisterDid {
#[arg(long)]
did: String,
#[arg(long)]
server: String,
#[arg(long, default_value_t = false)]
force: bool,
#[arg(long)]
domain: Option<String>,
},
ListDomains {
#[arg(long)]
server: String,
},
ListDids {
#[arg(long)]
context: Option<String>,
#[arg(long)]
server: Option<String>,
},
GetDid {
did: String,
},
DeleteDid {
did: String,
},
DidLog {
did: String,
#[arg(long)]
out: Option<std::path::PathBuf>,
},
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand)]
pub(crate) enum DidMgmtCommands {
Servers {
#[command(subcommand)]
command: DidMgmtServerCommands,
},
Dids {
#[command(subcommand)]
command: DidMgmtDidCommands,
},
}
#[derive(Subcommand)]
pub(crate) enum DidMgmtServerCommands {
Add {
#[arg(long)]
id: String,
#[arg(long)]
did: String,
#[arg(long)]
label: Option<String>,
},
List,
Update {
id: String,
#[arg(long)]
label: Option<String>,
},
Remove {
id: String,
},
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand)]
pub(crate) enum DidMgmtDidCommands {
Create {
#[arg(long)]
context: String,
#[arg(long)]
server: Option<String>,
#[arg(long)]
did_url: Option<String>,
#[arg(long)]
path: Option<String>,
#[arg(long)]
domain: Option<String>,
#[arg(long)]
label: Option<String>,
#[arg(long, default_value = "true")]
portable: bool,
#[arg(long)]
mediator_service: bool,
#[arg(long)]
services: Option<String>,
#[arg(long, default_value = "0")]
pre_rotation: u32,
#[arg(long)]
did_document: Option<String>,
#[arg(long)]
did_log: Option<String>,
#[arg(long)]
no_primary: bool,
#[arg(long)]
signing_key: Option<String>,
#[arg(long)]
ka_key: Option<String>,
#[arg(long)]
template: Option<String>,
#[arg(long)]
template_context: Option<String>,
#[arg(long = "var", value_parser = parse_key_value)]
vars: Vec<(String, String)>,
},
Edit {
#[arg(long)]
did: String,
#[arg(long)]
document: Option<std::path::PathBuf>,
#[arg(long)]
options_file: Option<std::path::PathBuf>,
#[arg(long)]
pre_rotation: Option<u32>,
#[arg(long)]
ttl: Option<u32>,
#[arg(long = "watcher")]
watchers: Vec<String>,
#[arg(long)]
no_watchers: bool,
#[arg(long)]
label: Option<String>,
#[arg(long)]
no_confirm: bool,
},
Register {
#[arg(long)]
did: String,
#[arg(long)]
server: String,
#[arg(long, default_value_t = false)]
force: bool,
#[arg(long)]
domain: Option<String>,
},
List {
#[arg(long)]
context: Option<String>,
#[arg(long)]
server: Option<String>,
},
Get {
did: String,
},
Delete {
did: String,
},
GetLog {
did: String,
#[arg(long)]
out: Option<std::path::PathBuf>,
},
ListDomains {
#[arg(long)]
server: String,
},
}
impl From<DidMgmtCommands> for WebvhCommands {
fn from(cmd: DidMgmtCommands) -> Self {
match cmd {
DidMgmtCommands::Servers { command } => match command {
DidMgmtServerCommands::Add { id, did, label } => {
WebvhCommands::AddServer { id, did, label }
}
DidMgmtServerCommands::List => WebvhCommands::ListServers,
DidMgmtServerCommands::Update { id, label } => {
WebvhCommands::UpdateServer { id, label }
}
DidMgmtServerCommands::Remove { id } => WebvhCommands::RemoveServer { id },
},
DidMgmtCommands::Dids { command } => match command {
DidMgmtDidCommands::Create {
context,
server,
did_url,
path,
domain,
label,
portable,
mediator_service,
services,
pre_rotation,
did_document,
did_log,
no_primary,
signing_key,
ka_key,
template,
template_context,
vars,
} => WebvhCommands::CreateDid {
context,
server,
did_url,
path,
domain,
label,
portable,
mediator_service,
services,
pre_rotation,
did_document,
did_log,
no_primary,
signing_key,
ka_key,
template,
template_context,
vars,
},
DidMgmtDidCommands::Edit {
did,
document,
options_file,
pre_rotation,
ttl,
watchers,
no_watchers,
label,
no_confirm,
} => WebvhCommands::EditDid {
did,
document,
options_file,
pre_rotation,
ttl,
watchers,
no_watchers,
label,
no_confirm,
},
DidMgmtDidCommands::Register {
did,
server,
force,
domain,
} => WebvhCommands::RegisterDid {
did,
server,
force,
domain,
},
DidMgmtDidCommands::List { context, server } => {
WebvhCommands::ListDids { context, server }
}
DidMgmtDidCommands::Get { did } => WebvhCommands::GetDid { did },
DidMgmtDidCommands::Delete { did } => WebvhCommands::DeleteDid { did },
DidMgmtDidCommands::GetLog { did, out } => WebvhCommands::DidLog { did, out },
DidMgmtDidCommands::ListDomains { server } => WebvhCommands::ListDomains { server },
},
}
}
}
#[derive(Subcommand)]
pub(crate) enum AuthCommands {
Logout,
Status,
SignChallenge {
challenge: String,
},
ShowToken,
}
#[derive(Subcommand)]
pub(crate) enum ConfigCommands {
Get,
Update {
#[arg(long)]
community_vta_did: Option<String>,
#[arg(long)]
community_vta_name: Option<String>,
#[arg(long)]
public_url: Option<String>,
},
ResolverUrl {
url: Option<String>,
#[arg(long, conflicts_with = "url")]
unset: bool,
},
}
#[derive(Subcommand)]
pub(crate) enum ServicesCommands {
List,
Rest {
#[command(subcommand)]
command: RestCommands,
},
Didcomm {
#[command(subcommand)]
command: DidcommCommands,
},
Webauthn {
#[command(subcommand)]
command: WebauthnCommands,
},
Report {
#[arg(long)]
since: Option<String>,
#[arg(long)]
until: Option<String>,
#[arg(long, default_value = "json")]
format: String,
},
}
#[derive(Subcommand)]
pub(crate) enum RestCommands {
Enable {
#[arg(long)]
url: String,
},
Update {
#[arg(long)]
url: String,
},
Disable,
Rollback,
}
#[derive(Subcommand)]
pub(crate) enum WebauthnCommands {
Enable {
#[arg(long)]
url: String,
},
Update {
#[arg(long)]
url: String,
},
Disable,
Rollback,
}
#[derive(Subcommand)]
pub(crate) enum DidcommCommands {
Enable {
#[arg(long)]
mediator_did: String,
#[arg(long)]
force: bool,
#[arg(long)]
handshake_timeout: Option<u64>,
},
Update {
#[arg(long = "mediator-did", visible_alias = "to")]
new_mediator_did: String,
#[arg(long, default_value_t = 86_400)]
drain_ttl: u64,
#[arg(long)]
force: bool,
#[arg(long)]
handshake_timeout: Option<u64>,
},
Disable {
#[arg(long, default_value_t = 86_400)]
drain_ttl: u64,
},
Rollback {
#[arg(long)]
drain_ttl: Option<u64>,
},
Drain {
#[command(subcommand)]
command: DrainCommands,
},
}
#[derive(Subcommand)]
pub(crate) enum DrainCommands {
List,
Cancel {
#[arg(long)]
mediator_did: String,
},
}
#[derive(Subcommand)]
pub(crate) enum ContextCommands {
List,
Get {
id: String,
},
Create {
#[arg(long)]
id: String,
#[arg(long)]
name: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
parent: Option<String>,
#[arg(long)]
admin_did: Option<String>,
#[arg(long)]
admin_label: Option<String>,
#[arg(long, requires = "admin_did")]
admin_expires: Option<String>,
},
Update {
id: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
did: Option<String>,
#[arg(long)]
description: Option<String>,
},
UpdateDid {
id: String,
did: String,
},
Delete {
id: String,
#[arg(long, short)]
force: bool,
},
Bootstrap {
#[arg(long)]
id: String,
#[arg(long)]
name: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
admin_label: Option<String>,
#[arg(long, conflicts_with_all = ["recipient_did", "recipient_nonce"])]
recipient: Option<std::path::PathBuf>,
#[arg(long, requires = "recipient_nonce", conflicts_with = "recipient")]
recipient_did: Option<String>,
#[arg(long, requires = "recipient_did", conflicts_with = "recipient")]
recipient_nonce: Option<String>,
},
Provision {
#[arg(long)]
id: String,
#[arg(long)]
name: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
admin_label: Option<String>,
#[arg(long)]
server: Option<String>,
#[arg(long)]
did_url: Option<String>,
#[arg(long, default_value = "true")]
portable: bool,
#[arg(long)]
mediator_service: bool,
#[arg(long, default_value = "0")]
pre_rotation: u32,
#[arg(long, conflicts_with_all = ["recipient_did", "recipient_nonce"])]
recipient: Option<std::path::PathBuf>,
#[arg(long, requires = "recipient_nonce", conflicts_with = "recipient")]
recipient_did: Option<String>,
#[arg(long, requires = "recipient_did", conflicts_with = "recipient")]
recipient_nonce: Option<String>,
},
Reprovision {
#[arg(long)]
id: String,
#[arg(long = "admin-key", alias = "key")]
admin_key: Option<String>,
#[arg(long)]
admin_label: Option<String>,
#[arg(long, conflicts_with_all = ["recipient_did", "recipient_nonce"])]
recipient: Option<std::path::PathBuf>,
#[arg(long, requires = "recipient_nonce", conflicts_with = "recipient")]
recipient_did: Option<String>,
#[arg(long, requires = "recipient_did", conflicts_with = "recipient")]
recipient_nonce: Option<String>,
},
}
#[derive(Subcommand)]
pub(crate) enum AclCommands {
List {
#[arg(long)]
context: Option<String>,
},
Get {
did: String,
},
Create {
#[arg(long)]
did: String,
#[arg(long)]
role: String,
#[arg(long)]
label: Option<String>,
#[arg(long, value_delimiter = ',')]
contexts: Vec<String>,
#[arg(long)]
expires: Option<String>,
#[arg(long)]
step_up_approver: Option<String>,
#[arg(long)]
step_up_require: Option<String>,
},
Update {
did: String,
#[arg(long)]
role: Option<String>,
#[arg(long)]
label: Option<String>,
#[arg(long, value_delimiter = ',')]
contexts: Option<Vec<String>>,
#[arg(long)]
step_up_approver: Option<String>,
#[arg(long)]
step_up_require: Option<String>,
},
Delete {
did: String,
},
}
#[derive(Subcommand)]
pub(crate) enum StepUpCommands {
Policy {
#[command(subcommand)]
command: PolicyCommands,
},
}
#[derive(Subcommand)]
pub(crate) enum PolicyCommands {
Show,
Set {
#[arg(long)]
from: String,
},
Disable,
}
#[derive(Subcommand)]
pub(crate) enum DeviceCommands {
List {
#[arg(long)]
service_kind: Option<String>,
},
Register {
#[arg(long, default_value = "ai-agent")]
service_kind: String,
#[arg(long)]
display_name: String,
#[arg(long)]
platform: Option<String>,
#[arg(long)]
hpke_public_key: Option<String>,
},
Disable {
device_id: String,
},
Wipe {
device_id: String,
#[arg(long)]
reason: String,
#[arg(long, default_value = "cache-and-keys")]
scope: String,
},
SetWake {
#[arg(long)]
gateway: String,
#[arg(long)]
handle: String,
#[arg(long, value_delimiter = ',')]
suggested_triggers: Vec<String>,
},
Heartbeat {
#[arg(long)]
platform: Option<String>,
},
}
#[derive(Subcommand)]
pub(crate) enum VaultCommands {
List {
#[arg(long)]
filters_file: Option<String>,
},
Get {
id: String,
},
Delete {
id: String,
#[arg(long)]
expected_version: Option<u32>,
},
Upsert {
#[arg(long)]
entry_file: String,
#[arg(long)]
secret_file: Option<String>,
},
Release {
id: String,
#[arg(long)]
target_file: Option<String>,
},
ProxyLogin {
#[arg(long)]
file: String,
},
SignTrustTask {
#[arg(long)]
file: String,
},
}
#[derive(Subcommand)]
pub(crate) enum AuthCredentialCommands {
Create {
#[arg(long)]
role: String,
#[arg(long)]
label: Option<String>,
#[arg(long, value_delimiter = ',')]
contexts: Vec<String>,
#[arg(long, conflicts_with_all = ["recipient_did", "recipient_nonce"])]
recipient: Option<std::path::PathBuf>,
#[arg(long, requires = "recipient_nonce", conflicts_with = "recipient")]
recipient_did: Option<String>,
#[arg(long, requires = "recipient_did", conflicts_with = "recipient")]
recipient_nonce: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub(crate) enum AuditCommands {
List {
#[arg(long)]
from: Option<u64>,
#[arg(long)]
to: Option<u64>,
#[arg(long)]
action: Option<String>,
#[arg(long)]
actor: Option<String>,
#[arg(long)]
outcome: Option<String>,
#[arg(long)]
context_id: Option<String>,
#[arg(long, default_value_t = 1)]
page: u64,
#[arg(long, default_value_t = 50)]
page_size: u64,
},
Retention {
#[command(subcommand)]
command: RetentionCommands,
},
}
#[derive(Subcommand, Debug)]
pub(crate) enum RetentionCommands {
Get,
Set {
#[arg(long)]
days: u32,
},
}
#[derive(Subcommand)]
pub(crate) enum KeyCommands {
Create {
#[arg(long)]
key_type: String,
#[arg(long)]
derivation_path: Option<String>,
#[arg(long)]
mnemonic: Option<String>,
#[arg(long)]
label: Option<String>,
#[arg(long = "context", alias = "context-id", value_name = "ID")]
context_id: Option<String>,
},
Import {
#[arg(long)]
key_type: String,
#[arg(long)]
private_key: Option<String>,
#[arg(long)]
private_key_file: Option<std::path::PathBuf>,
#[arg(long)]
label: Option<String>,
#[arg(long = "context", alias = "context-id", value_name = "ID")]
context_id: Option<String>,
},
Get {
key_id: String,
#[arg(long)]
secret: bool,
},
Revoke {
key_id: String,
},
Rename {
key_id: String,
new_key_id: String,
},
List {
#[arg(long, default_value = "50")]
limit: u64,
#[arg(long, default_value = "0")]
offset: u64,
#[arg(long)]
status: Option<String>,
#[arg(long)]
context: Option<String>,
},
Secrets {
key_ids: Vec<String>,
#[arg(long)]
context: Option<String>,
},
Bundle {
context: String,
#[arg(long, conflicts_with_all = ["recipient_did", "recipient_nonce"])]
recipient: Option<std::path::PathBuf>,
#[arg(long, requires = "recipient_nonce", conflicts_with = "recipient")]
recipient_did: Option<String>,
#[arg(long, requires = "recipient_did", conflicts_with = "recipient")]
recipient_nonce: Option<String>,
},
Seeds,
RotateSeed {
#[arg(long)]
mnemonic: Option<String>,
},
}
pub(crate) fn requires_auth(cmd: &Commands) -> bool {
if matches!(
cmd,
Commands::Vta {
command: VtaCommands::Restart
}
) {
return true;
}
if let Commands::DidTemplates { command } = cmd {
return is_online_template_cmd(command);
}
if let Commands::Bootstrap { command } = cmd {
return matches!(command, BootstrapCommands::ProvisionIntegration { .. });
}
if let Commands::Config {
command: ConfigCommands::ResolverUrl { .. },
} = cmd
{
return false;
}
!matches!(
cmd,
Commands::Health
| Commands::Auth { .. }
| Commands::Setup { .. }
| Commands::Vta { .. }
| Commands::Bootstrap { .. }
)
}
pub(crate) fn is_online_template_cmd(cmd: &DidTemplateCommands) -> bool {
!matches!(
cmd,
DidTemplateCommands::Validate { .. }
| DidTemplateCommands::Init { .. }
| DidTemplateCommands::ListBuiltins
)
}
pub(crate) fn retired_mediator_redirect<I, S>(args: I) -> Option<String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let argv: Vec<String> = args
.into_iter()
.map(|s| s.as_ref().to_string())
.skip(1) .filter(|a| !a.starts_with('-')) .collect();
if argv.first().map(|s| s.as_str()) != Some("mediator") {
return None;
}
Some(match argv.get(1).map(|s| s.as_str()) {
Some("migrate") => "pnm services didcomm update --mediator-did <did>".to_string(),
Some("rollback") => "pnm services didcomm rollback".to_string(),
Some("report") => "pnm services report".to_string(),
Some("drain") => match argv.get(2).map(|s| s.as_str()) {
Some("cancel") => "pnm services didcomm drain cancel --mediator-did <did>".to_string(),
Some("list") => "pnm services didcomm drain list".to_string(),
_ => "pnm services didcomm drain {list|cancel}".to_string(),
},
_ => "pnm services --help".to_string(),
})
}
pub(crate) fn print_banner() {
let cyan = "\x1b[36m";
let magenta = "\x1b[35m";
let yellow = "\x1b[33m";
let dim = "\x1b[2m";
let reset = "\x1b[0m";
eprintln!(
r#"
{cyan} ██████╗ {magenta}███╗ ██╗ {yellow}███╗ ███╗{reset}
{cyan} ██╔══██╗ {magenta}████╗ ██║ {yellow}████╗ ████║{reset}
{cyan} ██████╔╝ {magenta}██╔██╗ ██║ {yellow}██╔████╔██║{reset}
{cyan} ██╔═══╝ {magenta}██║╚██╗██║ {yellow}██║╚██╔╝██║{reset}
{cyan} ██║ {magenta}██║ ╚████║ {yellow}██║ ╚═╝ ██║{reset}
{cyan} ╚═╝ {magenta}╚═╝ ╚═══╝ {yellow}╚═╝ ╚═╝{reset}
{dim} Personal Network Manager v{version}{reset}
"#,
version = env!("CARGO_PKG_VERSION"),
);
}
pub(crate) fn install_force_exit_handler() {
use std::sync::atomic::{AtomicBool, Ordering};
static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false);
tokio::spawn(async {
loop {
if tokio::signal::ctrl_c().await.is_err() {
return;
}
if SHUTDOWN_REQUESTED.swap(true, Ordering::SeqCst) {
eprintln!("\nForcing exit.");
std::process::exit(130);
}
eprintln!("\nShutting down — press Ctrl-C again to force exit.");
}
});
}