feat: LLM-friendly CLI improvements#1
Conversation
Six changes to make the CLI more usable by AI agents and scripts:
1. dave deploy <project> --branch <branch> --environment <env> [--wait]
Composite command: resolves the latest release on a branch, creates a
deployment, and optionally blocks until it reaches a terminal state.
Replaces a 3-4 step manual workflow with a single atomic call.
2. dave releases latest <project> --branch <branch>
Returns the single most-recent release for a branch. Exits 3 (not
found) if none exists. Avoids the need to parse a list and take [0].
3. dave build dispatch --wait
After dispatching a build, polls for a new release on the branch and
blocks until one appears. Lets an agent know when a build has
produced something deployable.
4. JSON error output
When --json is set, errors are now written to stdout as
{"error":"..."} instead of to stderr as plain text. Agents can parse
failures with the same logic as successes.
5. Pagination hints in JSON list output
List commands in JSON mode now return {"hits":[...],"truncated":true}
when the result count equals the --limit, so callers know they may
not have seen all items.
6. Deletes skip --yes in --json mode
Destructive commands already guarded by --yes no longer require it
when --json is set, since the caller is clearly a script/agent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR makes the Dave CLI easier for automated callers (AI agents/CI) by adding composite commands, improving machine-readable output, and reducing interactive friction in --json mode.
Changes:
- Added
dave deploy(branch → latest release → create deployment, optionally--wait) anddave releases latest. - Improved scriptability: JSON-mode error output to stdout, JSON pagination hints for limited list endpoints, and skip
--yesfor deletes in JSON mode. - Updated API client + call sites so
CreateDeploymentreturns aDeploymentobject (enabling IDs in output).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/tui/releases_tab.go | Adapts TUI deploy action to new CreateDeployment return type. |
| internal/tui/mappings_tab.go | Adapts TUI workflow deploy action to new CreateDeployment return type. |
| internal/cli/root.go | Adds deploy command; prints JSON errors to stdout when --json is set. |
| internal/cli/releases.go | Adds releases latest; adds JSON list wrapper + truncation hint for releases list. |
| internal/cli/projects.go | Skips --yes in JSON mode for deletes; uses exitError for not-found. |
| internal/cli/output.go | Adds printJSONList wrapper and centralized exitError behavior. |
| internal/cli/mappings.go | Skips --yes in JSON mode for mapping deletes. |
| internal/cli/environments.go | Skips --yes in JSON mode for deletes; uses exitError for not-found. |
| internal/cli/deployments.go | Adds JSON list wrapper + truncation hint; returns created deployment JSON; uses exitError on wait timeout. |
| internal/cli/deploy.go | New composite deploy command (resolve release, create deployment, optionally wait). |
| internal/cli/build.go | Adds build dispatch --wait polling for a new release on the ref branch. |
| internal/changelog/entries.json | Adds v0.2.4 changelog entry for the new CLI behavior. |
| internal/api/client.go | Changes CreateDeployment to return *Deployment (decoded from API response). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Truncated bool `json:"truncated,omitempty"` | ||
| }{ | ||
| Hits: hits, | ||
| Truncated: count >= limit, |
There was a problem hiding this comment.
truncated is currently computed as count >= limit, which will incorrectly mark results as truncated when limit is 0 (and the comment above says “equals limit”). Consider guarding with limit > 0 and using count == limit to better match the intended semantics.
| Truncated: count >= limit, | |
| Truncated: limit > 0 && count == limit, |
| rows = append(rows, []string{r.ID, r.Branch, r.Version, sha}) | ||
| } | ||
| printTable(rows) | ||
| if len(releases) >= limit { |
There was a problem hiding this comment.
This hint will always trigger when --limit=0 (since len(releases) >= limit), producing a misleading message. Consider validating --limit is > 0, or at least guard this with if limit > 0 && len(releases) >= limit (and likely == if you want to match the “truncated when equals limit” semantics).
| if len(releases) >= limit { | |
| if limit > 0 && len(releases) == limit { |
| rows = append(rows, []string{d.ID, d.EnvironmentID, d.ReleaseID, status, conclusion}) | ||
| } | ||
| printTable(rows) | ||
| if len(deps) >= limit { |
There was a problem hiding this comment.
Same as releases list: if a caller passes --limit=0, this condition is always true and the message becomes misleading. Consider validating limit > 0 (or guarding this with if limit > 0 && len(deps) >= limit).
| if len(deps) >= limit { | |
| if limit > 0 && len(deps) >= limit { |
| tick := time.Duration(waitInterval) * time.Second | ||
| deadline := time.Now().Add(time.Duration(waitTimeout) * time.Second) | ||
|
|
There was a problem hiding this comment.
When --wait is set, waitInterval/waitTimeout are used directly to build tick/deadline. If a user passes 0 or a negative value, this can become a tight polling loop or an immediate timeout. Add validation (e.g., require interval > 0 and timeout > 0 when --wait is enabled) and return a user-facing error.
|
|
||
| if terminalConclusions[conclusion] { | ||
| if jsonMode { | ||
| return printJSON(info) |
There was a problem hiding this comment.
In --json mode, returning printJSON(info) here makes the command exit with status 0 even when the deployment concluded with failure/error/etc. This contradicts the help text/PR contract (exit 1 on failure). Consider printing the JSON payload and still terminating with ExitError (or return a sentinel error that preserves the JSON output) when conclusion != "success".
| return printJSON(info) | |
| if err := printJSON(info); err != nil { | |
| return err | |
| } | |
| if conclusion != "success" { | |
| os.Exit(ExitError) | |
| } | |
| return nil |
| cmd.Flags().IntVar(&interval, "interval", 10, "Poll interval in seconds (used with --wait)") | ||
| cmd.Flags().IntVar(&timeout, "timeout", 600, "Maximum seconds to wait before giving up (used with --wait)") |
There was a problem hiding this comment.
This wait loop uses interval/timeout directly to build durations. If a caller passes 0 or a negative value, time.Sleep(tick) can become a tight loop (or the deadline can be immediately exceeded). Add validation (e.g., require interval > 0 and timeout > 0 when --wait is set) and return a clear error.
| if time.Now().After(deadline) { | ||
| fmt.Fprintf(os.Stderr, "error: timed out waiting for deployment %s (status=%s conclusion=%s)\n", | ||
| exitError(ExitError, "timed out waiting for deployment %s (status=%s conclusion=%s)", | ||
| deploymentID, status, conclusion) |
There was a problem hiding this comment.
In this command, --json currently returns JSON with exit status 0 when a deployment reaches a terminal non-success conclusion (because the terminal path returns printJSON(info) and never sets a non-zero code). That undermines the documented behavior (“Exits 0 on success, 1 on failure/error/timeout”). Consider making the terminal-conclusion path exit ExitError when conclusion != "success" even in jsonMode (while still writing the JSON payload).
jebl01
left a comment
There was a problem hiding this comment.
-
Creating deployments from releases is something that was created so that deployments could be created for those mappings with auto set to false (but we don't have any - it was an ISO thing that never happened). However, it's very handy if you need to deploy a release OTHER than the latest one...
-
Not sure how useful this is (see 1)
-
Nice I guess. Dispathing workflows was kind of leaked into the CLI from the TUI when Claude generated it (nothing we had in the old CLI - because we didn't need it)
-
Good!
-
Good!
-
Project and Envoronment delete should not be allowed for AI to use, and confirmation is important here I think...
Why
The Dave CLI is increasingly used by AI agents and CI scripts, not just humans at a terminal. Several rough edges make it harder for automated callers:
--yesguards on destructive commands add friction when the caller is clearly a script.--jsonis set, so a script has to parse two different channels.--limitwith no signal that there are more results.What changed
1.
dave deploy— composite deploy command (new)Replaces this 3-step manual workflow:
With a single atomic call.
--waitblocks until the deployment reaches a terminal state (success/failure). Exits 0 on success, 1 on failure or timeout, 3 if no release exists for the branch.2.
dave releases latest— branch → release ID in one call (new)Returns exactly one release or exits 3 (not found). Avoids
list --branch x --limit 1and parsing[0].3.
dave build dispatch --wait— block until build produces a releaseAfter dispatching, polls the releases endpoint for a new release on the branch and blocks until one appears. Only meaningful when
--refis a branch name.4. JSON errors to stdout
When
--jsonis set, all errors (including ones that previously went silently to stderr) are now written to stdout as{"error":"..."}. An agent using--jsoncan parse failures with the same code as successes.5. Pagination hints in JSON list output
List commands in JSON mode now return:
{"hits": [...], "truncated": true}when the result count equals
--limit. Human-mode lists print a similar hint to stderr. This lets callers know they may not have seen everything and should re-query with a higher limit.6. Deletes skip
--yesin--jsonmodeprojects delete,environments delete, andmappings deleteno longer require--yeswhen--jsonis passed. The--jsonflag already signals a non-interactive caller.Testing
go build ./...passes cleanly.--json) callers — the--yesguard, stderr errors, and plain table output are all unchanged in that path.🤖 Generated with Claude Code