755 lines
27 KiB
Rust
755 lines
27 KiB
Rust
//! Relicario CLI — the platform layer for the Relicario password manager.
|
|
//!
|
|
//! See module docs for the unlock flow and vault layout.
|
|
|
|
mod commands;
|
|
mod device;
|
|
mod gitea;
|
|
mod helpers;
|
|
mod parse;
|
|
mod prompt;
|
|
mod session;
|
|
mod org_session;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use anyhow::Result;
|
|
use clap::{CommandFactory, Parser, Subcommand};
|
|
use clap_complete::{generate, Shell};
|
|
|
|
#[derive(Parser)]
|
|
#[command(
|
|
name = "relicario",
|
|
version,
|
|
about = "Relicario — git-backed password manager with reference-image two-factor unlock"
|
|
)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
command: Commands,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Commands {
|
|
/// Initialize a new vault in the current directory.
|
|
Init {
|
|
/// Carrier JPEG to embed the secret into.
|
|
#[arg(long)]
|
|
image: PathBuf,
|
|
/// Output path for the reference image (gitignored).
|
|
#[arg(long, default_value = "reference.jpg")]
|
|
output: PathBuf,
|
|
},
|
|
|
|
/// Add a new item. Type-specific flags populate the core; missing fields
|
|
/// are prompted for interactively.
|
|
Add {
|
|
#[command(subcommand)]
|
|
kind: AddKind,
|
|
},
|
|
|
|
/// Print an item. Secrets are masked by default; pass --show to reveal.
|
|
Get {
|
|
/// Item id or case-insensitive title substring.
|
|
query: String,
|
|
/// Print secret field values in plaintext.
|
|
#[arg(long)]
|
|
show: bool,
|
|
/// Copy the primary secret (Login.password, Card.number, etc.) to clipboard.
|
|
#[arg(long)]
|
|
copy: bool,
|
|
},
|
|
|
|
/// List items.
|
|
List {
|
|
#[arg(long)]
|
|
r#type: Option<String>,
|
|
#[arg(long)]
|
|
group: Option<String>,
|
|
#[arg(long)]
|
|
tag: Option<String>,
|
|
#[arg(long)]
|
|
trashed: bool,
|
|
},
|
|
|
|
/// Edit an item interactively.
|
|
Edit {
|
|
query: String,
|
|
/// Decode an `otpauth://` QR image to set the TOTP secret (login items only).
|
|
#[arg(long, value_name = "PATH")]
|
|
totp_qr: Option<PathBuf>,
|
|
},
|
|
|
|
/// View captured field history for an item. Values are masked by
|
|
/// default; pass `--show` to reveal them.
|
|
History {
|
|
query: String,
|
|
#[arg(long)]
|
|
show: bool,
|
|
/// Filter to a single field (matches against the synthetic key,
|
|
/// e.g. `login_password`, `card_number`, `totp_secret`).
|
|
#[arg(long)]
|
|
field: Option<String>,
|
|
},
|
|
|
|
/// Soft-delete an item (moves to trash; reversible via `restore`).
|
|
Rm { query: String },
|
|
|
|
/// Restore a soft-deleted item.
|
|
Restore { query: String },
|
|
|
|
/// Permanently purge an item (and its attachments).
|
|
Purge { query: String },
|
|
|
|
/// Trash operations.
|
|
Trash {
|
|
#[command(subcommand)]
|
|
action: TrashAction,
|
|
},
|
|
|
|
/// Backup operations: pack and unpack `.relbak` archives.
|
|
Backup {
|
|
#[command(subcommand)]
|
|
action: BackupAction,
|
|
},
|
|
|
|
/// Import items from another password manager into the unlocked vault.
|
|
Import {
|
|
#[command(subcommand)]
|
|
action: ImportAction,
|
|
},
|
|
|
|
/// Attach a file to an item.
|
|
Attach { query: String, file: PathBuf },
|
|
|
|
/// List attachments on an item.
|
|
Attachments { query: String },
|
|
|
|
/// Extract an attachment to disk.
|
|
Extract {
|
|
query: String,
|
|
aid: String,
|
|
#[arg(long)]
|
|
out: Option<PathBuf>,
|
|
},
|
|
|
|
/// Remove an individual attachment from an item (deletes the encrypted
|
|
/// blob and updates the item + manifest). Use `purge` to drop the entire
|
|
/// item and all its attachments at once.
|
|
Detach { query: String, aid: String },
|
|
|
|
/// Generate a password or passphrase. When run inside an initialized
|
|
/// vault, falls back to `settings generator-defaults` for unspecified
|
|
/// flags; outside a vault, uses built-in defaults (length 20, safe
|
|
/// symbol set, 5 BIP39 words, space separator).
|
|
#[command(alias = "gen")]
|
|
Generate {
|
|
#[arg(short = 'l', long)]
|
|
length: Option<u32>,
|
|
#[arg(long)]
|
|
bip39: bool,
|
|
#[arg(short = 'w', long)]
|
|
words: Option<u32>,
|
|
#[arg(long)]
|
|
symbols: Option<String>,
|
|
/// Separator for BIP39 words.
|
|
#[arg(long)]
|
|
separator: Option<String>,
|
|
},
|
|
|
|
/// View or change vault settings.
|
|
Settings {
|
|
#[command(subcommand)]
|
|
action: SettingsAction,
|
|
},
|
|
|
|
/// Sync with the git remote (pull --rebase + push).
|
|
Sync,
|
|
|
|
/// Print a summary of the vault: items, attachments, last commit.
|
|
Status,
|
|
|
|
/// Lock the vault (no-op in CLI; present for UX parity with the extension).
|
|
Lock,
|
|
|
|
/// Emit a shell completion script for the given shell.
|
|
///
|
|
/// For `--group <TAB>` autocomplete, the bash/zsh/fish scripts read
|
|
/// the plaintext `${RELICARIO_VAULT}/.relicario/groups.cache` file,
|
|
/// which the CLI refreshes on every manifest read. In debug builds, set
|
|
/// `RELICARIO_NO_GROUPS_CACHE=1` to opt out of the cache (completion
|
|
/// will fall back to no value enumeration).
|
|
///
|
|
/// Pipe stdout to your shell's completion location (e.g.
|
|
/// `relicario completions bash > /etc/bash_completion.d/relicario`).
|
|
Completions {
|
|
#[arg(value_enum)]
|
|
shell: Shell,
|
|
},
|
|
|
|
/// Rate a passphrase with zxcvbn — prints score (0-4) and estimated
|
|
/// guesses. Informational only; does not gate vault operations.
|
|
///
|
|
/// Pass `-` as the argument to read one line from stdin instead, which
|
|
/// keeps the passphrase out of shell history.
|
|
Rate {
|
|
/// Passphrase to score, or `-` to read from stdin.
|
|
passphrase: String,
|
|
},
|
|
|
|
/// Manage registered devices (signing keys + deploy keys).
|
|
Device {
|
|
#[command(subcommand)]
|
|
action: DeviceAction,
|
|
},
|
|
|
|
/// Recovery QR operations — generate or unwrap the 2FA recovery code.
|
|
RecoveryQr {
|
|
#[command(subcommand)]
|
|
cmd: RecoveryQrCmd,
|
|
},
|
|
|
|
/// Manage a multi-user org vault.
|
|
Org {
|
|
/// Path to the org vault directory (overrides RELICARIO_ORG_DIR).
|
|
#[arg(long, global = true)]
|
|
dir: Option<PathBuf>,
|
|
#[command(subcommand)]
|
|
subcommand: OrgCommands,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub(crate) enum AddKind {
|
|
Login {
|
|
#[arg(long)] title: Option<String>,
|
|
#[arg(long)] username: Option<String>,
|
|
#[arg(long)] url: Option<String>,
|
|
/// Prompt for password (vs reading from stdin or --password).
|
|
#[arg(long)] password_prompt: bool,
|
|
#[arg(long)] password: Option<String>,
|
|
#[arg(long)] group: Option<String>,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
#[arg(long)] favorite: bool,
|
|
/// Decode an `otpauth://` QR image to fill the TOTP secret.
|
|
#[arg(long, value_name = "PATH")] totp_qr: Option<PathBuf>,
|
|
},
|
|
SecureNote {
|
|
#[arg(long)] title: Option<String>,
|
|
#[arg(long)] body_prompt: bool,
|
|
#[arg(long)] group: Option<String>,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
},
|
|
Identity {
|
|
#[arg(long)] title: Option<String>,
|
|
#[arg(long)] full_name: Option<String>,
|
|
#[arg(long)] email: Option<String>,
|
|
#[arg(long)] phone: Option<String>,
|
|
#[arg(long)] date_of_birth: Option<String>,
|
|
#[arg(long)] group: Option<String>,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
},
|
|
Card {
|
|
#[arg(long)] title: Option<String>,
|
|
#[arg(long)] holder: Option<String>,
|
|
#[arg(long)] expiry: Option<String>, // MM/YYYY
|
|
#[arg(long, default_value = "credit")] kind: String,
|
|
#[arg(long)] group: Option<String>,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
},
|
|
Key {
|
|
#[arg(long)] title: Option<String>,
|
|
#[arg(long)] label: Option<String>,
|
|
#[arg(long)] algorithm: Option<String>,
|
|
#[arg(long)] group: Option<String>,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
},
|
|
Document {
|
|
#[arg(long)] title: Option<String>,
|
|
#[arg(long)] file: PathBuf,
|
|
#[arg(long)] group: Option<String>,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
},
|
|
Totp {
|
|
#[arg(long)] title: Option<String>,
|
|
#[arg(long)] issuer: Option<String>,
|
|
#[arg(long)] label: Option<String>,
|
|
#[arg(long)] secret: Option<String>, // base32
|
|
#[arg(long, default_value = "30")] period: u32,
|
|
#[arg(long, default_value = "6")] digits: u8,
|
|
#[arg(long, default_value = "sha1")] algorithm: String,
|
|
#[arg(long)] group: Option<String>,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub(crate) enum TrashAction {
|
|
/// List trashed items.
|
|
List,
|
|
/// Purge every trashed item past its retention window.
|
|
Empty,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub(crate) enum SettingsAction {
|
|
/// Show current settings as JSON.
|
|
Show,
|
|
/// Set trash retention (e.g., --days 30 or --forever).
|
|
TrashRetention {
|
|
#[arg(long)] days: Option<u32>,
|
|
#[arg(long)] forever: bool,
|
|
},
|
|
/// Set field history retention.
|
|
HistoryRetention {
|
|
#[arg(long)] last_n: Option<u32>,
|
|
#[arg(long)] days: Option<u32>,
|
|
#[arg(long)] forever: bool,
|
|
},
|
|
/// Set per-attachment max size in bytes.
|
|
AttachmentCap {
|
|
#[arg(long)] per_attachment_max_bytes: Option<u64>,
|
|
#[arg(long)] per_item_max_count: Option<u32>,
|
|
#[arg(long)] per_vault_soft_cap_bytes: Option<u64>,
|
|
#[arg(long)] per_vault_hard_cap_bytes: Option<u64>,
|
|
},
|
|
/// Update the default password / passphrase generator settings used by
|
|
/// `relicario generate` when run inside this vault. Pass `--bip39` or
|
|
/// `--random` to switch mode; per-attribute flags update fields of the
|
|
/// chosen mode.
|
|
GeneratorDefaults {
|
|
/// Switch the default mode to random-character password.
|
|
#[arg(long, conflicts_with = "bip39")]
|
|
random: bool,
|
|
/// Switch the default mode to BIP39 passphrase.
|
|
#[arg(long, conflicts_with = "random")]
|
|
bip39: bool,
|
|
/// Random mode: total password length.
|
|
#[arg(long)] length: Option<u32>,
|
|
/// BIP39 mode: number of words.
|
|
#[arg(long)] words: Option<u32>,
|
|
/// Random mode: symbol charset (`safe`, `extended`, or a custom literal).
|
|
#[arg(long)] symbols: Option<String>,
|
|
/// BIP39 mode: word separator.
|
|
#[arg(long)] separator: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub(crate) enum BackupAction {
|
|
/// Pack the local vault into a single encrypted `.relbak` file.
|
|
/// Backup passphrase is independent of the vault passphrase.
|
|
Export {
|
|
/// Output `.relbak` path.
|
|
out: PathBuf,
|
|
/// Bundle the reference JPEG into the encrypted envelope.
|
|
#[arg(long)]
|
|
include_image: bool,
|
|
/// Override the reference image path (defaults to the vault's
|
|
/// `reference.jpg` or `RELICARIO_IMAGE`).
|
|
#[arg(long)]
|
|
image: Option<PathBuf>,
|
|
/// Skip bundling `.git/` history.
|
|
#[arg(long)]
|
|
no_history: bool,
|
|
},
|
|
/// Unpack a `.relbak` file into a fresh vault directory.
|
|
Restore {
|
|
/// Input `.relbak` path.
|
|
input: PathBuf,
|
|
/// Target directory (must NOT already contain `.relicario/`).
|
|
/// Defaults to the current directory.
|
|
#[arg(default_value = ".")]
|
|
target: PathBuf,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub(crate) enum ImportAction {
|
|
/// Import a LastPass CSV export into the unlocked vault.
|
|
/// Each row creates a new item with a freshly-minted ID; title
|
|
/// collisions are kept (no dedup). Failed rows are skipped and
|
|
/// reported on stderr.
|
|
Lastpass {
|
|
/// Path to the LastPass-format CSV export.
|
|
csv: PathBuf,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub(crate) enum DeviceAction {
|
|
/// Register this machine as a new device.
|
|
///
|
|
/// Generates two ed25519 keypairs: one for signing commits, one for push
|
|
/// access (deploy key). The deploy public key is registered via the Gitea
|
|
/// API. Both private keys are stored locally in
|
|
/// `~/.config/relicario/devices/<name>/`. The vault's `.relicario/devices.json`
|
|
/// is updated and committed.
|
|
///
|
|
/// Required environment variables (or flags):
|
|
/// RELICARIO_GITEA_URL — e.g. https://git.example.com
|
|
/// RELICARIO_GITEA_TOKEN — personal access token with repo write access
|
|
/// RELICARIO_GITEA_OWNER — repository owner
|
|
/// RELICARIO_GITEA_REPO — repository name
|
|
Add {
|
|
/// Human-readable name for this device (e.g. "laptop-2026").
|
|
#[arg(long)]
|
|
name: String,
|
|
/// Gitea API base URL (overrides RELICARIO_GITEA_URL).
|
|
#[arg(long)]
|
|
gitea_url: Option<String>,
|
|
/// Gitea personal access token (overrides RELICARIO_GITEA_TOKEN).
|
|
#[arg(long)]
|
|
gitea_token: Option<String>,
|
|
/// Gitea repository owner (overrides RELICARIO_GITEA_OWNER).
|
|
#[arg(long)]
|
|
owner: Option<String>,
|
|
/// Gitea repository name (overrides RELICARIO_GITEA_REPO).
|
|
#[arg(long)]
|
|
repo: Option<String>,
|
|
/// Skip Gitea API registration (useful when the remote is not Gitea).
|
|
#[arg(long)]
|
|
no_gitea: bool,
|
|
},
|
|
/// Revoke a registered device.
|
|
///
|
|
/// Removes the device from `devices.json`, adds it to `revoked.json`,
|
|
/// deletes the deploy key from Gitea, and commits the change.
|
|
Revoke {
|
|
/// Name of the device to revoke.
|
|
#[arg(long)]
|
|
name: String,
|
|
},
|
|
/// List registered devices.
|
|
List,
|
|
}
|
|
|
|
#[derive(clap::Subcommand)]
|
|
pub(crate) enum RecoveryQrCmd {
|
|
/// Generate a recovery QR code and display it as ASCII art in the terminal.
|
|
Generate,
|
|
/// Unwrap a recovery QR payload (base64) to recover the image_secret as hex.
|
|
Unwrap,
|
|
}
|
|
|
|
#[derive(clap::Subcommand)]
|
|
pub(crate) enum OrgCommands {
|
|
/// Create a new org vault.
|
|
Init {
|
|
#[arg(long)]
|
|
name: String,
|
|
},
|
|
/// Add a member to the org.
|
|
AddMember {
|
|
/// OpenSSH ed25519 public key of the new member.
|
|
#[arg(long)]
|
|
key: String,
|
|
/// Display name.
|
|
#[arg(long)]
|
|
name: String,
|
|
/// Role: owner, admin, or member.
|
|
#[arg(long, default_value = "member")]
|
|
role: String,
|
|
},
|
|
/// Remove a member from the org.
|
|
RemoveMember {
|
|
/// Member ID prefix.
|
|
member_id: String,
|
|
},
|
|
/// Change a member's role.
|
|
SetRole {
|
|
member_id: String,
|
|
role: String,
|
|
},
|
|
/// Create a collection.
|
|
CreateCollection {
|
|
slug: String,
|
|
#[arg(long)]
|
|
name: String,
|
|
},
|
|
/// Grant a member access to a collection.
|
|
Grant {
|
|
member_id: String,
|
|
collection: String,
|
|
},
|
|
/// Revoke a member's access to a collection.
|
|
Revoke {
|
|
member_id: String,
|
|
collection: String,
|
|
},
|
|
/// Rotate the org master key (run after removing a member).
|
|
RotateKey,
|
|
/// Transfer ownership to another member (owner only). By default the caller
|
|
/// is demoted to admin; pass --keep-owner for explicit co-ownership.
|
|
TransferOwnership {
|
|
member_id: String,
|
|
/// Keep the caller as an owner too (co-ownership) instead of demoting.
|
|
#[arg(long)]
|
|
keep_owner: bool,
|
|
},
|
|
/// Delete the org (owner only; requires --confirm).
|
|
DeleteOrg {
|
|
#[arg(long)]
|
|
confirm: bool,
|
|
},
|
|
/// Show org members and collections.
|
|
Status,
|
|
/// Query the org audit log.
|
|
Audit {
|
|
#[arg(long)]
|
|
since: Option<String>,
|
|
#[arg(long)]
|
|
member: Option<String>,
|
|
#[arg(long)]
|
|
collection: Option<String>,
|
|
#[arg(long)]
|
|
action: Option<String>,
|
|
/// Output format: `table` (default) or `json`.
|
|
#[arg(long, default_value = "table")]
|
|
format: String,
|
|
},
|
|
/// Add an item to a collection in the org vault.
|
|
Add {
|
|
#[command(subcommand)]
|
|
kind: OrgAddKind,
|
|
},
|
|
/// Print an org item (secrets masked unless --show).
|
|
Get {
|
|
/// Item id or case-insensitive title substring.
|
|
query: String,
|
|
#[arg(long)] show: bool,
|
|
},
|
|
/// List org items visible to you (filtered by your collection grants).
|
|
List {
|
|
#[arg(long)] trashed: bool,
|
|
},
|
|
/// Edit an org item's fields (flag-driven; blank flags keep current values).
|
|
Edit {
|
|
/// Item id or case-insensitive title substring.
|
|
query: String,
|
|
#[arg(long)] title: Option<String>,
|
|
#[arg(long)] username: Option<String>,
|
|
#[arg(long)] url: Option<String>,
|
|
#[arg(long)] password: Option<String>,
|
|
#[arg(long)] body: Option<String>,
|
|
#[arg(long)] email: Option<String>,
|
|
#[arg(long)] phone: Option<String>,
|
|
#[arg(long)] full_name: Option<String>,
|
|
},
|
|
/// Soft-delete an org item (reversible via `org restore`).
|
|
Rm { query: String },
|
|
/// Restore a soft-deleted org item.
|
|
Restore { query: String },
|
|
/// Permanently purge an org item (deletes the encrypted blob).
|
|
Purge { query: String },
|
|
}
|
|
|
|
#[derive(clap::Subcommand)]
|
|
pub(crate) enum OrgAddKind {
|
|
/// A login (username / url / password).
|
|
Login {
|
|
#[arg(long)] collection: String,
|
|
#[arg(long)] title: String,
|
|
#[arg(long)] username: Option<String>,
|
|
#[arg(long)] url: Option<String>,
|
|
#[arg(long)] password: Option<String>,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
},
|
|
/// A secure note.
|
|
SecureNote {
|
|
#[arg(long)] collection: String,
|
|
#[arg(long)] title: String,
|
|
#[arg(long)] body: String,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
},
|
|
/// An identity record.
|
|
Identity {
|
|
#[arg(long)] collection: String,
|
|
#[arg(long)] title: String,
|
|
#[arg(long)] full_name: Option<String>,
|
|
#[arg(long)] email: Option<String>,
|
|
#[arg(long)] phone: Option<String>,
|
|
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
|
|
},
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
match cli.command {
|
|
Commands::Init { image, output } => commands::init::cmd_init(image, output),
|
|
Commands::Add { kind } => commands::add::cmd_add(kind),
|
|
Commands::Get { query, show, copy } => commands::get::cmd_get(query, show, copy),
|
|
Commands::List { r#type, group, tag, trashed } => commands::list::cmd_list(r#type, group, tag, trashed),
|
|
Commands::Edit { query, totp_qr } => commands::edit::cmd_edit(query, totp_qr),
|
|
Commands::History { query, show, field } => commands::list::cmd_history(query, show, field),
|
|
Commands::Rm { query } => commands::trash::cmd_rm(query),
|
|
Commands::Restore { query } => commands::trash::cmd_restore(query),
|
|
Commands::Purge { query } => commands::trash::cmd_purge(query),
|
|
Commands::Trash { action } => commands::trash::cmd_trash(action),
|
|
Commands::Backup { action } => commands::backup::cmd_backup(action),
|
|
Commands::Import { action } => commands::import::cmd_import(action),
|
|
Commands::Attach { query, file } => commands::attach::cmd_attach(query, file),
|
|
Commands::Attachments { query } => commands::attach::cmd_attachments(query),
|
|
Commands::Extract { query, aid, out } => commands::attach::cmd_extract(query, aid, out),
|
|
Commands::Detach { query, aid } => commands::attach::cmd_detach(query, aid),
|
|
Commands::Generate { length, bip39, words, symbols, separator } => {
|
|
commands::generate::cmd_generate(length, bip39, words, symbols, separator)
|
|
}
|
|
Commands::Settings { action } => commands::settings::cmd_settings(action),
|
|
Commands::Sync => commands::sync::cmd_sync(),
|
|
Commands::Status => commands::status::cmd_status(),
|
|
Commands::Lock => { eprintln!("no cached session to lock"); Ok(()) }
|
|
Commands::Completions { shell } => {
|
|
let mut cmd = Cli::command();
|
|
generate(shell, &mut cmd, "relicario", &mut std::io::stdout());
|
|
Ok(())
|
|
}
|
|
Commands::Rate { passphrase } => commands::rate::cmd_rate(passphrase),
|
|
Commands::Device { action } => commands::device::cmd_device(action),
|
|
Commands::RecoveryQr { cmd } => commands::recovery_qr::cmd_recovery_qr(cmd),
|
|
Commands::Org { dir, subcommand } => {
|
|
let dir_path = dir.as_deref();
|
|
match subcommand {
|
|
OrgCommands::Init { name } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_init(&d, &name)?;
|
|
}
|
|
OrgCommands::AddMember { key, name, role } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
let role = parse_org_role(&role)?;
|
|
commands::org::run_add_member(&d, &key, &name, role)?;
|
|
}
|
|
OrgCommands::RemoveMember { member_id } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_remove_member(&d, &member_id)?;
|
|
}
|
|
OrgCommands::SetRole { member_id, role } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
let role = parse_org_role(&role)?;
|
|
commands::org::run_set_role(&d, &member_id, role)?;
|
|
}
|
|
OrgCommands::CreateCollection { slug, name } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_create_collection(&d, &slug, &name)?;
|
|
}
|
|
OrgCommands::Grant { member_id, collection } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_grant(&d, &member_id, &collection)?;
|
|
}
|
|
OrgCommands::Revoke { member_id, collection } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_revoke(&d, &member_id, &collection)?;
|
|
}
|
|
OrgCommands::RotateKey => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_rotate_key(&d)?;
|
|
}
|
|
OrgCommands::TransferOwnership { member_id, keep_owner } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_transfer_ownership(&d, &member_id, keep_owner)?;
|
|
}
|
|
OrgCommands::DeleteOrg { confirm } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_delete_org(&d, confirm)?;
|
|
}
|
|
OrgCommands::Status => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_status(&d)?;
|
|
}
|
|
OrgCommands::Audit { since, member, collection, action, format } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_audit(&d, since.as_deref(), member.as_deref(),
|
|
collection.as_deref(), action.as_deref(), &format)?;
|
|
}
|
|
OrgCommands::Add { kind } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
let (collection, add_kind, tags) = match kind {
|
|
OrgAddKind::Login { collection, title, username, url, password, tags } => (
|
|
collection,
|
|
commands::org::OrgAddKind::Login { title, username, url, password },
|
|
tags,
|
|
),
|
|
OrgAddKind::SecureNote { collection, title, body, tags } => (
|
|
collection,
|
|
commands::org::OrgAddKind::SecureNote { title, body },
|
|
tags,
|
|
),
|
|
OrgAddKind::Identity { collection, title, full_name, email, phone, tags } => (
|
|
collection,
|
|
commands::org::OrgAddKind::Identity { title, full_name, email, phone },
|
|
tags,
|
|
),
|
|
};
|
|
commands::org::run_add(&d, &collection, add_kind, tags)?;
|
|
}
|
|
OrgCommands::Get { query, show } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_get(&d, &query, show)?;
|
|
}
|
|
OrgCommands::List { trashed } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_list(&d, trashed)?;
|
|
}
|
|
OrgCommands::Edit { query, title, username, url, password, body, email, phone, full_name } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_edit(&d, &query, title, username, url, password, body, email, phone, full_name)?;
|
|
}
|
|
OrgCommands::Rm { query } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_rm(&d, &query)?;
|
|
}
|
|
OrgCommands::Restore { query } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_restore(&d, &query)?;
|
|
}
|
|
OrgCommands::Purge { query } => {
|
|
let d = crate::org_session::org_dir(dir_path)?;
|
|
commands::org::run_purge(&d, &query)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_org_role(s: &str) -> anyhow::Result<relicario_core::OrgRole> {
|
|
match s {
|
|
"owner" => Ok(relicario_core::OrgRole::Owner),
|
|
"admin" => Ok(relicario_core::OrgRole::Admin),
|
|
"member" => Ok(relicario_core::OrgRole::Member),
|
|
other => anyhow::bail!("unknown role `{other}` — use owner, admin, or member"),
|
|
}
|
|
}
|
|
|
|
/// Check for test passphrase override (debug builds only; stripped from release).
|
|
#[cfg(debug_assertions)]
|
|
pub(crate) fn test_passphrase_override() -> Option<String> {
|
|
std::env::var("RELICARIO_TEST_PASSPHRASE").ok()
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
pub(crate) fn test_passphrase_override() -> Option<String> {
|
|
None
|
|
}
|
|
|
|
/// Check for test item secret override (debug builds only; stripped from release).
|
|
#[cfg(debug_assertions)]
|
|
pub(crate) fn test_item_secret_override() -> Option<String> {
|
|
std::env::var("RELICARIO_TEST_ITEM_SECRET").ok()
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
pub(crate) fn test_item_secret_override() -> Option<String> {
|
|
None
|
|
}
|
|
|
|
/// Check for test backup passphrase override (debug builds only; stripped from release).
|
|
#[cfg(debug_assertions)]
|
|
pub(crate) fn test_backup_passphrase_override() -> Option<String> {
|
|
std::env::var("RELICARIO_TEST_BACKUP_PASSPHRASE").ok()
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
pub(crate) fn test_backup_passphrase_override() -> Option<String> {
|
|
None
|
|
}
|
|
|
|
|
|
|