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
78 changes: 78 additions & 0 deletions F1-json-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# F1: --format json → --json (completed)

Implemented finding F1 from `AGENT_NATIVE_AUDIT.md`. vers-cli now standardizes on `--json` across the entire command surface, with `--format` retained as a hidden, deprecated alias.

## Changed files

```
README.md | 12 +++---
cmd/branch.go | 12 ++++--
cmd/commit.go | 36 ++++++++++++----
cmd/deploy.go | 12 ++++--
cmd/env.go | 10 +++--
cmd/info.go | 12 ++++--
cmd/pause.go | 12 ++++--
cmd/repo.go | 48 ++++++++++++++++-----
cmd/resize.go | 12 ++++--
cmd/resume.go | 12 ++++--
cmd/run.go | 12 ++++--
cmd/run_commit.go | 12 ++++--
cmd/status.go | 12 ++++--
cmd/tag.go | 24 +++++++----
internal/presenters/format.go | 24 +++++++++--
internal/presenters/format_test.go | 37 +++++++++++++----
16 files changed, 224 insertions(+), 75 deletions(-)
```

19 cobra `--format` registrations now have a sibling `--json` BoolVar; 19 `pres.ParseFormat` call sites now pass the JSON flag and propagate the validation error.

Commit: `6744100 feat: standardize on --json flag (deprecate --format json)` on branch `pi-parallel-d95f3d3f-0`.

## Design choices

1. **Centralized validation in `presenters.ParseFormat`.** New signature: `ParseFormat(quiet, jsonFlag bool, formatStr string) (OutputFormat, error)`. Precedence is `quiet > json > format > default`. Invalid `--format` values return an enumerated error rather than silently falling through to default-format (which was the previous behavior — a P3 violation).
2. **Cobra's `MarkDeprecated` handles the warning.** It prints `Flag --format has been deprecated, use --json instead` to stderr automatically when the flag is used, and hides the flag from `--help`. No custom warning code needed.
3. **Stdout/stderr separation preserved.** Deprecation warnings go to stderr; JSON data continues to land cleanly on stdout. `vers status --format json | jq` still works without contamination.

## Validation

- `go build ./...` — clean
- `go vet ./...` — clean
- `go test ./...` — all packages pass (including expanded `format_test.go` with 9 cases covering the new precedence and error semantics)

Smoke tests against built binary:

```
$ vers status --json
[] # exit 0, clean stdout

$ vers status --format json 2>/tmp/err
[] # exit 0, JSON on stdout
$ cat /tmp/err
Flag --format has been deprecated, use --json instead

$ vers status --format yaml
Flag --format has been deprecated, use --json instead
Error: --format must be "json" (got: "yaml"). Note: --format is deprecated, use --json instead
# exit 1
```

`vers status --help` no longer lists `--format`; long descriptions everywhere now read "Use --json for machine-readable output."

## Risks / open questions

- **Usage banner printed on RunE error.** When `--format yaml` is rejected, cobra prints the command's usage block after the error. This is pre-existing behavior — `SilenceUsage = true` is only set in the auth path in `cmd/root.go:210`. Out of scope for F1, but worth a follow-up if cleaner agent-facing errors are wanted (set `SilenceUsage = true` on the root command).
- **`alias` command is still missing JSON output entirely.** This is finding F2, separate task.
- **No tests assert deprecation warning emission to stderr.** The cobra `MarkDeprecated` behavior is library-provided and stable, so I didn't add an integration test capturing stderr; the unit tests exercise only the validation path. If desired, a small `cmd/`-level test could capture stderr from a `--format json` invocation.

## Recommended next step

Hand off to F2 (info → get) and F3 (pagination) as planned. After both land, audit the few commands that have a `--quiet` flag but no `--json` (`alias`, possibly `head`, `checkout`) and unify those — the `ParseFormat` machinery is already in place to extend them cheaply.

---

