diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/ngit/cli.rs | 4 | ||||
| -rw-r--r-- | src/bin/ngit/main.rs | 3 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/mod.rs | 1 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/whoami.rs | 180 |
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; | |||
| 15 | pub mod repo; | 15 | pub mod repo; |
| 16 | pub mod send; | 16 | pub mod send; |
| 17 | pub mod sync; | 17 | pub mod sync; |
| 18 | pub 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 @@ | |||
| 1 | use anyhow::{Context, Result}; | ||
| 2 | use ngit::{ | ||
| 3 | client::Params, | ||
| 4 | login::{ | ||
| 5 | SignerInfoSource, | ||
| 6 | existing::{get_signer_info, load_existing_login}, | ||
| 7 | }, | ||
| 8 | }; | ||
| 9 | use nostr_sdk::ToBech32; | ||
| 10 | use serde::Serialize; | ||
| 11 | |||
| 12 | use crate::{ | ||
| 13 | cli::{Cli, extract_signer_cli_arguments}, | ||
| 14 | client::{Client, Connect}, | ||
| 15 | git::Repo, | ||
| 16 | }; | ||
| 17 | |||
| 18 | #[derive(clap::Args)] | ||
| 19 | pub 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)] | ||
| 30 | struct 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)] | ||
| 39 | struct 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 | |||
| 50 | pub 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 | |||
| 128 | fn 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 | |||
| 137 | fn 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). | ||
| 149 | async 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 | } | ||