detached b2108ef

cli: tighten inferred remote output and filtering

commit
b2108ef4be3df18c5fbac3854ee6e3686364cae7
author
brice · · 2026-05-15 07:20 UTC
parent
2154bd4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +44 -27
M internal/adapters/in/cli/attachments.go +34 -18
@@ -86,22 +86,14 @@ func runAttachmentsList(ctx context.Context, cp ControlPlane, target string, jso
8686 if err != nil {
8787 return fmt.Errorf("failed to get attachments: %w", err)
8888 }
89-
9089 if len(images) == 0 {
91- fmt.Printf("No attachments configured for '%s'\n", target)
92- return nil
90+ _, err := fmt.Fprintf(out, "No attachments configured for '%s'\n", target)
91+ return err
9392 }
94-
9593 if jsonOut {
9694 return writeJSON(out, map[string]any{"target": target, "images": images})
9795 }
98-
99- fmt.Println(styles.Theme.Title.Render(fmt.Sprintf("Attachments for %s", target)))
100- fmt.Println()
101- for _, img := range images {
102- fmt.Printf(" %s\n", img)
103- }
104- return nil
96+ return renderAttachmentTargetList(out, fmt.Sprintf("Attachments for %s", target), images)
10597 }
10698
10799 attachments, err := cp.GetAllAttachmentsConfig(ctx)
@@ -109,29 +101,53 @@ func runAttachmentsList(ctx context.Context, cp ControlPlane, target string, jso
109101 return fmt.Errorf("failed to list attachments: %w", err)
110102 }
111103 if len(attachments) == 0 {
112- fmt.Println(styles.Theme.Muted.Render("No attachments configured"))
113- return nil
104+ return cliWriteLine(out, styles.Theme.Muted.Render("No attachments configured"))
114105 }
115106 if jsonOut {
116107 return writeJSON(out, map[string]any{"attachments": attachments})
117108 }
109+ return renderAllAttachmentsList(out, "Attachments", attachments)
110+}
118111
112+func renderAttachmentTargetList(out io.Writer, title string, images []string) error {
113+ if _, err := fmt.Fprintln(out, styles.Theme.Title.Render(title)); err != nil {
114+ return err
115+ }
116+ if _, err := fmt.Fprintln(out); err != nil {
117+ return err
118+ }
119+ for _, img := range images {
120+ if _, err := fmt.Fprintf(out, " %s\n", img); err != nil {
121+ return err
122+ }
123+ }
124+ return nil
125+}
126+
127+func renderAllAttachmentsList(out io.Writer, title string, attachments map[string][]string) error {
119128 targets := make([]string, 0, len(attachments))
120129 for t := range attachments {
121130 targets = append(targets, t)
122131 }
123132 sort.Strings(targets)
124133
125- fmt.Println(styles.Theme.Title.Render("Attachments"))
126- fmt.Println()
134+ if _, err := fmt.Fprintln(out, styles.Theme.Title.Render(title)); err != nil {
135+ return err
136+ }
137+ if _, err := fmt.Fprintln(out); err != nil {
138+ return err
139+ }
127140 for _, t := range targets {
128141 images := attachments[t]
129- fmt.Printf("%s\n", styles.Theme.Bold.Render(t))
142+ if _, err := fmt.Fprintf(out, "%s\n", styles.Theme.Bold.Render(t)); err != nil {
143+ return err
144+ }
130145 for _, img := range images {
131- fmt.Printf(" %s\n", img)
146+ if _, err := fmt.Fprintf(out, " %s\n", img); err != nil {
147+ return err
148+ }
132149 }
133150 }
134-
135151 return nil
136152 }
137153
@@ -86,22 +86,14 @@ func runAttachmentsList(ctx context.Context, cp ControlPlane, target string, jso
86 if err != nil {86 if err != nil {
87 return fmt.Errorf("failed to get attachments: %w", err)87 return fmt.Errorf("failed to get attachments: %w", err)
88 }88 }
89-
90 if len(images) == 0 {89 if len(images) == 0 {
91- fmt.Printf("No attachments configured for '%s'\n", target)90+ _, err := fmt.Fprintf(out, "No attachments configured for '%s'\n", target)
92- return nil91+ return err
93 }92 }
94-
95 if jsonOut {93 if jsonOut {
96 return writeJSON(out, map[string]any{"target": target, "images": images})94 return writeJSON(out, map[string]any{"target": target, "images": images})
97 }95 }
98-96+ return renderAttachmentTargetList(out, fmt.Sprintf("Attachments for %s", target), images)
99- fmt.Println(styles.Theme.Title.Render(fmt.Sprintf("Attachments for %s", target)))
100- fmt.Println()
101- for _, img := range images {
102- fmt.Printf(" %s\n", img)
103- }
104- return nil
105 }97 }
106 98
107 attachments, err := cp.GetAllAttachmentsConfig(ctx)99 attachments, err := cp.GetAllAttachmentsConfig(ctx)
@@ -109,29 +101,53 @@ func runAttachmentsList(ctx context.Context, cp ControlPlane, target string, jso
109 return fmt.Errorf("failed to list attachments: %w", err)101 return fmt.Errorf("failed to list attachments: %w", err)
110 }102 }
111 if len(attachments) == 0 {103 if len(attachments) == 0 {
112- fmt.Println(styles.Theme.Muted.Render("No attachments configured"))104+ return cliWriteLine(out, styles.Theme.Muted.Render("No attachments configured"))
113- return nil
114 }105 }
115 if jsonOut {106 if jsonOut {
116 return writeJSON(out, map[string]any{"attachments": attachments})107 return writeJSON(out, map[string]any{"attachments": attachments})
117 }108 }
109+ return renderAllAttachmentsList(out, "Attachments", attachments)
110+}
118 111
112+func renderAttachmentTargetList(out io.Writer, title string, images []string) error {
113+ if _, err := fmt.Fprintln(out, styles.Theme.Title.Render(title)); err != nil {
114+ return err
115+ }
116+ if _, err := fmt.Fprintln(out); err != nil {
117+ return err
118+ }
119+ for _, img := range images {
120+ if _, err := fmt.Fprintf(out, " %s\n", img); err != nil {
121+ return err
122+ }
123+ }
124+ return nil
125+}
126+
127+func renderAllAttachmentsList(out io.Writer, title string, attachments map[string][]string) error {
119 targets := make([]string, 0, len(attachments))128 targets := make([]string, 0, len(attachments))
120 for t := range attachments {129 for t := range attachments {
121 targets = append(targets, t)130 targets = append(targets, t)
122 }131 }
123 sort.Strings(targets)132 sort.Strings(targets)
124 133
125- fmt.Println(styles.Theme.Title.Render("Attachments"))134+ if _, err := fmt.Fprintln(out, styles.Theme.Title.Render(title)); err != nil {
126- fmt.Println()135+ return err
136+ }
137+ if _, err := fmt.Fprintln(out); err != nil {
138+ return err
139+ }
127 for _, t := range targets {140 for _, t := range targets {
128 images := attachments[t]141 images := attachments[t]
129- fmt.Printf("%s\n", styles.Theme.Bold.Render(t))142+ if _, err := fmt.Fprintf(out, "%s\n", styles.Theme.Bold.Render(t)); err != nil {
143+ return err
144+ }
130 for _, img := range images {145 for _, img := range images {
131- fmt.Printf(" %s\n", img)146+ if _, err := fmt.Fprintf(out, " %s\n", img); err != nil {
147+ return err
148+ }
132 }149 }
133 }150 }
134-
135 return nil151 return nil
136 }152 }
137 153
M internal/adapters/in/cli/push.go +9 -8
@@ -5,6 +5,7 @@ import (
55 "context"
66 "errors"
77 "fmt"
8+ "io"
89 "net/http"
910 "os"
1011 "os/exec"
@@ -92,7 +93,7 @@ Examples:
9293 if len(args) > 0 {
9394 imageArg = args[0]
9495 }
95- return runPush(cmd.Context(), pushRequest{
96+ return runPush(cmd.Context(), cmd.OutOrStdout(), pushRequest{
9697 ImageArg: imageArg,
9798 Domain: domainFlag,
9899 Tag: tag,
@@ -225,7 +226,7 @@ func resolveVersion(ctx context.Context, tag string) (string, error) {
225226 return version, nil
226227 }
227228
228-func runPush(ctx context.Context, req pushRequest) error {
229+func runPush(ctx context.Context, out io.Writer, req pushRequest) error {
229230 dockerfile, inferredRemote, handle, err := resolvePushTarget(ctx, req)
230231 if err != nil {
231232 return err
@@ -265,7 +266,9 @@ func runPush(ctx context.Context, req pushRequest) error {
265266 return err
266267 }
267268
268- fmt.Println(styles.RenderSuccess("Push complete"))
269+ if err := cliWriteLine(out, styles.RenderSuccess("Push complete")); err != nil {
270+ return err
271+ }
269272 if !skipExplicitDeploy {
270273 return deployAfterPush(ctx, handle.plane, pushDomain, req.NoConfirm)
271274 }
@@ -429,7 +432,7 @@ func selectDomain(routes []domain.Route, dockerfile string) (domain.Route, error
429432 labelDomainList = append(labelDomainList, labelDomain)
430433 }
431434 if labelDomains != "" {
432- for _, d := range strings.Split(labelDomains, ",") {
435+ for d := range strings.SplitSeq(labelDomains, ",") {
433436 d = strings.TrimSpace(d)
434437 if d != "" {
435438 labelDomainList = append(labelDomainList, d)
@@ -583,12 +586,10 @@ func splitLabelPairs(content string) []string {
583586
584587 // parseLabelPair parses a single "key=value" or "key=\"value\"" pair.
585588 func parseLabelPair(pair string) (key, value string, ok bool) {
586- idx := strings.Index(pair, "=")
587- if idx == -1 {
589+ key, value, ok = strings.Cut(pair, "=")
590+ if !ok {
588591 return "", "", false
589592 }
590- key = pair[:idx]
591- value = pair[idx+1:]
592593 // Strip surrounding quotes
593594 value = strings.Trim(value, "\"'")
594595 return key, value, true
@@ -5,6 +5,7 @@ import (
5 "context"5 "context"
6 "errors"6 "errors"
7 "fmt"7 "fmt"
8+ "io"
8 "net/http"9 "net/http"
9 "os"10 "os"
10 "os/exec"11 "os/exec"
@@ -92,7 +93,7 @@ Examples:
92 if len(args) > 0 {93 if len(args) > 0 {
93 imageArg = args[0]94 imageArg = args[0]
94 }95 }
95- return runPush(cmd.Context(), pushRequest{96+ return runPush(cmd.Context(), cmd.OutOrStdout(), pushRequest{
96 ImageArg: imageArg,97 ImageArg: imageArg,
97 Domain: domainFlag,98 Domain: domainFlag,
98 Tag: tag,99 Tag: tag,
@@ -225,7 +226,7 @@ func resolveVersion(ctx context.Context, tag string) (string, error) {
225 return version, nil226 return version, nil
226 }227 }
227 228
228-func runPush(ctx context.Context, req pushRequest) error {229+func runPush(ctx context.Context, out io.Writer, req pushRequest) error {
229 dockerfile, inferredRemote, handle, err := resolvePushTarget(ctx, req)230 dockerfile, inferredRemote, handle, err := resolvePushTarget(ctx, req)
230 if err != nil {231 if err != nil {
231 return err232 return err
@@ -265,7 +266,9 @@ func runPush(ctx context.Context, req pushRequest) error {
265 return err266 return err
266 }267 }
267 268
268- fmt.Println(styles.RenderSuccess("Push complete"))269+ if err := cliWriteLine(out, styles.RenderSuccess("Push complete")); err != nil {
270+ return err
271+ }
269 if !skipExplicitDeploy {272 if !skipExplicitDeploy {
270 return deployAfterPush(ctx, handle.plane, pushDomain, req.NoConfirm)273 return deployAfterPush(ctx, handle.plane, pushDomain, req.NoConfirm)
271 }274 }
@@ -429,7 +432,7 @@ func selectDomain(routes []domain.Route, dockerfile string) (domain.Route, error
429 labelDomainList = append(labelDomainList, labelDomain)432 labelDomainList = append(labelDomainList, labelDomain)
430 }433 }
431 if labelDomains != "" {434 if labelDomains != "" {
432- for _, d := range strings.Split(labelDomains, ",") {435+ for d := range strings.SplitSeq(labelDomains, ",") {
433 d = strings.TrimSpace(d)436 d = strings.TrimSpace(d)
434 if d != "" {437 if d != "" {
435 labelDomainList = append(labelDomainList, d)438 labelDomainList = append(labelDomainList, d)
@@ -583,12 +586,10 @@ func splitLabelPairs(content string) []string {
583 586
584 // parseLabelPair parses a single "key=value" or "key=\"value\"" pair.587 // parseLabelPair parses a single "key=value" or "key=\"value\"" pair.
585 func parseLabelPair(pair string) (key, value string, ok bool) {588 func parseLabelPair(pair string) (key, value string, ok bool) {
586- idx := strings.Index(pair, "=")589+ key, value, ok = strings.Cut(pair, "=")
587- if idx == -1 {590+ if !ok {
588 return "", "", false591 return "", "", false
589 }592 }
590- key = pair[:idx]
591- value = pair[idx+1:]
592 // Strip surrounding quotes593 // Strip surrounding quotes
593 value = strings.Trim(value, "\"'")594 value = strings.Trim(value, "\"'")
594 return key, value, true595 return key, value, true
M internal/adapters/in/cli/remote_inference.go +1 -1
@@ -194,7 +194,7 @@ func isRemoteNotFoundError(err error) bool {
194194 }
195195
196196 func filterPreviewRoutes(routes []domain.Route) []domain.Route {
197- filtered := routes[:0]
197+ filtered := make([]domain.Route, 0, len(routes))
198198 for _, route := range routes {
199199 if !strings.Contains(route.Domain, domain.DefaultPreviewSeparator) {
200200 filtered = append(filtered, route)
@@ -194,7 +194,7 @@ func isRemoteNotFoundError(err error) bool {
194 }194 }
195 195
196 func filterPreviewRoutes(routes []domain.Route) []domain.Route {196 func filterPreviewRoutes(routes []domain.Route) []domain.Route {
197- filtered := routes[:0]197+ filtered := make([]domain.Route, 0, len(routes))
198 for _, route := range routes {198 for _, route := range routes {
199 if !strings.Contains(route.Domain, domain.DefaultPreviewSeparator) {199 if !strings.Contains(route.Domain, domain.DefaultPreviewSeparator) {
200 filtered = append(filtered, route)200 filtered = append(filtered, route)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close