Implemented F1 (canonical `--json` flag).
Changed files: 16 (13 cmd files, presenters package, README).
Validation: `go build`, `go vet`, `go test ./...` all clean; manual smoke tests confirm `--json` works, legacy `--format json` works with stderr deprecation, invalid `--format` values error with the enumerated message.
Open risks/questions: usage banner on error is pre-existing; no integration test for deprecation warning.
Recommended next step: proceed with F2 and F3 as planned.
100 changes: 100 additions & 0 deletions F7-pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# F7: Pagination + Truncation Hints

Branch: `pi-parallel-d95f3d3f-2`
Commit: `bc31992 feat: paginate list commands with --limit and truncation hints`

## Implemented

Added `--limit N` (default 50; `0` = unbounded) and `--offset N` to every list-style command, plus a uniform truncation hint surface:

| Command | File | Notes |
|---|---|---|
| `vers status` | `cmd/status.go` | List mode only. Single-VM mode unchanged. |
| `vers commit list` | `cmd/commit.go` | |
| `vers repo list` | `cmd/repo.go` | |
| `vers repo tag list` | `cmd/repo.go` | Included for surface consistency (was implicit in scope). |
| `vers tag list` | `cmd/tag.go` | |
| `vers env list` | `cmd/env.go` | Sorted by key for stable pagination. JSON wraps as `[{key,value}]` envelope when truncated to preserve order; bare object map when not truncated. |
| `vers alias` (no arg) | `cmd/alias.go` | Sorted by name. |

## Output shapes

**JSON, not truncated** — bare items array (backwards-compat, identical to pre-change shape):
```json
[ {...}, {...} ]
```

**JSON, truncated** — envelope with hint and next_offset:
```json
{
"items": [ {...} ],
"total": 11,
"limit": 3,
"offset": 0,
"truncated": true,
"next_offset": 3,
"hint": "showing 3 of 11 — use --limit=N (0 for all) or --offset=3 for the next page"
}
```

**Text / quiet modes** — table or IDs on stdout, single-line hint on stderr:
```
$ vers status -q --limit 1
6781f925-09ba-48ab-8f2c-9adeb75eec6d
[stderr] (showing 1 of 2 — use --limit=N (0 for all) or --offset=1 for the next page)
```

## Implementation notes

- New file `internal/presenters/pagination.go` exposes:
- `PageInfo` struct (Total/Limit/Offset/Truncated/NextOffset/Hint)
- `ApplyPaging(total, limit, offset) (start, end, info)` — clamps and computes
- `PrintListJSON(items, info)` — bare array vs. envelope based on `info.Truncated`
- `PrintTruncationHint(w, info)` — no-op when not truncated; otherwise `(hint)\n`
- Unit tested in `internal/presenters/pagination_test.go` (10 sub-cases covering empty/full/truncated/unbounded/offset-past-end/negative offsets).

## Server-side pagination caveat

The Go SDK (`vers-sdk-go@v0.1.0-alpha.32`) **does not expose** `Limit`/`Offset`/`Cursor` query parameters on its list endpoints today. The `ListCommitsResponse` shape *includes* `Limit`/`Offset`/`Total` fields, suggesting the API server may accept them, but the SDK has no typed param surface for them.

Per the task instructions, I implemented client-side pagination after the full response. Marked clearly:

- One canonical TODO at the top of `internal/presenters/pagination.go` describing the constraint.
- Inline TODO comments at every call site (`cmd/status.go`, `cmd/commit.go`, `cmd/repo.go`, `cmd/tag.go`, `cmd/env.go`) noting where to plumb `--limit`/`--offset` to the SDK once supported.

When the SDK exposes the params, the change is local: replace the `ApplyPaging` block with a request that passes `limit`/`offset` through (e.g., via `option.WithQuery`), receives an already-paged response, and uses the API's `total` field instead of `len(items)` to detect truncation.

## Validation

