upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/ngit/cli.rs4
-rw-r--r--src/bin/ngit/main.rs3
-rw-r--r--src/bin/ngit/sub_commands/mod.rs1
-rw-r--r--src/bin/ngit/sub_commands/whoami.rs180
4 files changed, 187 insertions, 1 deletions
diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs
index 5feaf31..ccf25bb 100644
--- a/src/bin/ngit/cli.rs
+++ b/src/bin/ngit/cli.rs
@@ -143,6 +143,8 @@ pub enum AccountCommands {
143 ExportKeys, 143 ExportKeys,
144 /// create a new nostr account 144 /// create a new nostr account
145 Create(sub_commands::create::SubCommandArgs), 145 Create(sub_commands::create::SubCommandArgs),
146 /// show currently logged-in account(s)
147 Whoami(sub_commands::whoami::SubCommandArgs),
146} 148}
147 149
148#[derive(clap::Parser)] 150#[derive(clap::Parser)]
@@ -158,7 +160,7 @@ pub struct RepoSubCommandArgs {
158 /// Use local cache only, skip network fetch 160 /// Use local cache only, skip network fetch
159 #[arg(long)] 161 #[arg(long)]
160 pub offline: bool, 162 pub offline: bool,
161 /// Output repository info as JSON; is_nostr_repo is false when not in a nostr repository 163 /// Output repository info as JSON; `is_nostr_repo` is false when not in a nostr repository
162 #[arg(long)] 164 #[arg(long)]
163 pub json: bool, 165 pub json: bool,
164} 166}
diff --git a/src/bin/ngit/main.rs b/src/bin/ngit/main.rs
index 8f3b0da..6a5a8f0 100644
--- a/src/bin/ngit/main.rs
+++ b/src/bin/ngit/main.rs
@@ -48,6 +48,9 @@ async fn main() {
48 AccountCommands::Create(sub_args) => { 48 AccountCommands::Create(sub_args) => {
49 sub_commands::create::launch(&cli, sub_args).await 49 sub_commands::create::launch(&cli, sub_args).await
50 } 50 }
51 AccountCommands::Whoami(sub_args) => {
52 sub_commands::whoami::launch(&cli, sub_args).await
53 }
51 }, 54 },
52 Commands::Init(args) => sub_commands::init::launch(&cli, args).await, 55 Commands::Init(args) => sub_commands::init::launch(&cli, args).await,
53 Commands::Repo(args) => { 56 Commands::Repo(args) => {
diff --git a/src/bin/ngit/sub_commands/mod.rs b/src/bin/ngit/sub_commands/mod.rs
index 60dc413..c4d0821 100644
--- a/src/bin/ngit/sub_commands/mod.rs
+++ b/src/bin/ngit/sub_commands/mod.rs
@@ -15,3 +15,4 @@ pub mod pr_status;
15pub mod repo; 15pub mod repo;
16pub mod send; 16pub mod send;
17pub mod sync; 17pub mod sync;
18pub mod whoami;
diff --git a/src/bin/ngit/sub_commands/whoami.rs b/src/bin/ngit/sub_commands/whoami.rs
new file mode 100644
index 0000000..be79c79
--- /dev/null
+++ b/src/bin/ngit/sub_commands/whoami.rs
@@ -0,0 +1,180 @@
1use anyhow::{Context, Result};
2use ngit::{
3 client::Params,
4 login::{
5 SignerInfoSource,
6 existing::{get_signer_info, load_existing_login},
7 },
8};
9use nostr_sdk::ToBech32;
10use serde::Serialize;
11
12use crate::{
13 cli::{Cli, extract_signer_cli_arguments},
14 client::{Client, Connect},
15 git::Repo,
16};
17
18#[derive(clap::Args)]
19pub struct SubCommandArgs {
20 /// use local cache only, skip network fetch
21 #[arg(long, action)]
22 pub offline: bool,
23
24 /// output as JSON
25 #[arg(long, action)]
26 pub json: bool,
27}
28
29#[derive(Serialize)]
30struct UserJson {
31 name: String,
32 npub: String,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 nip05: Option<String>,
35 scope: String,
36}
37
38#[derive(Serialize)]
39struct WhoamiJson {
40 #[serde(skip_serializing_if = "Option::is_none")]
41 local: Option<UserJson>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 global: Option<UserJson>,
44 /// The account that would be used for operations in the current context
45 /// (local takes priority over global).
46 #[serde(skip_serializing_if = "Option::is_none")]
47 active: Option<UserJson>,
48}
49
50pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> {
51 let git_repo = Repo::discover()
52 .context("failed to find a git repository")
53 .ok();
54
55 let client = if command_args.offline {
56 None
57 } else {
58 Some(Client::new(Params::with_git_config_relay_defaults(
59 &git_repo.as_ref(),
60 )))
61 };
62
63 let signer_info = extract_signer_cli_arguments(args).unwrap_or(None);
64
65 // Try to load local login (silent, no prompts)
66 let local = load_user_for_scope(
67 git_repo.as_ref(),
68 signer_info.as_ref(),
69 client.as_ref(),
70 SignerInfoSource::GitLocal,
71 )
72 .await;
73
74 // Try to load global login (silent, no prompts)
75 let global = load_user_for_scope(
76 git_repo.as_ref(),
77 signer_info.as_ref(),
78 client.as_ref(),
79 SignerInfoSource::GitGlobal,
80 )
81 .await;
82
83 if let Some(client) = client {
84 client.disconnect().await?;
85 }
86
87 if command_args.json {
88 // active = local if present, else global
89 let active = local
90 .as_ref()
91 .map(|u| make_user_json(u, "local"))
92 .or_else(|| global.as_ref().map(|u| make_user_json(u, "global")));
93
94 let output = WhoamiJson {
95 local: local.as_ref().map(|u| make_user_json(u, "local")),
96 global: global.as_ref().map(|u| make_user_json(u, "global")),
97 active,
98 };
99 println!("{}", serde_json::to_string_pretty(&output)?);
100 } else {
101 match (local.as_ref(), global.as_ref()) {
102 (None, None) => {
103 println!("not logged in");
104 println!();
105 println!("use `ngit account login` to log in");
106 }
107 (Some(u), None) => {
108 println!("logged in to local repository as:");
109 print_user_human(u);
110 }
111 (None, Some(u)) => {
112 println!("logged in globally as:");
113 print_user_human(u);
114 }
115 (Some(local_u), Some(global_u)) => {
116 println!("local (active):");
117 print_user_human(local_u);
118 println!();
119 println!("global:");
120 print_user_human(global_u);
121 }
122 }
123 }
124
125 Ok(())
126}
127
128fn make_user_json(u: &(String, String, Option<String>), scope: &str) -> UserJson {
129 UserJson {
130 name: u.0.clone(),
131 npub: u.1.clone(),
132 nip05: u.2.clone(),
133 scope: scope.to_string(),
134 }
135}
136
137fn print_user_human(u: &(String, String, Option<String>)) {
138 let (name, npub, nip05) = u;
139 println!(" name: {name}");
140 println!(" npub: {npub}");
141 if let Some(nip05) = nip05 {
142 println!(" nip05: {nip05}");
143 }
144}
145
146/// Attempt to silently load a user from a specific config scope.
147/// Returns `Some((name, npub, nip05))` on success, `None` if not logged in
148/// via that scope or if the scope requires a password prompt (ncryptsec).
149async fn load_user_for_scope(
150 git_repo: Option<&Repo>,
151 signer_info: Option<&ngit::login::SignerInfo>,
152 client: Option<&Client>,
153 source: SignerInfoSource,
154) -> Option<(String, String, Option<String>)> {
155 // First verify signer info exists for this scope without building a full
156 // signer — avoids triggering password prompts for ncryptsec.
157 if get_signer_info(&git_repo, &signer_info.cloned(), &None, &Some(source.clone())).is_err() {
158 return None;
159 }
160
161 let result = load_existing_login(
162 &git_repo,
163 &signer_info.cloned(),
164 &None,
165 &Some(source),
166 client,
167 true, // silent — don't print "logged in as"
168 false, // don't prompt for password (ncryptsec users get None here)
169 false, // don't force a relay fetch if already cached
170 )
171 .await;
172
173 match result {
174 Ok((_, user_ref, _)) => {
175 let npub = user_ref.public_key.to_bech32().ok()?;
176 Some((user_ref.metadata.name, npub, user_ref.metadata.nip05))
177 }
178 Err(_) => None,
179 }
180}