auths_cli/commands/
registry.rs1use std::path::PathBuf;
6
7use anyhow::{Result, anyhow};
8use clap::{Parser, Subcommand};
9use serde::Serialize;
10
11use auths_sdk::storage::{
12 MergeOutcome, MergedCredentials, MergedKel, PushOutcome, pull_registry, push_registry,
13};
14
15use crate::commands::executable::ExecutableCommand;
16use crate::config::CliConfig;
17use crate::ux::format::{JsonResponse, Output, is_json_mode};
18
19#[derive(Parser, Debug, Clone)]
21#[command(
22 name = "registry",
23 about = "Push/pull the identity registry to/from a git remote",
24 after_help = "The registry travels as the packed git ref refs/auths/registry, so any
25git remote (a bare repo over file://, git://, ssh://, https://) can carry it
26between machines. Push is fast-forward-only (the registry is append-only).
27Pull authenticates every fetched KEL (prefix binding, signature replay,
28fork refusal) before merging it into the local registry.
29
30Examples:
31 auths registry push git://wire.local/registry.git # publish this machine's registry
32 auths registry pull git://wire.local/registry.git # merge another machine's events
33
34Related:
35 auths id show — The identity whose events the registry carries
36 auths device list — Devices learned from merged registries"
37)]
38pub struct RegistryCommand {
39 #[command(subcommand)]
40 pub subcommand: RegistrySubcommand,
41}
42
43#[derive(Subcommand, Debug, Clone)]
45pub enum RegistrySubcommand {
46 Push {
48 #[arg(value_name = "REMOTE")]
50 remote: String,
51 },
52 Pull {
54 #[arg(value_name = "REMOTE")]
56 remote: String,
57 },
58}
59
60#[derive(Debug, Serialize)]
61struct PushReport {
62 remote: String,
63 outcome: PushOutcome,
64}
65
66#[derive(Debug, Serialize)]
67struct PullReport {
68 remote: String,
69 merged: Vec<MergedKel>,
70 #[serde(flatten)]
71 credentials: MergedCredentials,
72}
73
74impl ExecutableCommand for RegistryCommand {
75 fn execute(&self, ctx: &CliConfig) -> Result<()> {
76 let registry_root = resolve_registry_root(ctx)?;
77 match &self.subcommand {
78 RegistrySubcommand::Push { remote } => {
79 let outcome = push_registry(®istry_root, remote)?;
80 if is_json_mode() {
81 JsonResponse::success(
82 "registry push",
83 PushReport {
84 remote: remote.clone(),
85 outcome,
86 },
87 )
88 .print()?;
89 } else {
90 let out = Output::new();
91 match outcome {
92 PushOutcome::Updated => {
93 out.print_success(&format!("Registry pushed to {remote}"));
94 }
95 PushOutcome::AlreadyCurrent => {
96 out.print_info(&format!("Remote {remote} is already current"));
97 }
98 }
99 }
100 Ok(())
101 }
102 RegistrySubcommand::Pull { remote } => {
103 let report = pull_registry(®istry_root, remote)?;
104 if is_json_mode() {
105 JsonResponse::success(
106 "registry pull",
107 PullReport {
108 remote: remote.clone(),
109 merged: report.merged,
110 credentials: report.credentials,
111 },
112 )
113 .print()?;
114 } else {
115 let out = Output::new();
116 out.print_success(&format!("Registry pulled from {remote}"));
117 for kel in &report.merged {
118 let line = match &kel.outcome {
119 MergeOutcome::Imported { events } => {
120 format!("{} imported ({events} events)", kel.prefix)
121 }
122 MergeOutcome::Advanced { events } => {
123 format!("{} advanced (+{events} events)", kel.prefix)
124 }
125 MergeOutcome::AlreadyCurrent => {
126 format!("{} already current", kel.prefix)
127 }
128 };
129 out.print_info(&line);
130 }
131 let creds = &report.credentials;
132 if creds.credentials_imported > 0 || creds.tel_events_imported > 0 {
133 out.print_info(&format!(
134 "{} credential(s) and {} status event(s) imported",
135 creds.credentials_imported, creds.tel_events_imported
136 ));
137 }
138 }
139 Ok(())
140 }
141 }
142 }
143}
144
145fn resolve_registry_root(ctx: &CliConfig) -> Result<PathBuf> {
148 if let Some(repo) = &ctx.repo_path {
149 return Ok(repo.clone());
150 }
151 auths_sdk::paths::auths_home_with_config(&ctx.env_config)
152 .map_err(|e| anyhow!("Could not locate ~/.auths: {e}"))
153}