docs/specs/2026-09-23-cli-output-refresh-design.md
284 lines · 10836 bytes
12 symbols in this file
CLI output refresh
Ref #254. Terminal rendering for tables, show views and help, with
piped output unchanged in shape, plus the fixes from the 2026-09-23 CLI
audit.
Problem
The plain output has had no design pass since v1.22.0.
- Tables have no header, no colour and no width limit. The CLI pads
tabs with
tabwriterfor the verbs inlistVerbs(cmd/gitbay/ssh.go) and nothing else. showviews print stored markup verbatim: event lines readreferenced in commit [6c4d1e1454](/krz/gitbay/commit/…) by [cmc](/cmc), and timestamps are whatever string the row holds (2026-09-23T23:26:00.570Z). The web prints2026-09-23 23:26 UTC.- Root help carries usage in the summary column. Noun help is two lines per verb with the full usage. Verb help is the same two lines; no flag has a description, and no command has an example.
pass()incmd/gitbay/main.goholds a second copy of each usage string, which drifts (build liststill readsrecent builds: <owner/name>).
Decision
The server renders. The CLI tells it the terminal's width and whether colour is wanted; the command's plain formatter chooses between terminal and plain rendering. The CLI adds a pager for long views and the grouped root help, and nothing else.
Rejected:
- The CLI rendering from
--json. A second renderer per command incmd/gitbay, which drifts from the server's, and stock ssh never sees the result. - A structured view format on the wire (typed columns, a document tree) styled by the CLI. A new protocol between two programs that ship together, for no visible gain over rendering on the server.
Transport
The CLI passes -o SetEnv=GITBAY_TERM=<cols>[,color] when stdout is a
terminal. color is left out when NO_COLOR is set and non-empty,
TERM is dumb, or --no-color is given (stripped by the CLI before
dispatch). Piped stdout sends nothing.
sshd accepts an env request named GITBAY_TERM and ignores every
other name, as today. runExec parses the value into Ctx.Term:
type Term struct {
Cols int // 0: plain output
Color bool
}
A missing or malformed value, or Cols under 40, is the zero value.
The HTTP surfaces never set it. Under the system-sshd forced command
(gitbayd shell) the variable arrives only if the operator adds
AcceptEnv GITBAY_TERM; without it output is plain. The Admin wiki
page says so.
SetEnv needs OpenSSH 7.8. The CLI shares one connection through
ControlMaster; the first task verifies that a multiplexed session
carries SetEnv. If it does not, the CLI sends a leading
--term=<cols>[,color] argument instead, and Dispatch strips it
wherever it appears, as it strips --json.
Stock ssh users opt in with ssh -o SetEnv=GITBAY_TERM=120,color.
alignColumns, listVerbs and the tabwriter in runSSH are
removed.
Tables
internal/control/table.go:
t := c.table("#", "STATE", "TITLE", "AUTHOR", "UPDATED")
for _, is := range issues {
t.row(ref("#", is.Number), state(is.State), text(is.Title), text(is.Author), age(is.UpdatedAt))
}
t.flush()
Cells are typed: ref, state, text, flex, age, num. The
flex column (a title or description, one per table) is the one that
shrinks first.
Plain (Term.Cols == 0): one row per item, cells joined by tabs, no
header, age as RFC3339 to the second in UTC (2026-09-23T23:26:00Z).
This is the current shape; only the timestamp format changes.
Terminal:
- Header row in capitals, dim when
Color. - Columns padded with two spaces between them.
- Width: when a row exceeds
Cols, the flexible column is cut with…, down to 8 cells. If that is not enough, the othertextcolumns are cut from the right.ref,state,ageandnumare never cut. Widths are measured in display cells (golang.org/x/text/width), not bytes. ageis relative:just nowunder a minute, then5m ago,2h ago,3d agounder 14 days,3w agounder 8 weeks, then2006-01-02.statecolour, ANSI 16-colour so the terminal's theme sets the shade, matching the web's state tokens: green (--ok) foropen,success,approved; magenta (--done) formerged; red (--bad) forfailed,error,changes requested; dim (--neutral) forclosed,draft,pending,canceled. Anything else is uncoloured.
An empty table prints nothing to list on stderr, as emit does now.
emitPage in terminal mode prints the next cursor on stderr as
more: gitbay <path> <args> --cursor <c>. Plain mode keeps the
next\t<c> row.
Every list command moves to table: each emit/emitPage whose
plain formatter prints one row per item. Mutations and single-value
reads keep their one line.
Show views
internal/termtext renders markdown (goldmark) and org (go-org) syntax
trees to terminal text at a given width:
- Paragraphs wrapped at
Cols - 2, indented two spaces. - Headings bold; list items with
•or their number, continuation lines hanging. - Code blocks indented four spaces, not wrapped, highlighted with
chroma's
terminal16formatter whenColor. - Emphasis bold or underlined when
Color, plain text otherwise. - Links as their text, followed by
(<url>)when the URL differs from the text. Relative forge links are made absolute fromserver.site_url. - Images as
[image: <alt>].
The same package has a plain mode: no ANSI, no wrapping, links reduced as above.
A view helper in internal/control lays out every show:
#253 Recorded CLI walkthrough on the landing page closed
author cmc, 2026-09-22 18:04 UTC
closed 2026-09-23 23:26 UTC by cmc in fc2380f3f0
labels docs, web
milestone v1.35.0
url https://gitbay.org/krz/gitbay/issues/253
<rendered body>
· cmc referenced this in 6c4d1e1454 2026-09-23 23:26 UTC
· cmc closed this in fc2380f3f0 2026-09-23 23:26 UTC
── cmc, 2026-09-23 23:40 UTC ──────────────────────────────────────
<rendered comment>
Title bold and state coloured when Color; events dim. Timestamps in
views use the web's format, 2006-01-02 15:04 UTC. Plain mode prints
the same lines without colour or wrapping, with RFC3339 timestamps.
Commands: issue show, mr show, build show, release show,
snippet show, repo show, milestone show, org show,
profile show, and any other show whose output has a body or more
than one field.
Pager
When stdout is a terminal, the CLI runs show, diff and log
verbs through a pager: $GITBAY_PAGER, else $PAGER, else
less -FRX. -F exits when the output fits one screen. An empty
GITBAY_PAGER disables it. build log --follow is never paged. The
pager's exit does not change the command's exit code.
Help
Command gains:
type Flag struct {
Name string // "--state"
Arg string // "open|closed|all", empty for a switch
Desc string // "which issues"
Default string // "open", empty for none
}
Flags []Flag
Examples []string // full argv after the program, owner/name explicit
help --json adds flags and examples to each entry.
Server help with a prefix, in terminal mode:
- Noun (a prefix matching more than one command): the noun's summary,
USAGE gitbay <noun> <verb> [<owner/name>] ..., thenREADandWRITEsections fromReadOnly, one line per verb with its summary, thengitbay <noun> <verb> --help for flags. - Verb (a prefix matching one command): summary,
USAGE,FLAGSone per line with description and(default …),--jsonlast, thenEXAMPLES.
Examples print with a gitbay prefix in terminal mode and
ssh <ssh host> in plain mode; both are valid because each example
names its repository.
Plain mode without a prefix stays one line per command. With a prefix, plain mode prints the same sections as terminal mode without colour.
The CLI's noun and verb --help already go to the server. pass()
drops its short text; the cobra command takes its one-line summary
from a table generated from the registry (go generate, checked by a
test that the file is current), so root and completion text never
drift.
Root help stays local to the CLI and is grouped:
WORK issue mr build release milestone label search
REPOSITORIES repo wiki status webhook init
YOU dashboard feed notifications auth profile snippet web
INSTANCE org explore register migrate remote admin audit
one noun per line with its summary. A test fails when a top-level command is in no section or two.
Registry test: every --flag token in Usage has a Flags entry
and every Flags entry appears in Usage; every command has a
non-empty Desc per flag and at least one example; every example
resolves through Lookup to its own command.
Audit fixes
release listtakes--limit/--cursor; the title column is empty when the title equals the tag.notifications device addprintsregistered device <n>.dashboardprintsnoneunder an empty section.
Two audit findings need no change. build log --follow already
says what to do when it gives up on a queued build
(buildfollow.go). A missing positional through c.usage() already
prints the registered usage and exits 2, which is the rule; converting
198 call sites to usageWith for an extra line is not worth the
churn.
- The stale
build listhelp goes with thepass()short text.
Rules
The Users wiki "Output rules" gains an "At a terminal" section:
headers, colour, width, relative ages, the pager, GITBAY_TERM,
NO_COLOR. The piped rules stand, with timestamps stated as RFC3339
to the second.
Testing
table: plain rows byte-identical to the current formatter for a fixture per cell type; terminal golden files at 60 and 120 columns, with and without colour; wide-character titles.termtext: golden files for markdown and org fixtures covering every node type above, at 60 columns, plain and colour.Termparsing: valid, malformed, under 40 columns, absent.- e2e: every
ReadOnlycommand inreadArgs(e2e/readonly_test.go) run twice through ssh: plain, asserting no\x1bin stdout; withGITBAY_TERM=60,color, asserting no line wider than 60 display cells after stripping ANSI, code blocks excepted. - CLI:
SetEnvsent only when stdout is a terminal;NO_COLOR,TERM=dumband--no-colordropcolor; pager selection order.
Delivery
Five MRs, each shippable alone:
- Audit fixes.
- Transport,
Term,table, and every list command. termtext,view, every show command, the pager.Flags/Examplesfor every command, help layouts, the generated summary table, grouped root help.- Users and Admin wiki pages, CHANGELOG.