docs/specs/2026-09-23-cli-output-refresh-design.md

3bcdce33fb9a2309312854331359d376171c7368
gitbay/docs/specs/2026-09-23-cli-output-refresh-design.md rendered · source · history · blame · raw

284 lines · 10836 bytes

  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.