Commit 9a566ebe6f

9a566ebe6f64d544ebacdbb64ad2b4a8c8baaf53

parent: 1f2d5bc590

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-24 00:23 UTC

docs/specs: CLI output refresh design

Ref #254
docs/specs/2026-09-23-cli-output-refresh-design.md added +283
@@ -0,0 +1,283 @@
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`, `age`, `num`. The first
88`text` column is the flexible one.
89
90Plain (`Term.Cols == 0`): one row per item, cells joined by tabs, no
91header, `age` as RFC3339 to the second in UTC (`2026-09-23T23:26:00Z`).
92This is the current shape; only the timestamp format changes.
93
94Terminal:
95
96- Header row in capitals, dim when `Color`.
97- Columns padded with two spaces between them.
98- Width: when a row exceeds `Cols`, the flexible column is cut with
99 `…`, down to 8 cells. If that is not enough, the other `text`
100 columns are cut from the right. `ref`, `state`, `age` and `num`
101 are never cut. Widths are measured in display cells
102 (`golang.org/x/text/width`), not bytes.
103- `age` is relative: `just now` under a minute, then `5m ago`,
104 `2h ago`, `3d ago` under 14 days, `3w ago` under 8 weeks, then
105 `2006-01-02`.
106- `state` colour, ANSI 16-colour so the terminal's theme sets the
107 shade, matching the web's state tokens: green (`--ok`) for `open`,
108 `success`, `approved`; magenta (`--done`) for `merged`; red (`--bad`)
109 for `failed`, `error`, `changes requested`; dim (`--neutral`) for
110 `closed`, `draft`, `pending`, `canceled`. Anything else is
111 uncoloured.
112
113An empty table prints `nothing to list` on stderr, as `emit` does now.
114
115`emitPage` in terminal mode prints the next cursor on stderr as
116`more: gitbay <path> <args> --cursor <c>`. Plain mode keeps the
117`next\t<c>` row.
118
119Every list command moves to `table`: each `emit`/`emitPage` whose
120plain formatter prints one row per item. Mutations and single-value
121reads keep their one line.
122
123## Show views
124
125`internal/termtext` renders markdown (goldmark) and org (go-org) syntax
126trees to terminal text at a given width:
127
128- Paragraphs wrapped at `Cols - 2`, indented two spaces.
129- Headings bold; list items with `•` or their number, continuation
130 lines hanging.
131- Code blocks indented four spaces, not wrapped, highlighted with
132 chroma's `terminal16` formatter when `Color`.
133- Emphasis bold or underlined when `Color`, plain text otherwise.
134- Links as their text, followed by ` (<url>)` when the URL differs
135 from the text. Relative forge links are made absolute from
136 `server.site_url`.
137- Images as `[image: <alt>]`.
138
139The same package has a plain mode: no ANSI, no wrapping, links
140reduced as above.
141
142A `view` helper in `internal/control` lays out every `show`:
143
144```
145#253 Recorded CLI walkthrough on the landing page closed
146
147 author cmc, 2026-09-22 18:04 UTC
148 closed 2026-09-23 23:26 UTC by cmc in fc2380f3f0
149 labels docs, web
150 milestone v1.35.0
151 url https://gitbay.org/krz/gitbay/issues/253
152
153 <rendered body>
154
155 · cmc referenced this in 6c4d1e1454 2026-09-23 23:26 UTC
156 · cmc closed this in fc2380f3f0 2026-09-23 23:26 UTC
157
158── cmc, 2026-09-23 23:40 UTC ──────────────────────────────────────
159 <rendered comment>
160```
161
162Title bold and state coloured when `Color`; events dim. Timestamps in
163views use the web's format, `2006-01-02 15:04 UTC`. Plain mode prints
164the same lines without colour or wrapping, with RFC3339 timestamps.
165Commands: `issue show`, `mr show`, `build show`, `release show`,
166`snippet show`, `repo show`, `milestone show`, `org show`,
167`profile show`, and any other `show` whose output has a body or more
168than one field.
169
170### Pager
171
172When stdout is a terminal, the CLI runs `show`, `diff` and `log`
173verbs through a pager: `$GITBAY_PAGER`, else `$PAGER`, else
174`less -FRX`. `-F` exits when the output fits one screen. An empty
175`GITBAY_PAGER` disables it. `build log --follow` is never paged. The
176pager's exit does not change the command's exit code.
177
178## Help
179
180`Command` gains:
181
182```go
183type Flag struct {
184 Name string // "--state"
185 Arg string // "open|closed|all", empty for a switch
186 Desc string // "which issues"
187 Default string // "open", empty for none
188}
189
190Flags []Flag
191Examples []string // full argv after the program, owner/name explicit
192```
193
194`help --json` adds `flags` and `examples` to each entry.
195
196Server `help` with a prefix, in terminal mode:
197
198- Noun (a prefix matching more than one command): the noun's summary,
199 `USAGE gitbay <noun> <verb> [<owner/name>] ...`, then `READ` and
200 `WRITE` sections from `ReadOnly`, one line per verb with its
201 summary, then `gitbay <noun> <verb> --help for flags.`
202- Verb (a prefix matching one command): summary, `USAGE`, `FLAGS`
203 one per line with description and `(default …)`, `--json` last,
204 then `EXAMPLES`.
205
206Examples print with a `gitbay ` prefix in terminal mode and
207`ssh <ssh host> ` in plain mode; both are valid because each example
208names its repository.
209
210Plain mode without a prefix stays one line per command. With a prefix,
211plain mode prints the same sections as terminal mode without colour.
212
213The CLI's noun and verb `--help` already go to the server. `pass()`
214drops its short text; the cobra command takes its one-line summary
215from a table generated from the registry (`go generate`, checked by a
216test that the file is current), so root and completion text never
217drift.
218
219Root help stays local to the CLI and is grouped:
220
221```
222WORK issue mr build release milestone label search
223REPOSITORIES repo wiki status webhook init
224YOU dashboard feed notifications auth profile snippet web
225INSTANCE org explore register migrate remote admin audit
226```
227
228one noun per line with its summary. A test fails when a top-level
229command is in no section or two.
230
231Registry test: every `--flag` token in `Usage` has a `Flags` entry
232and every `Flags` entry appears in `Usage`; every command has a
233non-empty `Desc` per flag and at least one example; every example
234resolves through `Lookup` to its own command.
235
236## Audit fixes
237
238- `release list` takes `--limit`/`--cursor`; the title column is
239 empty when the title equals the tag.
240- `notifications device add` prints `registered device <n>`.
241- `build log --follow` giving up on a queued build prints
242 `build <n> still queued after 10m; run build log again to keep
243 watching` and exits 1, unchanged.
244- Every missing positional goes through `usageWith` (`snippet show`,
245 `org label list`, and any other found by the test below).
246- `dashboard` prints `none` under an empty section.
247- The stale `build list` help goes with the `pass()` short text.
248
249## Rules
250
251The Users wiki "Output rules" gains an "At a terminal" section:
252headers, colour, width, relative ages, the pager, `GITBAY_TERM`,
253`NO_COLOR`. The piped rules stand, with timestamps stated as RFC3339
254to the second.
255
256## Testing
257
258- `table`: plain rows byte-identical to the current formatter for a
259 fixture per cell type; terminal golden files at 60 and 120 columns,
260 with and without colour; wide-character titles.
261- `termtext`: golden files for markdown and org fixtures covering
262 every node type above, at 60 columns, plain and colour.
263- `Term` parsing: valid, malformed, under 40 columns, absent.
264- e2e: every `ReadOnly` command in `readArgs`
265 (`e2e/readonly_test.go`) run twice through ssh: plain, asserting no
266 `\x1b` in stdout; with `GITBAY_TERM=60,color`, asserting no line
267 wider than 60 display cells after stripping ANSI, code blocks
268 excepted.
269- e2e: a missing positional on every command exits 2 with the
270 registered usage on stderr.
271- CLI: `SetEnv` sent only when stdout is a terminal; `NO_COLOR`,
272 `TERM=dumb` and `--no-color` drop `color`; pager selection order.
273
274## Delivery
275
276Five MRs, each shippable alone:
277
2781. Audit fixes.
2792. Transport, `Term`, `table`, and every list command.
2803. `termtext`, `view`, every show command, the pager.
2814. `Flags`/`Examples` for every command, help layouts, the generated
282 summary table, grouped root help.
2835. Users and Admin wiki pages, CHANGELOG.