cli: tighten inferred remote output and filtering
- commit
- b2108ef4be3df18c5fbac3854ee6e3686364cae7
- author
- brice · · 2026-05-15 07:20 UTC
- parent
- 2154bd4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 86 | 86 | if err != nil { | |
| 87 | 87 | return fmt.Errorf("failed to get attachments: %w", err) | |
| 88 | 88 | } | |
| 89 | - | ||
| 90 | 89 | 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 | |
| 93 | 92 | } | |
| 94 | - | ||
| 95 | 93 | if jsonOut { | |
| 96 | 94 | return writeJSON(out, map[string]any{"target": target, "images": images}) | |
| 97 | 95 | } | |
| 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) | |
| 105 | 97 | } | |
| 106 | 98 | ||
| 107 | 99 | attachments, err := cp.GetAllAttachmentsConfig(ctx) | |
| @@ -109,29 +101,53 @@ func runAttachmentsList(ctx context.Context, cp ControlPlane, target string, jso | |||
| 109 | 101 | return fmt.Errorf("failed to list attachments: %w", err) | |
| 110 | 102 | } | |
| 111 | 103 | 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")) | |
| 114 | 105 | } | |
| 115 | 106 | if jsonOut { | |
| 116 | 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 | 128 | targets := make([]string, 0, len(attachments)) | |
| 120 | 129 | for t := range attachments { | |
| 121 | 130 | targets = append(targets, t) | |
| 122 | 131 | } | |
| 123 | 132 | sort.Strings(targets) | |
| 124 | 133 | ||
| 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 | + | } | |
| 127 | 140 | for _, t := range targets { | |
| 128 | 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 | 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 | 151 | return nil | |
| 136 | 152 | } | |
| 137 | 153 | ||
| @@ -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 nil | 91 | + | 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 nil | 151 | return nil | ||
| 136 | } | 152 | } | ||
| 137 | 153 | ||||
M internal/adapters/in/cli/push.go +9 -8
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "context" | |
| 6 | 6 | "errors" | |
| 7 | 7 | "fmt" | |
| 8 | + | "io" | |
| 8 | 9 | "net/http" | |
| 9 | 10 | "os" | |
| 10 | 11 | "os/exec" | |
| @@ -92,7 +93,7 @@ Examples: | |||
| 92 | 93 | if len(args) > 0 { | |
| 93 | 94 | imageArg = args[0] | |
| 94 | 95 | } | |
| 95 | - | return runPush(cmd.Context(), pushRequest{ | |
| 96 | + | return runPush(cmd.Context(), cmd.OutOrStdout(), pushRequest{ | |
| 96 | 97 | ImageArg: imageArg, | |
| 97 | 98 | Domain: domainFlag, | |
| 98 | 99 | Tag: tag, | |
| @@ -225,7 +226,7 @@ func resolveVersion(ctx context.Context, tag string) (string, error) { | |||
| 225 | 226 | 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 | 230 | dockerfile, inferredRemote, handle, err := resolvePushTarget(ctx, req) | |
| 230 | 231 | if err != nil { | |
| 231 | 232 | return err | |
| @@ -265,7 +266,9 @@ func runPush(ctx context.Context, req pushRequest) error { | |||
| 265 | 266 | 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 | 272 | if !skipExplicitDeploy { | |
| 270 | 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 | 432 | labelDomainList = append(labelDomainList, labelDomain) | |
| 430 | 433 | } | |
| 431 | 434 | if labelDomains != "" { | |
| 432 | - | for _, d := range strings.Split(labelDomains, ",") { | |
| 435 | + | for d := range strings.SplitSeq(labelDomains, ",") { | |
| 433 | 436 | d = strings.TrimSpace(d) | |
| 434 | 437 | if d != "" { | |
| 435 | 438 | labelDomainList = append(labelDomainList, d) | |
| @@ -583,12 +586,10 @@ func splitLabelPairs(content string) []string { | |||
| 583 | 586 | ||
| 584 | 587 | // parseLabelPair parses a single "key=value" or "key=\"value\"" pair. | |
| 585 | 588 | 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 { | |
| 588 | 591 | return "", "", false | |
| 589 | 592 | } | |
| 590 | - | key = pair[:idx] | |
| 591 | - | value = pair[idx+1:] | |
| 592 | 593 | // Strip surrounding quotes | |
| 593 | 594 | value = strings.Trim(value, "\"'") | |
| 594 | 595 | 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, nil | 226 | 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 err | 232 | return err | ||
| @@ -265,7 +266,9 @@ func runPush(ctx context.Context, req pushRequest) error { | |||||
| 265 | return err | 266 | 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 "", "", false | 591 | return "", "", false | ||
| 589 | } | 592 | } | ||
| 590 | - | key = pair[:idx] | |||
| 591 | - | value = pair[idx+1:] | |||
| 592 | // Strip surrounding quotes | 593 | // Strip surrounding quotes | ||
| 593 | value = strings.Trim(value, "\"'") | 594 | value = strings.Trim(value, "\"'") | ||
| 594 | return key, value, true | 595 | return key, value, true | ||
M internal/adapters/in/cli/remote_inference.go +1 -1
| @@ -194,7 +194,7 @@ func isRemoteNotFoundError(err error) bool { | |||
| 194 | 194 | } | |
| 195 | 195 | ||
| 196 | 196 | func filterPreviewRoutes(routes []domain.Route) []domain.Route { | |
| 197 | - | filtered := routes[:0] | |
| 197 | + | filtered := make([]domain.Route, 0, len(routes)) | |
| 198 | 198 | for _, route := range routes { | |
| 199 | 199 | if !strings.Contains(route.Domain, domain.DefaultPreviewSeparator) { | |
| 200 | 200 | 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) | ||