Skip to content

feat(cli): add dub completion <shell> and dub man#107

Merged
dubscode merged 4 commits into
mainfrom
feature/dub-67-shell-completion-man-pages
May 25, 2026
Merged

feat(cli): add dub completion <shell> and dub man#107
dubscode merged 4 commits into
mainfrom
feature/dub-67-shell-completion-man-pages

Conversation

@dubscode

Copy link
Copy Markdown
Contributor

TL;DR

Adds two new CLI commands — dub completion <shell> and dub man — that emit shell-completion scripts and a roff-formatted man page to stdout, generated by introspecting the commander.js program tree. Homebrew formula now bundles the man page and per-shell completions automatically.

Why

DUB-67 is in the Tier 7 polish project: hand the CLI the same affordances every mature tool gives users — Tab-completion and man dub.

Manually-maintained completion tables drift the moment any subcommand or flag changes; generating from commander metadata keeps completions in sync with the binary's own --help.

Before

  • No way to Tab-complete dub subcommands, flags, or local branches; users either typed full commands or relied on history.
  • No man page — man dub returned 'No manual entry'.

After

  • dub completion bash|zsh|fish writes a working completion script that covers subcommands, per-command flags from commander, branch names for co/up/down/delete/track/untrack, and branch-valued option flags like --parent/--branch.
  • dub man emits roff for the standard man dub experience; mandoc/groff render the page cleanly.
  • Homebrew formula installs the man page and all three shell completions at install time via generate_completions_from_executable.

File-by-file

packages/cli/src/lib/completion.ts

new +471 / -0

Bash/zsh/fish generators. describeProgram walks the commander tree once; each generator produces shell-specific syntax. Branch-arg subcommands and branch-valued flags are routed through __dub_branches (git for-each-ref). Empty branch/file patterns short-circuit so we never emit case "$x" in ).

const BRANCH_ARG_COMMANDS = [
  'checkout', 'co', 'up', 'down', 'delete', 'untrack', 'track',
] as const;

const BRANCH_VALUE_FLAGS = ['--parent', '--branch', '--before', '--after'];

packages/cli/src/lib/man.ts

new +109 / -0

Roff generator with escapeRoff to neutralize backslashes, leading dots, and hyphens. Applies escaping to the .TH title args too so pre-release versions like 1.0.0-beta.1 stay safe.

function escapeRoff(value: string): string {
  if (!value) return '';
  const collapsed = value.replace(/[\r\n]+/g, ' ').trim();
  const backslashEscaped = collapsed.replace(/\\/g, '\\\\');
  const hyphenEscaped = backslashEscaped.replace(/-/g, '\\-');
  if (hyphenEscaped.startsWith('.') || hyphenEscaped.startsWith("'")) {
    return `\\&${hyphenEscaped}`;
  }
  return hyphenEscaped;
}

packages/cli/src/commands/completion.ts

new +26 / -0

Thin dispatcher: pick a shell, throw DubError with actionable recovery hints when the shell is unknown.

packages/cli/src/commands/man.ts

new +9 / -0

Thin wrapper that forwards the live program plus its version to the roff generator.

packages/cli/src/index.ts

mod +32 / -0

Wires the two new commands. Both write to stdout via process.stdout.write so users can redirect into a file. New imports stay alphabetical in the existing import block.

program
  .command('completion')
  .argument('<shell>', 'Shell to generate completions for: bash, zsh, or fish')
  .action((shell: string) => {
    process.stdout.write(completion(program, shell));
  });

program
  .command('man')
  .action(() => {
    process.stdout.write(man(program, { version }));
  });

packages/cli/src/commands/completion.test.ts

new +158 / -0

13 tests covering error path, structural assertions per shell (commands, branches, flags, headers), and host-shell syntax validation that gracefully skips when the shell is missing.

packages/cli/src/commands/man.test.ts

new +123 / -0

8 tests: .TH header shape, escape semantics (hyphens, backslashes, pre-release versions), section presence, nested subcommands, and a mandoc/groff render check.