- `go build ./...` — passes
- `go vet ./...` — clean
- `go test ./...` — all packages pass, including new `TestApplyPaging` (10 sub-cases)
- Live API checks against `/Users/tynandaly/basin/vers-cli`'s authenticated environment:
- `vers status --limit 1 --format json` → returns 1 of 2 VMs with truncation envelope. ✓
- `vers status --limit 1` text mode → table to stdout, hint to stderr. ✓
- `vers status -q --limit 1` quiet mode → ID to stdout, hint to stderr. ✓
- `vers status --limit 0` unbounded → bare array, no envelope. ✓
- `vers commit list --limit 3 --format json` → envelope with `total: 11`, `next_offset: 3`. ✓
- `vers commit list --limit 3 --offset 3 --format json` → next page works. ✓
- `vers commit list --help` → new flags documented. ✓

## Out of scope (left untouched)

- `--format json` → `--json` rename (separate worktree handles F1).
- `info` → `get` rename (separate worktree handles F8).
- Existing `--format string` flags retained as-is on every list command.
- Tests in `internal/handlers/` and `internal/mcp/` were not modified — handler signatures unchanged because pagination lives in the cmd layer.

## Open questions / risks

1. **JSON shape change for `env list` when truncated.** Historical shape was `{"KEY": "VALUE", ...}` (an object). The truncated envelope uses `items: [{key, value}, ...]` because Go map iteration order is unstable, and a sorted ordered list is required for stable `next_offset` paging. When *not* truncated, the historical map shape is preserved. Agents that hit the truncated branch will see a different shape — flagged here in case downstream MCP tooling depends on the map form.

2. **`commit list` text-mode header.** `RenderCommitsList` prints `"%d commit(s)"` from `view.Total`, which is the API-reported total, not the paged count. Left as-is because the API total is more useful information; the paged count is implicit in the row count plus the stderr hint. If desired, this could be tweaked to `"showing N of TOTAL commits"`.

3. **`repo list` / `tag list` / `repo tag list` have no API-reported total.** The truncation total is `len(response)` — i.e., what we got back from the API. If the server is silently capping the response, the agent would see a wrong total. Low risk today (these endpoints appear to return everything), but worth verifying once server-side pagination lands.

## Recommended next steps

1. **Coordinate the merge order with the F1 (`--format` → `--json`) and F8 (`info` → `get`) worktrees.** This branch only touches `--limit`/`--offset` and the `info` lines in renderers/help; conflicts should be limited to flag-init blocks and Long-help text, all easy three-way merges.
2. **Open an issue in `vers-sdk-go`** requesting typed `Limit`/`Offset` parameters on every list endpoint so the client-side trim can be removed and `total`/`next_offset` come from the API.
3. **Add an MCP-tool wrapper note** so the existing MCP server surfaces `--limit`/`--offset` in tool descriptions for agent consumers (out of scope for F7 itself; deserves a dedicated follow-up).
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ vers status
vers status -q

# Full JSON output
vers status --format json
vers status --json

# Detailed metadata for a VM (IP, lineage, timestamps)
vers info <vm-id>
vers info --format json
vers get <vm-id>
vers get --json

# Execute a command on a VM
vers execute <vm-id> <command> [args...]
Expand Down Expand Up @@ -91,7 +91,7 @@ vers commit <vm-id>
# List your commits
vers commit list
vers commit list -q # just IDs
vers commit list --format json
vers commit list --json
vers commit list --public # public commits

# View commit history (parent chain)
Expand All @@ -118,7 +118,7 @@ vers tag create production abc-123 -d "stable release"
# List all tags
vers tag list
vers tag list -q # just names
vers tag list --format json
vers tag list --json

# Get tag details
vers tag get <name>
Expand Down Expand Up @@ -147,11 +147,11 @@ vers commit delete $(vers commit list -q)
vers tag delete $(vers tag list -q)

# Get info on the first VM
vers info $(vers status -q | head -1)
vers get $(vers status -q | head -1)

