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
39 changes: 27 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ curl -sSL https://github.com/groundsgg/grounds-cli/releases/latest/download/inst
```bash
grounds login # OAuth device flow
grounds init # scaffold grounds.yaml
grounds workspace scan ../ --yes # discover sibling plugin repos
grounds push # first push
grounds cluster status # observe namespace
grounds logs <pushId> --follow # tail logs
Expand All @@ -42,23 +43,37 @@ grounds logs <pushId> --follow # tail logs

## Commands

| Command | What |
|---------------------------------------------|----------------------------------------------|
| `grounds login / logout` | Auth via Keycloak device flow |
| `grounds version [--check]` | Build info and optional release update check |
| `grounds completion <shell>` | Shell completions |
| `grounds doctor` | Diagnose env and warn about CLI updates |
| `grounds init` | Scaffold a grounds.yaml |
| `grounds cluster up/down/delete/status` | Workspace lifecycle |
| `grounds push [--target=dev]` | Build + deploy via Gradle plugin |
| `grounds push retry/list` | Re-run / list pushes |
| `grounds logs <pushId> [--follow]` | Stream logs |
| `grounds logs deployment <name> [--follow]` | Stream deployment logs |
| Command | What |
|-------------------------------------------------|----------------------------------------------|
| `grounds login / logout` | Auth via Keycloak device flow |
| `grounds version [--check]` | Build info and optional release update check |
| `grounds completion <shell>` | Shell completions |
| `grounds doctor` | Diagnose env and warn about CLI updates |
| `grounds init` | Scaffold a grounds.yaml |
| `grounds workspace scan/add/list/enable/doctor` | Manage local plugin workspace overrides |
| `grounds cluster up/down/delete/status` | Workspace lifecycle |
| `grounds push [--target=dev]` | Build + deploy via Gradle plugin |
| `grounds push retry/list` | Re-run / list pushes |
| `grounds logs <pushId> [--follow]` | Stream logs |
| `grounds logs deployment <name> [--follow]` | Stream deployment logs |

## Configuration

`~/.config/grounds/config.yaml` (XDG-aware). Overridable via flags or env vars (`GROUNDS_API_URL`, `GROUNDS_TOKEN`, `GROUNDS_CONFIG_DIR`).

Local plugin workspace overrides are stored in `~/.config/grounds/workspace.yaml`.
Default `grounds push` still uses the plugin sources pinned in committed `grounds.yaml`.
Use local plugin artifacts only when requested:

```bash
grounds workspace scan ../ # preview sibling plugin repos, then confirm
grounds workspace scan ../ --yes # write discovered mappings without prompting
grounds workspace add plugin-chat ../plugin-chat --variant paper
grounds push --local plugin-chat # override one plugin from the workspace
grounds push --local plugin-chat,plugin-permissions
grounds push --with-local # override every enabled workspace entry in grounds.yaml
```

## Troubleshooting

Run `grounds doctor`. If something looks off, the report tells you which check failed and how to fix it.
Expand Down
14 changes: 14 additions & 0 deletions cmd/grounds/commands/push/local_flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package push

import "testing"

func TestPushDefinesLocalOverrideFlags(t *testing.T) {
cmd := newPush()

if flag := cmd.Flag("local"); flag == nil {
t.Fatal("expected --local flag")
}
if flag := cmd.Flag("with-local"); flag == nil {
t.Fatal("expected --with-local flag")
}
}
60 changes: 58 additions & 2 deletions cmd/grounds/commands/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package push
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"

"github.com/spf13/cobra"
Expand All @@ -13,6 +15,7 @@ import (
"github.com/groundsgg/grounds-cli/internal/config"
"github.com/groundsgg/grounds-cli/internal/gradle"
"github.com/groundsgg/grounds-cli/internal/render"
internalworkspace "github.com/groundsgg/grounds-cli/internal/workspace"
)

func NewPushCommand() *cobra.Command {
Expand All @@ -25,10 +28,12 @@ func NewPushCommand() *cobra.Command {
func newPush() *cobra.Command {
var target string
var force bool
var local []string
var withLocal bool
cmd := &cobra.Command{
Use: "push [--target=dev|staging] [--force]",
Use: "push [--target=dev|staging] [--force] [--local=<id>[,<id>]] [--with-local]",
Short: "Build via Gradle plugin and deploy to a target",
Example: " grounds push\n grounds push --target=staging\n grounds push --force",
Example: " grounds push\n grounds push --target=staging\n grounds push --force\n grounds push --local=plugin-chat\n grounds push --with-local",
Long: `Build the current project with the grounds-push Gradle plugin and deploy it.

Targets:
Expand Down Expand Up @@ -78,6 +83,36 @@ image moved under a stable tag, or to re-observe the build flow.`,
if force {
args = append(args, "--force")
}
if withLocal || len(internalworkspace.NormalizeLocalIDs(local)) > 0 {
workspaceConfig, err := internalworkspace.Load("")
if err != nil {
return err
}
manifestPath := filepath.Join(filepath.Dir(wrapper), "grounds.yaml")
plan, err := internalworkspace.Resolve(ctx, manifestPath, workspaceConfig, internalworkspace.ResolveOptions{
LocalIDs: local,
WithLocal: withLocal,
Stdout: cmd.OutOrStdout(),
Stderr: cmd.ErrOrStderr(),
})
if err != nil {
return err
}
renderBundleSources(cmd.OutOrStdout(), plan)
file, err := os.CreateTemp("", "grounds-resolved-plugins-*.json")
if err != nil {
return err
}
resolvedPath := file.Name()
if err := file.Close(); err != nil {
return err
}
defer os.Remove(resolvedPath)
if err := internalworkspace.WritePlanFile(resolvedPath, plan); err != nil {
return err
}
args = append(args, "--resolved-plugins-file="+resolvedPath)
}
return gradle.Run(ctx, wrapper, args, cmd.OutOrStdout(), cmd.ErrOrStderr(), 0)
},
}
Expand All @@ -86,9 +121,30 @@ image moved under a stable tag, or to re-observe the build flow.`,
return []string{"dev", "staging"}, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().BoolVar(&force, "force", false, "skip contentHash dedup and force a fresh build")
cmd.Flags().StringArrayVar(&local, "local", nil, "use local workspace override for plugin id (repeatable, comma-separated)")
cmd.Flags().BoolVar(&withLocal, "with-local", false, "use all enabled local workspace overrides present in grounds.yaml")
return cmd
}

func renderBundleSources(out io.Writer, plan *internalworkspace.Plan) {
fmt.Fprintln(out, "Bundle sources:")
rows := make([][]any, 0, len(plan.EffectivePluginSources))
localPaths := map[string]string{}
for _, plugin := range plan.Plugins {
if plugin.LocalPath != "" {
localPaths[plugin.ID+"\x00"+plugin.Variant] = plugin.LocalPath
}
}
for _, source := range plan.EffectivePluginSources {
value := source.Source
if source.Effective == "local" {
value = localPaths[source.ID+"\x00"+source.Variant]
}
rows = append(rows, []any{source.ID, source.Variant, source.Effective, value})
}
render.Table(out, []string{"ID", "Variant", "Effective", "Value"}, rows)
}

func authRefreshError(err error) error {
return fmt.Errorf("auth refresh failed: %w\n ! Run %s to re-authenticate.", err, render.Command("grounds login"))
}
Expand Down
Loading
Loading