Skip to content

Commit cfcb157

Browse files
committed
cli: make list output readable
1 parent e3fd5b9 commit cfcb157

5 files changed

Lines changed: 139 additions & 35 deletions

File tree

internal/api/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func (e *APIError) Error() string {
156156
if e.Code == "policy_sidecar_required" {
157157
return "cannot create cella: the selected policy requires Cella's credential sidecar, but the server has no complete sidecar configuration for this CLI token.\n" +
158158
"This is not a local command syntax problem. Re-run `latere auth login` with the latest CLI, then retry.\n" +
159-
"To choose another policy, run `latere cella policy list` and retry with `latere cella create --policy <name>` using a selectable policy where SIDECAR is `no`.\n" +
159+
"To choose another policy, run `latere cella policy list` and retry with `latere cella create --policy <name>` using a selectable policy where sidecar is `no`.\n" +
160160
"If no such policy is available, ask your Latere admin/support to configure the CLI sidecar client or assign a non-sidecar policy.\n" +
161161
"server code: policy_sidecar_required"
162162
}

internal/api/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestAPIErrorPolicySidecarRequiredIsActionable(t *testing.T) {
1919
"latere auth login",
2020
"latere cella policy list",
2121
"latere cella create --policy <name>",
22-
"SIDECAR is `no`",
22+
"sidecar is `no`",
2323
"server code: policy_sidecar_required",
2424
} {
2525
if !strings.Contains(err, want) {

internal/commands/cella.go

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"path/filepath"
1717
"strconv"
1818
"strings"
19-
"text/tabwriter"
2019
"time"
2120

2221
"github.com/spf13/cobra"
@@ -303,14 +302,12 @@ cellas returned by the backend, including warm-pool cellas.`,
303302
if jsonF {
304303
return printJSON(sbs)
305304
}
306-
tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
307-
_, _ = fmt.Fprintln(tw, "NAME\tID\tSTATE\tTIER\tDISK\tCREATED")
308-
for _, s := range sbs {
309-
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%dGi\t%s\n",
310-
nameOrDash(s.Name), s.ID, s.State, defaultStr(s.Tier, "-"),
311-
s.DiskGB, humanAge(s.CreatedAt))
305+
if len(sbs) == 0 {
306+
fmt.Fprintln(os.Stdout, "No cellas are visible to this token.")
307+
return nil
312308
}
313-
return tw.Flush()
309+
printSandboxList(sbs)
310+
return nil
314311
},
315312
}
316313
cmd.Flags().StringVar(&apiURL, "api-url", "", "override Cella API base URL")
@@ -338,7 +335,7 @@ Use a selectable policy with:
338335
latere cella create --policy <name>
339336
340337
If create fails because the selected policy requires the sidecar, list
341-
policies and choose a selectable policy where SIDECAR is "no", or ask
338+
policies and choose a selectable policy where sidecar is "no", or ask
342339
an admin to configure the sidecar client for your token.`,
343340
Example: ` latere cella policy
344341
latere cella policy list
@@ -391,20 +388,19 @@ func runPolicyList(ctx context.Context, apiURL string, jsonF bool) error {
391388
}
392389

393390
func printPolicies(policies []policyDTO) {
394-
tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
395-
_, _ = fmt.Fprintln(tw, "NAME\tDEFAULT\tSELECTABLE\tSIDECAR\tCAPABILITY\tSOURCE\tDESCRIPTION")
396-
for _, p := range policies {
397-
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
398-
p.Name,
399-
yesNo(p.IsDefault),
400-
yesNo(p.Selectable),
401-
yesNo(p.SidecarRequired),
402-
defaultStr(p.CapabilityProfile, "-"),
403-
defaultStr(p.AssignmentSource, "-"),
404-
oneLine(defaultStr(p.Description, p.Label)),
405-
)
406-
}
407-
_ = tw.Flush()
391+
for i, p := range policies {
392+
if i > 0 {
393+
fmt.Fprintln(os.Stdout)
394+
}
395+
printWrappedField("policy", p.Name)
396+
printWrappedField("label", p.Label)
397+
printWrappedField("default", yesNo(p.IsDefault))
398+
printWrappedField("selectable", yesNo(p.Selectable))
399+
printWrappedField("sidecar", yesNo(p.SidecarRequired))
400+
printWrappedField("capability", defaultStr(p.CapabilityProfile, "-"))
401+
printWrappedField("source", defaultStr(p.AssignmentSource, "-"))
402+
printWrappedField("description", defaultStr(p.Description, "-"))
403+
}
408404
}
409405

410406
func newCeGetCmd() *cobra.Command {
@@ -1515,12 +1511,35 @@ func printJSON(v any) error {
15151511
return enc.Encode(v)
15161512
}
15171513

1514+
func printSandboxList(sbs []sandboxDTO) {
1515+
for i, s := range sbs {
1516+
if i > 0 {
1517+
fmt.Fprintln(os.Stdout)
1518+
}
1519+
printSandbox(s)
1520+
}
1521+
}
1522+
15181523
func printSandbox(s sandboxDTO) {
1524+
printWrappedField("cella", nameOrDash(s.Name))
1525+
printWrappedField("id", s.ID)
1526+
printWrappedField("state", s.State)
1527+
printWrappedField("tier", defaultStr(s.Tier, "-"))
1528+
if s.DiskGB > 0 {
1529+
printWrappedField("disk", fmt.Sprintf("%dGi", s.DiskGB))
1530+
}
15191531
if size := sandboxResourceSummary(s); size != "" {
1520-
fmt.Printf("%s %s %s %s\n", nameOrDash(s.Name), s.ID, s.State, size)
1521-
return
1532+
printWrappedField("resources", size)
1533+
}
1534+
if !s.CreatedAt.IsZero() {
1535+
printWrappedField("created", humanAge(s.CreatedAt)+" ago")
1536+
}
1537+
if !s.Deadline.IsZero() {
1538+
printWrappedField("deadline", s.Deadline.Format(time.RFC3339))
1539+
}
1540+
if s.Workdir != "" {
1541+
printWrappedField("workdir", s.Workdir)
15221542
}
1523-
fmt.Printf("%s %s %s\n", nameOrDash(s.Name), s.ID, s.State)
15241543
}
15251544

15261545
// sandboxResourceSummary renders the cpu_milli / memory_mb fields
@@ -1560,6 +1579,50 @@ func yesNo(v bool) string {
15601579
return "no"
15611580
}
15621581

1582+
func printWrappedField(label, value string) {
1583+
value = oneLine(value)
1584+
if value == "" {
1585+
return
1586+
}
1587+
const (
1588+
labelWidth = 12
1589+
maxWidth = 88
1590+
)
1591+
prefix := fmt.Sprintf("%-*s", labelWidth, label+":")
1592+
lines := wrapText(value, maxWidth-labelWidth)
1593+
if len(lines) == 0 {
1594+
fmt.Fprintln(os.Stdout, prefix)
1595+
return
1596+
}
1597+
fmt.Fprintln(os.Stdout, prefix+lines[0])
1598+
indent := strings.Repeat(" ", labelWidth)
1599+
for _, line := range lines[1:] {
1600+
fmt.Fprintln(os.Stdout, indent+line)
1601+
}
1602+
}
1603+
1604+
func wrapText(s string, width int) []string {
1605+
words := strings.Fields(s)
1606+
if len(words) == 0 {
1607+
return nil
1608+
}
1609+
if width <= 0 {
1610+
width = 76
1611+
}
1612+
var lines []string
1613+
line := words[0]
1614+
for _, word := range words[1:] {
1615+
if len(line)+1+len(word) > width {
1616+
lines = append(lines, line)
1617+
line = word
1618+
continue
1619+
}
1620+
line += " " + word
1621+
}
1622+
lines = append(lines, line)
1623+
return lines
1624+
}
1625+
15631626
func oneLine(s string) string {
15641627
return strings.Join(strings.Fields(s), " ")
15651628
}

internal/commands/policy_test.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,22 @@ func TestPolicyListPrintsCreateGuidanceFields(t *testing.T) {
5757
t.Fatalf("Authorization = %q, want bearer token", authz)
5858
}
5959
for _, want := range []string{
60-
"NAME",
61-
"DEFAULT",
62-
"SELECTABLE",
63-
"SIDECAR",
60+
"policy: agent-default",
61+
"default: yes",
62+
"selectable: yes",
63+
"sidecar: yes",
6464
"agent-default",
65-
"yes",
6665
"restricted-network",
6766
"restricted-no-network",
68-
"client",
67+
"source: client",
6968
} {
7069
if !strings.Contains(out, want) {
7170
t.Fatalf("policy list output missing %q:\n%s", want, out)
7271
}
7372
}
73+
if strings.Contains(out, "NAME") || strings.Contains(out, "\t") {
74+
t.Fatalf("policy list should be pure text, not a wide table:\n%s", out)
75+
}
7476
}
7577

7678
func TestPolicyListEmptyExplainsNextStep(t *testing.T) {
@@ -96,6 +98,45 @@ func TestPolicyListEmptyExplainsNextStep(t *testing.T) {
9698
}
9799
}
98100

101+
func TestSandboxListPrintsReadableRecords(t *testing.T) {
102+
out := capturePolicyStdout(t, func() {
103+
printSandboxList([]sandboxDTO{
104+
{
105+
ID: "sb-019dc976-2b28-7c55-8778-bf7d5ae6c58d",
106+
Name: "workspace-1",
107+
State: "stopped",
108+
Tier: "persistent",
109+
DiskGB: 5,
110+
CPUMilli: 1000,
111+
MemoryMB: 2048,
112+
},
113+
{
114+
ID: "sb-019dc976-2b28-7c55-8778-warm-pool",
115+
Name: "warm-pool",
116+
State: "running",
117+
Tier: "ephemeral",
118+
DiskGB: 5,
119+
},
120+
})
121+
})
122+
123+
for _, want := range []string{
124+
"cella: workspace-1",
125+
"id: sb-019dc976-2b28-7c55-8778-bf7d5ae6c58d",
126+
"state: stopped",
127+
"tier: persistent",
128+
"resources: cpu=1000m memory=2048Mi",
129+
"\n\ncella: warm-pool",
130+
} {
131+
if !strings.Contains(out, want) {
132+
t.Fatalf("sandbox list output missing %q:\n%s", want, out)
133+
}
134+
}
135+
if strings.Contains(out, "NAME") || strings.Contains(out, "\t") {
136+
t.Fatalf("sandbox list should be pure text, not a wide table:\n%s", out)
137+
}
138+
}
139+
99140
func writeTestToken(t *testing.T) {
100141
t.Helper()
101142
path := filepath.Join(t.TempDir(), "token.json")

internal/commands/root_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func TestHelpIncludesUserExamples(t *testing.T) {
9191
want: []string{
9292
"List Cella policy profiles visible to the current token.",
9393
"latere cella create --policy <name>",
94-
"choose a selectable policy where SIDECAR is \"no\"",
94+
"choose a selectable policy where sidecar is \"no\"",
9595
},
9696
},
9797
{

0 commit comments

Comments
 (0)