# JSON piped to jq
vers status --format json | jq '.[].vm_id'
vers info <vm-id> --format json | jq '.ip'
vers status --json | jq '.[].vm_id'
vers get <vm-id> --json | jq '.ip'
```

`ps` is an alias for `status`:
Expand Down
34 changes: 29 additions & 5 deletions cmd/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,40 @@ package cmd

import (
"fmt"
"sort"

pres "github.com/hdresearch/vers-cli/internal/presenters"
"github.com/hdresearch/vers-cli/internal/utils"
"github.com/spf13/cobra"
)

var (
aliasLimit int
aliasOffset int
)

var aliasCmd = &cobra.Command{
Use: "alias [name]",
Short: "Show VM ID for an alias, or list all aliases",
Long: `Look up the VM ID for a given alias, or list all aliases if no argument is provided.

Examples:
vers alias myvm # Show VM ID for alias 'myvm'
vers alias # List all aliases`,
vers alias # List all aliases

Pagination (when listing all):
--limit N Cap results at N (default 50). Use 0 for unbounded.
--offset N Skip the first N aliases (alphabetically by name).`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return listAliases()
return listAliases(cmd)
}
return showAlias(args[0])
},
}

func listAliases() error {
func listAliases(cmd *cobra.Command) error {
aliases, err := utils.LoadAliases()
if err != nil {
return fmt.Errorf("failed to load aliases: %w", err)
Expand All @@ -35,9 +46,20 @@ func listAliases() error {
return nil
}

for alias, vmID := range aliases {
fmt.Printf("%s -> %s\n", alias, vmID)
// Sort for stable pagination.
names := make([]string, 0, len(aliases))
for name := range aliases {
names = append(names, name)
}
sort.Strings(names)

// TODO: aliases are stored locally; if remote alias listing ever moves
// server-side, plumb aliasLimit/aliasOffset through to the request.
start, end, info := pres.ApplyPaging(len(names), aliasLimit, aliasOffset)
for _, name := range names[start:end] {
fmt.Printf("%s -> %s\n", name, aliases[name])
}
pres.PrintTruncationHint(cmd.ErrOrStderr(), info)
return nil
}

Expand All @@ -58,4 +80,6 @@ func showAlias(name string) error {

func init() {
rootCmd.AddCommand(aliasCmd)
aliasCmd.Flags().IntVar(&aliasLimit, "limit", 50, "Maximum number of aliases to return (0 = unbounded)")
aliasCmd.Flags().IntVar(&aliasOffset, "offset", 0, "Number of aliases to skip (for paging)")
}
12 changes: 9 additions & 3 deletions cmd/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
var (
alias string
branchCount int
branchJSON bool
branchFormat string
branchWait bool
)
Expand All @@ -21,7 +22,7 @@ var branchCmd = &cobra.Command{
Short: "Create a new VM from an existing VM",
Long: `Create a new VM (branch) from the state of an existing VM. If no VM ID or alias is provided, uses the current HEAD.

Use --format json for machine-readable output.
Use --json for machine-readable output.
Use --wait to block until new VMs are running.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -45,7 +46,10 @@ Use --wait to block until new VMs are running.`,
return err
}

format := pres.ParseFormat(false, branchFormat)
format, err := pres.ParseFormat(false, branchJSON, branchFormat)
if err != nil {
return err
}
switch format {
case pres.FormatJSON:
pres.PrintJSON(res)
Expand All @@ -62,6 +66,8 @@ func init() {
branchCmd.Flags().StringVarP(&alias, "alias", "n", "", "Alias for the new VM")
branchCmd.Flags().BoolP("checkout", "c", false, "Switch to the new VM after creation")
branchCmd.Flags().IntVar(&branchCount, "count", 1, "Number of branches to create")
branchCmd.Flags().StringVar(&branchFormat, "format", "", "Output format (json)")
branchCmd.Flags().BoolVar(&branchJSON, "json", false, "Output as JSON")
branchCmd.Flags().StringVar(&branchFormat, "format", "", "Output format (json) [deprecated: use --json]")
_ = branchCmd.Flags().MarkDeprecated("format", "use --json instead")
branchCmd.Flags().BoolVar(&branchWait, "wait", false, "Wait until new VMs are running")
}
Loading
Loading