fix(keys): honor -o json so stdout stays parseable (completes the #97 sweep) - #101
Merged
Conversation
Finishes #98. #97 fixed the -o json pollution across requests, read, models, projects, members, usage and billing, but `keys` was not in that sweep, so it was left as the last command whose stdout still broke pipes: $ dg -o json keys | jq -r '.keys[0].key_id' jq: parse error: Invalid numeric literal at line 1, column 9 "Fetching API keys..." and the Rich table were printed to stdout ahead of the JSON the framework serialises from the result. Applies the same pattern the other seven commands now use: - `status_console = Console(stderr=True)` for progress, errors, the empty-state notice and the delete confirmation — chrome never touches stdout - human rendering (list table, created-key details, key details, and both dry-run summaries) is gated on `get_output_format() == "default"` The created key's secret is unaffected: it already travels in `KeysResult.created_key.key`, so json/yaml/csv callers still receive it — only the human-facing echo is suppressed. One known limitation, unchanged in spirit from #97: in json mode the dry-run paths report status and message but not the would-be scopes/ttl/tags, since those are not modelled on the result. Worth a follow-up if callers need them. Verified live: all eight commands now emit parseable JSON on stdout (`-o yaml`/`-o csv` valid too), chrome still visible on stderr, and human output matches the already-merged `projects` behaviour under identical conditions. 1067 unit tests pass, including 4 new gating tests for keys.
…oads Review follow-ups on #101. Three defects that made "-o json keeps stdout parseable" true only in the narrow case, plus the shared status console the per-command copies were standing in for. Exit codes. main.py caught SystemExit and dropped the code on the floor, so every command exited 0 no matter what it reported -- including the auth-guard failure that already raised SystemExit(1). A caller writing `if dg -o json keys; then` took the success branch on a failed call, which defeats the point of a parseable stdout. main.py now carries the code through the post-run notifications and re-raises it, and BaseCommand maps a result status to an exit code once, in one table: error -> 1, cancelled -> 130, everything else -> 0. Payload integrity. _output_yaml and _output_csv printed through Rich, which treats square brackets as style markup and deletes them: an API key comment of "[ci] runner" came out as "runner", silently, with no error. Rich also hard-wrapped at the console width, injecting newlines into the middle of a csv field. Both now go through _write_payload with markup, highlighting and wrapping off. Still routed via `console` so --quiet keeps working. The JSON path was already safe -- print_json escapes rather than interprets -- and is left alone. The unknown-format complaint moved to stderr, so it cannot corrupt the JSON we fall back to. get_status_console() gives core one definition of the stderr chrome console and the seven commands fixed in #97 now use it. A per-command Console(stderr=True) silently missed core's agentic no-color settings, and a new command reaching for a bare Console() is exactly how keys regressed in the first place.
…etes Two defects found reviewing #101, both older than the -o json work but both in code this branch touches. --create --dry-run never ran. handle() read project_id and dry_run off kwargs with .get(), leaving them in the dict, then forwarded **kwargs alongside them to _create_key. Every argument arrived twice, so Python raised "got multiple values for argument 'project_id'" before the function body started; the error was swallowed into an error result and the process still exited 0. The dry-run gating added on this branch was unreachable, and the "known limitation" noted in the PR description described behaviour the path never had. Popping both fixes it -- fixing only dry_run just exposes the project_id collision behind it. --dry-run is the flag a cautious developer reaches for first on a command that mints credentials, so it should not print a private method name at them. --delete without --yes deleted nothing and blamed the user. BaseCommand.confirm returns its default whenever any parameter came from the command line, and --delete KEY_ID is itself such a parameter, so the prompt was unreachable and the command always returned "Cancelled by user" without asking or calling the API. _confirm_delete now prompts on stderr when someone is there to answer -- stderr so -o json stdout stays parseable -- and returns a usage error naming --yes when nobody is, rather than reporting a cancellation that never happened. With the exit-code mapping that error now exits 1 instead of 0. Also switches to core's get_status_console(), and adds tests for both dry-run paths, all four confirmation outcomes, and the error path -- the absence of a --create --dry-run test is why this shipped.
llms-full.txt documents "Exit codes: 0 = success, 1 = error, 2 = user interrupt". The status->code table landed cancelled on the shell's 130, which contradicts that and contradicts main.py's own KeyboardInterrupt handler, which already exits 2. deepctl-cmd-mcp returns status="cancelled" straight out of its KeyboardInterrupt handler, so Ctrl-C on `dg mcp` exited 130 where the docs promise 2. This also settles the versioning question: with error -> 1 and cancelled -> 2, the exit codes now match what was already published, so this is a bug fix restoring documented behaviour rather than a new contract. No breaking-change footer, no docs update needed.
Merged
GregHolmes
added a commit
that referenced
this pull request
Aug 18, 2026
The squash of #101 collapsed four commits into one subject line, so the generated 0.2.28 entry mentions only the keys `-o json` fix. Three of the changes in that release are visible to anyone upgrading -- exit codes are now enforced, yaml/csv output no longer strips bracketed values, and `keys --delete` actually confirms -- and the exit-code one will surface failures in pipelines that were ignoring them. Added under the generated `### Bug Fixes` list, inside the sections release-please owns, and mirrored into the release PR body so the published release notes and the committed changelog say the same thing.
GregHolmes
added a commit
that referenced
this pull request
Aug 19, 2026
main()'s module-level `console` was a plain rich Console(), which writes to stdout, and both of main()'s handlers print through it. So a crash, a bad flag, an unknown command, or a bare `dg` wrote human-readable prose to stdout -- `dg -o json not-a-command` put `Error: No such command ...` on stdout and left stderr empty, so anything piping stdout into jq parsed the error text instead of JSON. This is the root-handler half of the #97 sweep. That issue's scope covered moving errors to a stderr Console "so stdout stays clean", and #101 closed the sweep, but both only reached the command layer; main()'s own handlers were never moved. Reuse deepctl_core.output.stderr_console -- the same console print_error() writes to, and the pattern deepctl-cmd-mcp already follows -- so root-level and command-level diagnostics format identically, including the no-color handling for agentic/CI callers. Exit codes are unchanged (1 for errors, 2 for interrupt, 0 on success), and success paths still write their payload to stdout. Adds tests asserting a failing `dg -o json ...` writes nothing to stdout for both the unknown- command and bad-flag paths, and that the cancellation notice is on stderr -- the assertion that would have caught this during the original sweep.
GregHolmes
added a commit
that referenced
this pull request
Aug 19, 2026
…t-code + error-stream correctness (#102) Blocks #100 — merge this first, then let release-please regenerate the release PR as **0.3.0**. ## What this fixes **1. `dg update` on pip silently doesn't deliver the release (root `pyproject.toml`).** Root declared floors as low as `>=0.0.1`, and pip's default `only-if-needed` upgrade strategy leaves any sub-package whose installed version already satisfies its floor. Measured from published `deepctl==0.2.26` with the release wheels available: only 4 of 17 released packages upgraded; `dg --version` reported the new number while the keys fixes — including the `-o json` fix that headlines the release — never arrived. (uv resolves fresh and is unaffected, so the same `dg update` lands two users in different states.) Floors now match the versions being published. **2. Eight packages can import a symbol their declared core floor doesn't guarantee (`packages/*/pyproject.toml`).** `deepctl-cmd-{billing,keys,members,models,projects,read,requests,usage}` import `get_status_console` (new in core 0.2.16) at module scope while declaring `deepctl-core>=0.1.10`. PyPI's latest published core is 0.2.14, so `pip install --upgrade deepctl-cmd-keys` alone reproduces a broken CLI: the command vanishes and the ImportError prints to stdout. Same class of hand-bump as #92; release-please has no cross-package dependency automation, so these floors are hand-maintained. **3. A crash exited 2 — the code reserved for user interrupt (`src/deepctl/main.py`).** Per the published contract (0 = success, 1 = error, 2 = user interrupt), `main()`'s generic exception handler now exits 1. Because `cli()` runs with `standalone_mode=False`, Click usage errors (bad flag, unknown command, bare `dg`) propagate to this same handler and move from 2 to 1 as well — consistent with the contract, which reserves 2 for interrupt. Also repairs three tests whose `patch.object(cli, "__call__", ...)` was inert (dunder lookup bypasses instance attributes) and adds a usage-error exit-code test. **4. …but that alone demoted a real Ctrl-C to 1 (`src/deepctl/main.py`).** With `standalone_mode=False`, Click catches a `KeyboardInterrupt` raised during command execution and re-raises it as `click.exceptions.Abort` — a `RuntimeError` subclass, not a `KeyboardInterrupt`. In this repo the path is more direct still: `BaseCommand` catches the interrupt itself and raises `click.Abort()`. So a mid-command Ctrl-C — the common case — bypassed the `KeyboardInterrupt` handler entirely and landed in the generic handler that 3 just changed, exiting 1 and printing an empty `Error: ` (`str(Abort())` is `""`). Before 3 that path exited 2 correctly by accident. `Abort` is now caught alongside `KeyboardInterrupt`, so user cancellation (Ctrl-C, Ctrl-D at a prompt) always exits 2, with a test on the `Abort` delivery path. **5. Root diagnostics printed to stdout, corrupting `-o json` (`src/deepctl/main.py`).** `main()`'s module-level console was a plain rich `Console()`, which writes to stdout, and both of its handlers print through it — so a crash, bad flag, unknown command, or bare `dg` wrote human-readable prose to stdout. `dg -o json not-a-command` put `Error: No such command ...` on **stdout** and left stderr empty, so anything piping stdout into `jq` parsed the error text instead of JSON. This is the root-handler half of the #97 sweep: that issue's scope covered moving errors to a stderr console "so stdout stays clean", and #101 closed the sweep, but both only ever reached the command layer. Now aliases `deepctl_core.output.stderr_console` — the same console `print_error()` writes to, and the pattern `deepctl-cmd-mcp` already follows — so root-level and command-level diagnostics format identically, including the no-color handling for agentic/CI callers. Exit codes are unchanged and success paths still write their payload to stdout. Adds tests asserting a failing `dg -o json ...` writes nothing to stdout (unknown-command and bad-flag paths) and that the cancellation notice is on stderr. ## Why the BREAKING CHANGE footer The exit-code enforcement (#101) landed as `fix:`, so release-please would ship it as patch 0.2.28 with no version signal — and #100 currently confirms that: root reads `0.2.28` and its diff contains no `⚠ BREAKING CHANGES` section at all. There is no machine-readable breaking marker anywhere in the cycle; the behavior change exists only as hand-written prose in `21b8333` / `e327a5e`. The break belongs to #101's already-merged code, so it cannot come from a conventional-commit type on this PR's own diff — it has to be injected where the version arithmetic can see it. With `bump-minor-pre-major`, that makes root **0.3.0**. The signal is deliberately stated in three places, because which one release-please actually reads depends on how this PR is merged: - **`BREAKING CHANGE:` footer on `914e132`** — the primary. That commit touches only `src/deepctl/main.py` and its tests, i.e. a root-only path, so the break is attributed to root alone and the eight sub-packages stay patch bumps. This is the one that survives a **merge commit**, and it is the only variant that produces the intended release shape. - **`fix!:` in the title** — insurance. This repo's recent PRs were squash-merged, and with `squash_merge_commit_title: PR_TITLE` / `squash_merge_commit_message: PR_BODY` a squash discards every commit message, footer included. The `!` keeps root at 0.3.0 in that case. - **`BREAKING CHANGE:` footer at the foot of this description** — so a squash also carries the descriptive text into the `⚠ BREAKING CHANGES` section rather than just the subject line. Trade-off to know before merging: **prefer a merge commit.** A squash collapses all five commits into one that touches root *and* the eight `packages/deepctl-cmd-*/pyproject.toml` files, so the break gets attributed to those eight paths too — they would take minor bumps (keys 0.0.3 → 0.1.0, usage 0.1.13 → 0.2.0, …) with a breaking-change entry about CLI exit codes that has nothing to do with them, and root's floors here (`>=0.0.4`) would then sit below what actually published, re-seeding the staleness this PR exists to fix. Root reaches 0.3.0 either way; only the sub-package shape differs. ## After merge — steps on the regenerated #100 1. `uv lock` and commit (version bumps stale the lock; CI runs `uv sync --locked` — this is the `eff5291` wall and recurs every release until release.yml regenerates the lock itself). 2. Re-apply the behavior-change prose (`git show 21b8333 e327a5e`), changing `0.2.26 to 0.2.28` → `0.2.26 to 0.3.0`, and fold in the exit-code and output-stream details from this PR: crashes and usage errors move 2 → 1, `2` stays reserved for user interrupt (Ctrl-C during a command still exits 2), and root error/cancellation output moves from stdout to stderr. Decide there whether the stream move gets its own `⚠ BREAKING CHANGES` line or reads as a plain fix — root lands on 0.3.0 either way, so it is a notes-wording call, not a version call. 3. Verify root reads 0.3.0 across manifest / `pyproject.toml` / `__init__.py` / `CHANGELOG` heading, and the `⚠ BREAKING CHANGES` section renders the footer text. Verification here: full suite **1094 passed / 6 skipped**; `make check` clean (ruff + mypy, 115 files); `uv lock --check` clean after both floor commits; live probes — `--version` → 0, bare `dg` → 1, bad flag → 1, unknown command → 1, mid-command Ctrl-C → 2; `dg -o json not-a-command` writes **0 bytes** to stdout with the error on stderr, while `dg -o json models` still emits valid JSON on stdout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) BREAKING CHANGE: `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code.
GregHolmes
added a commit
that referenced
this pull request
Aug 19, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.3.0</summary> ## [0.3.0](v0.2.27...v0.3.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** cap mcp <2 (fixes broken dg mcp), commit uv.lock, require twine >=7 ([#95](#95)) ([997cd36](997cd36)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **deps:** raise root dependency floors to the versions this release publishes ([c0b0023](c0b0023)) * exit 1, not 2, when a command crashes or is misused ([914e132](914e132)) * keep exit 2 when Ctrl-C interrupts a running command ([b0e80e2](b0e80e2)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) * **release:** bump pypi-publish action to v1.14.2 for Metadata-Version 2.5 ([#94](#94)) ([582cd83](582cd83)) * send root error and interrupt output to stderr, not stdout ([f4b7c48](f4b7c48)) * **web:** repair broken Heap snippet, upgrade astro 6→7, clear all 20 npm alerts ([#96](#96)) ([11928fe](11928fe)) ### Behavior changes Alongside the exit-code change above, upgrading to 0.3.0 changes these: * The full exit-code contract is now enforced end to end: `0` = success, `1` = error, `2` = user interrupt. Crashes **and usage errors** (bad flag, unknown command, bare `dg`) exit `1`; `2` is reserved for cancellation, so Ctrl-C during a running command and Ctrl-D at a prompt both still exit `2`. * Error and cancellation messages are written to **stderr** instead of stdout. `dg -o json …` therefore keeps stdout machine-readable when a command fails — previously a failure printed `Error: …` prose to stdout, so a script piping stdout into `jq` parsed the error text instead of JSON. Successful commands still write their payload to stdout. * `-o yaml` and `-o csv` no longer drop square-bracketed text from values. Output was passed through a renderer that read `[...]` as style markup and deleted it, so an API key comment of `[ci] runner` was emitted as `runner`. Long values are also no longer hard-wrapped mid-field. * `dg keys --delete KEY_ID` now asks for confirmation on stderr instead of always reporting `Cancelled by user` without deleting. In a non-interactive context it exits `1` and tells you to pass `--yes`. * `dg keys --create --dry-run` now reports what it would create. It previously failed with an internal `TypeError`. ### Previously unreleased 0.2.27 was tagged on 2026-08-17 but never reached PyPI — its publish step failed with `InvalidDistribution: Invalid distribution metadata: '2.5' is not a valid metadata version`, which [#94](#94) and [#95](#95) then fixed. PyPI therefore goes straight from 0.2.26 to 0.3.0, and this release is the first published build to include the 0.2.27 changes: * SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals ([#92](#92)) ([50d96cf](50d96cf)) * **speak:** default to Flux TTS (`flux-alexis-en`) instead of Aura 2 ([#89](#89)) ([5a0b698](5a0b698)). This changes the default model for `dg speak`, so synthesised audio differs unless you pass an `aura-*` model explicitly. * **mcp:** swallow broken/closed-pipe on dg mcp startup notifications and error path ([#88](#88)) ([b24396e](b24396e)) Six packages tagged in that cycle also reach PyPI for the first time here: `deepctl-cmd-listen` 0.0.14, `deepctl-cmd-login` 0.1.17, `deepctl-cmd-skills` 0.0.7, `deepctl-cmd-speak` 0.0.4, `deepctl-cmd-update` 0.2.6 and `deepctl-telemetry` 0.0.6. Because 0.2.27 never published, `dg update` on pip also had to be repaired for this release to arrive at all: root's inter-package dependency floors were lower than the versions being published, so pip's default `only-if-needed` strategy left most sub-packages stale and `dg --version` reported the new number while the fixes never landed. Floors now match the published versions exactly. </details> <details><summary>deepctl-core: 0.2.16</summary> ## [0.2.16](deepctl-core-v0.2.15...deepctl-core-v0.2.16) (2026-08-19) ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) ### Behavior changes * Commands now map their result status to a process exit code (`error` → `1`, `cancelled` → `2`, otherwise `0`), and `BaseCommand.exit_code_for()` exposes that mapping. Exit codes were previously discarded, so every command exited `0`. * `-o yaml` and `-o csv` payloads are written verbatim; the renderer no longer interprets `[...]` as markup or wraps long values. * New `get_status_console()` returns the shared stderr console for status output. Commands should use it instead of declaring their own. Packages that import it require `deepctl-core>=0.2.16`. </details> <details><summary>deepctl-cmd-projects: 0.2.0</summary> ## [0.2.0](deepctl-cmd-projects-v0.1.13...deepctl-cmd-projects-v0.2.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-usage: 0.2.0</summary> ## [0.2.0](deepctl-cmd-usage-v0.1.13...deepctl-cmd-usage-v0.2.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-mcp: 0.1.15</summary> ## [0.1.15](deepctl-cmd-mcp-v0.1.14...deepctl-cmd-mcp-v0.1.15) (2026-08-19) ### Bug Fixes * **deps:** cap mcp <2 (fixes broken dg mcp), commit uv.lock, require twine >=7 ([#95](#95)) ([997cd36](997cd36)) </details> <details><summary>deepctl-cmd-models: 0.1.0</summary> ## [0.1.0](deepctl-cmd-models-v0.0.2...deepctl-cmd-models-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-keys: 0.1.0</summary> ## [0.1.0](deepctl-cmd-keys-v0.0.3...deepctl-cmd-keys-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-read: 0.1.0</summary> ## [0.1.0](deepctl-cmd-read-v0.0.2...deepctl-cmd-read-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-requests: 0.1.0</summary> ## [0.1.0](deepctl-cmd-requests-v0.0.2...deepctl-cmd-requests-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-billing: 0.1.0</summary> ## [0.1.0](deepctl-cmd-billing-v0.0.2...deepctl-cmd-billing-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-members: 0.1.0</summary> ## [0.1.0](deepctl-cmd-members-v0.0.3...deepctl-cmd-members-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Last command in the
-o jsonsweep started by #97, plus the review follow-ups that came out of it.#97 fixed requests, read, models, projects, members, usage, billing — but
keyswasn't in that sweep, so it was left as the one account command whose stdout still broke pipes:Fetching API keys...and the Rich table were going to stdout ahead of the JSON.The keys fix — same pattern as the merged seven
status_consolefor progress, errors and the empty-state notice, so chrome never touches stdoutget_output_format() == "default": the list table, created-key details, key details, and both dry-run summariesThe created key's secret is unaffected — it already travels in
KeysResult.created_key.key, so json/yaml/csv callers still receive it; only the human echo is suppressed. There's a test asserting exactly that.Review follow-ups
Reviewing the above turned up four more defects. All predate this branch, but three of them sit in code it touches and all four make "
-o jsonkeeps stdout parseable" true only in the narrow case, so they're fixed here rather than deferred.dg keys --create --dry-runnever ran.handlereadproject_idanddry_runwith.get()and then forwarded**kwargsalongside them, so every argument arrived twice and Python raisedgot multiple values for argument 'project_id'before the body started — swallowed into an error result, exit 0. The dry-run gating added here was unreachable, and this PR's original "known limitation" note described behaviour that path never had. Popping both fixes it; fixing onlydry_runjust exposes theproject_idcollision behind it.Failures exited 0.
main.pycaughtSystemExitand discarded the code, so no command could ever signal failure — including the auth-guard failure that already raisedSystemExit(1).if dg -o json keys; thentook the success branch on a failed call. The code is now carried through the post-run notifications and re-raised, andBaseCommandmaps status to exit code in one table, matching the contract already published inllms-full.txt(0 = success, 1 = error, 2 = user interrupt):error→ 1,cancelled→ 2, everything else → 0. Exiting 0 on failure violated that documented contract, so this restores published behaviour rather than introducing a new one — no docs change needed.-o yamland-o csvsilently deleted user data. Both printed through Rich, which treats square brackets as style markup and removes them — a key comment of[ci] runnercame out asrunner, no error. Rich also hard-wrapped at the console width, injecting newlines into the middle of a csv field. Both now write the payload verbatim (markup, highlighting and wrapping off), still viaconsoleso--quietkeeps working. The JSON path was already safe (print_jsonescapes rather than interprets) and is untouched.dg keys --delete IDwithout--yesdeleted nothing and blamed the user.BaseCommand.confirmreturns its default whenever any parameter came from the command line, and--delete KEY_IDis itself such a parameter — so the prompt was unreachable and the command always returnedCancelled by userwithout asking or calling the API. It now prompts on stderr when someone is there to answer (stderr, so-o jsonstdout stays clean), and returns a usage error naming--yeswhen nobody is.Also:
get_status_console()in core replaces the eight per-commandConsole(stderr=True)declarations. A per-command console silently missed core's agentic no-color settings, and a new command reaching for a bareConsole()is exactly howkeysregressed in the first place — this makes it correct by default.Verification
dg -o json keys | jq -r '.keys[0].key_id'-o json-o yaml/-o csvfor keys-o jsonkeys --create --dry-rundry_run, calls nothing ✓ (was a TypeError)keys --delete IDwithout--yes--yes, exit 1, no API call ✓--help/--version→ 0, unknown command → 2 ✓ — matches documented contractruff checkandmypycleanStill open in #98
#98 stays open for one item: the promise that stdout auto-switches to JSON when piped.
dg keys | jqstill gets the Rich table, because the auto-switch (setup_output) sits behindis_agentic(), which needs 3+ soft signals; a plain pipe from an interactive shell scores 1. Lowering that threshold changes whatdg <anything> | lessdoes for every command, so it's a product decision rather than a bug fix — deliberately not made here.Note on #99
#99 fixed the stdout bug with a central format-aware console before #97 landed. Now that #97 is merged, #99 is redundant and would conflict, so I've closed it. Its one good idea — a shared status-console primitive instead of per-command declarations — is implemented here as
get_status_console().