From f50964e11593e98591a668c218f0c619794cf875 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Thu, 20 Aug 2026 17:17:37 +0200 Subject: [PATCH 1/3] Move each helper to the package that uses it TakeJsonFlag sat in dispatch with all five of its callers outside it, so every part imported dispatch for that one function. It moves down into internal/api, which is where CLAUDE.md says shared work goes, and no part imports another package now beyond api. TakeServerFlag has one caller, inside Dispatch, so it stops being exported. ValidImei had all three of its callers in internal/device and moves there. --- internal/account/account.go | 3 +-- internal/api/arguments.go | 17 +++++++++++++++++ internal/api/devices.go | 14 -------------- internal/device/device.go | 23 ++++++++++++++++++----- internal/dispatch/dispatch.go | 20 ++------------------ internal/dispatch/dispatch_test.go | 2 +- internal/fleet/fleet.go | 3 +-- internal/key/key.go | 3 +-- internal/member/member.go | 3 +-- 9 files changed, 42 insertions(+), 46 deletions(-) create mode 100644 internal/api/arguments.go diff --git a/internal/account/account.go b/internal/account/account.go index 6b18a44..fda4769 100644 --- a/internal/account/account.go +++ b/internal/account/account.go @@ -12,11 +12,10 @@ import ( "strings" "github.com/siliconwitchery/superstack-cli/internal/api" - "github.com/siliconwitchery/superstack-cli/internal/dispatch" ) func Balance(session api.Session, arguments []string) error { - positionals, jsonOutput := dispatch.TakeJsonFlag(arguments) + positionals, jsonOutput := api.TakeJsonFlag(arguments) if len(positionals) > 1 { return errors.New("account balance takes at most one fleet id") diff --git a/internal/api/arguments.go b/internal/api/arguments.go new file mode 100644 index 0000000..7cf8abf --- /dev/null +++ b/internal/api/arguments.go @@ -0,0 +1,17 @@ +package api + +func TakeJsonFlag(arguments []string) ([]string, bool) { + positionals := []string{} + jsonOutput := false + + for _, argument := range arguments { + if argument == "--json" { + jsonOutput = true + continue + } + + positionals = append(positionals, argument) + } + + return positionals, jsonOutput +} diff --git a/internal/api/devices.go b/internal/api/devices.go index 60ebd62..99d4081 100644 --- a/internal/api/devices.go +++ b/internal/api/devices.go @@ -45,17 +45,3 @@ func FetchDevices(session Session) ([]DeviceEntry, error) { return devices, nil } - -func ValidImei(imei string) bool { - if len(imei) != 15 { - return false - } - - for _, digit := range imei { - if digit < '0' || digit > '9' { - return false - } - } - - return true -} diff --git a/internal/device/device.go b/internal/device/device.go index 1edbe68..e09adc9 100644 --- a/internal/device/device.go +++ b/internal/device/device.go @@ -12,7 +12,6 @@ import ( "time" "github.com/siliconwitchery/superstack-cli/internal/api" - "github.com/siliconwitchery/superstack-cli/internal/dispatch" ) func Claim(session api.Session, arguments []string) error { @@ -24,7 +23,7 @@ func Claim(session api.Session, arguments []string) error { imei := arguments[0] - if !api.ValidImei(imei) { + if !validImei(imei) { return errors.New("the IMEI is the 15-digit number printed on the device") } @@ -93,7 +92,7 @@ func Claim(session api.Session, arguments []string) error { } func List(session api.Session, arguments []string) error { - positionals, jsonOutput := dispatch.TakeJsonFlag(arguments) + positionals, jsonOutput := api.TakeJsonFlag(arguments) if len(positionals) > 1 { return errors.New("device list takes at most one fleet id") @@ -262,7 +261,7 @@ func Rename(session api.Session, arguments []string) error { imei := arguments[0] - if !api.ValidImei(imei) { + if !validImei(imei) { return errors.New("the IMEI is the 15-digit number printed on the device") } @@ -310,7 +309,7 @@ func Release(session api.Session, arguments []string) error { imei := arguments[0] - if !api.ValidImei(imei) { + if !validImei(imei) { return errors.New("the IMEI is the 15-digit number printed on the device") } @@ -383,3 +382,17 @@ func Release(session api.Session, arguments []string) error { return nil } + +func validImei(imei string) bool { + if len(imei) != 15 { + return false + } + + for _, digit := range imei { + if digit < '0' || digit > '9' { + return false + } + } + + return true +} diff --git a/internal/dispatch/dispatch.go b/internal/dispatch/dispatch.go index 0c3fcdb..aaa30f0 100644 --- a/internal/dispatch/dispatch.go +++ b/internal/dispatch/dispatch.go @@ -9,7 +9,7 @@ import ( "github.com/siliconwitchery/superstack-cli/internal/api" ) -func TakeServerFlag(arguments []string) ([]string, string, error) { +func takeServerFlag(arguments []string) ([]string, string, error) { remaining := []string{} base := api.DefaultBase @@ -40,22 +40,6 @@ func TakeServerFlag(arguments []string) ([]string, string, error) { return remaining, strings.TrimSuffix(base, "/"), nil } -func TakeJsonFlag(arguments []string) ([]string, bool) { - positionals := []string{} - jsonOutput := false - - for _, argument := range arguments { - if argument == "--json" { - jsonOutput = true - continue - } - - positionals = append(positionals, argument) - } - - return positionals, jsonOutput -} - type Command struct { Name string Arguments string @@ -141,7 +125,7 @@ func printHelp(session api.Session, sections []Section) { } func Dispatch(sections []Section, version string, arguments []string, in io.Reader, out io.Writer) error { - arguments, base, err := TakeServerFlag(arguments) + arguments, base, err := takeServerFlag(arguments) if err != nil { return err diff --git a/internal/dispatch/dispatch_test.go b/internal/dispatch/dispatch_test.go index 1829742..fc3f9f0 100644 --- a/internal/dispatch/dispatch_test.go +++ b/internal/dispatch/dispatch_test.go @@ -68,7 +68,7 @@ func TestTakeServerFlag(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - remaining, base, err := TakeServerFlag(test.arguments) + remaining, base, err := takeServerFlag(test.arguments) if test.wantError != "" { if err == nil || !strings.Contains(err.Error(), test.wantError) { diff --git a/internal/fleet/fleet.go b/internal/fleet/fleet.go index 8c1f3db..9a9dcdb 100644 --- a/internal/fleet/fleet.go +++ b/internal/fleet/fleet.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/siliconwitchery/superstack-cli/internal/api" - "github.com/siliconwitchery/superstack-cli/internal/dispatch" ) func Create(session api.Session, arguments []string) error { @@ -62,7 +61,7 @@ func Create(session api.Session, arguments []string) error { } func List(session api.Session, arguments []string) error { - positionals, jsonOutput := dispatch.TakeJsonFlag(arguments) + positionals, jsonOutput := api.TakeJsonFlag(arguments) if len(positionals) != 0 { return errors.New("fleet list takes no arguments") diff --git a/internal/key/key.go b/internal/key/key.go index 067a05e..273fc86 100644 --- a/internal/key/key.go +++ b/internal/key/key.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/siliconwitchery/superstack-cli/internal/api" - "github.com/siliconwitchery/superstack-cli/internal/dispatch" ) func Create(session api.Session, arguments []string) error { @@ -69,7 +68,7 @@ func Create(session api.Session, arguments []string) error { } func List(session api.Session, arguments []string) error { - positionals, jsonOutput := dispatch.TakeJsonFlag(arguments) + positionals, jsonOutput := api.TakeJsonFlag(arguments) if len(positionals) > 1 { return errors.New("key list takes at most one fleet id") diff --git a/internal/member/member.go b/internal/member/member.go index 0ef4636..deff734 100644 --- a/internal/member/member.go +++ b/internal/member/member.go @@ -12,7 +12,6 @@ import ( "strings" "github.com/siliconwitchery/superstack-cli/internal/api" - "github.com/siliconwitchery/superstack-cli/internal/dispatch" ) func Add(session api.Session, arguments []string) error { @@ -61,7 +60,7 @@ func Add(session api.Session, arguments []string) error { } func List(session api.Session, arguments []string) error { - positionals, jsonOutput := dispatch.TakeJsonFlag(arguments) + positionals, jsonOutput := api.TakeJsonFlag(arguments) if len(positionals) != 1 { return errors.New("member list takes a fleet id") From 9d4f66d713191a44ca542dce341c18d7f377ca58 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Thu, 20 Aug 2026 17:17:48 +0200 Subject: [PATCH 2/3] Say what is wrong when --server has no address --server / trimmed to nothing and then blamed the network: "the server could not be reached, check your connection". --server= said "--server needs an address", which is what both should say. The address is now trimmed before it is tested for emptiness, so every way of writing an empty one reaches the same message. --- internal/dispatch/dispatch.go | 14 ++++++++------ internal/dispatch/dispatch_test.go | 5 +++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/dispatch/dispatch.go b/internal/dispatch/dispatch.go index aaa30f0..f0218d3 100644 --- a/internal/dispatch/dispatch.go +++ b/internal/dispatch/dispatch.go @@ -17,7 +17,7 @@ func takeServerFlag(arguments []string) ([]string, string, error) { for index := 0; index < len(arguments); index++ { switch { case arguments[index] == "--server": - if index+1 == len(arguments) || arguments[index+1] == "" { + if index+1 == len(arguments) { return nil, "", errors.New("--server needs an address") } @@ -28,16 +28,18 @@ func takeServerFlag(arguments []string) ([]string, string, error) { case strings.HasPrefix(arguments[index], "--server="): base = strings.TrimPrefix(arguments[index], "--server=") - if base == "" { - return nil, "", errors.New("--server needs an address") - } - default: remaining = append(remaining, arguments[index]) } } - return remaining, strings.TrimSuffix(base, "/"), nil + base = strings.TrimRight(base, "/") + + if base == "" { + return nil, "", errors.New("--server needs an address") + } + + return remaining, base, nil } type Command struct { diff --git a/internal/dispatch/dispatch_test.go b/internal/dispatch/dispatch_test.go index fc3f9f0..237f270 100644 --- a/internal/dispatch/dispatch_test.go +++ b/internal/dispatch/dispatch_test.go @@ -64,6 +64,11 @@ func TestTakeServerFlag(t *testing.T) { arguments: []string{"--server", "", "login"}, wantError: "needs an address", }, + { + name: "an address of nothing but slashes", + arguments: []string{"--server", "//", "login"}, + wantError: "needs an address", + }, } for _, test := range tests { From 1c7f2aab09c3b5826ae4dade2272e21c3a8dd898 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Thu, 20 Aug 2026 17:18:01 +0200 Subject: [PATCH 3/3] Drop short mode, and say what the CLI does today Go caches per package, and the cached result is keyed on the linked test binary, so a change to one part leaves the others cached and only a change the login tests link re-runs their thirty seconds. -short skipped those same tests on a flag typed by hand, which gets it wrong in exactly the run where login is what you edited. The README intro promised uploading Lua code and streaming logs. Both print "is not available yet", so it now says what the tree has and names the two that are coming. --- README.md | 14 ++++++++------ internal/login/login_test.go | 4 ---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 11221af..33e9c5c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Superstack CLI -`superstack` is the command line interface to Superstack: log in, claim -devices, upload Lua code, and stream logs from your fleet. It is a single -static binary for managing Superstack from a terminal. +`superstack` is the command line interface to Superstack: log in, create +fleets, claim devices, and manage who can reach them. It is a single static +binary for managing Superstack from a terminal. + +Uploading Lua code and streaming logs are not available yet. ## Install @@ -60,11 +62,11 @@ static binary for managing Superstack from a terminal. ./superstack ``` -1. Run the fast tests while you work. The login tests wait real seconds for - the poll interval, and `-short` skips them: +1. Run the tests while you work. Go caches per package, so a change to one + part leaves every other part cached: ```sh - CGO_ENABLED=0 go test -short ./... + CGO_ENABLED=0 go test ./... ``` 1. Run every check before opening a pull request: diff --git a/internal/login/login_test.go b/internal/login/login_test.go index 8c80944..8840533 100644 --- a/internal/login/login_test.go +++ b/internal/login/login_test.go @@ -296,10 +296,6 @@ func TestLogin(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - if testing.Short() && (len(test.pollAnswers) > 0 || strings.Contains(test.deviceAnswer, `"expires_in":0`)) { - t.Skip("the poll loop waits real seconds") - } - apitest.IsolateKeyStorage(t) deviceInterval := test.deviceInterval