Repository: git-town/git-town

Issue:
CLI help for `hack` doesn't show `--no-stash` option
This might not be fixable without updates or changes to the CLI framework we use for command-line flag parsing.

See https://github.com/git-town/git-town/pull/5779#discussion_r2557234759.

Hints from issue comments:
@kevgo: Cobra doesn't support defining a negated form of a boolean flag (e.g. `--no-stash`) for an existing flag (`--stash`). To work around that, we expose two separate flags: `--stash` and `--no-stash`.

Because those flags represent opposite sides of the same option, we hide the negated versions so they don't show up as standalone flags in the help output. If we wouldn't do that, users see both variants listed independently and separate from each other due to alphabetic sorting. Here is what `git town help hack` would print without hiding the negated flags:

```
Flags:
      --auto-resolve      auto-resolve phantom merge conflicts
  -b, --beam              beam some commits from this branch to the new branch
  -c, --commit            commit the stashed changes into the new branch
  -d, --detached          don't update the perennial root branch
      --dry-run           print but do not run the Git commands
  -h, --help              help for hack
  -m, --message string    the commit message
      --no-auto-resolve   don't auto-resolve
      --no-detached       don't detach
      --no-stash          don't stash
      --no-sync           don't sync
      --propose           propose the new branch
  -p, --prototype         create a prototype branch
      --stash             stash uncommitted changes when creating branches
      --sync              sync branches (default true)
  -v, --verbose           display all Git commands run under the hood
```

Would you prefer this output?@stephenwade: Yes, I would prefer that output. I think it's better that all the flags are discoverable even if they're not sorted ideally.

[kong](https://github.com/alecthomas/kong) might also be an option for argument parsing, since it supports negatable flags.@kevgo: Thanks for the input, I think you have convinced me 🙂  The parsing of the negated flags already works. The CLI help just doesn't print them.@kevgo: Implementing this also enabled (enforced) documenting the negated options properly on the website. Thanks again for the suggestion @stephenwade!

Solution patch:
diff --git a/Makefile b/Makefile
index 55d188007..04c280bff 100644
--- a/Makefile
+++ b/Makefile
@@ -210,6 +210,10 @@ unit: install  # runs only the unit tests for changed code
 
 unit-all: install  # runs all the unit tests
 	env GOGC=off go test -count=1 -shuffle=on -timeout=60s $(UNIT_TEST_DIRS)
+	make --no-print-directory unit-text-runner
+
+unit-text-runner: tools/rta@${RTA_VERSION} node_modules
+	@tools/rta npm run unit
 
 unit-race: install  # runs all the unit tests with race detector
 	env GOGC=off go test -count=1 -timeout 60s -race $(UNIT_TEST_DIRS)
diff --git a/contest.json b/contest.json
index 5f721aee4..cfed1e610 100644
--- a/contest.json
+++ b/contest.json
@@ -27,7 +27,7 @@
     {
       "type": "test-file",
       "files": "**/*.md",
-      "run": "tools/rta npm exec text-runner {{file}}"
+      "run": "make docs"
     },
 
     {
diff --git a/package.json b/package.json
index 8119d1893..cd6e536a0 100644
--- a/package.json
+++ b/package.json
@@ -4,5 +4,8 @@
     "gherkin-lint": "4.2.4",
     "text-runner": "7.4.0",
     "textrun-make": "0.6.0"
+  },
+  "scripts": {
+    "unit": "tools/rta node --test text-runner-tests/*.test.ts"
   }
 }
