Repository: lightninglabs/taproot-assets

Issue:
[bug]: Repeated command "s" shortcut under universe
### Background

When running `tapcli u s`, the command that is run is `tapcli universe sync`. However, my intention was to run `tapcli universe stats`. Looking at the help output for the `universe` command, we can see the `s` shortcut is configured for `sync` and `stats`, and it appears `sync` wins the race.

### Your environment

```sh
tapcli getinfo                       # version of `tapd`, `lnd`, and network
{
    "version": "0.6.1-alpha commit=v0.6.1",
    "lnd_version": "0.19.2-beta",
    "network": "signet",
    "lnd_identity_pubkey": "03982243c20775f96bc22b3d989278ce1a6fe9a4d77a72af86182e2b5bfabf382f",
    "node_alias": "voltage-cash-node-2",
    "block_height": 2284458,
    "block_hash": "00000018e24cd9809033a30c9c4e407be89110678088b5fefa719806b953912b",
    "sync_to_chain": true
}

uname -mrsv                          # operating system 
Linux 6.12.10-76061203-generic #202412060638~1751390742~22.04~79b9668 SMP PREEMPT_DYNAMIC Tue J x86_64

bitcoind --version || btcd --version # version of `btcd`, `bitcoind`, or other backend

# it's running on voltage. so i'm not 100%.
```

* It's a little cold in my house.

### Steps to reproduce

1. Run `tapcli u --help`
2. Notice that `sync` and `stats` both have an alternative command of `s`
3. Cry one small tear

### Expected behavior

My screen should have been filled with the glorious statistics from my wonderful tapd server.

### Actual behavior

I got an inglorious help screen for the `sync` command.

### A comment

I would have just gone ahead and opened a PR for this, but I wasn't sure what solution was desired since there are a few to choose from.


Hints from issue comments:
@ffranr: Thank you for opening the issue!

Solution patch:
diff --git a/cmd/commands/universe.go b/cmd/commands/universe.go
index 5b4f76da..53f8a3ea 100644
--- a/cmd/commands/universe.go
+++ b/cmd/commands/universe.go
@@ -1315,7 +1315,7 @@ func universeInfo(ctx *cli.Context) error {
 
 var universeStatsCommand = cli.Command{
 	Name:      "stats",
-	ShortName: "s",
+	ShortName: "st",
 	Usage:     "query for stats related to the active local Universe server",
 	Description: `
 	Query for a set of aggregate statistics related to the local Universe
diff --git a/go.mod b/go.mod
index 5b897bb9..037c8a81 100644
--- a/go.mod
+++ b/go.mod
@@ -30,7 +30,7 @@ require (
 	github.com/lightninglabs/lndclient v0.19.0-9
 	github.com/lightninglabs/neutrino/cache v1.1.2
 	github.com/lightninglabs/taproot-assets/taprpc v1.0.9
-	github.com/lightningnetwork/lnd v0.19.2-beta.rc2
+	github.com/lightningnetwork/lnd v0.19.2-beta
 	github.com/lightningnetwork/lnd/cert v1.2.2
 	github.com/lightningnetwork/lnd/clock v1.1.1
 	github.com/lightningnetwork/lnd/fn/v2 v2.0.8
diff --git a/go.sum b/go.sum
index 6c0bbbfa..7f4d4719 100644
--- a/go.sum
+++ b/go.sum
@@ -1149,8 +1149,8 @@ github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9
 github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
 github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY=
 github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI=
-github.com/lightningnetwork/lnd v0.19.2-beta.rc2 h1:vPIMjQr8SWZJHn/j3QSFct4AbCVa0WA7k0j0lHqFDAA=
-github.com/lightningnetwork/lnd v0.19.2-beta.rc2/go.mod h1:+yKUfIGKKYRHGewgzQ6xi0S26DIfBiMv1zCdB3m6YxA=
+github.com/lightningnetwork/lnd v0.19.2-beta h1:3SKVrKYFY4IJLlrMf7cDzZcBeT+MxjI9Xy6YpY+EEX4=
+github.com/lightningnetwork/lnd v0.19.2-beta/go.mod h1:+yKUfIGKKYRHGewgzQ6xi0S26DIfBiMv1zCdB3m6YxA=
 github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI=
 github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U=
 github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0=



Test patch:
diff --git a/cmd/commands/commands_test.go b/cmd/commands/commands_test.go
new file mode 100644
index 00000000..a279fbed
--- /dev/null
+++ b/cmd/commands/commands_test.go
@@ -0,0 +1,82 @@
+package commands
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/urfave/cli"
+)
+
+// TestCommandShortNamesUnique ensures that all command short names are unique
+// within their respective command groups at the same level to avoid conflicts.
+func TestCommandShortNamesUnique(t *testing.T) {
+	// Create a new app to get all commands.
+	app := NewApp()
+
+	// Helper function to check short names within a group of commands at
+	// the same level.
+	//
+	// Note that we define using var here to avoid recursion issues.
+	var checkLevel func(commands []cli.Command, groupPath string)
+	checkLevel = func(commands []cli.Command, groupPath string) {
+		shortNames := make(map[string][]string)
+
+		// Check short names only at this level (not recursively).
+		for _, cmd := range commands {
+			// Check if command has a short name.
+			if cmd.ShortName == "" {
+				continue
+			}
+
+			// Command has a short name, so we add it to the map.
+			commandPath := groupPath
+			if commandPath != "" {
+				commandPath += " "
+			}
+
+			commandPath += cmd.Name
+			shortNames[cmd.ShortName] = append(
+				shortNames[cmd.ShortName], commandPath,
+			)
+		}
+
+		// Check for duplicates at this level.
+		var duplicates []string
+		for shortName, paths := range shortNames {
+			if len(paths) > 1 {
+				duplicates = append(duplicates, shortName)
+			}
+		}
+
+		// Fail the test if any duplicates were found at this level.
+		require.Empty(t, duplicates, "Found duplicate short names at "+
+			"command level '%s'", groupPath)
+
+		// Log all short names for reference (only in verbose mode).
+		if testing.Verbose() {
+			t.Logf("Level '%s' has %d unique short names:",
+				groupPath, len(shortNames))
+			for shortName, paths := range shortNames {
+				t.Logf("  %s -> %s", shortName, paths[0])
+			}
+		}
+
+		// Recursively check subcommands at their respective levels.
+		for _, cmd := range commands {
+			if len(cmd.Subcommands) > 0 {
+				// Formulate subgroup path.
+				subGroupPath := groupPath
+				if subGroupPath != "" {
+					subGroupPath += " "
+				}
+				subGroupPath += cmd.Name
+
+				// Recursively check subcommands.
+				checkLevel(cmd.Subcommands, subGroupPath)
+			}
+		}
+	}
+
+	// Check top-level commands.
+	checkLevel(app.Commands, "")
+}