Skip to content

feat(update): machine-readable update checks — update/upgrade --check [--json] (#276) - #299

Merged
apotema merged 2 commits into
mainfrom
feat/276-update-check-json
Jul 8, 2026
Merged

feat(update): machine-readable update checks — update/upgrade --check [--json] (#276)#299
apotema merged 2 commits into
mainfrom
feat/276-update-check-json

Conversation

@apotema

@apotema apotema commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Closes #276. Unblocks labelle-studio#7 — studio can now ask "what's outdated?" without parsing human output and without mutating anything.

What

Two read-only, non-mutating reporting modes over the versions the CLI already knows:

  • labelle update --check [--json] — running CLI binary vs the newest published release (releases.labelle.games/cli/latest.txt). Never downloads/installs.
  • labelle upgrade [dir] --check [--json]project.labelle pins (core/engine/gfx/labelle/assembler + plugins) vs the bundled compatible set this CLI targets (the same set upgrade all would apply). Never rewrites project.labelle. Fully offline — a "read-only mode over what upgrade already queries," as the issue asks.

Contract:

  • --json implies --check — a tool asking for machine output can never accidentally trigger a mutating install/upgrade.
  • Exit codes (issue's cheap-scripting ask): 0 = up to date, 2 = at least one update available. Offline/unknown is not counted as an update (exit 0); the checked/error fields carry that nuance.

JSON schema (stable — studio consumes this)

Adopts the concrete sketch from #278 that @apotema endorsed in the issue thread: {cli:{installed,latest,update_available}, packages:[{name,pinned,latest,update_available}]}, plus a per-entry checked/error to distinguish "up to date" from "offline / no-known-latest". Both commands emit the same top-level shape; each fills only its half. All keys are always present (null when unknown) for a stable typed shape — the same convention as the --progress=json NDJSON feed.

{
  "cli": {                       // update --check → populated; upgrade --check → null
    "installed": "1.55.1",
    "latest": "1.56.0",          // null when the fetch failed
    "update_available": true,
    "checked": true,             // false = couldn't determine (offline)
    "error": null                // string when checked=false
  },
  "packages": [                  // upgrade --check → populated; update --check → []
    { "name": "core",      "pinned": "1.20.0", "latest": "1.21.0", "update_available": true,  "checked": true,  "error": null },
    { "name": "engine",    "pinned": "1.65.0", "latest": "1.65.0", "update_available": false, "checked": true,  "error": null },
    { "name": "assembler", "pinned": null,     "latest": "0.40.0", "update_available": false, "checked": true,  "error": null },  // unpinned → on CLI default
    { "name": "my-plugin", "pinned": "4.0.1",  "latest": null,     "update_available": false, "checked": false, "error": "no known latest version for this package" },
    { "name": "local-dep", "pinned": "0.1.0",  "latest": null,     "update_available": false, "checked": false, "error": "local path override — not version-comparable" }
  ]
}
  • update --check --json{"cli":{…},"packages":[]}
  • upgrade --check --json{"cli":null,"packages":[…]}

Studio composes the two: update --check answers "is the installed CLI stale?" (network); upgrade --check answers "are this project's pins behind?" (offline). The CLI has no plugin-version registry, so plugin latest is null/checked:false — the pin is still surfaced so studio can display it.

Real output

$ labelle upgrade --check --json .   # exit 2
{"cli":null,"packages":[{"name":"core","pinned":"1.20.0","latest":"1.21.0","update_available":true,"checked":true,"error":null}, …]}

$ labelle update --check 2.0.0       # exit 2
labelle: CLI update available: 1.55.1 -> 2.0.0
  run `labelle update` to install it

Implementation

  • New pure, unit-tested module src/cli/update_check.zig — status model, JSON/human serialization, exit-code logic (no network, no fs; all inputs supplied by the callers).
  • update.zig / upgrade.zig are thin wrappers: the CLI-latest fetch and the project.labelle read live there and feed the pure core.
  • Fixes a latent upgrade dispatch bug where a leading --check/--force token was captured as the project dir (would have sent --check to readProjectConfig).
  • Help text + usage doc-comments updated.

Tests

zig build test425/425 passed. New coverage: status computation (up-to-date / outdated / offline / unpinned / unknown-latest / local-override), exit codes, and JSON shape assertions (both report variants, null-key stability); flag parsing for both commands (incl. --json-implies---check). Non-mutation verified by hashing project.labelle before/after --check.

https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j

… [--json] (#276)

Read-only, non-mutating version checks so labelle-studio (#7) can surface
"CLI x.y.z available" and per-project pin-vs-latest without parsing human
output or mutating anything.

- `labelle update --check [--json]`: running CLI vs newest published
  release (R2 latest.txt); never installs a binary.
- `labelle upgrade [dir] --check [--json]`: project.labelle pins
  (core/engine/gfx/labelle/assembler + plugins) vs the bundled compatible
  set this CLI targets; never rewrites project.labelle. Fully offline.
- `--json` implies `--check` (a tool asking for JSON never triggers a
  mutation). Exit 2 = updates available, 0 = up to date (cheap scripting).
- Stable schema `{cli, packages}` adopting the #278 sketch the issue
  endorses; per-entry checked/error distinguishes up-to-date from
  offline / no-known-latest / local-override.

New shared, pure, unit-tested reporting module src/cli/update_check.zig
(JSON shape + status computation + exit codes). Also fixes a latent
dispatch bug where a leading `--check`/`--force` was captured as the
project dir.

Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@apotema, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ae80f8d2-d7c7-4ab3-8d83-4639088ce68a

📥 Commits

Reviewing files that changed from the base of the PR and between 5d43020 and 5936920.

📒 Files selected for processing (2)
  • src/cli/update.zig
  • src/cli/upgrade.zig
📝 Walkthrough

Walkthrough

Adds read-only --check/--json modes to labelle update and labelle upgrade, introducing a new update_check.zig module defining CLI/package status types, comparison logic, JSON/human output, and exit-code semantics. CLI argument parsing and help text are updated accordingly, with corresponding tests added.

Changes

Update/Upgrade check reporting

Layer / File(s) Summary
Update-check report data model
src/cli/update_check.zig
New module defines CliStatus, PackageStatus, Report types, cliStatus/packageStatus computation, anyUpdateAvailable/exitCode, JSON/human writers, and extensive ZSpec tests.
update --check flag and fetch refactor
src/cli/update.zig
Adds UpdateArgs/parseUpdateArgs, fetchLatestCliVersion, cmdUpdateCheck; refactors cmdUpdate to reuse fetch helper; minor PowerShell formatting fix; adds ParseUpdateArgsSpec tests.
upgrade --check flag and cmdUpgradeCheck
src/cli/upgrade.zig
Extends UpgradeArgs with check/json and reportOnly(); adds cmdUpgradeCheck to report pin status for core/engine/gfx/labelle/assembler/plugins; adds new parsing tests.
CLI argument parsing and help text
src/cli.zig, src/cli/help.zig
Reworks upgrade token parsing so dash-prefixed tokens are always flags; imports and exports update_check specs; updates help text and examples for --check/--json.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as cli.zig
  participant Update as update.zig
  participant Upgrade as upgrade.zig
  participant Report as update_check.zig
  participant Server as release server

  CLI->>Update: cmdUpdateCheck(--check/--json)
  Update->>Server: fetch latest.txt
  Server-->>Update: version string
  Update->>Report: cliStatus(installed, latest)
  Report-->>Update: CliStatus
  Update->>Report: writeJson/writeHumanCli
  Update->>Update: exitCode(report)

  CLI->>Upgrade: cmdUpgrade(--check/--json)
  Upgrade->>Upgrade: reportOnly() true
  Upgrade->>Report: packageStatus(pins, latest)
  Report-->>Upgrade: PackageStatus[]
  Upgrade->>Report: writeJson/writeHumanPackages
  Upgrade->>Upgrade: exitCode(report)
Loading

Poem

A rabbit checks its watch, hop hop hop,
"Is my burrow's version up to top?"
With --check and --json, no digging, no fuss,
Just facts in a report, no mutation for us.
Exit code two means "go get the new carrot!" 🥕
Exit code zero means "stay put, sweet parrot!" 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the new update-check and JSON reporting modes for update and upgrade.
Description check ✅ Passed The description matches the changeset and explains the new read-only update check behavior.
Linked Issues check ✅ Passed The PR implements the requested non-mutating --check modes, JSON output, exit codes, and version reporting for update and upgrade.
Out of Scope Changes check ✅ Passed The changes stay focused on update-check support, help text, parsing, and related tests with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/276-update-check-json

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/upgrade.zig (1)

50-68: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Same unknown-flag pass-through gap as update.zig's parseUpdateArgs.

An unrecognized -- token (e.g. a mistyped --jso) is appended to positionals instead of being rejected. Since it's not "assembler"/"all", cmdUpgrade would delegate it as a "subcommand" to assembler_proc.runSubcommand(..., "upgrade", ...) — a mutating path — rather than failing fast. Lower direct risk than the update.zig case (the assembler binary gets a chance to reject it), but the same defensive gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/upgrade.zig` around lines 50 - 68, `parseUpgradeArgs` is currently
treating unknown `--` options as positionals, which can leak mistyped flags into
`cmdUpgrade` and then `assembler_proc.runSubcommand`. Update `parseUpgradeArgs`
to explicitly reject unrecognized arguments that look like flags (for example
any token starting with `--` or `-` that is not one of the supported options)
instead of appending them to `positionals`, while still allowing valid
subcommand positionals like `assembler` and `all` to pass through.
🧹 Nitpick comments (1)
src/cli.zig (1)

752-783: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the upgrade dispatch loop into a standalone testable function.

This inline loop is exactly the fix location for the leading-dash-as-dir bug described in the comment, but unlike parseRunArgs/parseWasmServeArgs/parseDirAndScene (which are extracted functions with dedicated testIter-based specs), it lives directly in main() and has no unit test exercising it — the new tests only cover upgrade.zig's parseUpgradeArgs, which operates on the already-split cmd_args slice, not on this dir/subcommand-splitting logic itself.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.zig` around lines 752 - 783, The upgrade argument splitting loop in
main() should be extracted into a standalone helper so it can be unit tested
like parseRunArgs, parseWasmServeArgs, and parseDirAndScene. Move the
dir/subcommand parsing logic currently handling seen_subcommand, dir_set, and
appendExtraArg into a dedicated function with a testIter-based spec, and keep
main() only responsible for wiring it into parseUpgradeArgs. This ensures the
leading-dash-as-dir behavior is covered directly by tests instead of only being
indirectly exercised through already-split args.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli/update.zig`:
- Around line 27-41: `parseUpdateArgs` is treating any unknown token, including
mistyped `--` flags, as `version_arg` instead of rejecting it. Update
`parseUpdateArgs` to explicitly detect unrecognized `--`-prefixed arguments and
fail fast rather than assigning them to `out.version_arg`, and make sure
`cmdUpdate` only receives a validated version/option set. Follow the pattern
used by the other flag parsers in this CLI so `--check`/`--json` behavior in
`reportOnly()` cannot be bypassed by typos.

---

Outside diff comments:
In `@src/cli/upgrade.zig`:
- Around line 50-68: `parseUpgradeArgs` is currently treating unknown `--`
options as positionals, which can leak mistyped flags into `cmdUpgrade` and then
`assembler_proc.runSubcommand`. Update `parseUpgradeArgs` to explicitly reject
unrecognized arguments that look like flags (for example any token starting with
`--` or `-` that is not one of the supported options) instead of appending them
to `positionals`, while still allowing valid subcommand positionals like
`assembler` and `all` to pass through.

---

Nitpick comments:
In `@src/cli.zig`:
- Around line 752-783: The upgrade argument splitting loop in main() should be
extracted into a standalone helper so it can be unit tested like parseRunArgs,
parseWasmServeArgs, and parseDirAndScene. Move the dir/subcommand parsing logic
currently handling seen_subcommand, dir_set, and appendExtraArg into a dedicated
function with a testIter-based spec, and keep main() only responsible for wiring
it into parseUpgradeArgs. This ensures the leading-dash-as-dir behavior is
covered directly by tests instead of only being indirectly exercised through
already-split args.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bb547b0d-9570-44ea-94f0-50614e7da1eb

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0c155 and 5d43020.

📒 Files selected for processing (5)
  • src/cli.zig
  • src/cli/help.zig
  • src/cli/update.zig
  • src/cli/update_check.zig
  • src/cli/upgrade.zig

Comment thread src/cli/update.zig Outdated
…Rabbit #299)

`parseUpdateArgs` treated any unrecognized token as the target version, so a
typo'd flag bypassed the read-only `--check` guard and fell into the
binary-replacing install path. The exploit: `labelle update --chek 1.60.0` —
the dropped `--chek` leaves `1.60.0` as version_arg, `reportOnly()` is false,
and the CLI INSTALLS 1.60.0 instead of doing a read-only check.

Both parsers now reject unknown `--`-prefixed tokens with a clear message +
`error.InvalidArguments` (mirrors the `labelle status` unknown-flag
convention; distinct nonzero exit, so it never collides with the 0/2
up-to-date/updates-available codes). Bare non-dash tokens stay valid version
positionals. `parseUpgradeArgs` had the same class of bug (a typo'd flag
became a positional and reached the mutating path) and is fixed symmetrically.

Regression tests for both parsers (verified they fail without the fix).

Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j
@apotema
apotema merged commit ac64c50 into main Jul 8, 2026
10 checks passed
@apotema
apotema deleted the feat/276-update-check-json branch July 8, 2026 14:08
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.

Machine-readable update checks: labelle update --check / upgrade --check [--json]

1 participant