2026-05-15 15:08:20 +02:00
|
|
|
use anyhow::{Context, Result, bail};
|
|
|
|
|
use clap::{Args, Parser, Subcommand};
|
|
|
|
|
use geth_config::GethPaths;
|
|
|
|
|
use geth_control::{ControlRequest, ControlResponse};
|
2026-05-16 00:17:08 +02:00
|
|
|
use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport};
|
2026-05-15 15:08:20 +02:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Parser)]
|
|
|
|
|
#[command(name = "geth", about = "Personal local-first Iroh mesh runtime")]
|
|
|
|
|
pub struct Cli {
|
|
|
|
|
#[arg(long, global = true)]
|
|
|
|
|
pub json: bool,
|
|
|
|
|
#[arg(long, global = true)]
|
|
|
|
|
pub jsonl: bool,
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
pub command: Command,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum Command {
|
|
|
|
|
Init,
|
|
|
|
|
Daemon {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: DaemonCommand,
|
|
|
|
|
},
|
|
|
|
|
Status,
|
|
|
|
|
Node {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: NodeCommand,
|
|
|
|
|
},
|
|
|
|
|
Resource {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: ResourceCommand,
|
|
|
|
|
},
|
|
|
|
|
Keychain {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: KeychainCommand,
|
|
|
|
|
},
|
|
|
|
|
Auth {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: AuthCommand,
|
|
|
|
|
},
|
|
|
|
|
Secret {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: SecretCommand,
|
|
|
|
|
},
|
|
|
|
|
Cas {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: CasCommand,
|
|
|
|
|
},
|
|
|
|
|
Kv {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: KvCommand,
|
|
|
|
|
},
|
|
|
|
|
Pubsub {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: PubsubCommand,
|
|
|
|
|
},
|
|
|
|
|
Pipe {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: PipeCommand,
|
|
|
|
|
},
|
|
|
|
|
Db {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: DbCommand,
|
|
|
|
|
},
|
|
|
|
|
Document {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: DocumentCommand,
|
|
|
|
|
},
|
|
|
|
|
Ssh {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: SshCommand,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum DaemonCommand {
|
|
|
|
|
Run,
|
2026-05-16 00:17:08 +02:00
|
|
|
Service {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: ServiceCommand,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum ServiceCommand {
|
|
|
|
|
Install {
|
|
|
|
|
#[arg(long, default_value = "auto")]
|
|
|
|
|
manager: String,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
bin: Option<PathBuf>,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
start: bool,
|
|
|
|
|
},
|
|
|
|
|
Uninstall {
|
|
|
|
|
#[arg(long, default_value = "auto")]
|
|
|
|
|
manager: String,
|
|
|
|
|
},
|
|
|
|
|
Start {
|
|
|
|
|
#[arg(long, default_value = "auto")]
|
|
|
|
|
manager: String,
|
|
|
|
|
},
|
|
|
|
|
Stop {
|
|
|
|
|
#[arg(long, default_value = "auto")]
|
|
|
|
|
manager: String,
|
|
|
|
|
},
|
|
|
|
|
Status {
|
|
|
|
|
#[arg(long, default_value = "auto")]
|
|
|
|
|
manager: String,
|
|
|
|
|
},
|
|
|
|
|
Print {
|
|
|
|
|
#[arg(long, default_value = "auto")]
|
|
|
|
|
manager: String,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
bin: Option<PathBuf>,
|
|
|
|
|
},
|
2026-05-15 15:08:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum NodeCommand {
|
|
|
|
|
Id,
|
|
|
|
|
Status,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum ResourceCommand {
|
|
|
|
|
List,
|
|
|
|
|
Create { kind: String, name: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum KeychainCommand {
|
2026-05-16 16:34:20 +02:00
|
|
|
Init {
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
admin_key: Option<PathBuf>,
|
|
|
|
|
},
|
2026-05-15 15:08:20 +02:00
|
|
|
Status,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum AuthCommand {
|
|
|
|
|
Explain {
|
|
|
|
|
subject: String,
|
|
|
|
|
resource: String,
|
|
|
|
|
capability: String,
|
|
|
|
|
},
|
2026-05-16 16:32:03 +02:00
|
|
|
Grant {
|
|
|
|
|
subject: String,
|
|
|
|
|
resource: String,
|
|
|
|
|
capability: String,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
grant_id: Option<String>,
|
|
|
|
|
},
|
|
|
|
|
Revoke {
|
|
|
|
|
resource: String,
|
|
|
|
|
grant_id: String,
|
|
|
|
|
},
|
2026-05-15 15:08:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum SecretCommand {
|
|
|
|
|
Status,
|
2026-05-17 02:58:58 +02:00
|
|
|
Create {
|
|
|
|
|
resource: String,
|
|
|
|
|
},
|
|
|
|
|
Rotate {
|
|
|
|
|
resource: String,
|
|
|
|
|
},
|
|
|
|
|
Bearer {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: SecretBearerCommand,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum SecretBearerCommand {
|
|
|
|
|
Create {
|
|
|
|
|
resource: String,
|
|
|
|
|
#[arg(long = "capability", required = true)]
|
|
|
|
|
capabilities: Vec<String>,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
expires_at_ms: Option<i64>,
|
|
|
|
|
},
|
|
|
|
|
List,
|
|
|
|
|
Revoke {
|
|
|
|
|
resource: String,
|
|
|
|
|
secret: String,
|
|
|
|
|
},
|
2026-05-15 15:08:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum CasCommand {
|
|
|
|
|
Add {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
},
|
|
|
|
|
Get {
|
|
|
|
|
hash: String,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
out: PathBuf,
|
|
|
|
|
},
|
|
|
|
|
Hash {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
},
|
|
|
|
|
Has {
|
|
|
|
|
hash: String,
|
|
|
|
|
},
|
2026-05-16 16:36:35 +02:00
|
|
|
Pin {
|
|
|
|
|
hash: String,
|
|
|
|
|
},
|
|
|
|
|
Unpin {
|
|
|
|
|
hash: String,
|
|
|
|
|
},
|
2026-05-16 21:10:25 +02:00
|
|
|
Cleanup {
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
dry_run: bool,
|
|
|
|
|
},
|
2026-05-15 15:08:20 +02:00
|
|
|
List,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum KvCommand {
|
|
|
|
|
Create {
|
|
|
|
|
name: String,
|
|
|
|
|
},
|
|
|
|
|
Set {
|
|
|
|
|
name: String,
|
|
|
|
|
key: String,
|
|
|
|
|
value: String,
|
|
|
|
|
},
|
|
|
|
|
Get {
|
|
|
|
|
name: String,
|
|
|
|
|
key: String,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum PubsubCommand {
|
|
|
|
|
Pub { topic: String, message: String },
|
|
|
|
|
Sub { topic: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum PipeCommand {
|
|
|
|
|
Listen { name: String },
|
|
|
|
|
Connect { target: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum DbCommand {
|
|
|
|
|
Add { name: String, path: PathBuf },
|
|
|
|
|
Status { name: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum DocumentCommand {
|
|
|
|
|
Create { name: String },
|
|
|
|
|
Status { name: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum SshCommand {
|
2026-05-16 00:17:08 +02:00
|
|
|
Proxy {
|
|
|
|
|
node: String,
|
|
|
|
|
},
|
|
|
|
|
Cert {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: SshCertCommand,
|
|
|
|
|
},
|
|
|
|
|
Revocation {
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: SshRevocationCommand,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum SshCertCommand {
|
|
|
|
|
Request {
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
public_key: PathBuf,
|
|
|
|
|
#[arg(long, default_value = "user")]
|
|
|
|
|
kind: String,
|
|
|
|
|
#[arg(long = "principal", required = true)]
|
|
|
|
|
principals: Vec<String>,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
valid_for: Option<String>,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
renewal_of: Option<String>,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
reason: Option<String>,
|
|
|
|
|
},
|
|
|
|
|
Requests,
|
|
|
|
|
Approve {
|
|
|
|
|
request_id: String,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
ca_key: PathBuf,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
valid_for: Option<String>,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
serial: Option<u64>,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
out: Option<PathBuf>,
|
|
|
|
|
},
|
|
|
|
|
Import {
|
|
|
|
|
request_id: String,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
cert: PathBuf,
|
|
|
|
|
},
|
|
|
|
|
List,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
|
|
|
pub enum SshRevocationCommand {
|
|
|
|
|
Add {
|
|
|
|
|
kind: String,
|
|
|
|
|
target: String,
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
reason: Option<String>,
|
|
|
|
|
},
|
|
|
|
|
List,
|
|
|
|
|
Export {
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
out: PathBuf,
|
2026-05-17 18:29:47 +02:00
|
|
|
#[arg(long, default_value = "jsonl")]
|
|
|
|
|
format: String,
|
2026-05-16 00:17:08 +02:00
|
|
|
},
|
2026-05-15 15:08:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Args)]
|
|
|
|
|
pub struct EmptyArgs {}
|
|
|
|
|
|
|
|
|
|
pub async fn run() -> Result<()> {
|
|
|
|
|
let cli = Cli::parse();
|
|
|
|
|
let paths = GethPaths::resolve().context("resolve geth paths")?;
|
|
|
|
|
match cli.command {
|
|
|
|
|
Command::Init => {
|
|
|
|
|
let node = geth_node::init_node(&paths).context("initialize geth node")?;
|
|
|
|
|
println!("initialized geth home: {}", node.paths.home().display());
|
|
|
|
|
println!("agent: {}", node.agent_id);
|
|
|
|
|
println!("node: {}", node.node_id);
|
|
|
|
|
}
|
|
|
|
|
Command::Daemon {
|
|
|
|
|
command: DaemonCommand::Run,
|
|
|
|
|
} => {
|
|
|
|
|
geth_node::run_daemon(paths)
|
|
|
|
|
.await
|
|
|
|
|
.context("run geth daemon")?;
|
|
|
|
|
}
|
2026-05-16 00:17:08 +02:00
|
|
|
Command::Daemon {
|
|
|
|
|
command: DaemonCommand::Service { command },
|
|
|
|
|
} => {
|
|
|
|
|
let report =
|
|
|
|
|
run_service_command(&paths, command).context("manage geth user service")?;
|
|
|
|
|
print_service_report(report, cli.json || cli.jsonl)?;
|
|
|
|
|
}
|
2026-05-15 15:08:20 +02:00
|
|
|
command => {
|
|
|
|
|
let request = request_for_command(command)?;
|
|
|
|
|
let response = geth_node::send_control(&paths, request)
|
|
|
|
|
.await
|
|
|
|
|
.with_context(|| {
|
|
|
|
|
format!("connect to daemon at {}", paths.socket_path().display())
|
|
|
|
|
})?;
|
|
|
|
|
print_response(response, cli.json || cli.jsonl)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|
|
|
|
Ok(match command {
|
|
|
|
|
Command::Status => ControlRequest::Status,
|
|
|
|
|
Command::Node {
|
|
|
|
|
command: NodeCommand::Id,
|
|
|
|
|
} => ControlRequest::NodeId,
|
|
|
|
|
Command::Node {
|
|
|
|
|
command: NodeCommand::Status,
|
|
|
|
|
} => ControlRequest::Status,
|
|
|
|
|
Command::Resource {
|
|
|
|
|
command: ResourceCommand::List,
|
|
|
|
|
} => ControlRequest::ResourceList,
|
|
|
|
|
Command::Resource {
|
|
|
|
|
command: ResourceCommand::Create { kind, name },
|
|
|
|
|
} => ControlRequest::ResourceCreate { kind, name },
|
|
|
|
|
Command::Keychain {
|
2026-05-16 16:34:20 +02:00
|
|
|
command: KeychainCommand::Init { admin_key },
|
|
|
|
|
} => ControlRequest::KeychainInit {
|
|
|
|
|
admin_key_path: admin_key,
|
2026-05-15 15:08:20 +02:00
|
|
|
},
|
|
|
|
|
Command::Keychain {
|
|
|
|
|
command: KeychainCommand::Status,
|
|
|
|
|
} => ControlRequest::KeychainStatus,
|
|
|
|
|
Command::Auth {
|
|
|
|
|
command:
|
|
|
|
|
AuthCommand::Explain {
|
|
|
|
|
subject,
|
|
|
|
|
resource,
|
|
|
|
|
capability,
|
|
|
|
|
},
|
|
|
|
|
} => ControlRequest::AuthExplain {
|
|
|
|
|
subject,
|
|
|
|
|
resource,
|
|
|
|
|
capability,
|
|
|
|
|
},
|
2026-05-16 16:32:03 +02:00
|
|
|
Command::Auth {
|
|
|
|
|
command:
|
|
|
|
|
AuthCommand::Grant {
|
|
|
|
|
subject,
|
|
|
|
|
resource,
|
|
|
|
|
capability,
|
|
|
|
|
grant_id,
|
|
|
|
|
},
|
|
|
|
|
} => ControlRequest::AuthGrant {
|
|
|
|
|
subject,
|
|
|
|
|
resource,
|
|
|
|
|
capability,
|
|
|
|
|
grant_id,
|
|
|
|
|
},
|
|
|
|
|
Command::Auth {
|
|
|
|
|
command: AuthCommand::Revoke { resource, grant_id },
|
|
|
|
|
} => ControlRequest::AuthRevoke { resource, grant_id },
|
2026-05-16 22:18:49 +02:00
|
|
|
Command::Secret { command } => match command {
|
|
|
|
|
SecretCommand::Status => ControlRequest::SecretStatus,
|
|
|
|
|
SecretCommand::Create { resource } => ControlRequest::SecretCreate { resource },
|
|
|
|
|
SecretCommand::Rotate { resource } => ControlRequest::SecretRotate { resource },
|
2026-05-17 02:58:58 +02:00
|
|
|
SecretCommand::Bearer { command } => match command {
|
|
|
|
|
SecretBearerCommand::Create {
|
|
|
|
|
resource,
|
|
|
|
|
capabilities,
|
|
|
|
|
expires_at_ms,
|
|
|
|
|
} => ControlRequest::SecretBearerCreate {
|
|
|
|
|
resource,
|
|
|
|
|
capabilities,
|
|
|
|
|
expires_at_ms,
|
|
|
|
|
},
|
|
|
|
|
SecretBearerCommand::List => ControlRequest::SecretBearerList,
|
|
|
|
|
SecretBearerCommand::Revoke { resource, secret } => {
|
|
|
|
|
ControlRequest::SecretBearerRevoke { resource, secret }
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-05-15 15:08:20 +02:00
|
|
|
},
|
|
|
|
|
Command::Cas { command } => match command {
|
|
|
|
|
CasCommand::Add { path } => ControlRequest::CasAdd { path },
|
|
|
|
|
CasCommand::Get { hash, out } => ControlRequest::CasGet {
|
|
|
|
|
hash: hash.into(),
|
|
|
|
|
out,
|
|
|
|
|
},
|
|
|
|
|
CasCommand::Hash { path } => ControlRequest::CasHash { path },
|
|
|
|
|
CasCommand::Has { hash } => ControlRequest::CasHas { hash: hash.into() },
|
2026-05-16 16:36:35 +02:00
|
|
|
CasCommand::Pin { hash } => ControlRequest::CasPin { hash: hash.into() },
|
|
|
|
|
CasCommand::Unpin { hash } => ControlRequest::CasUnpin { hash: hash.into() },
|
2026-05-16 21:10:25 +02:00
|
|
|
CasCommand::Cleanup { dry_run } => ControlRequest::CasCleanup { dry_run },
|
2026-05-15 15:08:20 +02:00
|
|
|
CasCommand::List => ControlRequest::CasList,
|
|
|
|
|
},
|
2026-05-16 21:52:35 +02:00
|
|
|
Command::Kv { command } => match command {
|
|
|
|
|
KvCommand::Create { name } => ControlRequest::KvCreate { name },
|
|
|
|
|
KvCommand::Set { name, key, value } => ControlRequest::KvSet { name, key, value },
|
|
|
|
|
KvCommand::Get { name, key } => ControlRequest::KvGet { name, key },
|
2026-05-15 15:08:20 +02:00
|
|
|
},
|
2026-05-17 18:23:51 +02:00
|
|
|
Command::Pubsub { command } => match command {
|
|
|
|
|
PubsubCommand::Pub { topic, message } => ControlRequest::PubsubPub { topic, message },
|
|
|
|
|
PubsubCommand::Sub { topic } => ControlRequest::PubsubSub { topic },
|
2026-05-15 15:08:20 +02:00
|
|
|
},
|
|
|
|
|
Command::Pipe { command } => ControlRequest::ModuleStub {
|
|
|
|
|
module: "pipe".to_owned(),
|
|
|
|
|
command: format!("{command:?}"),
|
|
|
|
|
},
|
2026-05-16 21:13:33 +02:00
|
|
|
Command::Db { command } => match command {
|
|
|
|
|
DbCommand::Add { name, path } => ControlRequest::DbAdd { name, path },
|
|
|
|
|
DbCommand::Status { name } => ControlRequest::DbStatus { name },
|
2026-05-15 15:08:20 +02:00
|
|
|
},
|
2026-05-16 22:15:18 +02:00
|
|
|
Command::Document { command } => match command {
|
|
|
|
|
DocumentCommand::Create { name } => ControlRequest::DocumentCreate { name },
|
|
|
|
|
DocumentCommand::Status { name } => ControlRequest::DocumentStatus { name },
|
2026-05-15 15:08:20 +02:00
|
|
|
},
|
2026-05-16 00:17:08 +02:00
|
|
|
Command::Ssh { command } => match command {
|
|
|
|
|
SshCommand::Proxy { node } => ControlRequest::ModuleStub {
|
|
|
|
|
module: "ssh-proxy".to_owned(),
|
|
|
|
|
command: format!("proxy {node}"),
|
|
|
|
|
},
|
|
|
|
|
SshCommand::Cert { command } => match command {
|
|
|
|
|
SshCertCommand::Request {
|
|
|
|
|
public_key,
|
|
|
|
|
kind,
|
|
|
|
|
principals,
|
|
|
|
|
valid_for,
|
|
|
|
|
renewal_of,
|
|
|
|
|
reason,
|
|
|
|
|
} => ControlRequest::SshCertRequest {
|
|
|
|
|
public_key_path: public_key,
|
|
|
|
|
cert_kind: kind,
|
|
|
|
|
principals,
|
|
|
|
|
requested_validity: valid_for,
|
|
|
|
|
renewal_of,
|
|
|
|
|
reason,
|
|
|
|
|
},
|
|
|
|
|
SshCertCommand::Requests => ControlRequest::SshCertRequests,
|
|
|
|
|
SshCertCommand::Approve {
|
|
|
|
|
request_id,
|
|
|
|
|
ca_key,
|
|
|
|
|
valid_for,
|
|
|
|
|
serial,
|
|
|
|
|
out,
|
|
|
|
|
} => ControlRequest::SshCertApprove {
|
|
|
|
|
request_id,
|
|
|
|
|
ca_key_path: ca_key,
|
|
|
|
|
valid_for,
|
|
|
|
|
serial,
|
|
|
|
|
out,
|
|
|
|
|
},
|
|
|
|
|
SshCertCommand::Import { request_id, cert } => ControlRequest::SshCertImport {
|
|
|
|
|
request_id,
|
|
|
|
|
cert_path: cert,
|
|
|
|
|
},
|
|
|
|
|
SshCertCommand::List => ControlRequest::SshCertList,
|
|
|
|
|
},
|
|
|
|
|
SshCommand::Revocation { command } => match command {
|
|
|
|
|
SshRevocationCommand::Add {
|
|
|
|
|
kind,
|
|
|
|
|
target,
|
|
|
|
|
reason,
|
|
|
|
|
} => ControlRequest::SshRevocationAdd {
|
|
|
|
|
kind,
|
|
|
|
|
target,
|
|
|
|
|
reason,
|
|
|
|
|
},
|
|
|
|
|
SshRevocationCommand::List => ControlRequest::SshRevocationList,
|
2026-05-17 18:29:47 +02:00
|
|
|
SshRevocationCommand::Export { out, format } => {
|
|
|
|
|
ControlRequest::SshRevocationExport { out, format }
|
|
|
|
|
}
|
2026-05-16 00:17:08 +02:00
|
|
|
},
|
2026-05-15 15:08:20 +02:00
|
|
|
},
|
|
|
|
|
Command::Init | Command::Daemon { .. } => bail!("command is handled directly"),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 00:17:08 +02:00
|
|
|
fn run_service_command(paths: &GethPaths, command: ServiceCommand) -> Result<ServiceReport> {
|
|
|
|
|
Ok(match command {
|
|
|
|
|
ServiceCommand::Install {
|
|
|
|
|
manager,
|
|
|
|
|
bin,
|
|
|
|
|
start,
|
|
|
|
|
} => {
|
|
|
|
|
geth_node::init_node(paths).context("initialize geth home before service install")?;
|
|
|
|
|
let manager = manager.parse::<ServiceManager>()?;
|
|
|
|
|
let executable = service_executable(bin)?;
|
|
|
|
|
geth_node::service::install_user_service(
|
|
|
|
|
paths,
|
|
|
|
|
ServiceInstallOptions {
|
|
|
|
|
manager,
|
|
|
|
|
executable,
|
|
|
|
|
start,
|
|
|
|
|
},
|
|
|
|
|
)?
|
|
|
|
|
}
|
|
|
|
|
ServiceCommand::Uninstall { manager } => {
|
|
|
|
|
geth_node::service::uninstall_user_service(manager.parse::<ServiceManager>()?)?
|
|
|
|
|
}
|
|
|
|
|
ServiceCommand::Start { manager } => {
|
|
|
|
|
geth_node::service::start_user_service(manager.parse::<ServiceManager>()?)?
|
|
|
|
|
}
|
|
|
|
|
ServiceCommand::Stop { manager } => {
|
|
|
|
|
geth_node::service::stop_user_service(manager.parse::<ServiceManager>()?)?
|
|
|
|
|
}
|
|
|
|
|
ServiceCommand::Status { manager } => {
|
|
|
|
|
geth_node::service::status_user_service(manager.parse::<ServiceManager>()?)?
|
|
|
|
|
}
|
|
|
|
|
ServiceCommand::Print { manager, bin } => {
|
|
|
|
|
let executable = service_executable(bin)?;
|
|
|
|
|
geth_node::service::print_user_service(
|
|
|
|
|
paths,
|
|
|
|
|
manager.parse::<ServiceManager>()?,
|
|
|
|
|
&executable,
|
|
|
|
|
)?
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn service_executable(bin: Option<PathBuf>) -> Result<PathBuf> {
|
|
|
|
|
bin.map(Ok)
|
|
|
|
|
.unwrap_or_else(std::env::current_exe)
|
|
|
|
|
.context("resolve current geth executable")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 15:08:20 +02:00
|
|
|
fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|
|
|
|
if json {
|
|
|
|
|
println!("{}", serde_json::to_string_pretty(&response)?);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
match response {
|
|
|
|
|
ControlResponse::Status(status) => {
|
|
|
|
|
println!("geth daemon: running");
|
|
|
|
|
println!("home: {}", status.home.display());
|
|
|
|
|
println!("socket: {}", status.socket.display());
|
|
|
|
|
println!("agent: {}", status.agent_id);
|
|
|
|
|
println!("node: {}", status.node_id);
|
2026-05-16 01:54:00 +02:00
|
|
|
println!(
|
|
|
|
|
"endpoint: {}",
|
|
|
|
|
status.endpoint_id.as_deref().unwrap_or("not started")
|
|
|
|
|
);
|
2026-05-16 03:17:45 +02:00
|
|
|
println!("iroh relay: {}", status.iroh_relay_mode);
|
2026-05-16 14:33:45 +02:00
|
|
|
println!(
|
|
|
|
|
"iroh discovery: {}",
|
|
|
|
|
if status.iroh_local_discovery {
|
|
|
|
|
"local-network enabled"
|
|
|
|
|
} else {
|
|
|
|
|
"local-network disabled"
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-05-15 15:08:20 +02:00
|
|
|
println!("iroh: {}", status.iroh);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::NodeId(node) => {
|
|
|
|
|
println!("agent: {}", node.agent_id);
|
|
|
|
|
println!("node: {}", node.node_id);
|
|
|
|
|
println!(
|
|
|
|
|
"endpoint: {}",
|
|
|
|
|
node.endpoint_id
|
|
|
|
|
.as_deref()
|
|
|
|
|
.unwrap_or("not started in bootstrap")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::ResourceList { resources } => {
|
|
|
|
|
if resources.is_empty() {
|
|
|
|
|
println!("no resources");
|
|
|
|
|
} else {
|
|
|
|
|
for resource in resources {
|
|
|
|
|
println!("{}\t{}\t{}", resource.kind, resource.name, resource.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::ResourceCreated { resource } => {
|
|
|
|
|
println!(
|
|
|
|
|
"created resource: {} {} ({})",
|
|
|
|
|
resource.kind, resource.name, resource.id
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::CasAdded { hash, size_bytes } => {
|
|
|
|
|
println!("{hash} {size_bytes} bytes");
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::CasGot {
|
|
|
|
|
hash,
|
|
|
|
|
out,
|
|
|
|
|
size_bytes,
|
|
|
|
|
} => {
|
|
|
|
|
println!("wrote {hash} to {} ({size_bytes} bytes)", out.display());
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::CasHash { hash } => println!("{hash}"),
|
|
|
|
|
ControlResponse::CasHas { hash, present } => println!("{hash}: {present}"),
|
2026-05-16 16:36:35 +02:00
|
|
|
ControlResponse::CasPinned { hash, pinned } => {
|
|
|
|
|
println!("{hash}: pinned={pinned}");
|
|
|
|
|
}
|
2026-05-16 21:10:25 +02:00
|
|
|
ControlResponse::CasCleanup {
|
|
|
|
|
removed,
|
|
|
|
|
retained_pinned,
|
|
|
|
|
dry_run,
|
|
|
|
|
} => {
|
|
|
|
|
let action = if dry_run { "would remove" } else { "removed" };
|
|
|
|
|
println!("{action}: {}", removed.len());
|
|
|
|
|
for hash in removed {
|
|
|
|
|
println!(" {hash}");
|
|
|
|
|
}
|
|
|
|
|
println!("retained_pinned: {}", retained_pinned.len());
|
|
|
|
|
for hash in retained_pinned {
|
|
|
|
|
println!(" {hash}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-15 15:08:20 +02:00
|
|
|
ControlResponse::CasList { blobs } => {
|
|
|
|
|
for blob in blobs {
|
2026-05-16 16:36:35 +02:00
|
|
|
let pin = if blob.pinned { "pinned" } else { "unpinned" };
|
|
|
|
|
println!("{}\t{} bytes\t{}", blob.hash, blob.size_bytes, pin);
|
2026-05-15 15:08:20 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::KeychainStatus(status) => {
|
|
|
|
|
println!("initialized: {}", status.initialized);
|
|
|
|
|
println!("admin_keys: {}", status.admin_keys);
|
|
|
|
|
println!("users: {}", status.users);
|
|
|
|
|
println!("devices: {}", status.devices);
|
|
|
|
|
println!("nodes: {}", status.nodes);
|
|
|
|
|
}
|
2026-05-16 16:34:20 +02:00
|
|
|
ControlResponse::KeychainInitialized { ops } => {
|
|
|
|
|
println!("initialized keychain");
|
|
|
|
|
for op in ops {
|
|
|
|
|
println!("recorded keychain op: {}", op.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-16 22:18:49 +02:00
|
|
|
ControlResponse::SecretStatus { secrets } => {
|
|
|
|
|
if secrets.is_empty() {
|
|
|
|
|
println!("no resource secrets");
|
|
|
|
|
} else {
|
|
|
|
|
for secret in secrets {
|
|
|
|
|
println!("{}\t{}\tepoch {}", secret.id, secret.resource, secret.epoch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::SecretCreated { secret } => {
|
|
|
|
|
println!("resource secret: {}", secret.id);
|
|
|
|
|
println!("resource: {}", secret.resource);
|
|
|
|
|
println!("epoch: {}", secret.epoch);
|
|
|
|
|
}
|
2026-05-17 02:58:58 +02:00
|
|
|
ControlResponse::SecretBearerCreated { access } => {
|
|
|
|
|
println!("bearer secret: {}", access.secret);
|
|
|
|
|
println!("resource: {}", access.resource);
|
|
|
|
|
println!(
|
|
|
|
|
"capabilities: {}",
|
|
|
|
|
access
|
|
|
|
|
.capabilities
|
|
|
|
|
.iter()
|
|
|
|
|
.map(ToString::to_string)
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(",")
|
|
|
|
|
);
|
|
|
|
|
if let Some(expires_at) = access.expires_at {
|
|
|
|
|
println!("expires_at_ms: {}", expires_at.0);
|
|
|
|
|
}
|
|
|
|
|
println!("may_delegate: {}", access.may_delegate);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::SecretBearerList { access } => {
|
|
|
|
|
if access.is_empty() {
|
|
|
|
|
println!("no bearer access");
|
|
|
|
|
} else {
|
|
|
|
|
for item in access {
|
|
|
|
|
println!(
|
|
|
|
|
"{}\t{}\t{}\tmay_delegate={}",
|
|
|
|
|
item.secret,
|
|
|
|
|
item.resource,
|
|
|
|
|
item.capabilities
|
|
|
|
|
.iter()
|
|
|
|
|
.map(ToString::to_string)
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(","),
|
|
|
|
|
item.may_delegate
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::SecretBearerRevoked { resource, secret } => {
|
|
|
|
|
println!("revoked bearer secret: {secret}");
|
|
|
|
|
println!("resource: {resource}");
|
|
|
|
|
}
|
2026-05-15 15:08:20 +02:00
|
|
|
ControlResponse::AuthExplain(explain) => {
|
|
|
|
|
println!("allowed: {}", explain.allowed);
|
|
|
|
|
println!("subject: {}", explain.subject);
|
|
|
|
|
println!("resource: {}", explain.resource);
|
|
|
|
|
println!("capability: {}", explain.capability);
|
|
|
|
|
println!("reason: {}", explain.reason);
|
|
|
|
|
println!("evaluated_ops: {}", explain.evaluated_ops);
|
|
|
|
|
}
|
2026-05-16 16:32:03 +02:00
|
|
|
ControlResponse::AuthOpRecorded { op } => {
|
|
|
|
|
println!("recorded auth op: {}", op.id);
|
|
|
|
|
println!("resource: {}", op.resource);
|
|
|
|
|
}
|
2026-05-16 00:17:08 +02:00
|
|
|
ControlResponse::SshCertRequested { request } => {
|
|
|
|
|
println!("ssh cert request: {}", request.id);
|
|
|
|
|
println!("status: {}", request.status);
|
|
|
|
|
println!("kind: {}", request.cert_kind);
|
|
|
|
|
println!("principals: {}", request.principals.join(","));
|
|
|
|
|
println!("public_key_fingerprint: {}", request.public_key_fingerprint);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::SshCertRequests { requests } => {
|
|
|
|
|
if requests.is_empty() {
|
|
|
|
|
println!("no ssh certificate requests");
|
|
|
|
|
} else {
|
|
|
|
|
for request in requests {
|
|
|
|
|
println!(
|
|
|
|
|
"{}\t{}\t{}\t{}\t{}",
|
|
|
|
|
request.id,
|
|
|
|
|
request.status,
|
|
|
|
|
request.cert_kind,
|
|
|
|
|
request.principals.join(","),
|
|
|
|
|
request.public_key_fingerprint
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::SshCertApproved { approval } => {
|
|
|
|
|
println!("approved ssh cert request: {}", approval.request_id);
|
|
|
|
|
println!("valid_for: {}", approval.valid_for);
|
|
|
|
|
if let Some(serial) = approval.serial {
|
|
|
|
|
println!("serial: {serial}");
|
|
|
|
|
}
|
|
|
|
|
if let Some(output_path) = approval.output_path {
|
|
|
|
|
println!("expected_certificate: {output_path}");
|
|
|
|
|
}
|
|
|
|
|
println!("signing_command:");
|
|
|
|
|
println!("{}", shell_quote_command(&approval.signing_command));
|
|
|
|
|
println!("note: {}", approval.note);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::SshCertImported { certificate } => {
|
|
|
|
|
println!("imported ssh certificate: {}", certificate.id);
|
|
|
|
|
println!("request: {}", certificate.request_id);
|
|
|
|
|
println!("fingerprint: {}", certificate.certificate_fingerprint);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::SshCertList {
|
|
|
|
|
requests,
|
|
|
|
|
certificates,
|
|
|
|
|
} => {
|
|
|
|
|
println!("requests:");
|
|
|
|
|
if requests.is_empty() {
|
|
|
|
|
println!(" none");
|
|
|
|
|
} else {
|
|
|
|
|
for request in requests {
|
|
|
|
|
println!(
|
|
|
|
|
" {}\t{}\t{}\t{}",
|
|
|
|
|
request.id,
|
|
|
|
|
request.status,
|
|
|
|
|
request.cert_kind,
|
|
|
|
|
request.principals.join(",")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
println!("certificates:");
|
|
|
|
|
if certificates.is_empty() {
|
|
|
|
|
println!(" none");
|
|
|
|
|
} else {
|
|
|
|
|
for certificate in certificates {
|
|
|
|
|
println!(
|
|
|
|
|
" {}\t{}\t{}",
|
|
|
|
|
certificate.id, certificate.request_id, certificate.certificate_fingerprint
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::SshRevocationAdded { revocation } => {
|
|
|
|
|
println!("added ssh revocation: {}", revocation.id);
|
|
|
|
|
println!("kind: {}", revocation.kind);
|
|
|
|
|
println!("target: {}", revocation.target);
|
|
|
|
|
if let Some(reason) = revocation.reason {
|
|
|
|
|
println!("reason: {reason}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::SshRevocationList { revocations } => {
|
|
|
|
|
if revocations.is_empty() {
|
|
|
|
|
println!("no ssh revocations");
|
|
|
|
|
} else {
|
|
|
|
|
for revocation in revocations {
|
|
|
|
|
println!(
|
|
|
|
|
"{}\t{}\t{}\t{}",
|
|
|
|
|
revocation.id,
|
|
|
|
|
revocation.kind,
|
|
|
|
|
revocation.target,
|
|
|
|
|
revocation.reason.unwrap_or_default()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-17 18:29:47 +02:00
|
|
|
ControlResponse::SshRevocationExported {
|
|
|
|
|
out,
|
|
|
|
|
format,
|
|
|
|
|
count,
|
|
|
|
|
note,
|
|
|
|
|
} => {
|
2026-05-16 00:17:08 +02:00
|
|
|
println!("exported {count} ssh revocations to {}", out.display());
|
2026-05-17 18:29:47 +02:00
|
|
|
println!("format: {format}");
|
|
|
|
|
println!("note: {note}");
|
2026-05-16 00:17:08 +02:00
|
|
|
}
|
2026-05-16 21:13:33 +02:00
|
|
|
ControlResponse::DbAdded { db } => {
|
|
|
|
|
println!("registered db: {}", db.name);
|
|
|
|
|
println!("id: {}", db.id);
|
|
|
|
|
println!("resource: {}", db.resource);
|
|
|
|
|
println!("path: {}", db.path);
|
|
|
|
|
println!("sync_status: {}", db.sync_status);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::DbStatus { db } => {
|
|
|
|
|
println!("db: {}", db.name);
|
|
|
|
|
println!("id: {}", db.id);
|
|
|
|
|
println!("resource: {}", db.resource);
|
|
|
|
|
println!("path: {}", db.path);
|
|
|
|
|
println!("path_exists: {}", db.path_exists);
|
|
|
|
|
println!(
|
|
|
|
|
"size_bytes: {}",
|
|
|
|
|
db.size_bytes
|
|
|
|
|
.map(|size| size.to_string())
|
|
|
|
|
.unwrap_or_else(|| "unknown".to_owned())
|
|
|
|
|
);
|
|
|
|
|
println!("schema_metadata: {}", db.schema_metadata);
|
|
|
|
|
println!("sync_status: {}", db.sync_status);
|
|
|
|
|
}
|
2026-05-16 21:52:35 +02:00
|
|
|
ControlResponse::KvCreated { kv } => {
|
|
|
|
|
println!("created kv: {}", kv.name);
|
|
|
|
|
println!("id: {}", kv.id);
|
|
|
|
|
println!("resource: {}", kv.resource);
|
|
|
|
|
println!("sync_status: {}", kv.sync_status);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::KvSet { entry } => {
|
|
|
|
|
println!("set {} {}", entry.store, entry.key);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::KvGet { entry } => {
|
|
|
|
|
if let Some(entry) = entry {
|
|
|
|
|
println!("{}", entry.value);
|
|
|
|
|
} else {
|
|
|
|
|
println!("not found");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-16 22:15:18 +02:00
|
|
|
ControlResponse::DocumentCreated { document } => {
|
|
|
|
|
println!("created document: {}", document.name);
|
|
|
|
|
println!("id: {}", document.id);
|
|
|
|
|
println!("resource: {}", document.resource);
|
|
|
|
|
println!("sync_status: {}", document.sync_status);
|
|
|
|
|
println!("state_bytes: {}", document.state_bytes);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::DocumentStatus { document } => {
|
|
|
|
|
println!("document: {}", document.name);
|
|
|
|
|
println!("id: {}", document.id);
|
|
|
|
|
println!("resource: {}", document.resource);
|
|
|
|
|
println!("sync_status: {}", document.sync_status);
|
|
|
|
|
println!("state_bytes: {}", document.state_bytes);
|
|
|
|
|
}
|
2026-05-17 18:23:51 +02:00
|
|
|
ControlResponse::PubsubPublished { message } => {
|
|
|
|
|
println!("published: {}", message.topic);
|
|
|
|
|
println!("published_at_ms: {}", message.published_at.0);
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::PubsubMessages {
|
|
|
|
|
topic,
|
|
|
|
|
messages,
|
|
|
|
|
note,
|
|
|
|
|
} => {
|
|
|
|
|
println!("topic: {topic}");
|
|
|
|
|
println!("messages: {}", messages.len());
|
|
|
|
|
for message in messages {
|
|
|
|
|
println!("{}\t{}", message.published_at.0, message.message);
|
|
|
|
|
}
|
|
|
|
|
println!("note: {note}");
|
|
|
|
|
}
|
2026-05-15 15:08:20 +02:00
|
|
|
ControlResponse::NotImplemented { module, command } => {
|
|
|
|
|
println!("{module} {command}: not implemented yet");
|
|
|
|
|
}
|
|
|
|
|
ControlResponse::Error { message } => bail!(message),
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-05-16 00:17:08 +02:00
|
|
|
|
|
|
|
|
fn print_service_report(report: ServiceReport, json: bool) -> Result<()> {
|
|
|
|
|
if json {
|
|
|
|
|
println!(
|
|
|
|
|
"{}",
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"manager": report.manager.to_string(),
|
|
|
|
|
"action": format!("{:?}", report.action),
|
|
|
|
|
"service_name": report.service_name,
|
|
|
|
|
"definition_path": report.definition_path,
|
|
|
|
|
"definition": report.definition,
|
|
|
|
|
"commands": report.commands,
|
|
|
|
|
"note": report.note,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
println!("service: {}", report.service_name);
|
|
|
|
|
println!("manager: {}", report.manager);
|
|
|
|
|
println!("action: {:?}", report.action);
|
|
|
|
|
if let Some(path) = report.definition_path {
|
|
|
|
|
println!("definition: {}", path.display());
|
|
|
|
|
}
|
|
|
|
|
if !report.commands.is_empty() {
|
|
|
|
|
println!("commands:");
|
|
|
|
|
for command in report.commands {
|
|
|
|
|
println!(" {}", shell_quote_command(&command));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(definition) = report.definition {
|
|
|
|
|
println!("definition_body:");
|
|
|
|
|
print!("{definition}");
|
|
|
|
|
}
|
|
|
|
|
println!("note: {}", report.note);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn shell_quote_command(command: &[String]) -> String {
|
|
|
|
|
command
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|arg| {
|
|
|
|
|
if arg
|
|
|
|
|
.bytes()
|
|
|
|
|
.all(|byte| byte.is_ascii_alphanumeric() || b"-_./:=+@,".contains(&byte))
|
|
|
|
|
{
|
|
|
|
|
arg.clone()
|
|
|
|
|
} else {
|
|
|
|
|
format!("'{}'", arg.replace('\'', "'\\''"))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(" ")
|
|
|
|
|
}
|