feat(cli): generate section-1 man SYNOPSIS and OPTIONS from Typer - #1524
feat(cli): generate section-1 man SYNOPSIS and OPTIONS from Typer#1524FBISiri wants to merge 2 commits into
Conversation
The eight bundled section-1 pages (apropos, cat, find, grep, head, ls, tail, tree) declared `generated: hand`, and their SYNOPSIS and OPTIONS blocks were hand restatements of the POSIX verb definitions in cli/commands/posix.py. They had already drifted: grep(1)'s SYNOPSIS listed --json/--plain/--project/--local/--cloud while its OPTIONS documented only -F and the pagination flags. Render both blocks from the Typer command tree and lock them with a byte-equality drift test, mirroring what basicmachines-co#1478 did for section-3 PARAMETERS from the MCP registry: - man/__init__.py: render_cli_synopsis()/render_options() from a resolved Click command; _SYNOPSIS_BODY_RE/_OPTIONS_RE + extract/replace helpers; declare_ownership(text, owner=...) with declare_registry_ownership kept as a thin wrapper. - scripts/update_man_pages.py: a section-1 branch in the existing `just man-regen` pipeline; resolve_cli_command() walks the command tree (apropos maps to `bm man apropos`). - tests/test_man_pages.py: drift test, ownership, shared/global flags in OPTIONS, alias + paired-boolean rendering, block-scoped replacement. Per the maintainer's call (basicmachines-co#610), the ownership token is `generated: cli`. OPTIONS renders the complete public option list including the shared output/routing flags (grep(1) grows from four bullets to the full set), and preserves CLI syntax PARAMETERS has no concept of: flag aliases (`-F, --literal`) and paired booleans (`--frontmatter / --no-frontmatter`). Curated sections (NAME, DESCRIPTION, EXAMPLES, SEE ALSO) stay byte-identical and hand-owned; the groff sources are out of scope. Refs basicmachines-co#610 Signed-off-by: FBISiri <masteragentsiri@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e49576a209
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| metavar = param.name.upper() | ||
| tokens.append(metavar if param.required else f"[{metavar}]") | ||
| for param in command.params: | ||
| if param.param_type_name != "option" or getattr(param, "hidden", False): |
There was a problem hiding this comment.
Replace the speculative visibility fallback with a typed parameter
Because command and its parameters are typed as Any, this fallback silently treats an unsupported parameter shape without hidden as public and emits it into the manual instead of failing regeneration. Define a narrow protocol for the Click attributes consumed here, or use Typer's vendored Click types and access param.hidden explicitly; the same fallback also appears in render_options.
AGENTS.md reference: AGENTS.md:L132-L133
Useful? React with 👍 / 👎.
| bm man apropos QUERY [--project PROJECT] [--json] [--plain] [--local] | ||
| [--cloud] |
There was a problem hiding this comment.
Preserve mutually exclusive option groups in generated synopses
The regenerated synopsis presents --json/--plain and --local/--cloud as independently combinable options, but the CLI explicitly rejects both pairs: tests/cli/test_cli_man_lookup.py:124-139 verifies that these invocations exit 1. The previous synopsis used | groups, so the generator needs explicit knowledge of these cross-parameter constraints rather than flattening every standalone Click option.
Useful? React with 👍 / 👎.
| elif param.is_flag: | ||
| tokens.append(f"[{opt}]") | ||
| else: | ||
| tokens.append(f"[{opt} {param.name.upper()}]") |
There was a problem hiding this comment.
Render repeatable Click options as repeatable
When a Click option has multiple=True, this branch still emits exactly one occurrence. The current find --meta option is an Optional[list[str]] explicitly documented as repeatable, yet its generated synopsis regresses from [--meta PREDICATE ...] to [--meta META]; inspect the option's multiplicity and retain repetition notation so multi-predicate usage remains accurately discoverable.
Useful? React with 👍 / 👎.
| name: write-note # page name without section suffix | ||
| summary: create or overwrite a markdown note in the knowledge base | ||
| generated: hand # hand | registry | typer (regeneration ownership) | ||
| generated: hand # hand | registry | cli (regeneration ownership) |
There was a problem hiding this comment.
Add the cli owner to the shipped Manpage schema
The documentation now declares cli as a valid ownership token and all eight regenerated section-1 pages use it, but plugins/claude-code/schemas/manpage.md:18 still permits only [registry, typer, hand]. A user following this document and copying the shipped opt-in schema will therefore get an enum-mismatch warning when validating these pages; update both that enum and its remaining typer prose in the same change.
Useful? React with 👍 / 👎.
Address the chatgpt-codex review on basicmachines-co#1524: - Replace the speculative `getattr(param, "hidden", False)` fallback in render_cli_synopsis/render_options with a structural ClickParam/ClickCommand Protocol and read `param.hidden` directly, so an unexpected parameter shape fails fast instead of being silently treated as public (AGENTS.md: no speculative getattr). The Protocol stays structural, so no Click import is pulled into the lightweight `basic_memory.man` module. - Render mutually exclusive option pairs (--json/--plain, --local/--cloud) as a single `[--a | --b]` alternative in the shell SYNOPSIS, matching the CLI's own rejection of both flags together, instead of flattening them into freely combinable tokens. - Keep Click `multiple=True` options repeatable: `find --meta` renders `[--meta META ...]` again rather than degrading to `[--meta META]`. - Add `cli` to the shipped Manpage schema ownership enum and fix its residual `typer` prose, so a user copying the opt-in schema validates the regenerated section-1 pages without an enum-mismatch warning. Regenerated all 8 section-1 pages via scripts/update_man_pages.py; regeneration is idempotent. Added regression tests for the grouped mutex pairs, the repeatable `...` notation, and hidden-option exclusion. Refs basicmachines-co#610 Signed-off-by: FBISiri <masteragentsiri@gmail.com>
|
Status check-in, plus the bits that are actually mine to flag. This is the slice (B) implementation following your 09-07 direction: Rather than make you reverse-engineer +706/-98, the short review map:
One judgment call worth your eye, since it's the only spot where I picked semantics over mechanics: mutually-exclusive groups are declared explicitly, not inferred. So: would you rather the generator infer groups from Click metadata instead of relying on a declared list? That's a real design change and I'd much rather do it now than after merge. CI is 10/10 green on |
What
Implements the section-1 slice of #610: the eight bundled section-1 man pages
(
apropos,cat,find,grep,head,ls,tail,tree) now have theirSYNOPSIS and OPTIONS blocks generated from the Typer command tree and
locked by a byte-equality drift test — the CLI counterpart of what #1478 did for
section-3 PARAMETERS from the MCP registry.
Why — the drift being fixed
These pages declared
generated: hand, and their SYNOPSIS/OPTIONS were manualrestatements of the POSIX verb definitions in
cli/commands/posix.py. They hadalready drifted:
grep(1)'s SYNOPSIS listed--json,--plain,--project,--local,--cloud, while its OPTIONS block documented only-Fand thepagination flags. Generating both blocks from the single source of truth (the
command tree) makes that class of drift impossible and CI-visible.
How
src/basic_memory/man/__init__.py—render_cli_synopsis()/render_options()render from a resolved Click command (introspected via itspublic attributes; no Typer/Click import at module load). Added
_SYNOPSIS_BODY_RE/_OPTIONS_REplusextract_*/replace_*helpers thatmirror the existing PARAMETERS machinery.
declare_registry_ownership()is nowa thin wrapper over a parameterized
declare_ownership(text, owner=...), sosection-3 call sites are untouched.
scripts/update_man_pages.py— a section-1 branch inside the existingjust man-regenpipeline;resolve_cli_command()walks the command tree(
apropos→bm man apropos, a verb on themansubgroup).tests/test_man_pages.py— drift test (byte-equal to the live rendering,failure message points at
just man-regen),generated: cliownership,shared/global flags present in OPTIONS, alias + paired-boolean rendering,
block-scoped replacement (curated sections byte-identical).
docs/manual-pages.md— ownership token note + roadmap updated.Maintainer-ratified decisions (from #610)
generated: cliis the ownership token (notregistry/typer); section-3stays
registry.output/routing flags earlier hand-written blocks left out —
grep(1)growsfrom four bullets to the full set. That growth is the point, not a regression.
-F, --literal, and paired booleans as--frontmatter / --no-frontmatter(verified against Click 8.x
Option.secondary_opts).Scope / boundaries
come out byte-identical — verified programmatically across all eight pages.
man/bm.1,man/basic-memory.1) are out ofscope and untouched.
cli/commands/posix.pyneeded no changes: introspection confirmed everyoption already carries
help=text.find(1)shipped two shell forms in two fenced blocks; the generator collapsesthem to the one Typer advertises, so the whole SYNOPSIS body (not just the
first fence) is replaced.
Follow-up (out of this slice's write scope)
plugins/claude-code/schemas/manpage.mdstill lists thegeneratedenum as[registry, typer, hand]. Per the maintainer'sclidecision it should gaincli(its validation iswarnand it is not applied to the bundled pages, sonothing breaks meanwhile). The section-1 page →
bmcommand mapping lives in thegenerator (
SECTION1_COMMAND_PATHS) rather than acommand:frontmatter field,because
man1/*.mdis only ever rewritten byjust man-regen.Verification
uv run pytest tests/test_man_pages.py -q→ 48 passed (man/__init__.py100% cov)uv run pytest tests/cli/test_man_command.py tests/cli/test_cli_man_lookup.py -q→ 35 passedjust man-regen && git diff --exit-code→ idempotent, no diff on second runruff check/ruff format --checkclean on changed files;ty checkcleanRefs #610