Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/grounds/commands/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import (

func NewBundleCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "bundle",
Short: "Inspect available platform-test bundles",
Use: "bundle",
Short: "Inspect available platform-test bundles",
Example: " grounds bundle list\n grounds bundle show main\n grounds bundle show 0.4.0",
}
cmd.AddCommand(newList(), newShow())
return cmd
Expand Down
7 changes: 5 additions & 2 deletions cmd/grounds/commands/bundle/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"text/tabwriter"

"github.com/spf13/cobra"

"github.com/groundsgg/grounds-cli/internal/render"
)

func newList() *cobra.Command {
Expand All @@ -14,7 +16,7 @@ func newList() *cobra.Command {
Short: "List released platform-bundle versions",
Long: `Lists released library-platform-bundle versions, newest first.
Drafts and prereleases are filtered out. The version with (latest) is
the same one 'grounds cluster up --bundle main' would track today.`,
the same one ` + "`grounds cluster up --bundle main`" + ` would track today.`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
c, err := buildClient(ctx, cmd)
Expand All @@ -26,7 +28,8 @@ the same one 'grounds cluster up --bundle main' would track today.`,
return err
}
if len(releases) == 0 {
fmt.Fprintln(cmd.OutOrStdout(), "no released bundles found")
render.StatusLine(cmd.OutOrStdout(), render.StatusWarn, "Bundle", "No released bundles found")
render.DetailLine(cmd.OutOrStdout(), render.StatusWarn, "Try "+render.Command("grounds bundle show main")+" to inspect the current bundle.")
return nil
}
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
Expand Down
2 changes: 1 addition & 1 deletion cmd/grounds/commands/bundle/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func newShow() *cobra.Command {
Use: "show <ref>",
Short: "Show the components in a bundle",
Long: `Fetches the parsed bundle.yaml at the given ref and prints the
component table. <ref> accepts the same shapes as 'cluster up --bundle':
component table. <ref> accepts the same shapes as ` + "`grounds cluster up --bundle`" + `:
semver, "v…", the full release tag, or "main" for the latest commit.

Examples:
Expand Down
6 changes: 5 additions & 1 deletion cmd/grounds/commands/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import (
)

func NewClusterCommand() *cobra.Command {
cmd := &cobra.Command{Use: "cluster", Short: "Manage your dev workspace lifecycle"}
cmd := &cobra.Command{
Use: "cluster",
Short: "Manage your dev workspace lifecycle",
Example: " grounds cluster status\n grounds cluster up\n grounds cluster down\n grounds cluster delete",
}
cmd.AddCommand(newUp(), newDown(), newDelete(), newStatus())
return cmd
}
Expand Down
9 changes: 5 additions & 4 deletions cmd/grounds/commands/cluster/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package cluster
import (
"context"
"errors"
"fmt"
"os"

"github.com/spf13/cobra"
"golang.org/x/term"

"github.com/groundsgg/grounds-cli/internal/render"
"github.com/groundsgg/grounds-cli/internal/ui"
)

Expand Down Expand Up @@ -36,7 +36,7 @@ func newDelete() *cobra.Command {
return errors.New("non-interactive delete requires --yes <namespace>")
}
} else {
fmt.Fprintln(cmd.OutOrStdout(), "⚠ This will permanently delete", s.Namespace, "and all its data.")
render.StatusLine(cmd.OutOrStdout(), render.StatusWarn, "Workspace", "This will permanently delete "+s.Namespace+" and all its data")
if err := ui.AskTypeName(os.Stdin, cmd.OutOrStdout(), s.Namespace, s.Namespace); err != nil {
return err
}
Expand All @@ -48,9 +48,10 @@ func newDelete() *cobra.Command {
}
switch res.State {
case "deleted":
fmt.Fprintln(cmd.OutOrStdout(), "✔ Deleted.")
render.StatusLine(cmd.OutOrStdout(), render.StatusOK, "Workspace", "Deleted "+s.Namespace)
case "deleting":
fmt.Fprintln(cmd.OutOrStdout(), "→ Stuck Terminating; will be cleaned up by the janitor on next run.")
render.StatusLine(cmd.OutOrStdout(), render.StatusWarn, "Workspace", "Delete is still in progress")
render.DetailLine(cmd.OutOrStdout(), render.StatusWarn, "Cleanup will continue automatically.")
}
return nil
},
Expand Down
2 changes: 0 additions & 2 deletions cmd/grounds/commands/cluster/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cluster

import (
"context"
"fmt"

"github.com/spf13/cobra"

Expand All @@ -23,7 +22,6 @@ func newDown() *cobra.Command {
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), "✔ Paused.")
render.Status(cmd.OutOrStdout(), s)
return nil
},
Expand Down
4 changes: 2 additions & 2 deletions cmd/grounds/commands/cluster/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cluster

import (
"context"
"fmt"

"github.com/spf13/cobra"

Expand All @@ -23,7 +22,8 @@ func newStatus() *cobra.Command {
s, err := c.GetCluster(ctx)
if err != nil {
if apiErr, ok := err.(*api.Error); ok && apiErr.StatusCode == 404 {
fmt.Fprintln(cmd.OutOrStdout(), "→ no workspace yet. Run 'grounds push' to create one.")
render.StatusLine(cmd.OutOrStdout(), render.StatusWarn, "Workspace", "No workspace found")
render.DetailLine(cmd.OutOrStdout(), render.StatusWarn, "Run "+render.Command("grounds push")+" to create one.")
return nil
}
return err
Expand Down
23 changes: 13 additions & 10 deletions cmd/grounds/commands/cluster/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ func newUp() *cobra.Command {
var bundleRef string
var overridePath string
cmd := &cobra.Command{
Use: "up [--profile=minigame|platform] [--bundle=<ref> [--override=<file>]]",
Short: "Spawn or resume the workspace",
Use: "up [--profile=minigame|platform] [--bundle=<ref> [--override=<file>]]",
Short: "Spawn or resume the workspace",
Example: " grounds cluster up\n grounds cluster up --profile=platform\n grounds cluster up --bundle=0.4.0 --override=./overrides/me.yaml",
Long: `Create the workspace if it doesn't exist, or resume it from a paused state.

Profiles:
Expand Down Expand Up @@ -69,7 +70,7 @@ Profile is locked once a workspace exists. To switch, ` + "`grounds cluster dele
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), "✔ Active.")
render.StatusLine(cmd.OutOrStdout(), render.StatusOK, "Workspace", "Active")
render.Status(cmd.OutOrStdout(), s)
return nil
},
Expand Down Expand Up @@ -112,13 +113,15 @@ func loadBundleRequest(bundleRef, overridePath string) (*api.BundleUpRequest, er
func renderBundleResult(w interface {
Write(p []byte) (int, error)
}, res *api.BundleUpResult) {
fmt.Fprintf(w, "✔ %s — bundle %s — %s\n", res.State, res.BundleVersion, res.Namespace)
fmt.Fprintf(w, " components: %d resolved, %d succeeded, %d failed\n",
res.Components.Resolved, len(res.Components.Succeeded), len(res.Components.Failed))
status := render.StatusOK
summary := fmt.Sprintf("%s with bundle %s in namespace %s", res.State, res.BundleVersion, res.Namespace)
if len(res.Components.Failed) > 0 {
fmt.Fprintln(w, " failed:")
for _, f := range res.Components.Failed {
fmt.Fprintf(w, " - %s: %s\n", f.Name, f.Error)
}
status = render.StatusWarn
}
render.StatusLine(w, status, "Workspace", summary)
render.DetailLine(w, status, fmt.Sprintf("Components: %d resolved, %d succeeded, %d failed",
res.Components.Resolved, len(res.Components.Succeeded), len(res.Components.Failed)))
for _, f := range res.Components.Failed {
render.DetailLine(w, render.StatusError, fmt.Sprintf("%s: %s", f.Name, f.Error))
}
}
32 changes: 32 additions & 0 deletions cmd/grounds/commands/cluster/up_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package cluster

import (
"bytes"
"os"
"path/filepath"
"testing"

"github.com/fatih/color"

"github.com/groundsgg/grounds-cli/internal/api"
)

func TestLoadBundleRequest(t *testing.T) {
Expand Down Expand Up @@ -89,3 +94,30 @@ func writeTempYAML(t *testing.T, content string) string {
}
return path
}

func TestRenderBundleResult(t *testing.T) {
color.NoColor = true
defer func() { color.NoColor = false }()

res := &api.BundleUpResult{
State: "active",
BundleVersion: "0.4.0",
Namespace: "dev-lukas",
}
res.Components.Resolved = 2
res.Components.Succeeded = []string{"api"}
res.Components.Failed = append(res.Components.Failed, struct {
Name string `json:"name"`
Error string `json:"error"`
}{Name: "worker", Error: "image pull failed"})

var buf bytes.Buffer
renderBundleResult(&buf, res)

want := "[!] Workspace - active with bundle 0.4.0 in namespace dev-lukas\n" +
" ! Components: 2 resolved, 1 succeeded, 1 failed\n" +
" ✗ worker: image pull failed\n"
if got := buf.String(); got != want {
t.Fatalf("renderBundleResult output = %q, want %q", got, want)
}
}
5 changes: 3 additions & 2 deletions cmd/grounds/commands/devspace/devspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import (

func NewDevspaceCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "devspace",
Short: "DevSpace integration helpers",
Use: "devspace",
Short: "DevSpace integration helpers",
Example: " grounds devspace generate plugin-social --bundle main\n grounds devspace generate plugin-social --override ./me.yaml",
}
cmd.AddCommand(newGenerate())
return cmd
Expand Down
7 changes: 6 additions & 1 deletion cmd/grounds/commands/devspace/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"gopkg.in/yaml.v3"

"github.com/groundsgg/grounds-cli/internal/api"
"github.com/groundsgg/grounds-cli/internal/render"
)

func newGenerate() *cobra.Command {
Expand Down Expand Up @@ -67,7 +68,7 @@ Examples:
if err := os.WriteFile(outputPath, yaml, 0o644); err != nil {
return fmt.Errorf("writing %s: %w", outputPath, err)
}
fmt.Fprintf(cmd.OutOrStdout(), "✔ Wrote %d bytes to %s\n", len(yaml), outputPath)
render.StatusLine(cmd.OutOrStdout(), render.StatusOK, "DevSpace", generateSuccessSummary(outputPath))
return nil
},
}
Expand All @@ -77,6 +78,10 @@ Examples:
return cmd
}

func generateSuccessSummary(outputPath string) string {
return "Wrote " + outputPath
}

// loadGenerateInputs picks the bundle-ref + per-component override
// out of the override file (when present), with --bundle on the
// command line winning over the file's `bundle:` field.
Expand Down
8 changes: 7 additions & 1 deletion cmd/grounds/commands/devspace/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import (
"testing"
)

func TestGenerateSuccessSummary(t *testing.T) {
if got := generateSuccessSummary("./devspace.yaml"); got != "Wrote ./devspace.yaml" {
t.Fatalf("generateSuccessSummary = %q", got)
}
}

func TestLoadGenerateInputs(t *testing.T) {
t.Run("bundle flag only, no override", func(t *testing.T) {
bundle, override, err := loadGenerateInputs("0.4.0", "", "plugin-social")
Expand Down Expand Up @@ -71,7 +77,7 @@ overrides: {}
}
})

