Skip to content

feat: LLM-friendly CLI improvements#1

Open
lvanderbijl-kry wants to merge 1 commit into
mainfrom
llm-friendly-cli
Open

feat: LLM-friendly CLI improvements#1
lvanderbijl-kry wants to merge 1 commit into
mainfrom
llm-friendly-cli

Conversation

@lvanderbijl-kry
Copy link
Copy Markdown
Contributor

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:

  • A full deploy workflow requires 3–4 separate commands with manual ID-passing between them — plenty of room for an agent to hallucinate an ID or misparse output.
  • --yes guards on destructive commands add friction when the caller is clearly a script.
  • Errors go to stderr as plain text even when --json is set, so a script has to parse two different channels.
  • List commands silently truncate at the --limit with no signal that there are more results.
  • There's no single command to turn a branch name into a release ID.

What changed

1. dave deploy — composite deploy command (new)

dave deploy <project-id> --branch <branch> --environment <env> [--wait]

Replaces this 3-step manual workflow:

dave releases latest my-service --branch main          # get release ID
dave deployments create my-service --environment prod --release <id>   # deploy
dave deployments wait my-service <deployment-id>       # block

With a single atomic call. --wait blocks 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)

dave releases latest <project-id> --branch <branch>

Returns exactly one release or exits 3 (not found). Avoids list --branch x --limit 1 and parsing [0].

3. dave build dispatch --wait — block until build produces a release

dave build dispatch <project-id> --ref main --wait

After dispatching, polls the releases endpoint for a new release on the branch and blocks until one appears. Only meaningful when --ref is a branch name.

4. JSON errors to stdout

When --json is set, all errors (including ones that previously went silently to stderr) are now written to stdout as {"error":"..."}. An agent using --json can 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 --yes in --json mode

projects delete, environments delete, and mappings delete no longer require --yes when --json is passed. The --json flag already signals a non-interactive caller.

Testing

  • go build ./... passes cleanly.
  • No existing behaviour changed for human (non---json) callers — the --yes guard, stderr errors, and plain table output are all unchanged in that path.

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings April 20, 2026 16:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and dave releases latest.
  • Improved scriptability: JSON-mode error output to stdout, JSON pagination hints for limited list endpoints, and skip --yes for deletes in JSON mode.
  • Updated API client + call sites so CreateDeployment returns a Deployment object (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.

Comment thread internal/cli/output.go
Truncated bool `json:"truncated,omitempty"`
}{
Hits: hits,
Truncated: count >= limit,
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
Truncated: count >= limit,
Truncated: limit > 0 && count == limit,

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/releases.go
rows = append(rows, []string{r.ID, r.Branch, r.Version, sha})
}
printTable(rows)
if len(releases) >= limit {
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
if len(releases) >= limit {
if limit > 0 && len(releases) == limit {

Copilot uses AI. Check for mistakes.
rows = append(rows, []string{d.ID, d.EnvironmentID, d.ReleaseID, status, conclusion})
}
printTable(rows)
if len(deps) >= limit {
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
if len(deps) >= limit {
if limit > 0 && len(deps) >= limit {

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/build.go
Comment on lines +68 to +70
tick := time.Duration(waitInterval) * time.Second
deadline := time.Now().Add(time.Duration(waitTimeout) * time.Second)

Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/deploy.go

if terminalConclusions[conclusion] {
if jsonMode {
return printJSON(info)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Suggested change
return printJSON(info)
if err := printJSON(info); err != nil {
return err
}
if conclusion != "success" {
os.Exit(ExitError)
}
return nil

Copilot uses AI. Check for mistakes.
Comment thread internal/cli/deploy.go
Comment on lines +120 to +121
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)")
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 183 to 185
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)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@jebl01 jebl01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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...

  2. Not sure how useful this is (see 1)

  3. 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)

  4. Good!

  5. Good!

  6. Project and Envoronment delete should not be allowed for AI to use, and confirmation is important here I think...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants