From 2de3ff78ee866fd524ce083e5a77483744ec99fa Mon Sep 17 00:00:00 2001 From: reis <1l0@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:02:47 +0900 Subject: [PATCH] Add `nip` command (#83) --- main.go | 1 + nip.go | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 nip.go diff --git a/main.go b/main.go index 79af04e..c3438e9 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,7 @@ var app = &cli.Command{ fsCmd, publish, git, + nip, }, Version: version, Flags: []cli.Flag{ diff --git a/nip.go b/nip.go new file mode 100644 index 0000000..c7f75a1 --- /dev/null +++ b/nip.go @@ -0,0 +1,186 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os/exec" + "runtime" + "strings" + + "github.com/urfave/cli/v3" +) + +var nip = &cli.Command{ + Name: "nip", + Usage: "get the description of a NIP from its number", + Description: `fetches the NIPs README from GitHub and parses it to find the description of the given NIP number. + +example: + nak nip 1 + nak nip list + nak nip open 1`, + ArgsUsage: "", + Commands: []*cli.Command{ + { + Name: "list", + Usage: "list all NIPs", + Action: func(ctx context.Context, c *cli.Command) error { + return iterateNips(func(nip, desc, link string) bool { + stdout(nip + ": " + desc) + return true + }) + }, + }, + { + Name: "open", + Usage: "open the NIP page in the browser", + Action: func(ctx context.Context, c *cli.Command) error { + reqNum := c.Args().First() + if reqNum == "" { + return fmt.Errorf("missing NIP number") + } + + normalize := func(s string) string { + s = strings.ToLower(s) + s = strings.TrimPrefix(s, "nip-") + s = strings.TrimLeft(s, "0") + if s == "" { + s = "0" + } + return s + } + + reqNum = normalize(reqNum) + + foundLink := "" + err := iterateNips(func(nip, desc, link string) bool { + nipNum := normalize(nip) + if nipNum == reqNum { + foundLink = link + return false + } + return true + }) + + if err != nil { + return err + } + + if foundLink == "" { + return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum)) + } + + url := "https://github.com/nostr-protocol/nips/blob/master/" + foundLink + fmt.Println("Opening " + url) + + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "windows": + cmd = exec.Command("cmd", "/c", "start", url) + default: + cmd = exec.Command("xdg-open", url) + } + + return cmd.Start() + }, + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + reqNum := c.Args().First() + if reqNum == "" { + return fmt.Errorf("missing NIP number") + } + + normalize := func(s string) string { + s = strings.ToLower(s) + s = strings.TrimPrefix(s, "nip-") + s = strings.TrimLeft(s, "0") + if s == "" { + s = "0" + } + return s + } + + reqNum = normalize(reqNum) + + found := false + err := iterateNips(func(nip, desc, link string) bool { + nipNum := normalize(nip) + + if nipNum == reqNum { + stdout(strings.TrimSpace(desc)) + found = true + return false + } + return true + }) + + if err != nil { + return err + } + + if !found { + return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum)) + } + return nil + }, +} + +func iterateNips(yield func(nip, desc, link string) bool) error { + resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md") + if err != nil { + return fmt.Errorf("failed to fetch NIPs README: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read NIPs README: %w", err) + } + bodyStr := string(body) + epoch := strings.Index(bodyStr, "## List") + + lines := strings.SplitSeq(bodyStr[epoch+8:], "\n") + for line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "##") { + break + } + if !strings.HasPrefix(line, "- [NIP-") { + continue + } + + start := strings.Index(line, "[") + end := strings.Index(line, "]") + if start == -1 || end == -1 || end < start { + continue + } + + content := line[start+1 : end] + + parts := strings.SplitN(content, ":", 2) + if len(parts) != 2 { + continue + } + + nipPart := parts[0] + descPart := parts[1] + + rest := line[end+1:] + linkStart := strings.Index(rest, "(") + linkEnd := strings.Index(rest, ")") + link := "" + if linkStart != -1 && linkEnd != -1 && linkEnd > linkStart { + link = rest[linkStart+1 : linkEnd] + } + + if !yield(nipPart, strings.TrimSpace(descPart), link) { + break + } + } + return nil +}