diff options
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | skills/ngit/SKILL.md | 2 | ||||
| -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 |
6 files changed, 190 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 37200c9..182fb6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
| @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 9 | 9 | ||
| 10 | ### Added | 10 | ### Added |
| 11 | 11 | ||
| 12 | - `ngit account whoami` — show the currently logged-in account(s) | ||
| 12 | - `ngit pr` subcommand group: `list`, `view`, `checkout`, `apply`, `send`, `close`, `reopen`, `ready`, `comment`, `merge`; replaces the former top-level `ngit list`, `ngit checkout`, and `ngit apply` commands (hard-migrated); `ngit send` remains at the top level unchanged | 13 | - `ngit pr` subcommand group: `list`, `view`, `checkout`, `apply`, `send`, `close`, `reopen`, `ready`, `comment`, `merge`; replaces the former top-level `ngit list`, `ngit checkout`, and `ngit apply` commands (hard-migrated); `ngit send` remains at the top level unchanged |
| 13 | - `ngit pr view <id>` — view a PR with its full details and all comments (author, timestamp, body) in chronological order | 14 | - `ngit pr view <id>` — view a PR with its full details and all comments (author, timestamp, body) in chronological order |
| 14 | - `ngit pr close <id>` / `ngit pr reopen <id>` — change PR status (author or maintainer only) | 15 | - `ngit pr close <id>` / `ngit pr reopen <id>` — change PR status (author or maintainer only) |
| @@ -143,7 +144,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 143 | ### Added | 144 | ### Added |
| 144 | 145 | ||
| 145 | - **Pull Requests Support**: Introduced complete PR functionality for large contributions that would be too big for relays as patches: | 146 | - **Pull Requests Support**: Introduced complete PR functionality for large contributions that would be too big for relays as patches: |
| 146 | |||
| 147 | - Generate PR events for oversized patches automatically | 147 | - Generate PR events for oversized patches automatically |
| 148 | - Support PR updates and PR as patch revision | 148 | - Support PR updates and PR as patch revision |
| 149 | - List open/draft proposals on repo relays/servers as `pr/*` branches and all proposals as `refs/pr/*` and `refs/pr/pr-by-id/head` | 149 | - List open/draft proposals on repo relays/servers as `pr/*` branches and all proposals as `refs/pr/*` and `refs/pr/pr-by-id/head` |
| @@ -154,12 +154,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 154 | - **NIP-22 Status Events Support**: Read and process NIP-22 style status events for proposals and PRs | 154 | - **NIP-22 Status Events Support**: Read and process NIP-22 style status events for proposals and PRs |
| 155 | 155 | ||
| 156 | - **ngit sync command**: New command to synchronize git servers with nostr state | 156 | - **ngit sync command**: New command to synchronize git servers with nostr state |
| 157 | |||
| 158 | - Optional `--force` flag for forced synchronization eg deleting refs on non-GRASP servers | 157 | - Optional `--force` flag for forced synchronization eg deleting refs on non-GRASP servers |
| 159 | - `--ref-name` parameter to limit sync to a single reference | 158 | - `--ref-name` parameter to limit sync to a single reference |
| 160 | 159 | ||
| 161 | - **ngit init improvements** (simple model for non-grasp servers): | 160 | - **ngit init improvements** (simple model for non-grasp servers): |
| 162 | |||
| 163 | - Use user's grasp list for defaults instead of hardcoded options | 161 | - Use user's grasp list for defaults instead of hardcoded options |
| 164 | - List and allow selection/deselection of non-grasp servers | 162 | - List and allow selection/deselection of non-grasp servers |
| 165 | - Check and fetch origin refs when missing locally | 163 | - Check and fetch origin refs when missing locally |
diff --git a/skills/ngit/SKILL.md b/skills/ngit/SKILL.md index 6da6305..7621086 100644 --- a/skills/ngit/SKILL.md +++ b/skills/ngit/SKILL.md | |||
| @@ -143,6 +143,8 @@ ngit issue reopen <ID|nevent> | |||
| 143 | ## Account management | 143 | ## Account management |
| 144 | 144 | ||
| 145 | ```bash | 145 | ```bash |
| 146 | ngit account whoami --json | ||
| 147 | ngit account whoami --json --offline # use cache, no network | ||
| 146 | ngit account login # interactive, stores nsec in global git config | 148 | ngit account login # interactive, stores nsec in global git config |
| 147 | ngit account login --bunker-url bunker://... # NIP-46 remote signer | 149 | ngit account login --bunker-url bunker://... # NIP-46 remote signer |
| 148 | ngit account login --local # this repo only | 150 | ngit account login --local # this repo only |
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 | } | ||