use std::fs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::RngCore;
use void_core::collab::manifest::{
ecies_wrap_key, save_manifest, Contributor, ContributorId, Manifest, RepoKey,
};
use void_core::config::{Config, CoreConfig, UserConfig};
use crate::context::load_public_identity;
use crate::output::{CliError, ErrorCode};
pub(crate) struct NewRepoOpts {
pub repo_name: Option<String>,
pub repo_id: Option<String>,
pub repo_secret: Option<String>,
}
pub(crate) struct ManifestResult {
pub manifest: Manifest,
pub username: String,
}
pub(crate) fn create_void_dir_structure(void_dir: &Path) -> Result<(), CliError> {
fs::create_dir_all(void_dir)
.map_err(|e| CliError::internal(format!("failed to create .void directory: {e}")))?;
fs::create_dir_all(void_dir.join("objects"))
.map_err(|e| CliError::internal(format!("failed to create objects directory: {e}")))?;
fs::create_dir_all(void_dir.join("refs/heads"))
.map_err(|e| CliError::internal(format!("failed to create refs/heads: {e}")))?;
fs::create_dir_all(void_dir.join("refs/tags"))
.map_err(|e| CliError::internal(format!("failed to create refs/tags: {e}")))?;
fs::create_dir_all(void_dir.join("staged"))
.map_err(|e| CliError::internal(format!("failed to create staged: {e}")))?;
fs::write(void_dir.join("HEAD"), "ref: refs/heads/trunk\n")
.map_err(|e| CliError::internal(format!("failed to write HEAD: {e}")))?;
Ok(())
}
pub(crate) fn setup_owner_manifest(
void_dir: &Path,
repo_key: &RepoKey,
opts: NewRepoOpts,
) -> Result<ManifestResult, CliError> {
let (username, signing_pubkey, recipient_pubkey, _nostr_pubkey) =
load_public_identity().map_err(|_| {
CliError::new(
ErrorCode::InvalidArgs,
"no identity found — run 'void identity init' first",
)
})?;
let username = username.unwrap_or_else(|| "anonymous".to_string());
let mut manifest = Manifest::new(signing_pubkey.clone(), None);
manifest.repo_id = opts.repo_id;
manifest.repo_name = opts.repo_name;
let wrapped = ecies_wrap_key(repo_key, &recipient_pubkey)
.map_err(|e| CliError::internal(format!("failed to wrap key: {e}")))?;
manifest
.read_keys
.wrapped
.insert(signing_pubkey.clone(), wrapped);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
manifest.contributors.push(Contributor {
identity: ContributorId::new(signing_pubkey.clone(), recipient_pubkey),
name: Some(username.clone()),
nostr_pubkey: None,
added_at: timestamp,
added_by: signing_pubkey,
signature: vec![],
});
save_manifest(void_dir, &manifest)
.map_err(|e| CliError::internal(format!("failed to write manifest: {e}")))?;
let config = Config {
version: Some(1),
created: Some(chrono::Utc::now().to_rfc3339()),
repo_secret: opts.repo_secret,
repo_id: manifest.repo_id.clone(),
repo_name: manifest.repo_name.clone(),
ipfs: None,
tor: None,
user: UserConfig {
name: Some(username.clone()),
email: None,
},
core: CoreConfig::default(),
remote: Default::default(),
};
void_core::config::save(void_dir, &config)
.map_err(|e| CliError::internal(format!("failed to write config file: {e}")))?;
Ok(ManifestResult { manifest, username })
}
pub(crate) fn generate_repo_secret() -> String {
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
hex::encode(bytes)
}