Emit "data": [] for a list that found nothing
What changes for callers
An empty basecamp todos list printed an envelope with no data field, while an empty messages list printed "data": []. In a live agent eval, the agent's --jq '[.data[] | ...]' exited 1 with cannot iterate over: null, and it told the user the listing had failed. After this change, every command that hands the output layer a nil slice emits "data": [].
Before and after, run against a local bc3 (todos list -p 311619837 --status archived; the API returns [] for that list):
| origin/main | this branch | |
|---|---|---|
--json | {"ok":true,"summary":"0 todos",...} (no data) | {"ok":true,"data":[],"summary":"0 todos",...} |
--jq '[.data[] | .id]' | exit 1, cannot iterate over: null | [], exit 0 |
--count | 1 | 0 |
Cause
Writer.OK normalizes data with a JSON round-trip. A nil []basecamp.Todo marshals to null, unmarshals to nil, and omitempty on Response.Data drops the field. A nil []map[string]any skips the round-trip and printed "data": null instead. todos list across all lists builds its result with var result []basecamp.Todo and appends, so it stays nil when nothing matches. Other commands build lists the same way, so the fix belongs in the envelope, not in each command.
Fix
Writer.OK turns a nil slice into an empty slice of the same type before anything else reads it. This covers the JSON, --jq, --agent/--quiet, --count, styled and Markdown outputs. Styled and Markdown now show the empty-list rendering (No results) in place of (no data) / No data, the same as commands that already returned empty slices.
What stays the same:
- Untyped
niland nil pointers stay absent. - Nil maps stay
null. - Byte slices are excluded. A nil
json.RawMessagemeans "no body" and still printsnull, because an emptyRawMessageis invalid JSON and would fail to encode.
Tests
TestWriterOKNilSliceIsEmptyArraycovers a nil[]basecamp.Todoin JSON and quiet mode, and a nil[]map[string]any. It fails on main.TestWriterOKNilNonSliceUnchangedpins the existing output for untyped nil, a nil pointer, a nil map and a niljson.RawMessage.TestTodosListEmptyEmitsEmptyDataArrayrunstodos listacross lists and for a single list, with no matching to-dos. The across-lists case fails on main. The single-list case already passed and guards against a regression.
bin/ci is green locally.
Summary by cubic
Fixes empty list results so they emit "data": [] instead of omitting the field or printing null. A nil slice was normalized to null and then dropped by omitempty, so todos list with no matches had no data field at all — --jq '[.data[] | ...]' failed with "cannot iterate over: null" and --count printed 1 instead of 0.
Writer.OK now converts a nil slice to an empty slice of the same type before writing the response, so every command that returns an empty list behaves consistently across JSON, quiet, count, styled, and markdown outputs. Untyped nil, nil pointers, nil maps, and byte slices keep their existing output.