Skip to content

Commit 8a435c9

Browse files
authored
Merge pull request #1358 from entireio/linear-brewing-cocke
auth: tab-complete context names for `auth use`
2 parents ace2b06 + 813e29e commit 8a435c9

2 files changed

Lines changed: 99 additions & 1 deletion

File tree

cmd/entire/cli/auth_context.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ func newAuthUseCmd() *cobra.Command {
3131
"Control-plane commands (auth status/list/revoke, org/project/repo/grant) still\n" +
3232
"target the configured auth host (ENTIRE_AUTH_BASE_URL / the default), so\n" +
3333
"switching to a context on a different login server does not retarget them yet.",
34-
Args: cobra.ExactArgs(1),
34+
Args: cobra.ExactArgs(1),
35+
ValidArgsFunction: completeContextNames,
3536
RunE: func(cmd *cobra.Command, args []string) error {
3637
if err := auth.SetCurrentContext(args[0]); err != nil {
3738
return err //nolint:wrapcheck // already a user-facing message
@@ -43,6 +44,34 @@ func newAuthUseCmd() *cobra.Command {
4344
}
4445
}
4546

47+
// completeContextNames is the ValidArgsFunction for commands taking a single
48+
// <context> positional. It offers the stored context names, each annotated
49+
// (shell-completion descriptions, after a tab) with handle, core URL, and an
50+
// "(active)" marker for the current context. Errors are swallowed because
51+
// completion runs on every TAB press; a failed read just yields no suggestions.
52+
func completeContextNames(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
53+
if len(args) != 0 {
54+
// <context> is a single positional; nothing to complete past it.
55+
return nil, cobra.ShellCompDirectiveNoFileComp
56+
}
57+
all, current, err := auth.Contexts()
58+
if err != nil {
59+
return nil, cobra.ShellCompDirectiveNoFileComp
60+
}
61+
out := make([]string, 0, len(all))
62+
for _, c := range all {
63+
desc := c.Handle
64+
if c.CoreURL != "" {
65+
desc += " " + c.CoreURL
66+
}
67+
if c.Name == current {
68+
desc += " (active)"
69+
}
70+
out = append(out, c.Name+"\t"+desc)
71+
}
72+
return out, cobra.ShellCompDirectiveNoFileComp
73+
}
74+
4675
// warnIfCrossCoreContext warns when the now-active context authenticates
4776
// against a different core than the control plane targets. Clone resolves
4877
// per-cluster and is unaffected, but auth status/list/revoke and the

cmd/entire/cli/auth_context_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/entireio/cli/cmd/entire/cli/auth"
1313
"github.com/entireio/cli/internal/entireclient/tokenstore"
14+
"github.com/spf13/cobra"
1415
)
1516

1617
// TestResolveStatusTarget_PrefersActiveContext pins the multi-core fix: status
@@ -89,6 +90,74 @@ func TestRunAuthContexts(t *testing.T) {
8990
}
9091
}
9192

93+
func TestCompleteContextNames(t *testing.T) {
94+
cfgDir := t.TempDir()
95+
t.Setenv("ENTIRE_CONFIG_DIR", cfgDir)
96+
restore := tokenstore.UseFileBackendForTesting(filepath.Join(t.TempDir(), "tokens.json"))
97+
t.Cleanup(restore)
98+
99+
exp := time.Now().Add(time.Hour).Unix()
100+
101+
// Two contexts; the second one recorded with activate=true is current.
102+
if _, err := auth.RecordLoginContext(makeContextJWT(t, fmt.Sprintf(`{"iss":"https://core-a.example.com","handle":"alice","exp":%d}`, exp)), "", false); err != nil {
103+
t.Fatalf("record core-a: %v", err)
104+
}
105+
currentName, err := auth.RecordLoginContext(makeContextJWT(t, fmt.Sprintf(`{"iss":"https://core-b.example.com","handle":"bob","exp":%d}`, exp)), "", true)
106+
if err != nil {
107+
t.Fatalf("record core-b: %v", err)
108+
}
109+
110+
got, directive := completeContextNames(nil, nil, "")
111+
if directive != cobra.ShellCompDirectiveNoFileComp {
112+
t.Fatalf("directive = %v, want NoFileComp", directive)
113+
}
114+
if len(got) != 2 {
115+
t.Fatalf("completions = %v, want 2 entries", got)
116+
}
117+
118+
// Each entry is "name\tdescription" carrying handle and core URL; the
119+
// active context is annotated "(active)" and no other entry is.
120+
var activeCount int
121+
for _, entry := range got {
122+
name, desc, found := strings.Cut(entry, "\t")
123+
if !found {
124+
t.Fatalf("entry %q missing tab-separated description", entry)
125+
}
126+
if name == currentName {
127+
if !strings.Contains(desc, "(active)") {
128+
t.Fatalf("active entry %q missing (active) marker", entry)
129+
}
130+
if !strings.Contains(desc, "bob") || !strings.Contains(desc, "core-b.example.com") {
131+
t.Fatalf("active entry %q missing handle/core URL", entry)
132+
}
133+
activeCount++
134+
} else if strings.Contains(desc, "(active)") {
135+
t.Fatalf("non-active entry %q wrongly marked (active)", entry)
136+
}
137+
}
138+
if activeCount != 1 {
139+
t.Fatalf("want exactly one (active) entry, got %d", activeCount)
140+
}
141+
142+
// Past the single positional: nothing to complete.
143+
got, directive = completeContextNames(nil, []string{"already"}, "")
144+
if got != nil || directive != cobra.ShellCompDirectiveNoFileComp {
145+
t.Fatalf("with an arg present, want (nil, NoFileComp), got (%v, %v)", got, directive)
146+
}
147+
}
148+
149+
func TestCompleteContextNames_NoContexts(t *testing.T) {
150+
cfgDir := t.TempDir()
151+
t.Setenv("ENTIRE_CONFIG_DIR", cfgDir)
152+
restore := tokenstore.UseFileBackendForTesting(filepath.Join(t.TempDir(), "tokens.json"))
153+
t.Cleanup(restore)
154+
155+
got, directive := completeContextNames(nil, nil, "")
156+
if len(got) != 0 || directive != cobra.ShellCompDirectiveNoFileComp {
157+
t.Fatalf("no contexts: want (empty, NoFileComp), got (%v, %v)", got, directive)
158+
}
159+
}
160+
92161
func TestWarnIfCrossCoreContext(t *testing.T) {
93162
cfgDir := t.TempDir()
94163
t.Setenv("ENTIRE_CONFIG_DIR", cfgDir)

0 commit comments

Comments
 (0)