diff --git a/text-runner/command-summary.ts b/text-runner/command-summary.ts
new file mode 100644
index 000000000..56665a612
--- /dev/null
+++ b/text-runner/command-summary.ts
@@ -0,0 +1,87 @@
+import { deepEqual } from "node:assert/strict"
+import { exec } from "node:child_process"
+import { promisify } from "node:util"
+import * as textRunner from "text-runner"
+
+const execAsync = promisify(exec)
+
+export async function commandSummary(action: textRunner.actions.Args) {
+  const text = action.region.text()
+  const command = extractCommand(text)
+  action.name(`arguments for "${command}"`)
+  const documentedArgs = extractArgs(text)
+  const actualArgs = await commandArgs(command)
+  const documentedArgsStr = JSON.stringify(documentedArgs, null, 2)
+  const actualArgsStr = JSON.stringify(actualArgs, null, 2)
+  if (documentedArgsStr !== actualArgsStr) {
+    action.log(`DOCUMENTED:\n${documentedArgsStr}`)
+    action.log(`ACTUAL:\n${actualArgsStr}`)
+    deepEqual(documentedArgs, actualArgs)
+  }
+}
+
+export function extractCommand(text: string): string {
+  const match = text.match(/^git town ([^<[(]+?)(?:\s+-|\s+<|\s+\[|\s+\(|$)/)
+  return match?.[1]?.trim() ?? ""
+}
+
+export function extractArgs(text: string): string[][] {
+  const args: string[][] = []
+  // Match all optional arguments in square brackets: [-p | --prototype]
+  const matches = text.matchAll(/\[([^\]]+)\]/g)
+  for (const match of matches) {
+    const argText = match[1]
+    if (!argText.trim().startsWith("-")) {
+      // this element doesn't contain a flag (doesn't start with -)
+      continue
+    }
+    const normalizedArgText = argText.replace(/<.+?>/g, "string")
+    // Split by | to get the different variations of the flag
+    const variations = normalizedArgText.split("|").map((v) => v.trim())
+    args.push(variations)
+  }
+  return args.sort()
+}
+
+async function commandArgs(command: string): Promise<string[][]> {
+  const output = await commandHelp(command)
+  return parseCommandHelpOutput(output)
+}
+
+async function commandHelp(command: string): Promise<string> {
+  const result = await execAsync(`git town ${command} --help`)
+  return result.stdout
+}
+
+export function parseCommandHelpOutput(help: string): string[][] {
+  const result: string[][] = []
+  const lines = help.split("\n")
+  let inFlagsSection = false
+  for (const line of lines) {
+    if (line.includes("Flags:")) {
+      inFlagsSection = true
+      continue
+    }
+    if (!inFlagsSection) {
+      continue
+    }
+    // Stop if we hit an empty line (end of flags section)
+    if (line.trim() === "") {
+      break
+    }
+    // Parse flag line - format: "  -b, --beam             description"
+    // The description starts after 2 or more spaces
+    const match = line.match(/^\s+(.+?)\s{2,}/)
+    if (match) {
+      const flagsPart = match[1].trim()
+      const flags = flagsPart.split(/,\s+/).map((flag) => {
+        // Remove default value notation like [="all"]
+        return flag.replace(/\[="[^"]*"\]/, "")
+      })
+      if (flags.length > 0) {
+        result.push(flags)
+      }
+    }
+  }
+  return result.sort()
+}
diff --git a/tools/ensure_no_files_with_dashes.sh b/tools/ensure_no_files_with_dashes.sh
index f3ab90efb..0cf01a37a 100755
--- a/tools/ensure_no_files_with_dashes.sh
+++ b/tools/ensure_no_files_with_dashes.sh
@@ -14,6 +14,7 @@ files_with_dashes=$(find . -name '*-*' |
 	grep -v './dprint-changelog.json' |
 	grep -v './cucumber-sort.json' |
 	grep -v './.gherkin-*' |
+	grep -v './text-runner' |
 	grep -v 'tools(/|\\)rta')
 if [ -n "$files_with_dashes" ]; then
 	count=$(echo "$files_with_dashes" | wc -l)
diff --git a/website/src/commands/append.md b/website/src/commands/append.md
index ed2674737..d53ee9c7e 100644
--- a/website/src/commands/append.md
+++ b/website/src/commands/append.md
@@ -1,9 +1,13 @@
 # git town append
 
+<a type="command-summary">
+
 ```command-summary
-git town append <branch-name> [-p | --prototype] [-d | --detached] [-c | --commit] [-m | --message <message>] [--propose] [--dry-run] [-v | --verbose]
+git town append <branch-name> [--auto-resolve] [-b | --beam] [-p | --prototype] [-d | --detached] [-c | --commit] [-m | --message <message>] [--propose] [--push] [--stash] [--sync] [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _append_ command creates a new feature branch with the given name as a
 direct child of the current branch and brings over all uncommitted changes to
 the new branch.
diff --git a/website/src/commands/branch.md b/website/src/commands/branch.md
index 2c256d9a3..e0fc2b416 100644
--- a/website/src/commands/branch.md
+++ b/website/src/commands/branch.md
@@ -1,9 +1,13 @@
 # git town branch
 
+<a type="command-summary">
+
 ```command-summary
-git town branch [-d | --display-types] [-o | --order] [-v | --verbose]
+git town branch [-d | --display-types <branch-types>] [-o | --order <asc|desc>] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _branch_ command is Git Town's equivalent of the
 [git branch](https://git-scm.com/docs/git-branch) command. It displays the local
 branch hierarchy, and the types of all branches except for main and feature
diff --git a/website/src/commands/completions.md b/website/src/commands/completions.md
index 3815e2fab..bed10c63e 100644
--- a/website/src/commands/completions.md
+++ b/website/src/commands/completions.md
@@ -1,9 +1,13 @@
 # git town completions
 
+<a type="command-summary">
+
 ```command-summary
-git town completions (bash|fish|powershell|zsh) [--no-descriptions]
+git town completions (bash|fish|powershell|zsh) [--no-descriptions] [-h | --help]
 ```
 
+</a>
+
 The _completions_ command outputs shell scripts that enable auto-completion for
 Git Town in Bash, Fish, PowerShell, or Zsh. When set up, typing
 `git town <tab key>` in your terminal will auto-complete subcommands.
diff --git a/website/src/commands/compress.md b/website/src/commands/compress.md
index 6309996a3..398b84fdd 100644
--- a/website/src/commands/compress.md
+++ b/website/src/commands/compress.md
@@ -1,9 +1,13 @@
 # git town compress
 
+<a type="command-summary">
+
 ```command-summary
-git town compress [-m <text> | --message <text>] [-s | --stack] [--dry-run] [-v | --verbose]
+git town compress [-m | --message <text>] [--no-verify] [-s | --stack] [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _compress_ command squashes all commits on a branch into a single commit.
 
 Assuming you have a feature branch with these commits:
diff --git a/website/src/commands/config-get-parent.md b/website/src/commands/config-get-parent.md
index 14f2694e6..d80e3a0a0 100644
--- a/website/src/commands/config-get-parent.md
+++ b/website/src/commands/config-get-parent.md
@@ -1,9 +1,13 @@
 # git town config get-parent
 
+<a type="command-summary">
+
 ```command-summary
-git town config get-parent [<branch-name>] [-v | --verbose]
+git town config get-parent [<branch-name>] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _config get-parent_ command outputs the parent branch of the current or
 given branch.
 
diff --git a/website/src/commands/config-remove.md b/website/src/commands/config-remove.md
index 3957082f3..7c146b656 100644
--- a/website/src/commands/config-remove.md
+++ b/website/src/commands/config-remove.md
@@ -1,9 +1,13 @@
 # git town config remove
 
+<a type="command-summary">
+
 ```command-summary
-git town config remove [-v | --verbose]
+git town config remove [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _config remove_ command removes all Git Town related configuration from the
 current Git repository.
 
diff --git a/website/src/commands/config.md b/website/src/commands/config.md
index 2098ed36d..5ec20e6bb 100644
--- a/website/src/commands/config.md
+++ b/website/src/commands/config.md
@@ -1,12 +1,13 @@
 # git town config
 
+<a type="command-summary">
+
 ```command-summary
-git town config [-v | --verbose]
-git town config get-parent [<branch-name>] [-v | --verbose]
-git town config remove [-v | --verbose]
-git town init [-v | --verbose]
+git town config [-d | --display-types <type>] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _config_ command displays and updates the local Git Town configuration.
 
 ## Subcommands
diff --git a/website/src/commands/continue.md b/website/src/commands/continue.md
index 48ff56fda..aeefc15fa 100644
--- a/website/src/commands/continue.md
+++ b/website/src/commands/continue.md
@@ -1,9 +1,13 @@
 # git town continue
 
+<a type="command-summary">
+
 ```command-summary
-git town continue [-v | --verbose]
+git town continue [-h | --help] [-v | --verbose]
 ```
 
+</a>
+
 When a Git Town command encounters a problem that it cannot resolve, for example
 a merge conflict, it stops to give the user an opportunity to resolve the issue.
 Once you have resolved the issue, run the _continue_ command to tell Git Town to
diff --git a/website/src/commands/contribute.md b/website/src/commands/contribute.md
index de98eb7ff..49c4de85c 100644
--- a/website/src/commands/contribute.md
+++ b/website/src/commands/contribute.md
@@ -1,9 +1,13 @@
 # git town contribute
 
+<a type="command-summary">
+
 ```command-summary
-git town contribute [<branch-name>...] [-v | --verbose]
+git town contribute [<branch-name>...] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _contribute_ command makes some of your branches
 [contribution](../branch-types.md#contribution-branches) branches.
 
diff --git a/website/src/commands/delete.md b/website/src/commands/delete.md
index 108a12735..242f967e0 100644
--- a/website/src/commands/delete.md
+++ b/website/src/commands/delete.md
@@ -1,9 +1,13 @@
 # git town delete
 
+<a type="command-summary">
+
 ```command-summary
-git town delete [<branch-name>...] [-v | --verbose]
+git town delete [<branch-name>...] [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _delete_ command deletes the given branch from the local and if possible the
 remote repository, removes commits of deleted branches from their descendents
 (unless when using the
diff --git a/website/src/commands/detach.md b/website/src/commands/detach.md
index b1b521cf8..43835fdc3 100644
--- a/website/src/commands/detach.md
+++ b/website/src/commands/detach.md
@@ -1,9 +1,13 @@
 # git town detach
 
+<a type="command-summary">
+
 ```command-summary
-git town detach [--dry-run] [-v | --verbose]
+git town detach [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _detach_ command removes the current branch from the stack it is in and
 makes it a stand-alone top-level branch that ships directly into your main
 branch.
diff --git a/website/src/commands/diff-parent.md b/website/src/commands/diff-parent.md
index 282889afe..9e322430e 100644
--- a/website/src/commands/diff-parent.md
+++ b/website/src/commands/diff-parent.md
@@ -1,9 +1,13 @@
 # git town diff-parent
 
+<a type="command-summary">
+
 ```command-summary
-git town diff-parent [-v | --verbose]
+git town diff-parent [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _diff-parent_ command displays the changes made on a feature branch, i.e.
 the diff between the current branch and its parent branch.
 
diff --git a/website/src/commands/down.md b/website/src/commands/down.md
index dbd3de609..d68d26111 100644
--- a/website/src/commands/down.md
+++ b/website/src/commands/down.md
@@ -1,9 +1,13 @@
 # git town down
 
+<a type="command-summary">
+
 ```command-summary
-git town down [-m | --merge] [-v | --verbose]
+git town down [-m | --merge] [-d | --display-types <type>] [-o | --order <asc|desc>] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _down_ command moves one position down in the current stack by switching to
 the parent of the current branch. After successfully switching branches, it
 displays the branch hierarchy to show your new position in the stack.
diff --git a/website/src/commands/feature.md b/website/src/commands/feature.md
index d0e60da13..4b877ce01 100644
--- a/website/src/commands/feature.md
+++ b/website/src/commands/feature.md
@@ -1,9 +1,13 @@
 # git town feature
 
+<a type="command-summary">
+
 ```command-summary
-git town feature [<branch-name>...] [-v | --verbose]
+git town feature [<branch-name>...] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _feature_ command makes some of your branches
 [feature](../branch-types.md#feature-branches) branches.
 
diff --git a/website/src/commands/hack.md b/website/src/commands/hack.md
index b1b348638..a9da758e5 100644
--- a/website/src/commands/hack.md
+++ b/website/src/commands/hack.md
@@ -1,9 +1,13 @@
 # git town hack
 
+<a type="command-summary">
+
 ```command-summary
-git town hack [<branch-name>...] [-b | --beam] [-p | --prototype] [-d | --detached] [-c | --commit] [-m | --message <message>] [--propose] [--stash | --no-stash] [--sync] [--auto-resolve] [--dry-run] [-v | --verbose]
+git town hack [<branch-name>...] [-b | --beam] [-p | --prototype] [-d | --detached] [-c | --commit] [-m | --message <message>] [--propose] [--stash] [--sync] [--auto-resolve] [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _hack_ command ("let's start hacking") creates a new feature branch with the
 given name off the [main branch](../preferences/main-branch.md) and brings all
 uncommitted changes over to it.
diff --git a/website/src/commands/help.md b/website/src/commands/help.md
index 8ebf6ecf2..6adfcf769 100644
--- a/website/src/commands/help.md
+++ b/website/src/commands/help.md
@@ -1,7 +1,11 @@
 # git town help
 
+<a type="command-summary">
+
 ```command-summary
-git town help
+git town help [-h | --help]
 ```
 
+</a>
+
 The _help_ command displays detailed information about each Git Town command.
diff --git a/website/src/commands/init.md b/website/src/commands/init.md
index 52fe134f3..25171432a 100644
--- a/website/src/commands/init.md
+++ b/website/src/commands/init.md
@@ -1,9 +1,13 @@
 # git town init
 
+<a type="command-summary">
+
 ```command-summary
-git town init [-v | --verbose]
+git town init [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _init_ command launches Git Town's setup assistant. The setup assistant
 walks you through all configuration options for Git Town and gives you a chance
 to adjust them.
diff --git a/website/src/commands/merge.md b/website/src/commands/merge.md
index 93c514a20..7f2306378 100644
--- a/website/src/commands/merge.md
+++ b/website/src/commands/merge.md
@@ -1,9 +1,13 @@
 # git town merge
 
+<a type="command-summary">
+
 ```command-summary
-git town merge [--dry-run] [-v | --verbose]
+git town merge [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _merge_ command merges the current branch into the branch ahead of it in the
 current stack.
 
diff --git a/website/src/commands/observe.md b/website/src/commands/observe.md
index 030a0c9e9..7487414cb 100644
--- a/website/src/commands/observe.md
+++ b/website/src/commands/observe.md
@@ -1,9 +1,13 @@
 # git town observe
 
+<a type="command-summary">
+
 ```command-summary
-git town observe [<branch-name>...] [-v | --verbose]
+git town observe [<branch-name>...] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _observe_ command makes some of your branches
 [observed](../branch-types.md#observed-branches) branches.
 
diff --git a/website/src/commands/offline.md b/website/src/commands/offline.md
index dc0bb82d5..dd98955d2 100644
--- a/website/src/commands/offline.md
+++ b/website/src/commands/offline.md
@@ -1,9 +1,13 @@
 # git town offline
 
+<a type="command-summary">
+
 ```command-summary
-git town offline [<status>] [-v | --verbose]
+git town offline [<status>] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _offline_ configuration command displays or changes Git Town's offline mode.
 Git Town skips all network operations in offline mode.
 
diff --git a/website/src/commands/park.md b/website/src/commands/park.md
index c2710f3f0..816e68f23 100644
--- a/website/src/commands/park.md
+++ b/website/src/commands/park.md
@@ -1,9 +1,13 @@
 # git town park
 
+<a type="command-summary">
+
 ```command-summary
-git town park [<branch-name>...] [-v | --verbose]
+git town park [<branch-name>...] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _park_ command [parks](../branch-types.md#parked-branches) some of your
 branches.
 
diff --git a/website/src/commands/prepend.md b/website/src/commands/prepend.md
index 9f4f1b174..648a0d723 100644
--- a/website/src/commands/prepend.md
+++ b/website/src/commands/prepend.md
@@ -1,9 +1,13 @@
 # git town prepend
 
+<a type="command-summary">
+
 ```command-summary
-git town prepend [<branch-name>...] [-b | --beam] [--body <string>] [--propose] [-p | --prototype] [-t <text> | --title <text>] [-d | --detached] [-c | --commit] [-m | --message <message>] [--dry-run] [-v | --verbose]
+git town prepend [<branch-name>...] [--auto-resolve] [-b | --beam] [--body <string>] [--propose] [-p | --prototype] [--push] [--stash] [-t | --title <text>] [-d | --detached] [-c | --commit] [-m | --message <message>] [--sync] [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _prepend_ command creates a new feature branch as the parent of the current
 branch. It does that by inserting the new feature branch between the current
 feature branch and it's existing parent.
diff --git a/website/src/commands/propose.md b/website/src/commands/propose.md
index f55c01fc0..922566b8b 100644
--- a/website/src/commands/propose.md
+++ b/website/src/commands/propose.md
@@ -1,9 +1,13 @@
 # git town propose
 
+<a type="command-summary">
+
 ```command-summary
-git town propose [-b <text> | --body <text>] [-f <path> | --body-file <path>] [-t <text> | --title <text>] [-s | --stack] [--auto-resolve] [--dry-run] [-v | --verbose]
+git town propose [-b | --body <text>] [-f | --body-file <path>] [-t | --title <text>] [-s | --stack] [--auto-resolve] [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _propose_ command helps create a new pull request (also known as merge
 request) for the current feature branch. It opens your forge's website to create
 a new proposal in your browser and pre-populates information like branch and
diff --git a/website/src/commands/prototype.md b/website/src/commands/prototype.md
index fb3077316..f575d2258 100644
--- a/website/src/commands/prototype.md
+++ b/website/src/commands/prototype.md
@@ -1,9 +1,13 @@
 # git town prototype
 
+<a type="command-summary">
+
 ```command-summary
-git town prototype [<branch-name>...] [-v | --verbose]
+git town prototype [<branch-name>...] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _prototype_ command marks some of your branches as
 [prototype branches](../branch-types.md#prototype-branches).
 
diff --git a/website/src/commands/rename.md b/website/src/commands/rename.md
index 895a00256..da27982d7 100644
--- a/website/src/commands/rename.md
+++ b/website/src/commands/rename.md
@@ -1,9 +1,13 @@
 # git town rename
 
+<a type="command-summary">
+
 ```command-summary
-git town rename [<old-name>] <new-name> [-f | --force] [--dry-run] [-v | --verbose]
+git town rename [<old-name>] <new-name> [-f | --force] [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _rename_ command renames the current branch and its tracking branch. The
 branch to rename must be fully [synced](sync.md). Updates all affected
 [proposals](propose.md).
diff --git a/website/src/commands/repo.md b/website/src/commands/repo.md
index c2a8a07c3..e6248f974 100644
--- a/website/src/commands/repo.md
+++ b/website/src/commands/repo.md
@@ -1,9 +1,13 @@
 # git town repo
 
+<a type="command-summary">
+
 ```command-summary
-git town repo [<remote-name>] [-v | --verbose]
+git town repo [<remote-name>] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _repo_ command ("show me the repository") opens the homepage of the current
 repository in your browser. Git Town can display repositories hosted on
 [GitHub](https://github.com), [GitLab](https://gitlab.com),
diff --git a/website/src/commands/runlog.md b/website/src/commands/runlog.md
index 3707e197a..832a9093b 100644
--- a/website/src/commands/runlog.md
+++ b/website/src/commands/runlog.md
@@ -1,9 +1,13 @@
 # git town runlog
 
+<a type="command-summary">
+
 ```command-summary
-git town runlog [-v | --verbose]
+git town runlog [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 Git Town records the SHA of all local and remote branches before and after each
 command runs into an immutable, append-only log file called the _runlog_.
 
diff --git a/website/src/commands/set-parent.md b/website/src/commands/set-parent.md
index 15ba1b0cb..465f0b1f2 100644
--- a/website/src/commands/set-parent.md
+++ b/website/src/commands/set-parent.md
@@ -1,9 +1,13 @@
 # git town set-parent
 
+<a type="command-summary">
+
 ```command-summary
-git town set-parent [<branch>] [-v | --verbose]
+git town set-parent [<branch>] [--auto-resolve] [--none] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _set-parent_ command moves a branch and all its children below another
 branch. Consider this stack:
 
diff --git a/website/src/commands/ship.md b/website/src/commands/ship.md
index e095d586e..015762fbc 100644
--- a/website/src/commands/ship.md
+++ b/website/src/commands/ship.md
@@ -1,9 +1,13 @@
 # git town ship
 
+<a type="command-summary">
+
 ```command-summary
-git town ship [<branch-name>] [-m <text> | --message <text>] [-s <name> | --strategy <name>] [-p | --to-parent] [--dry-run] [-v | --verbose]
+git town ship [<branch-name>] [-m | --message <text>] [-f | --message-file <path>] [-s | --strategy <name>] [-p | --to-parent] [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 _Notice: Most people don't need to use this command. The recommended way to
 merge your feature branches is to use the web UI or merge queue of your code
 hosting service, as you would normally do. `git town ship` is for edge cases
diff --git a/website/src/commands/skip.md b/website/src/commands/skip.md
index 94aee2355..ebf8fd815 100644
--- a/website/src/commands/skip.md
+++ b/website/src/commands/skip.md
@@ -1,9 +1,13 @@
 # git town skip
 
+<a type="command-summary">
+
 ```command-summary
-git town skip [-v | --verbose]
+git town skip [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _skip_ command allows to skip a Git branch with merge conflicts when syncing
 all feature branches.
 
diff --git a/website/src/commands/status-reset.md b/website/src/commands/status-reset.md
index 48a42df11..540a86b54 100644
--- a/website/src/commands/status-reset.md
+++ b/website/src/commands/status-reset.md
@@ -1,9 +1,13 @@
 # git town status reset
 
+<a type="command-summary">
+
 ```command-summary
-git town status reset [-v | --verbose]
+git town status reset [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _status reset_ command deletes the persisted runstate. This is only needed
 if the runstate is corrupted and causes Git Town to crash.
 
diff --git a/website/src/commands/status-show.md b/website/src/commands/status-show.md
index dd1072fe9..860f6a0d1 100644
--- a/website/src/commands/status-show.md
+++ b/website/src/commands/status-show.md
@@ -1,9 +1,13 @@
 # git town status show
 
+<a type="command-summary">
+
 ```command-summary
-git town status show [-v | --verbose]
+git town status show [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _status show_ command displays Git Town's runstate, i.e. detailed
 information about the currently suspended or previously executed Git Town
 command, including its path on the filesystem.
diff --git a/website/src/commands/status.md b/website/src/commands/status.md
index 8dedac956..5b59f412f 100644
--- a/website/src/commands/status.md
+++ b/website/src/commands/status.md
@@ -1,10 +1,13 @@
 # git town status
 
+<a type="command-summary">
+
 ```command-summary
-git town status [-p | --pending] [-v | --verbose]
-git town status reset [-v | --verbose]
+git town status [-p | --pending] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _status_ command indicates whether Git Town has encountered a merge conflict
 and which commands you can run to continue, skip, or undo it.
 
diff --git a/website/src/commands/swap.md b/website/src/commands/swap.md
index 6ed44e698..fd297ca86 100644
--- a/website/src/commands/swap.md
+++ b/website/src/commands/swap.md
@@ -1,9 +1,13 @@
 # git town swap
 
+<a type="command-summary">
+
 ```command-summary
-git town swap [--dry-run] [-v | --verbose]
+git town swap [--dry-run] [--auto-resolve] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _swap_ command switches the position of the current branch with the branch
 ahead of it in the current stack, i.e. moves the current branch one position
 forward in the stack.
diff --git a/website/src/commands/switch.md b/website/src/commands/switch.md
index 258c7df7e..729901f26 100644
--- a/website/src/commands/switch.md
+++ b/website/src/commands/switch.md
@@ -1,9 +1,13 @@
 # git town switch
 
+<a type="command-summary">
+
 ```command-summary
-git town switch [<branch-name-regex>...] [-a | --all] [-d | --display-types] [-m | --merge] [-o | --order] [-t <name> | --type <name>] [-v | --verbose]
+git town switch [<branch-name-regex>...] [-a | --all] [-d | --display-types <type>] [-m | --merge] [-o | --order <asc|desc>] [-t | --type <name>] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _switch_ command displays the branch hierarchy on your machine and allows
 switching the current Git workspace to another local Git branch using VIM motion
 commands. It can filter the list of branches to particular branch types and
diff --git a/website/src/commands/sync.md b/website/src/commands/sync.md
index 6d8456f8c..9b5873ad4 100644
--- a/website/src/commands/sync.md
+++ b/website/src/commands/sync.md
@@ -1,9 +1,13 @@
 # git town sync
 
+<a type="command-summary">
+
 ```command-summary
-git town sync [-a | --all] [--no-push] [-s | --stack] [-d | --detached] [-p | --prune] [--dry-run] [-v | --verbose]
+git town sync [-a | --all] [--auto-resolve] [--push] [-s | --stack] [-d | --detached] [-p | --prune] [--dry-run] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _sync_ command ("synchronize this branch") updates your local Git workspace
 with what happened in the rest of the repository.
 
diff --git a/website/src/commands/undo.md b/website/src/commands/undo.md
index 0d5c3bdee..22af3c439 100644
--- a/website/src/commands/undo.md
+++ b/website/src/commands/undo.md
@@ -1,9 +1,13 @@
 # git town undo
 
+<a type="command-summary">
+
 ```command-summary
-git town undo [-v | --verbose]
+git town undo [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _undo_ command reverts the last fully executed Git Town command. It performs
 the opposite activities that the last command did and leaves your repository in
 the state it was before you ran the problematic command.
diff --git a/website/src/commands/up.md b/website/src/commands/up.md
index 62baa1571..8b11fcd8f 100644
--- a/website/src/commands/up.md
+++ b/website/src/commands/up.md
@@ -1,9 +1,13 @@
 # git town up
 
+<a type="command-summary">
+
 ```command-summary
-git town up [-m | --merge] [-v | --verbose]
+git town up [-d | --display-types <type>] [-m | --merge] [-o | --order <asc|desc>] [-v | --verbose] [-h | --help]
 ```
 
+</a>
+
 The _up_ command moves one position up in the current stack by switching to a
 child of the current branch. After successfully switching branches, it displays
 the branch hierarchy to show your new position in the stack.
diff --git a/website/src/commands/walk.md b/website/src/commands/walk.md
index 657736544..571e63189 100644
--- a/website/src/commands/walk.md
+++ b/website/src/commands/walk.md
@@ -1,9 +1,13 @@
 # git town walk
 
+<a type="command-summary">
+
 ```command-summary
-git town walk [-a | --all] [-s | --stack] [--dry-run] [-v | --verbose] [<command and arguments>]
+git town walk [-a | --all] [-s | --stack] [--dry-run] [-v | --verbose] [<command and arguments>] [-h | --help]
 ```
 
+</a>
+
 The _walk_ command ("walking the branch hierarchy") executes a given command for
 each feature branch. It stops if the command exits with an error, giving you a
 chance to investigate and fix the issue.



Test patch:
diff --git a/text-runner-tests/command-summary.test.ts b/text-runner-tests/command-summary.test.ts
new file mode 100644
index 000000000..9b3500ee5
--- /dev/null
+++ b/text-runner-tests/command-summary.test.ts
@@ -0,0 +1,141 @@
+import { deepEqual, equal } from "node:assert/strict"
+import { suite, test } from "node:test"
+import { extractArgs, extractCommand, parseCommandHelpOutput } from "../text-runner/command-summary.ts"
+
+suite("extractCommand", () => {
+  const tests = {
+    "git town append": "append",
+    "git town config get-parent": "config get-parent",
+    "git town sync": "sync",
+    "git town ship": "ship",
+    "git town append <branch-name> [-p | --prototype]": "append",
+    "git town sync --all": "sync",
+    "git town ship --all": "ship",
+    "git town completions (bash|fish|powershell|zsh) [--no-descriptions] [-h | --help]": "completions",
+    "git town config get-parent [<branch-name>] [-v | --verbose] [-h | --help]": "config get-parent",
+  }
+  for (const [give, want] of Object.entries(tests)) {
+    test(`${give} -> ${want}`, () => {
+      equal(extractCommand(give), want)
+    })
+  }
+})
+
+suite("extractArgs", () => {
+  const tests = {
+    "git town append <branch-name> [-p | --prototype] [-d | --detached] [-c | --commit] [-m | --message <message>] [--propose] [--dry-run] [-v | --verbose]":
+      [
+        ["--dry-run"],
+        ["--propose"],
+        ["-c", "--commit"],
+        ["-d", "--detached"],
+        ["-m", "--message string"],
+        ["-p", "--prototype"],
+        ["-v", "--verbose"],
+      ],
+    "git town completions (bash|fish|powershell|zsh) [--no-descriptions] [-h | --help]": [
+      ["--no-descriptions"],
+      ["-h", "--help"],
+    ],
+    "git town config get-parent [<branch-name>] [-v | --verbose] [-h | --help]": [
+      ["-h", "--help"],
+      ["-v", "--verbose"],
+    ],
+  }
+  for (const [give, want] of Object.entries(tests)) {
+    test(`${give} -> ${want}`, () => {
+      deepEqual(extractArgs(give), want)
+    })
+  }
+})
+
+suite("parseCommandHelpOutput", () => {
+  test("append", () => {
+    const give = `
+Create a new feature branch as a child of the current branch.
+
+Consider this stack:
+
+main
+ \
+* feature-1
+
+We are on the "feature-1" branch,
+which is a child of branch "main".
+After running "git town append feature-2",
+the repository will have these branches:
+
+main
+ \
+  feature-1
+   \
+*   feature-2
+
+The new branch "feature-2"
+is a child of "feature-1".
+
+If there are no uncommitted changes,
+it also syncs all affected branches.
+
+Usage:
+  git-town append <branch> [flags]
+
+Flags:
+      --auto-resolve     auto-resolve phantom merge conflicts
+  -b, --beam             beam some commits from this branch to the new branch
+  -c, --commit           commit the stashed changes into the new branch
+  -d, --detached         don't update the perennial root branch
+      --dry-run          print but do not run the Git commands
+  -h, --help             help for append
+  -m, --message string   the commit message
+      --propose          propose the new branch
+  -p, --prototype        create a prototype branch
+      --push             push local branches
+      --stash            stash uncommitted changes when creating branches
+      --sync             sync branches (default true)
+  -v, --verbose          display all Git commands run under the hood
+`
+    const have = parseCommandHelpOutput(give)
+    const want = [
+      ["--auto-resolve"],
+      ["--dry-run"],
+      ["--propose"],
+      ["--push"],
+      ["--stash"],
+      ["--sync"],
+      ["-b", "--beam"],
+      ["-c", "--commit"],
+      ["-d", "--detached"],
+      ["-h", "--help"],
+      ["-m", "--message string"],
+      ["-p", "--prototype"],
+      ["-v", "--verbose"],
+    ]
+    deepEqual(have, want)
+  })
+
+  test("branch", () => {
+    const give = `
+Display the local branch hierarchy and types.
+
+Git Town's equivalent of the "git branch" command.
+
+Usage:
+  git-town branch [flags]
+
+Flags:
+  -d, --display-types string[="all"]   display the branch types
+  -h, --help                           help for branch
+  -o, --order string                   sort order for branch list (asc or desc)
+  -v, --verbose                        display all Git commands run under the hood
+`
+    const have = parseCommandHelpOutput(give)
+    const want = [
+      ["-d", "--display-types string"],
+      ["-h", "--help"],
+      ["-o", "--order string"],
+      ["-v", "--verbose"],
+    ]
+    deepEqual(have, want)
+  })
+})