t.Run("component not in override file → override is nil", func(t *testing.T) {
t.Run("component missing from override file uses nil override", func(t *testing.T) {
path := writeTempYAML(t, `
bundle: 0.4.0
overrides:
Expand Down
37 changes: 13 additions & 24 deletions cmd/grounds/commands/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,20 @@ func runDoctorChecks(ctx context.Context, out io.Writer, checks []doctorCheck, i
}

func printCheckResult(out io.Writer, r checkResult) {
fmt.Fprintf(out, "%s %s - %s\n", statusBadge(r.status), r.name, r.summary)
render.StatusLine(out, renderStatus(r.status), r.name, r.summary)
for _, detail := range r.details {
fmt.Fprintf(out, " %s %s\n", detailIcon(r.status), detail)
render.DetailLine(out, renderStatus(r.status), detail)
}
}

func renderStatus(status checkStatus) render.StatusKind {
switch status {
case statusWarn:
return render.StatusWarn
case statusError:
return render.StatusError
default:
return render.StatusOK
}
}

Expand Down Expand Up @@ -126,28 +137,6 @@ func printDoctorFooter(out io.Writer, results []checkResult, strict bool) error
return nil
}

func statusBadge(status checkStatus) string {
switch status {
case statusWarn:
return render.Yellow("[!]")
case statusError:
return render.Red("[✗]")
default:
return render.Green("[✓]")
}
}

func detailIcon(status checkStatus) string {
switch status {
case statusError:
return render.Red("✗")
case statusWarn:
return render.Yellow("!")
default:
return "•"
}
}

func categoryWord(count int) string {
if count == 1 {
return "category"
Expand Down
6 changes: 4 additions & 2 deletions cmd/grounds/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

"github.com/charmbracelet/huh"
"github.com/spf13/cobra"

"github.com/groundsgg/grounds-cli/internal/render"
)

type initFlags struct {
Expand Down Expand Up @@ -73,7 +75,7 @@ func writeGroundsYaml(out io.Writer, f *initFlags) error {
if err := os.WriteFile(path, []byte(body), 0644); err != nil {
return err
}
fmt.Fprintln(out, "→ Wrote grounds.yaml")
fmt.Fprintln(out, "Next: grounds push")
render.StatusLine(out, render.StatusOK, "Init", "Wrote grounds.yaml")
render.DetailLine(out, render.StatusOK, "Next: run "+render.Command("grounds push")+".")
return nil
}
3 changes: 3 additions & 0 deletions cmd/grounds/commands/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ func TestInit_NonInteractive(t *testing.T) {
if !bytes.Contains(body, []byte("name: my-arena")) {
t.Errorf("body = %s", body)
}
if got := buf.String(); got != "[✓] Init - Wrote grounds.yaml\n • Next: run `grounds push`.\n" {
t.Fatalf("output = %q", got)
}
}
Loading
Loading