docs/specs/2026-09-23-cli-output-refresh-design.md
284 lines · 10836 bytes
12 symbols in this file
1# CLI output refresh
2
3Ref #254. Terminal rendering for tables, `show` views and help, with
4piped output unchanged in shape, plus the fixes from the 2026-09-23 CLI
5audit.
6
7## Problem
8
9The plain output has had no design pass since v1.22.0.
10
11- Tables have no header, no colour and no width limit. The CLI pads
12 tabs with `tabwriter` for the verbs in `listVerbs`
13 (`cmd/gitbay/ssh.go`) and nothing else.
14- `show` views print stored markup verbatim: event lines read
15 `referenced in commit [6c4d1e1454](/krz/gitbay/commit/…) by [cmc](/cmc)`,
16 and timestamps are whatever string the row holds
17 (`2026-09-23T23:26:00.570Z`). The web prints `2026-09-23 23:26 UTC`.
18- Root help carries usage in the summary column. Noun help is two
19 lines per verb with the full usage. Verb help is the same two lines;
20 no flag has a description, and no command has an example.
21- `pass()` in `cmd/gitbay/main.go` holds a second copy of each usage
22 string, which drifts (`build list` still reads
23 `recent builds: <owner/name>`).
24
25## Decision
26
27The server renders. The CLI tells it the terminal's width and whether
28colour is wanted; the command's plain formatter chooses between
29terminal and plain rendering. The CLI adds a pager for long views and
30the grouped root help, and nothing else.
31
32Rejected:
33
34- The CLI rendering from `--json`. A second renderer per command in
35 `cmd/gitbay`, which drifts from the server's, and stock ssh never
36 sees the result.
37- A structured view format on the wire (typed columns, a document
38 tree) styled by the CLI. A new protocol between two programs that
39 ship together, for no visible gain over rendering on the server.
40
41## Transport
42
43The CLI passes `-o SetEnv=GITBAY_TERM=<cols>[,color]` when stdout is a
44terminal. `color` is left out when `NO_COLOR` is set and non-empty,
45`TERM` is `dumb`, or `--no-color` is given (stripped by the CLI before
46dispatch). Piped stdout sends nothing.
47
48sshd accepts an `env` request named `GITBAY_TERM` and ignores every
49other name, as today. `runExec` parses the value into `Ctx.Term`:
50
51```go
52type Term struct {
53 Cols int // 0: plain output
54 Color bool
55}
56```
57
58A missing or malformed value, or `Cols` under 40, is the zero value.
59The HTTP surfaces never set it. Under the system-sshd forced command
60(`gitbayd shell`) the variable arrives only if the operator adds
61`AcceptEnv GITBAY_TERM`; without it output is plain. The Admin wiki
62page says so.
63
64`SetEnv` needs OpenSSH 7.8. The CLI shares one connection through
65ControlMaster; the first task verifies that a multiplexed session
66carries `SetEnv`. If it does not, the CLI sends a leading
67`--term=<cols>[,color]` argument instead, and `Dispatch` strips it
68wherever it appears, as it strips `--json`.
69
70Stock ssh users opt in with `ssh -o SetEnv=GITBAY_TERM=120,color`.
71
72`alignColumns`, `listVerbs` and the `tabwriter` in `runSSH` are
73removed.
74
75## Tables
76
77`internal/control/table.go`:
78
79```go
80t := c.table("#", "STATE", "TITLE", "AUTHOR", "UPDATED")
81for _, is := range issues {
82 t.row(ref("#", is.Number), state(is.State), text(is.Title), text(is.Author), age(is.UpdatedAt))
83}
84t.flush()
85```
86
87Cells are typed: `ref`, `state`, `text`, `flex`, `age`, `num`. The
88`flex` column (a title or description, one per table) is the one that
89shrinks first.
90
91Plain (`Term.Cols == 0`): one row per item, cells joined by tabs, no
92header, `age` as RFC3339 to the second in UTC (`2026-09-23T23:26:00Z`).
93This is the current shape; only the timestamp format changes.
94
95Terminal:
96
97- Header row in capitals, dim when `Color`.
98- Columns padded with two spaces between them.
99- Width: when a row exceeds `Cols`, the flexible column is cut with
100 `…`, down to 8 cells. If that is not enough, the other `text`
101 columns are cut from the right. `ref`, `state`, `age` and `num`
102 are never cut. Widths are measured in display cells
103 (`golang.org/x/text/width`), not bytes.
104- `age` is relative: `just now` under a minute, then `5m ago`,
105 `2h ago`, `3d ago` under 14 days, `3w ago` under 8 weeks, then
106 `2006-01-02`.
107- `state` colour, ANSI 16-colour so the terminal's theme sets the
108 shade, matching the web's state tokens: green (`--ok`) for `open`,
109 `success`, `approved`; magenta (`--done`) for `merged`; red (`--bad`)
110 for `failed`, `error`, `changes requested`; dim (`--neutral`) for
111 `closed`, `draft`, `pending`, `canceled`. Anything else is
112 uncoloured.
113
114An empty table prints `nothing to list` on stderr, as `emit` does now.
115
116`emitPage` in terminal mode prints the next cursor on stderr as
117`more: gitbay <path> <args> --cursor <c>`. Plain mode keeps the
118`next\t<c>` row.
119
120Every list command moves to `table`: each `emit`/`emitPage` whose
121plain formatter prints one row per item. Mutations and single-value
122reads keep their one line.
123
124## Show views
125
126`internal/termtext` renders markdown (goldmark) and org (go-org) syntax
127trees to terminal text at a given width:
128
129- Paragraphs wrapped at `Cols - 2`, indented two spaces.
130- Headings bold; list items with `•` or their number, continuation
131 lines hanging.
132- Code blocks indented four spaces, not wrapped, highlighted with
133 chroma's `terminal16` formatter when `Color`.
134- Emphasis bold or underlined when `Color`, plain text otherwise.
135- Links as their text, followed by ` (<url>)` when the URL differs
136 from the text. Relative forge links are made absolute from
137 `server.site_url`.
138- Images as `[image: <alt>]`.
139
140The same package has a plain mode: no ANSI, no wrapping, links
141reduced as above.
142
143A `view` helper in `internal/control` lays out every `show`:
144
145```
146#253 Recorded CLI walkthrough on the landing page closed
147
148 author cmc, 2026-09-22 18:04 UTC
149 closed 2026-09-23 23:26 UTC by cmc in fc2380f3f0
150 labels docs, web
151 milestone v1.35.0
152 url https://gitbay.org/krz/gitbay/issues/253
153
154 <rendered body>
155
156 · cmc referenced this in 6c4d1e1454 2026-09-23 23:26 UTC
157 · cmc closed this in fc2380f3f0 2026-09-23 23:26 UTC
158
159── cmc, 2026-09-23 23:40 UTC ──────────────────────────────────────
160 <rendered comment>
161```
162
163Title bold and state coloured when `Color`; events dim. Timestamps in
164views use the web's format, `2006-01-02 15:04 UTC`. Plain mode prints
165the same lines without colour or wrapping, with RFC3339 timestamps.
166Commands: `issue show`, `mr show`, `build show`, `release show`,
167`snippet show`, `repo show`, `milestone show`, `org show`,
168`profile show`, and any other `show` whose output has a body or more
169than one field.
170
171### Pager
172
173When stdout is a terminal, the CLI runs `show`, `diff` and `log`
174verbs through a pager: `$GITBAY_PAGER`, else `$PAGER`, else
175`less -FRX`. `-F` exits when the output fits one screen. An empty
176`GITBAY_PAGER` disables it. `build log --follow` is never paged. The
177pager's exit does not change the command's exit code.
178
179## Help
180
181`Command` gains:
182
183```go
184type Flag struct {
185 Name string // "--state"
186 Arg string // "open|closed|all", empty for a switch
187 Desc string // "which issues"
188 Default string // "open", empty for none
189}
190
191Flags []Flag
192Examples []string // full argv after the program, owner/name explicit
193```
194
195`help --json` adds `flags` and `examples` to each entry.
196
197Server `help` with a prefix, in terminal mode:
198
199- Noun (a prefix matching more than one command): the noun's summary,
200 `USAGE gitbay <noun> <verb> [<owner/name>] ...`, then `READ` and
201 `WRITE` sections from `ReadOnly`, one line per verb with its
202 summary, then `gitbay <noun> <verb> --help for flags.`
203- Verb (a prefix matching one command): summary, `USAGE`, `FLAGS`
204 one per line with description and `(default …)`, `--json` last,
205 then `EXAMPLES`.
206
207Examples print with a `gitbay ` prefix in terminal mode and
208`ssh <ssh host> ` in plain mode; both are valid because each example
209names its repository.
210
211Plain mode without a prefix stays one line per command. With a prefix,
212plain mode prints the same sections as terminal mode without colour.
213
214The CLI's noun and verb `--help` already go to the server. `pass()`
215drops its short text; the cobra command takes its one-line summary
216from a table generated from the registry (`go generate`, checked by a
217test that the file is current), so root and completion text never
218drift.
219
220Root help stays local to the CLI and is grouped:
221
222```
223WORK issue mr build release milestone label search
224REPOSITORIES repo wiki status webhook init
225YOU dashboard feed notifications auth profile snippet web
226INSTANCE org explore register migrate remote admin audit
227```
228
229one noun per line with its summary. A test fails when a top-level
230command is in no section or two.
231
232Registry test: every `--flag` token in `Usage` has a `Flags` entry
233and every `Flags` entry appears in `Usage`; every command has a
234non-empty `Desc` per flag and at least one example; every example
235resolves through `Lookup` to its own command.
236
237## Audit fixes
238
239- `release list` takes `--limit`/`--cursor`; the title column is
240 empty when the title equals the tag.
241- `notifications device add` prints `registered device <n>`.
242- `dashboard` prints `none` under an empty section.
243
244Two audit findings need no change. `build log --follow` already
245says what to do when it gives up on a queued build
246(`buildfollow.go`). A missing positional through `c.usage()` already
247prints the registered usage and exits 2, which is the rule; converting
248198 call sites to `usageWith` for an extra line is not worth the
249churn.
250- The stale `build list` help goes with the `pass()` short text.
251
252## Rules
253
254The Users wiki "Output rules" gains an "At a terminal" section:
255headers, colour, width, relative ages, the pager, `GITBAY_TERM`,
256`NO_COLOR`. The piped rules stand, with timestamps stated as RFC3339
257to the second.
258
259## Testing
260
261- `table`: plain rows byte-identical to the current formatter for a
262 fixture per cell type; terminal golden files at 60 and 120 columns,
263 with and without colour; wide-character titles.
264- `termtext`: golden files for markdown and org fixtures covering
265 every node type above, at 60 columns, plain and colour.
266- `Term` parsing: valid, malformed, under 40 columns, absent.
267- e2e: every `ReadOnly` command in `readArgs`
268 (`e2e/readonly_test.go`) run twice through ssh: plain, asserting no
269 `\x1b` in stdout; with `GITBAY_TERM=60,color`, asserting no line
270 wider than 60 display cells after stripping ANSI, code blocks
271 excepted.
272- CLI: `SetEnv` sent only when stdout is a terminal; `NO_COLOR`,
273 `TERM=dumb` and `--no-color` drop `color`; pager selection order.
274
275## Delivery
276
277Five MRs, each shippable alone:
278
2791. Audit fixes.
2802. Transport, `Term`, `table`, and every list command.
2813. `termtext`, `view`, every show command, the pager.
2824. `Flags`/`Examples` for every command, help layouts, the generated
283 summary table, grouped root help.
2845. Users and Admin wiki pages, CHANGELOG.