Skip to main content

systemprompt_cli/commands/core/services/
keygen.rs

1//! `services keygen` — mint an ed25519 bundle signing key.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::path::PathBuf;
7
8use anyhow::{Context, Result};
9use clap::Args;
10use serde::Serialize;
11use systemprompt_config::write_private_atomic;
12
13use super::signing::BundleSigningKey;
14use crate::shared::CommandOutput;
15
16#[derive(Debug, Clone, Args)]
17pub struct KeygenArgs {
18    #[arg(
19        long,
20        help = "Write the base64 seed to this file instead of printing it"
21    )]
22    pub out: Option<PathBuf>,
23}
24
25#[derive(Debug, Serialize)]
26pub struct KeygenOutcome {
27    pub public_key: String,
28    pub key_id: String,
29    pub seed: Option<String>,
30    pub seed_file: Option<String>,
31}
32
33pub fn execute(args: &KeygenArgs) -> Result<CommandOutput> {
34    let key = BundleSigningKey::generate();
35    let outcome = write_key(&key, args.out.as_deref())?;
36    Ok(CommandOutput::card_value("Bundle Signing Key", &outcome))
37}
38
39fn write_key(key: &BundleSigningKey, out: Option<&std::path::Path>) -> Result<KeygenOutcome> {
40    let mut outcome = KeygenOutcome {
41        public_key: key.public_key.clone(),
42        key_id: key.key_id.clone(),
43        seed: None,
44        seed_file: None,
45    };
46
47    match out {
48        Some(path) => {
49            if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
50                std::fs::create_dir_all(parent)
51                    .with_context(|| format!("Failed to create {}", parent.display()))?;
52            }
53            write_private_atomic(path, key.seed_b64().as_bytes())
54                .with_context(|| format!("Failed to write {}", path.display()))?;
55            outcome.seed_file = Some(path.display().to_string());
56        },
57        None => outcome.seed = Some(key.seed_b64()),
58    }
59    Ok(outcome)
60}