Compare commits

...

6 Commits

Author SHA1 Message Date
fiatjaf
210cf66d5f git: fix a bunch of small bugs. 2025-11-30 08:57:27 -03:00
fiatjaf
f9335b0ab4 git: fetch repo from owner+identifier on init, and other things. 2025-11-27 23:59:46 -03:00
fiatjaf
16916d7d95 nip: display markdown directly, default to list. 2025-11-27 12:14:02 -03:00
fiatjaf
3ff4dbe196 force update golang version.
fixes nostr:nevent1qvzqqqqqqypzqwlsccluhy6xxsr6l9a9uhhxf75g85g8a709tprjcn4e42h053vaqydhwumn8ghj7un9d3shjtnhv4ehgetjde38gcewvdhk6tcprfmhxue69uhhq7tjv9kkjepwve5kzar2v9nzucm0d5hsqgzdaekrxfhwrex49f6htd7rvmnfxs40ypga9mx7hvssaz347mxees2gpdzr
2025-11-27 07:06:53 -03:00
reis
2de3ff78ee Add nip command (#83) 2025-11-26 09:02:47 -03:00
fiatjaf
03c1bf832e fix README misformatting. 2025-11-25 22:44:03 -03:00
6 changed files with 518 additions and 186 deletions

View File

@@ -188,7 +188,6 @@ you can also display a QR code for the bunker URI by adding the `--qrcode` flag:
~> nak bunker --persist --sec ncryptsec1... relay.nsec.app nos.lol
```
```shell
then later just
```shell

421
git.go
View File

@@ -8,6 +8,7 @@ import (
"path/filepath"
"slices"
"strings"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
@@ -92,6 +93,9 @@ aside from those, there is also:
}
}
var defaultOwner string
var defaultIdentifier string
// check if nip34.json already exists
existingConfig, err := readNip34ConfigFile("")
if err == nil {
@@ -99,47 +103,118 @@ aside from those, there is also:
if !c.Bool("force") && !c.Bool("interactive") {
return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update")
}
}
// get repository base directory name for defaults
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
baseName := filepath.Base(cwd)
// get earliest unique commit
var earliestCommit string
if output, err := exec.Command("git", "rev-list", "--max-parents=0", "HEAD").Output(); err == nil {
earliest := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(earliest) > 0 {
earliestCommit = earliest[0]
}
}
// extract clone URLs from nostr:// git remotes
// (this is just for migrating from ngit)
var defaultCloneURLs []string
if output, err := exec.Command("git", "remote", "-v").Output(); err == nil {
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, remote := range remotes {
if strings.Contains(remote, "nostr://") {
parts := strings.Fields(remote)
if len(parts) >= 2 {
nostrURL := parts[1]
// parse nostr://npub.../relay_hostname/identifier
if owner, identifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 {
relayURL := relays[0]
// convert to https://relay_hostname/npub.../identifier.git
cloneURL := fmt.Sprintf("http%s/%s/%s.git",
relayURL[2:], nip19.EncodeNpub(owner), identifier)
defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL)
defaultIdentifier = existingConfig.Identifier
defaultOwner = existingConfig.Owner
} else {
// extract info from nostr:// git remotes (this is just for migrating from ngit)
if output, err := exec.Command("git", "remote", "-v").Output(); err == nil {
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, remote := range remotes {
if strings.Contains(remote, "nostr://") {
parts := strings.Fields(remote)
if len(parts) >= 2 {
nostrURL := parts[1]
// parse nostr://npub.../relay_hostname/identifier
if remoteOwner, remoteIdentifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 {
defaultIdentifier = remoteIdentifier
defaultOwner = nip19.EncodeNpub(remoteOwner)
}
}
}
}
}
}
// get repository base directory name for defaults
if defaultIdentifier == "" {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
defaultIdentifier = filepath.Base(cwd)
}
// prompt for identifier first
var identifier string
if c.String("identifier") != "" {
identifier = c.String("identifier")
} else if c.Bool("interactive") {
if err := survey.AskOne(&survey.Input{
Message: "identifier",
Default: defaultIdentifier,
}, &identifier); err != nil {
return err
}
} else {
identifier = defaultIdentifier
}
// prompt for owner pubkey
var owner nostr.PubKey
var ownerStr string
if c.String("owner") != "" {
owner, err = parsePubKey(ownerStr)
if err != nil {
return fmt.Errorf("invalid owner pubkey: %w", err)
}
ownerStr = nip19.EncodeNpub(owner)
} else if c.Bool("interactive") {
for {
if err := survey.AskOne(&survey.Input{
Message: "owner (npub or hex)",
Default: defaultOwner,
}, &ownerStr); err != nil {
return err
}
owner, err = parsePubKey(ownerStr)
if err == nil {
ownerStr = nip19.EncodeNpub(owner)
break
}
}
} else {
return fmt.Errorf("owner pubkey is required (use --owner or --interactive)")
}
// try to fetch existing repository announcement (kind 30617)
var fetchedRepo *nip34.Repository
if existingConfig.Identifier == "" {
log(" searching for existing events... ")
repo, _, err := fetchRepositoryAndState(ctx, owner, identifier, nil)
if err == nil && repo.Event.ID != nostr.ZeroID {
fetchedRepo = &repo
log("found one from %s.\n", repo.Event.CreatedAt.Time().Format(time.DateOnly))
} else {
log("none found.\n")
}
}
// set config with fetched values or defaults
var config Nip34Config
if fetchedRepo != nil {
config = RepositoryToConfig(*fetchedRepo)
} else if existingConfig.Identifier != "" {
config = existingConfig
} else {
// get earliest unique commit
var earliestCommit string
if output, err := exec.Command("git", "rev-list", "--max-parents=0", "HEAD").Output(); err == nil {
earliestCommit = strings.TrimSpace(string(output))
}
config = Nip34Config{
Identifier: identifier,
Owner: ownerStr,
Name: identifier,
Description: "",
Web: []string{},
GraspServers: []string{"gitnostr.com", "relay.ngit.dev"},
EarliestUniqueCommit: earliestCommit,
Maintainers: []string{},
}
}
// helper to get value from flags, existing config, or default
getValue := func(existingVal, flagVal, defaultVal string) string {
if flagVal != "" {
@@ -161,21 +236,84 @@ aside from those, there is also:
return defaultVals
}
config := Nip34Config{
Identifier: getValue(existingConfig.Identifier, c.String("identifier"), baseName),
Name: getValue(existingConfig.Name, c.String("name"), baseName),
Description: getValue(existingConfig.Description, c.String("description"), ""),
Web: getSliceValue(existingConfig.Web, c.StringSlice("web"), []string{}),
Owner: getValue(existingConfig.Owner, c.String("owner"), ""),
GraspServers: getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), []string{"gitnostr.com", "relay.ngit.dev"}),
EarliestUniqueCommit: getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), earliestCommit),
Maintainers: getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), []string{}),
}
// override with flags and existing config
config.Identifier = getValue(existingConfig.Identifier, c.String("identifier"), config.Identifier)
config.Name = getValue(existingConfig.Name, c.String("name"), config.Name)
config.Description = getValue(existingConfig.Description, c.String("description"), config.Description)
config.Web = getSliceValue(existingConfig.Web, c.StringSlice("web"), config.Web)
config.Owner = getValue(existingConfig.Owner, c.String("owner"), config.Owner)
config.GraspServers = getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), config.GraspServers)
config.EarliestUniqueCommit = getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), config.EarliestUniqueCommit)
config.Maintainers = getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), config.Maintainers)
if c.Bool("interactive") {
if err := promptForConfig(&config); err != nil {
// prompt for name
if err := survey.AskOne(&survey.Input{
Message: "name",
Default: config.Name,
}, &config.Name); err != nil {
return err
}
// prompt for description
if err := survey.AskOne(&survey.Input{
Message: "description",
Default: config.Description,
}, &config.Description); err != nil {
return err
}
// prompt for grasp servers
graspServers, err := promptForStringList("grasp servers", config.GraspServers, []string{
"gitnostr.com",
"relay.ngit.dev",
"pyramid.fiatjaf.com",
"git.shakespeare.dyi",
}, graspServerHost, nil)
if err != nil {
return err
}
config.GraspServers = graspServers
// prompt for web URLs
webURLs, err := promptForStringList("web URLs", config.Web, []string{
fmt.Sprintf("https://gitworkshop.dev/%s/%s",
nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)),
config.Identifier,
),
}, func(s string) string {
return "http" + nostr.NormalizeURL(s)[2:]
}, nil)
if err != nil {
return err
}
config.Web = webURLs
// prompt for earliest unique commit
if err := survey.AskOne(&survey.Input{
Message: "earliest unique commit",
Default: config.EarliestUniqueCommit,
}, &config.EarliestUniqueCommit); err != nil {
return err
}
// Prompt for maintainers
maintainers, err := promptForStringList("maintainers", config.Maintainers, []string{}, nil, func(s string) bool {
pk, err := parsePubKey(s)
if err != nil {
return false
}
if pk.Hex() == config.Owner {
return false
}
return true
})
if err != nil {
return err
}
config.Maintainers = maintainers
log("\n")
}
if err := config.Validate(); err != nil {
@@ -197,7 +335,7 @@ aside from those, there is also:
log("edit %s if needed, then run %s to publish.\n",
color.CyanString("nip34.json"),
color.CyanString("nak git announce"))
color.CyanString("nak git sync"))
return nil
},
@@ -266,22 +404,7 @@ aside from those, there is also:
}
// write nip34.json inside cloned directory
localConfig := Nip34Config{
Identifier: repo.ID,
Name: repo.Name,
Description: repo.Description,
Web: repo.Web,
Owner: nip19.EncodeNpub(repo.Event.PubKey),
GraspServers: make([]string, 0, len(repo.Relays)),
EarliestUniqueCommit: repo.EarliestUniqueCommitID,
Maintainers: make([]string, 0, len(repo.Maintainers)),
}
for _, r := range repo.Relays {
localConfig.GraspServers = append(localConfig.GraspServers, nostr.NormalizeURL(r))
}
for _, m := range repo.Maintainers {
localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m))
}
localConfig := RepositoryToConfig(repo)
if err := localConfig.Validate(); err != nil {
return fmt.Errorf("invalid config: %w", err)
@@ -423,8 +546,7 @@ aside from those, there is also:
pushSuccesses := 0
for _, relay := range repo.Relays {
relayURL := nostr.NormalizeURL(relay)
remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://")
remoteName = strings.TrimPrefix(remoteName, "ws://")
remoteName := gitRemoteName(relayURL)
log("pushing to %s...\n", color.CyanString(remoteName))
pushArgs := []string{"push", remoteName, fmt.Sprintf("%s:refs/heads/%s", localBranch, remoteBranch)}
@@ -617,50 +739,45 @@ aside from those, there is also:
func promptForStringList(
name string,
existing []string,
defaults []string,
alternatives []string,
normalize func(string) string,
validate func(string) bool,
) ([]string, error) {
options := make([]string, 0, len(defaults)+len(existing)+1)
options := make([]string, 0, len(defaults)+len(alternatives)+1)
options = append(options, defaults...)
options = append(options, "add another")
// add existing not in options
for _, item := range existing {
for _, item := range alternatives {
if !slices.Contains(options, item) {
options = append(options, item)
}
}
selected := make([]string, len(existing))
copy(selected, existing)
options = append(options, "add another")
selected := make([]string, len(defaults))
copy(selected, defaults)
for {
prompt := &survey.MultiSelect{
newSelected := []string{}
if err := survey.AskOne(&survey.MultiSelect{
Message: name,
Options: options,
Default: selected,
PageSize: 20,
}
if err := survey.AskOne(prompt, &selected); err != nil {
}, &newSelected); err != nil {
return nil, err
}
selected = newSelected
if slices.Contains(selected, "add another") {
selected = slices.DeleteFunc(selected, func(s string) bool { return s == "add another" })
singular := name
if strings.HasSuffix(singular, "s") {
singular = singular[:len(singular)-1]
}
newPrompt := &survey.Input{
Message: fmt.Sprintf("enter new %s", singular),
}
var newItem string
if err := survey.AskOne(newPrompt, &newItem); err != nil {
if err := survey.AskOne(&survey.Input{
Message: fmt.Sprintf("enter new %s", strings.TrimSuffix(name, "s")),
}, &newItem); err != nil {
return nil, err
}
@@ -690,97 +807,6 @@ func promptForStringList(
return selected, nil
}
func promptForConfig(config *Nip34Config) error {
log("\nenter repository details (use arrow keys to navigate, space to select/deselect, enter to confirm):\n\n")
// prompt for identifier
identifierPrompt := &survey.Input{
Message: "identifier",
Default: config.Identifier,
}
if err := survey.AskOne(identifierPrompt, &config.Identifier); err != nil {
return err
}
// prompt for name
namePrompt := &survey.Input{
Message: "name",
Default: config.Name,
}
if err := survey.AskOne(namePrompt, &config.Name); err != nil {
return err
}
// prompt for description
descPrompt := &survey.Input{
Message: "description",
Default: config.Description,
}
if err := survey.AskOne(descPrompt, &config.Description); err != nil {
return err
}
// prompt for owner
for {
ownerPrompt := &survey.Input{
Message: "owner (npub or hex)",
Default: config.Owner,
}
if err := survey.AskOne(ownerPrompt, &config.Owner); err != nil {
return err
}
if pubkey, err := parsePubKey(config.Owner); err == nil {
config.Owner = pubkey.Hex()
break
}
}
// prompt for grasp servers
graspServers, err := promptForStringList("grasp servers", config.GraspServers, []string{
"gitnostr.com",
"relay.ngit.dev",
"pyramid.fiatjaf.com",
"git.shakespeare.dyi",
}, graspServerHost, nil)
if err != nil {
return err
}
config.GraspServers = graspServers
// prompt for web URLs
webURLs, err := promptForStringList("web URLs", config.Web, []string{
fmt.Sprintf("https://gitworkshop.dev/%s/%s",
nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)),
config.Identifier,
),
}, func(s string) string {
return "http" + nostr.NormalizeURL(s)[2:]
}, nil)
if err != nil {
return err
}
config.Web = webURLs
// Prompt for maintainers
maintainers, err := promptForStringList("maintainers", config.Maintainers, []string{}, nil, func(s string) bool {
pk, err := parsePubKey(s)
if err != nil {
return false
}
if pk.Hex() == config.Owner {
return false
}
return true
})
if err != nil {
return err
}
config.Maintainers = maintainers
log("\n")
return nil
}
func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.RepositoryState, error) {
// read current nip34.json
localConfig, err := readNip34ConfigFile("")
@@ -914,7 +940,7 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.
func fetchFromRemotes(ctx context.Context, targetDir string, repo nip34.Repository) {
// fetch from each grasp remote
for _, grasp := range repo.Relays {
remoteName := "nip34/grasp/" + strings.Split(grasp, "/")[2]
remoteName := gitRemoteName(grasp)
logverbose("fetching from %s...\n", remoteName)
fetchCmd := exec.Command("git", "fetch", remoteName)
@@ -940,14 +966,16 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) {
return
}
// delete all nip34/grasp/ remotes
// delete all nip34/grasp/ remotes that we don't have anymore in repo
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
for i, remote := range remotes {
remote = strings.TrimSpace(remote)
remotes[i] = remote
if strings.HasPrefix(remote, "nip34/grasp/") {
if !slices.Contains(repo.Relays, nostr.NormalizeURL(remote[12:])) {
graspURL := rebuildGraspURLFromRemote(remote)
if !slices.Contains(repo.Relays, nostr.NormalizeURL(graspURL)) {
delCmd := exec.Command("git", "remote", "remove", remote)
if dir != "" {
delCmd.Dir = dir
@@ -961,7 +989,7 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) {
// create new remotes for each grasp server
for _, relay := range repo.Relays {
remote := "nip34/grasp/" + strings.TrimPrefix(relay, "wss://")
remote := gitRemoteName(relay)
if !slices.Contains(remotes, remote) {
// construct the git URL
@@ -1367,8 +1395,6 @@ func figureOutBranches(c *cli.Command, refspec string, isPush bool) (
return localBranch, remoteBranch, nil
}
func graspServerHost(s string) string { return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2] }
type Nip34Config struct {
Identifier string `json:"identifier"`
Name string `json:"name"`
@@ -1380,6 +1406,26 @@ type Nip34Config struct {
Maintainers []string `json:"maintainers"`
}
func RepositoryToConfig(repo nip34.Repository) Nip34Config {
config := Nip34Config{
Identifier: repo.ID,
Name: repo.Name,
Description: repo.Description,
Web: repo.Web,
Owner: nip19.EncodeNpub(repo.Event.PubKey),
GraspServers: make([]string, 0, len(repo.Relays)),
EarliestUniqueCommit: repo.EarliestUniqueCommitID,
Maintainers: make([]string, 0, len(repo.Maintainers)),
}
for _, r := range repo.Relays {
config.GraspServers = append(config.GraspServers, graspServerHost(r))
}
for _, m := range repo.Maintainers {
config.Maintainers = append(config.Maintainers, nip19.EncodeNpub(m))
}
return config
}
func (localConfig Nip34Config) Validate() error {
_, err := parsePubKey(localConfig.Owner)
if err != nil {
@@ -1430,3 +1476,18 @@ func (localConfig Nip34Config) ToRepository() nip34.Repository {
return localRepo
}
func gitRemoteName(graspURL string) string {
host := graspServerHost(graspURL)
host = strings.Replace(host, ":", "__", 1)
return "nip34/grasp/" + host
}
func rebuildGraspURLFromRemote(remoteName string) string {
host := strings.TrimPrefix(remoteName, "nip34/grasp/")
return strings.Replace(host, "__", ":", 1)
}
func graspServerHost(s string) string {
return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2]
}

27
go.mod
View File

@@ -1,10 +1,11 @@
module github.com/fiatjaf/nak
go 1.24.1
go 1.25
require (
fiatjaf.com/lib v0.3.1
fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8
fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/bep/debounce v1.2.1
github.com/btcsuite/btcd/btcec/v2 v2.3.6
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
@@ -28,21 +29,31 @@ require (
)
require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/FastFilter/xorfilter v0.2.1 // indirect
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bluekeyes/go-gitdiff v0.7.1 // indirect
github.com/btcsuite/btcd v0.24.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/glamour v0.10.0 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elliotchance/pie/v2 v2.7.0 // indirect
github.com/elnosh/gonuts v0.4.2 // indirect
@@ -50,6 +61,7 @@ require (
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-git/go-git/v5 v5.16.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hablullah/go-hijri v1.0.2 // indirect
github.com/hablullah/go-juliandays v1.0.0 // indirect
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
@@ -57,12 +69,18 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/templexxx/cpu v0.0.1 // indirect
@@ -75,6 +93,9 @@ require (
github.com/valyala/fasthttp v1.59.0 // indirect
github.com/wasilibs/go-re2 v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
go.etcd.io/bbolt v1.4.2 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect

53
go.sum
View File

@@ -1,19 +1,26 @@
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8 h1:R16mnlJ3qvVar7G4rzY+Z+mEAf2O6wpHTlRlHAt2Od8=
fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8/go.mod h1:QEGyTgAjjTFwDx2BJGZiCdmoAcWA/G+sQy7wDqKzSPU=
fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606 h1:wQHJ0TFA0Fuq92p/6u6AbsBFq6ZVToSdxV6puXVIruI=
fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg=
github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ=
@@ -48,6 +55,20 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
@@ -56,6 +77,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWs
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -72,6 +94,8 @@ github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXR
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
@@ -105,6 +129,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
@@ -112,6 +138,7 @@ github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw=
github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
@@ -136,6 +163,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
@@ -150,12 +179,17 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -163,6 +197,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -178,6 +216,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
@@ -219,9 +261,16 @@ github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2e
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

View File

@@ -49,6 +49,7 @@ var app = &cli.Command{
fsCmd,
publish,
git,
nip,
},
Version: version,
Flags: []cli.Flag{

201
nip.go Normal file
View File

@@ -0,0 +1,201 @@
package main
import (
"context"
"fmt"
"io"
"net/http"
"os/exec"
"runtime"
"strings"
"github.com/charmbracelet/glamour"
"github.com/urfave/cli/v3"
)
type nipInfo struct {
nip, desc, link string
}
var nip = &cli.Command{
Name: "nip",
Usage: "list NIPs or get the description of a NIP from its number",
Description: `lists NIPs, fetches and displays NIP text, or opens a NIP page in the browser.
examples:
nak nip # list all NIPs
nak nip 29 # shows nip29 details
nak nip open 29 # opens nip29 in browser`,
ArgsUsage: "[NIP number]",
Commands: []*cli.Command{
{
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 := ""
for info := range listnips() {
nipNum := normalize(info.nip)
if nipNum == reqNum {
foundLink = info.link
break
}
}
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 == "" {
// list all NIPs
for info := range listnips() {
stdout(info.nip + ": " + info.desc)
}
return nil
}
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)
var foundLink string
for info := range listnips() {
nipNum := normalize(info.nip)
if nipNum == reqNum {
foundLink = info.link
break
}
}
if foundLink == "" {
return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum))
}
// fetch the NIP markdown
url := "https://raw.githubusercontent.com/nostr-protocol/nips/master/" + foundLink
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to fetch NIP: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read NIP: %w", err)
}
// render markdown
rendered, err := glamour.Render(string(body), "auto")
if err != nil {
return fmt.Errorf("failed to render markdown: %w", err)
}
fmt.Print(rendered)
return nil
},
}
func listnips() <-chan nipInfo {
ch := make(chan nipInfo)
go func() {
defer close(ch)
resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md")
if err != nil {
// TODO: handle error? but since chan, maybe send error somehow, but for now, just close
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return
}
bodyStr := string(body)
epoch := strings.Index(bodyStr, "## List")
if epoch == -1 {
return
}
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]
}
ch <- nipInfo{nipPart, strings.TrimSpace(descPart), link}
}
}()
return ch
}