apps/docs/content/docs/guides/shell-integration.mdx

mod +44 / -0

Adds 'Shell completions' and 'Man page' sections to the existing shell-integration guide with per-shell install snippets.

homebrew/dubstack.rb

mod +7 / -0

Formula now generates dub.1 and per-shell completions from the just-installed binary, so packaged users get them with brew install dubstack. Adds a test do check that dub man emits a .TH header.

man1.mkpath
(man1/"dub.1").write Utils.safe_popen_read("#{bin}/dub", "man")
generate_completions_from_executable(bin/"dub", "completion")

README.md

mod +21 / -0

New dub completion <shell> / dub man section in the command reference linking to the docs guide.

Where to focus review

  1. Bash branch-name completion safety - packages/cli/src/lib/completion.ts:218-225: Branch names from git for-each-ref flow through __dub_compreply_lines, which sets a local IFS=newline before compgen -W so branch names with shell-special characters survive word-splitting. Worth confirming the IFS scope is local to the function.
  2. Roff escaping on user-supplied text - packages/cli/src/lib/man.ts:18-99: escapeRoff neutralizes backslashes, hyphens, and leading dots; it is now applied to the .TH date + version args too. Confirm hyphens-as-\- is desirable for arbitrary descriptions (it is — groff treats - as a minus and \- as the ASCII hyphen, which is what man expects in copy).
  3. Generator commands honor the live program tree - packages/cli/src/lib/completion.ts:83-91: Both generators take the live program and walk its commands/options. Future commands added to commander.js automatically appear in completions and the man page with no extra wiring; this is the explicit non-goal of avoiding hand-maintained lists.
  4. Homebrew formula side-effects at install time - homebrew/dubstack.rb:12-25: Utils.safe_popen_read runs the just-installed binary during the formula's install phase. Confirms the binary is on the path and doesn't depend on a non-existent state directory (the man command is read-only and doesn't touch .git/dubstack).

Test plan

  • unit: Completion generators (bash/zsh/fish) - packages/cli/src/commands/completion.test.ts — 13 tests covering error path, command/alias/flag presence, branch-arg routing, and host-shell syntax validation.
  • unit: Man page generator - packages/cli/src/commands/man.test.ts — 8 tests covering header escaping, sections, nested subcommands, backslash/hyphen escaping, and mandoc render.
  • manual: End-to-end CLI smoke: bash/zsh/mandoc - pnpm build then dub completion bash | bash -n, dub completion zsh | zsh -n, dub man | mandoc -Tutf8 — all OK. fish not installed locally; covered by the in-test parser.
  • build: tsup build of the CLI - pnpm build succeeded — dist/index.js 877.95 KB.

Quality gates

  • Lint + format: pnpm checks - passed (biome check . — 334 files checked, 0 errors.)
  • Typecheck: pnpm typecheck - passed (tsc --noEmit across all 3 workspace packages — clean.)
  • Tests: pnpm test - passed (vitest — 1466 tests pass across 131 test files (21 new tests for this issue).)

Self-QA

See QA fallback evidence.

Deterministic CLI proof: pnpm checks/typecheck/test all green; bash/zsh syntax-checked outputs; mandoc renders the man page; 21 new tests cover the generators directly.

  • dub completion bash | bash -n returns 0
  • dub completion zsh | zsh -n returns 0
  • dub completion fish | fish -n returns 0 (skipped locally — fish not installed; in-test coverage exercises the generator)
  • dub man | mandoc -Tutf8 renders DUB(1) with NAME/SYNOPSIS/DESCRIPTION/COMMANDS sections

Acceptance criteria

  • dub completion <shell> works for bash, zsh, fish - Three generators in packages/cli/src/lib/completion.ts; each output validates against its native parser in tests.
  • Branch-name completion in relevant commands - __dub_branches helper invokes git for-each-ref refs/heads/; gated on BRANCH_ARG_COMMANDS (co/checkout, up, down, delete, track, untrack) plus BRANCH_VALUE_FLAGS (--parent, --branch, --before, --after).
  • dub man emits valid roff - man.test.ts asserts .TH header, section structure, and renders the output through mandoc/groff when present.
  • Homebrew formula bundles man pages - homebrew/dubstack.rb writes dub.1 to man1/ and runs generate_completions_from_executable. New test do asserts dub man emits a .TH header.
  • Tests for each shell - completion.test.ts has describe blocks for bash, zsh, fish — 13 tests total. Each block syntax-checks its output via spawnSync against the native shell when available.
  • Docs at apps/docs/content/docs/guides/shell-integration.mdx - Added 'Shell completions' and 'Man page' sections to the existing shell-integration guide; README updated with a cross-link.

Adversarial review

Iterations: 1

Remaining critical/major: 0/0

Remaining minor/nitpick: 0/0

  • Critical: bash COMPREPLY word-splitting on branch names with shell-special characters — fixed by introducing __dub_compreply_lines which sets local IFS=newline before compgen.
  • Major: roff .TH title args (date + version) were not escaped — fixed by passing both through escapeRoff so pre-release versions like 1.0.0-beta.1 render correctly.
  • Major: bash flag list interpolated into double-quoted string — fixed by single-quoting the completions argument since flags from commander are always kebab-case and safe to single-quote.
  • Major: Homebrew sha256 placeholder — pre-existing in the formula on main; out of scope for DUB-67.

Dependencies

  • No external dependencies detected: n/a

Rollout

Pure additive: two new commands and a Homebrew formula tweak. Nothing existing changes behavior.

  • On merge - Ship in next dubstack release: tsup bundles the new commands into dist/index.js automatically. Users on the latest npm/Homebrew release pick up the commands once they upgrade.
  • Post-release - Update docs site: Fumadocs picks up the new shell-integration.mdx sections on next docs build/deploy.
  • Homebrew bump - Refresh formula sha256: When the next release tarball is published, update homebrew/dubstack.rb sha256 to point at the new tarball. The PLACEHOLDER string is pre-existing and unrelated to DUB-67.

Commit

feat(cli): add `dub completion <shell>` and `dub man`

Adds shell-completion script generation for bash, zsh, and fish, plus a
roff man-page generator, both driven by introspecting the live commander.js
program tree. Output streams to stdout for users to redirect into the
shell completion or MANPATH location of their choice.

Completion covers top-level subcommands, per-command flags (read from
commander metadata), local-branch completion for co/up/down/delete/
track/untrack via `git for-each-ref`, and file completion where commands
take file args. The Homebrew formula now bundles `dub.1` and installs
completions for all three shells via `generate_completions_from_executable`,
so packaged users get them without extra steps.

Docs updated in apps/docs/content/docs/guides/shell-integration.mdx and
README.md. 21 new tests cover bash/zsh/fish syntax (validated against the
host shell when present) and mandoc/groff rendering of the man page.

Completes DUB-67
Copilot AI review requested due to automatic review settings May 25, 2026 20:38
@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
dubstack Skipped Skipped May 25, 2026 9:06pm

Comment thread packages/cli/src/lib/completion.ts Fixed
Comment thread packages/cli/src/lib/completion.ts Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds generated shell completions and a generated roff man page to the DubStack CLI, driven by introspecting the commander program tree, plus docs/Homebrew integration so packaged installs include these assets.

Changes:

  • Introduces dub completion <shell> (bash/zsh/fish) and dub man commands that emit generated scripts/pages to stdout.
  • Adds completion + man generators (packages/cli/src/lib/completion.ts, packages/cli/src/lib/man.ts) and unit tests validating structure and optional host-tool parsing.
  • Updates documentation and Homebrew formula to install/generated man page and completions at install time.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
README.md Documents new dub completion / dub man usage.
packages/cli/src/lib/completion.ts Implements bash/zsh/fish completion generation from commander metadata.
packages/cli/src/lib/man.ts Implements roff man page generation from commander metadata.
packages/cli/src/commands/completion.ts Adds CLI dispatcher for shell selection + error handling.
packages/cli/src/commands/completion.test.ts Adds tests for completion outputs and optional shell syntax checks.
packages/cli/src/commands/man.ts Adds CLI wrapper for man page generation.
packages/cli/src/commands/man.test.ts Adds tests for man output structure and optional formatter rendering.
packages/cli/src/index.ts Wires completion and man commands into the CLI.
apps/docs/content/docs/guides/shell-integration.mdx Adds docs sections describing completions and man page installation.
homebrew/dubstack.rb Generates and installs man page + completions during brew install, and tests for .TH header.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/lib/completion.ts
Comment thread packages/cli/src/lib/completion.ts Outdated
Comment thread packages/cli/src/lib/completion.ts Outdated
Comment thread packages/cli/src/lib/completion.ts
Comment thread packages/cli/src/lib/completion.ts Outdated
Comment thread packages/cli/src/lib/man.ts Outdated
Comment thread packages/cli/src/lib/man.ts Outdated
Comment thread README.md Outdated
Comment thread apps/docs/content/docs/guides/shell-integration.mdx Outdated
Adversarial review round 2 found two correctness bugs in the branch-
completion router: dub revert (whose target positional is a PR number or
SHA) was routed to git-branch completion, and dub trunk returned local
branches instead of its subcommand list. Replace the regex with an
explicit allow-list (co, up, down, delete, track, untrack) and have zsh
prefer subcommand completion whenever a command has any. Add negative
tests covering both regressions.

Also: escapeFish now doubles backslashes; escapeZsh now also escapes
backticks and dollar signs; the zsh __dub_branches helper early-returns
on an empty repo so _describe does not show a spurious blank candidate;
main installs an EPIPE handler so dub man piped to head exits cleanly;
the Homebrew formula passes err: :merge so a broken dub man surfaces in
brew test instead of writing a partial man page; docs recommend a
user-writable zsh completions directory over the system fpath entry.

Completes DUB-67
Comment thread packages/cli/src/lib/completion.ts Fixed
…cape hardening

Addresses Copilot and CodeQL review feedback on PR #107:

Bash now walks the argv tokens to find the deepest matching command path,
so dub config ai-provider --<Tab> dispatches to ai-provider's flags
rather than config's. Zsh emits a second-level state machine for parent
commands with subcommands; fish gains a __dub_using_nested predicate.

Branch-valued option flags (--parent, --branch, --before, --after) and
file-valued flags now drive the right completer in every shell, not just
bash. Branch-name completion is now allow-list only: up and down were
removed (they take a numeric step count), and the broad regex over the
command description is gone so dub revert <target> no longer offers
branches for what is a PR number or SHA.

escapeZsh now escapes backslashes too (CodeQL js/incomplete-sanitization
on alerts 7+8). FILE_VALUE_FLAGS now lists the real --by-file flag
instead of a phantom --input-file. The man-page renderer recurses through
nested subcommands so dub config ai-provider and similar are documented
with their own options instead of just their names; the alias suffix now
keeps its leading space outside escapeRoff so labels read "checkout
(aliases: co)".

Tests: +7 covering nested paths, branch-valued zsh/fish routing, alias
spacing, and CodeQL backslash escape coverage. 35 completion + man tests
pass, full suite 1480/1480.

Completes DUB-67
CI Linux runs the man render check through groff, which emits SGR (ANSI)
escape sequences for bold/underline (\e[1m, \e[7m, \e[4m, ...). The reverse-
video sequence around "DUB" then "(1)" was breaking the substring assertion
that the rendered output contains "DUB(1)". macOS mandoc was untouched
because it uses backspace overstrike (N\bN) instead.

Strip ANSI CSI sequences alongside the existing backspace stripper. The
stripped output now contains "DUB(1)" on both formatters.

Completes DUB-67
@dubscode dubscode merged commit 5abe326 into main May 25, 2026
12 checks passed
@dubscode dubscode deleted the feature/dub-67-shell-completion-man-pages branch May 25, 2026 21:21
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.10.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants