feat(cmd/dbc): versioned JSON schema, --json on all commands, NDJSON progress#365
feat(cmd/dbc): versioned JSON schema, --json on all commands, NDJSON progress#365
Conversation
amoeba
left a comment
There was a problem hiding this comment.
I took a quick look and made some comments in line. Other thoughts:
-
I'm not so sure about
install --jsonstreaming. it means the return shape varies based on the state of the system. i.e., run the same command twice and your return shape is different. Could we remove that bit and add it if someone ever asks for it?2. Did you consider making the error responses structured like${subcommand}.errorinstead of every error being"kind": "error"? -
The binary structure of the
kindresponses seems like it could be two fields. For example, the current output,
$ dbc init --json
{
"schema_version": 1,
"kind": "init.response",
"payload": {
"driver_list_path": "/Users/bryce/src/columnar-tech/dbc/dbc.toml",
"created": true
}
}
could be
{
"schema_version": 1,
"command": "init",
"status": "success",
"payload": {
"driver_list_path": "/Users/bryce/src/columnar-tech/dbc/dbc.toml",
"created": true
}
}- There's an encoding bug here,
$ go run ./cmd/dbc add --json "mysql<1.0.0"
{"schema_version":1,"kind":"add.response","payload":{"driver_list_path":"/Users/bryce/src/columnar-tech/dbc/dbc.toml","drivers":[{"name":"mysql","version_constraint":"\u003c1.0.0"}]}}
- What made you go with locking rather than atomic writes? Were we vulnerable to concurrent mutations before?
I've been playing around with a GUI being built using Tauri or electron. Either way the
That wasn't something I thought of since the assumption was that we'd have specific shapes for the kinds and ALL errors would have the same shape, hence
I don't think the "status" field is necessary since the kind will either be "error" to indicate an error, or something else indicating success. The format of the kind being something like
Yup, we were vulnerable because all of these paths would open the file first, read it, do something and then write it back out. At any point in there the file could be mutated causing the final write to clobber anything that was modified in between when we opened and read it and when we wrote out the result. Consider the scenario of running The better solution here is file lock of some kind to prevent the concurrent mutation in the first place |
|
Thanks for the responses @zeroshade. I'm generally on board with this. I'm still a bit uneasy about having And item (3) above seems like a real bug worth addressing here. Other than that, I'm +1. |
- fslock: stop deleting lock file on Release() to prevent TOCTOU race where another process can grab the file between Close() and Remove() - install: add checksum to already-installed JSON path; emit bare hex (no sha256: prefix) for consistency with sync lockfile; surface checksum errors in JSON mode instead of silently ignoring them - add: serialize all drivers in --json mode, not just Driver[0] - sync: populate Skipped vs Installed correctly; route errors through JSON envelope in --json mode; emit skipped phase instead of installed for already-present drivers - auth login --json: suppress plaintext output, add IsJSONMode(), emit AuthLoginResponse on success and on error - completions: add --json flag to init, add, remove in bash and zsh Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- sync --json: set s.status=1 on error so FinalOutput suppresses the success sync.status envelope and the process exits non-zero - auth login --json: delay success envelope until post-login work completes (via authLoginCompleteMsg), set status=1 on failure - install: surface checksum errors on already-installed path instead of silently omitting the checksum field schema_version kept at 1: add.response is new/unreleased so the driver->drivers rename is not a compatibility break. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- main.go: suppress formatErr plaintext output in JSON mode so structured error envelopes are not followed by a plain-text error line - auth login: emit success envelope only after ignorable license errors (ErrTrialExpired/ErrNoTrialLicense) by routing them through authLoginCompleteMsg when credentials were already saved; set status=1 on non-ignorable JSON-mode failures - install: move already-installed checksum computation into Update() via alreadyInstalledChecksumMsg so checksum failures set status=1 and exit non-zero; also set status=1 on JSON-mode install errors Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
FinalOutput() was entering the already-installed success path even when m.status != 0 (e.g. after a checksum failure), producing an error envelope followed by a success install.status. Guard with an early return when status is non-zero. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…lure Adds TestInstallJSON_AlreadyInstalledChecksumFailure which installs a driver, removes the shared library to force checksum() to fail, then reinstalls with --json. Asserts status==1 and no install.status success envelope in the output, locking in the m.status!=0 guard on FinalOutput(). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The previous test used runCmdErr which never calls FinalOutput(), so it could not catch a future re-introduction of the bug. The updated test runs the program manually, calls FinalOutput() on the finished model, and asserts it returns empty when status != 0. Verified to fail when the m.status != 0 guard is removed. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Change the install JSON error path from tea.Println to fmt.Fprintln(os.Stdout, ...) to match the sync pattern and make it capturable in tests. Update the regression test to redirect os.Stdout via os.Pipe so it can assert that the kind:error envelope is present and no install.status success envelope appears. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add jsonWriter io.Writer field to progressiveInstallModel (defaults to os.Stdout via jsonOut() helper) so JSON error output bypasses neither the BubbleTea output channel nor hardcodes global stdout. Updated test injects a bytes.Buffer, asserts kind:error envelope is captured there, and includes teaOut+finalOutput in the combined no-install.status check. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… test Move jsonWriter io.Writer from progressiveInstallModel to baseModel so any caller can inject it without type-asserting the concrete model. Test now sets it on baseModel directly. Also decode the captured JSON envelope and assert kind==error and code==install_failed rather than substring match. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… write Store JSON error envelope in jsonErrorOutput field and return it from FinalOutput() so it flows through tea.WithOutput like all other output. Remove jsonWriter from baseModel and jsonOut() helper entirely. Test no longer needs injection: reads error envelope from FinalOutput() and decodes the full envelope shape. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
main.go: bypass the --quiet gate for JSON-mode models so structured error envelopes are always surfaced to machine consumers. subcommand_test.go: runCmdErr now appends FinalOutput() and calls prog.Wait(), mirroring main.go ordering; non-JSON errors fall back to the formatted plaintext message as before. install_test.go: regression test now uses runCmdErr so JSON error capture goes through the shared harness rather than manual prog.Run(). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ness Add IsJSONMode() bool to infoModel, searchModel, uninstallModel, initModel, addModel, and removeModel so the --quiet bypass in main.go covers all JSON commands, not just install/sync/auth. Update runCmdErr to check IsJSONMode() (matching main.go) when deciding whether to append plaintext formatErr, so JSON-mode commands don't get both a JSON envelope and a plaintext error line. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
info: guard FinalOutput with status check and handle errors in Update(); prevents panic from calling driverInfoJSON on zero-value Driver. search: return error envelope when status != 0 instead of empty search.results payload with a success shape. add/init/remove: return error envelopes in JSON mode on failure instead of empty string, so JSON consumers receive structured output on error. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…t, remove Add assertJSONErrorEnvelope helper to subcommand_test.go and regression tests: - TestInfo_JSON_DriverNotFound: asserts info_failed envelope on registry failure - TestSearch_JSON_CompleteRegistryFailure: asserts search_failed envelope - TestAdd_JSON_DriverNotFound: asserts add_failed envelope - TestInit_JSON_Failure: asserts init_failed envelope on unwritable directory - TestRemove_JSON_DriverNotFound: asserts remove_failed envelope Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…lure The chmod approach fails in the System-level test run which uses sudo and ignores read-only permissions. Use a pre-existing dbc.toml file to trigger the init_failed error instead, which works regardless of privileges. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…tests assertJSONErrorEnvelope now requires every non-empty output line to be a JSON object, catching any regression that mixes plaintext and structured output. TestInfo_JSON_DriverNotFound and TestAdd_JSON_DriverNotFound now use getTestDriverRegistry with a nonexistent driver, exercising the findDriver path rather than the registry-failure path. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add TestInfo_JSON_RegistryFailure and TestAdd_JSON_RegistryFailure to cover the complete-registry-failure path with --json alongside the missing-driver path covered by existing tests. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Extend assertJSONErrorEnvelope with variadic msgSubstrings to check that registry error details are preserved in the JSON payload message. Update TestInfo_JSON_RegistryFailure and TestAdd_JSON_RegistryFailure to assert the message contains the injected error text. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
46f871e to
a14621f
Compare
|
Fixed the last issue |
Summary
Improves the
--jsonoutput to be more structured and versioned. All changes are additive — existing human-readable output and exit codes are unchanged.Changes
internal/jsonschema— versioned schema packageExtracts a new
internal/jsonschemapackage with typed Go structs for every--jsonpayload the CLI emits. All structs carryschema_version: 1and akinddiscriminator.--jsonon all commandsinstall— refactored to usejsonschema.InstallStatusenvelopeuninstall/search/info— refactored to use jsonschema packageinit/add/remove— new--jsonflag emittinginit.response,add.response,remove.responsesync— new--jsonflag emitting NDJSON progress events + finalsync.statusNDJSON progress streaming from
install --jsonWhen
--jsonis set,dbc installemits one NDJSON line per phase (download.start,download.progress,extract.start,verify.start, etc.) to stdout as they occur, followed by the terminalinstall.statusenvelope.sha256 checksum verification on install
After downloading,
dbc installnow computes and verifies the sha256 of the tarball. The checksum is included in theinstall.statusJSON payload.--no-verifybypasses both signature and checksum checks;--insecure-no-checksumbypasses only the checksum.File-locking on mutating commands
Adds
internal/fslock(advisoryflock/LockFileEx) wrappinginstall,uninstall,sync,add,removeto prevent concurrent mutations. Returns a clear error with the owner PID if the lock cannot be acquired within 10s.auth login --json—verification_uri_completeWhen
--jsonis set, the device-code flow immediately emits anauth.device_codeenvelope containingverification_uri_complete(the pre-filled URL) so a consumer can open a browser directly if desired.Dead code removal
Removes the unused
TuiCmdstruct andcmd/dbc/view_config.go.Testing
All existing tests pass. New tests added for every changed command.