Skip to content

Add man page generator for tagoio CLI#34

Open
bgelatti wants to merge 7 commits intomasterfrom
feat/cli-man-page
Open

Add man page generator for tagoio CLI#34
bgelatti wants to merge 7 commits intomasterfrom
feat/cli-man-page

Conversation

@bgelatti
Copy link
Copy Markdown
Collaborator

@bgelatti bgelatti commented May 5, 2026

Closes tago-io/project-sdk-and-tools#3

Problem

The tagoio CLI ships no man(1) page. Users have no offline reference (man tagoio returns "No manual entry"), and fish-shell users can't enable tab-completion since fish_update_completions works by scraping installed man pages.

Investigation

  • Commander already exposes the full command surface as structured metadata (program.commands, each with options, registeredArguments, description, and addHelpText blocks). That's exactly the data a man page needs — there's no missing source of truth, only a missing emitter.
  • A small audit against clig.dev man-page guidelines confirmed the conventional sections (NAME, SYNOPSIS, DESCRIPTION, GLOBAL OPTIONS, COMMANDS, EXIT STATUS, ENVIRONMENT, FILES, SEE ALSO, AUTHOR), the synopsis convention (<required> / [optional] / [args...]), and the practice of bolding flags / italicizing metavariables.
  • Existing libraries (marked-man, ronn, help2man, pandoc) all expect either hand-written Markdown or run-time parsing of --help text — both of which reintroduce the drift problem we're trying to solve. Closest commander-specific alternatives are unmaintained.

Solution

Three tightly-scoped commits on top of feat/cli-refactoring-deps:

  1. refactor(index): extract buildProgram for build-time tool reuse — pulls command registration out of initiateCMD into buildProgram(defaultEnv), returning a configured Command without parsing argv. Guarded the auto-run with an import.meta.url check so build tools can import this module without triggering program.parse(). Single source of truth for the command tree.

  2. feat(man): generate tagoio.1 from commander definitions — new src/lib/generate-man.ts walks buildProgram() and emits a roff page. Wired into the build chain (npm run man runs first), into prepublishOnly, and into package.json (man field + files array) so npm i -g symlinks it into <prefix>/share/man/man1/tagoio.1. A Vitest snapshot test acts as the CI drift gate; a companion mandoc/groff integration test catches roff syntax errors. The interactive prompt-driven flows (start-config, backup restore, export-setup) are excluded from the coverage threshold since they're covered by manual smokes.

  3. fix(man): correct FILES paths and add EXIT STATUS per clig.dev audit — verified the actual filesystem contracts in code (src/lib/token.ts, src/lib/dotenv-config.ts) and corrected the FILES section to list ./tagoconfig.json, ./.tago-lock.<env>.lock, and ./.tagoio/personal.env accurately. Added the EXIT STATUS section.

Why a hand-rolled walker (≈250 LoC) is the right choice for this CLI

  • The command tree IS the source of truth; the walker is a thin reflection of what commander already knows. Zero drift by construction.
  • All ready-made alternatives reintroduce a problem we don't have:
    • marked-man / ronn / pandoc require Markdown source — either hand-authored (drift) or auto-generated from commander first (which is just our walker with extra steps).
    • help2man parses --help heuristically and depends on GNU conventions; it's a system dependency and brittle on CI.
    • No maintained commander-to-man npm package exists in the modern ecosystem.
  • Snapshot testing pins the output byte-for-byte, so any commander-API surprise surfaces in CI immediately rather than at publish time.
  • Industry pattern matches: Go's gh and kubectl do exactly this with cobra; npm is the only big CLI using Markdown + marked-man, and they do so because their docs team writes prose by hand — not our use case.

If we ever need per-command man pages (tagoio-deploy.1, etc.) it'd be worth re-evaluating Markdown sources, but for a single umbrella page the walker wins on simplicity.

Test plan

  • npm run man produces a byte-stable man/tagoio.1 (verified by snapshot test)
  • npm test fails locally and in CI when a flag/command is added without regenerating the snapshot — drift gate
  • mandoc -Tutf8 man/tagoio.1 exits 0 (integration test)
  • npm run build regenerates the page automatically as a pre-step
  • npm pack --dry-run shows man/tagoio.1 (13.7kB) in the tarball
  • After npm i -g ./tago-io-cli-3.2.0.tgz, man tagoio renders the page on macOS
  • Fish users can run fish_update_completions and get tab-completion (zsh / bash do not — explicitly out of scope; documented in README)

bgelatti added 5 commits May 5, 2026 13:52
Pull the command-registration logic out of initiateCMD into a standalone
buildProgram(defaultEnv) that returns a configured Command without
parsing argv. The runtime CLI keeps the same behaviour while the new
function becomes the single source of truth for the man-page generator.
Guard the auto-run with an import.meta.url check so importing this
module from a build tool no longer triggers program.parse().
Add a build-time roff generator at src/lib/generate-man.ts that walks
buildProgram() and emits a single tagoio.1 page. Wire npm run man into
the build chain plus prepublishOnly, ship the file via package.json
files and man fields, gitignore the artifact, and document the install
path (man tagoio plus fish_update_completions for fish users) in the
README. A vitest snapshot test acts as the drift gate so CI fails when
a flag or command changes without a matching snapshot regeneration; a
companion mandoc/groff integration test catches roff syntax errors.
The interactive prompt-driven flows (start-config, backup restore,
export-setup) are excluded from the coverage threshold since they are
covered by manual smokes, not unit tests.
The FILES section claimed ~/.tago-lock but the runtime writes per-environment
locks at ./.tago-lock.<env>.lock (verified in src/lib/token.ts), and was
missing ./.tagoio/personal.env where set-env persists TAGOIO_DEFAULT. Add an
EXIT STATUS section documenting 0 (success) and 1 (any failure via
errorHandler) as clig.dev recommends. Snapshot regenerated to match.
CI's `npm ci` was failing because package.json declared
oxfmt@^0.47.0 / [email protected] but the committed lockfile still
resolved to ^0.46.0 / 1.61.0. Regenerated the lockfile so
transitive @oxfmt/* and @oxlint/* bindings align with the
declared versions.
Replaces the developer-oriented stdout/stderr/JSON breakdown with a
purpose-first summary: official CLI to TagoIO, manages the four
top-level resources, suitable for interactive and CI/CD use.
@bgelatti bgelatti changed the base branch from feat/cli-refactoring-deps to master May 7, 2026 13:08
Copy link
Copy Markdown
Member

@vitorfdl vitorfdl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header and Footer must be readable

bgelatti added 2 commits May 7, 2026 10:40
Static text bodies (description, exit-status, environment, files, see-also,
author) move out of the inline roff arrays in generate-man.ts and into a
dedicated MAN_CONTENT module written as plain English. The generator wraps
each value with escapeRoff() and emits the same structural roff. Editing prose
no longer requires counting backslashes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants