Scaffold bellows: gh-parity command tree, Forgejo client, codegen and CI #1

Open
ivuorinen wants to merge 27 commits from feat/scaffold into main
Owner

Scaffold for bellows, a Forgejo CLI aiming at 1:1 parity with gh 2.101.0. .NET 10, Native AOT, System.CommandLine, and a Kiota client generated from the Forgejo API spec. Targets Forgejo 15.x LTS, 16.x and Codeberg.

Every gh command exists with its flags and aliases, but each one only reports its parity status and exits 1. Design: docs/superpowers/specs/2026-09-26-bellows-scaffold-design.md (§10 lists where the build diverged from the design). Plan: docs/superpowers/plans/2026-09-26-bellows-scaffold.md.

What's in it

  • bellows CLI (src/Bellows.Cli): all 227 gh commands as stubs, built from a generated table. Hand-written commands are registered first, and any path they occupy skips its stub. Help answers to --help/-? everywhere, and to -h on every command that doesn't bind -h to --hostname.
  • Core (src/Bellows.Core): Forgejo version parsing, plus version gates for the 15 API operations added in 16.0.
  • Forgejo client (src/Bellows.Forgejo): the Kiota client generated from the pinned v16.0.5 spec. The token is only ever sent to its own server.
  • Codegen (tools/Bellows.Codegen): the Forgejo spec fetcher, gate generator, gh reference parser, parity manifest and stub generator. Generated output is committed, and builds never run codegen.
  • CI (.forgejo/workflows/ci.yml), four jobs:
    • lint
    • build + test
    • codegen drift, which also fails if git ignores a generated file
    • AOT smoke, which uploads the binary together with the license notices

Verification

  • Each of the 24 code-bearing commits was checked from a fresh worktree:
    • dotnet restore --locked-mode
    • Release build with 0 warnings
    • tests, ending at 338
  • The tip also passes scripts/aot-smoke.sh: a 3.1 MB native binary with no IL warnings.
  • dotnet format --verify-no-changes is clean.
  • Regenerating the gates, stubs and Kiota client reproduces the committed output byte for byte.

Audit

A full nitpicker audit ran over this branch. The record is in docs/audit/ (the architecture profile and the findings store).

  • 22 findings. 19 fixed here, 1 invalid, 2 open.
  • The two High findings:
    • The SDK was not pinned exactly, so any other SDK patch would fail the locked restore in CI.
    • Template .gitignore rules for Releases/ and Logs/ had silently dropped 10 generated Kiota files from git, so a fresh clone would not compile.
  • Invalid: fork-PR runner exposure. git.esle.fi is single-user and registration is disabled.
  • Still open:
    • ci-219e9277: once these jobs have reported, make lint, build-test, codegen-drift and aot-smoke required checks on main.
    • audit-cf75603a: the API client, token provider and version gates are not called by any command yet. They are wired in the config/auth cycle.

Notes for review

  • global.json pins SDK 10.0.400 with rollForward: disable. Other SDK patches are refused on purpose, because the lock files record AOT packages that follow the SDK's bundled runtime. To move SDKs, bump the pin and run dotnet restore --force-evaluate in the same commit.
  • Stub help text is gh's own wording, so it mentions GitHub until each command is implemented.
Scaffold for **bellows**, a Forgejo CLI aiming at 1:1 parity with `gh` 2.101.0. .NET 10, Native AOT, System.CommandLine, and a Kiota client generated from the Forgejo API spec. Targets Forgejo 15.x LTS, 16.x and Codeberg. Every `gh` command exists with its flags and aliases, but each one only reports its parity status and exits 1. Design: `docs/superpowers/specs/2026-09-26-bellows-scaffold-design.md` (§10 lists where the build diverged from the design). Plan: `docs/superpowers/plans/2026-09-26-bellows-scaffold.md`. ## What's in it - **`bellows` CLI** (`src/Bellows.Cli`): all 227 `gh` commands as stubs, built from a generated table. Hand-written commands are registered first, and any path they occupy skips its stub. Help answers to `--help`/`-?` everywhere, and to `-h` on every command that doesn't bind `-h` to `--hostname`. - **Core** (`src/Bellows.Core`): Forgejo version parsing, plus version gates for the 15 API operations added in 16.0. - **Forgejo client** (`src/Bellows.Forgejo`): the Kiota client generated from the pinned v16.0.5 spec. The token is only ever sent to its own server. - **Codegen** (`tools/Bellows.Codegen`): the Forgejo spec fetcher, gate generator, `gh` reference parser, parity manifest and stub generator. Generated output is committed, and builds never run codegen. - **CI** (`.forgejo/workflows/ci.yml`), four jobs: - lint - build + test - codegen drift, which also fails if git ignores a generated file - AOT smoke, which uploads the binary together with the license notices ## Verification - Each of the 24 code-bearing commits was checked from a fresh worktree: - `dotnet restore --locked-mode` - Release build with 0 warnings - tests, ending at 338 - The tip also passes `scripts/aot-smoke.sh`: a 3.1 MB native binary with no IL warnings. - `dotnet format --verify-no-changes` is clean. - Regenerating the gates, stubs and Kiota client reproduces the committed output byte for byte. ## Audit A full nitpicker audit ran over this branch. The record is in `docs/audit/` (the architecture profile and the findings store). - **22 findings.** 19 fixed here, 1 invalid, 2 open. - **The two High findings:** - The SDK was not pinned exactly, so any other SDK patch would fail the locked restore in CI. - Template `.gitignore` rules for `Releases/` and `Logs/` had silently dropped 10 generated Kiota files from git, so a fresh clone would not compile. - **Invalid:** fork-PR runner exposure. git.esle.fi is single-user and registration is disabled. - **Still open:** - `ci-219e9277`: once these jobs have reported, make `lint`, `build-test`, `codegen-drift` and `aot-smoke` required checks on `main`. - `audit-cf75603a`: the API client, token provider and version gates are not called by any command yet. They are wired in the config/auth cycle. ## Notes for review - `global.json` pins SDK **10.0.400** with `rollForward: disable`. Other SDK patches are refused on purpose, because the lock files record AOT packages that follow the SDK's bundled runtime. To move SDKs, bump the pin and run `dotnet restore --force-evaluate` in the same commit. - Stub help text is `gh`'s own wording, so it mentions GitHub until each command is implemented.
Design for the .NET 10 Native AOT solution scaffold: System.CommandLine
parsing with Spectre.Console rendering, a Kiota client generated from the
pinned Forgejo v16 spec with version gates derived from v15, the gh 2.101.0
stub tree, XDG config/state layout, and Forgejo CI.
Thirteen TDD tasks from solution foundation through codegen, the gh stub
tree, parity test, Native AOT smoke check, Forgejo CI and CLAUDE.md.
Scaffolds the solution, shared build settings, central package versions
and the first CLI test project alongside the entry point.
gh prints a subcommand alias once per name of its parent group
("gh ext ls, gh extension ls, gh extensions ls"), so the parser
recorded "ls" three times. Found by the parity test.
Publishing bellows for one RID restored Bellows.Core and Bellows.Forgejo
for that RID too, adding a runtime section to their lock files. With the
same RuntimeIdentifiers as Bellows.Cli the lock files already cover every
release RID, and an AOT publish leaves the tree clean.
The lock files record Microsoft.NET.ILLink.Tasks and
Microsoft.DotNet.ILCompiler, whose versions follow the SDK's bundled
runtime. With rollForward latestFeature, any other 10.0.x SDK (including
the 10.0.401 that setup-dotnet installs) failed `dotnet restore
--locked-mode` with NU1004 in every CI job. Pin 10.0.400, the SDK the
locks were generated with, and refuse all others.
src/Directory.Build.props now sets IsAotCompatible and the release
RuntimeIdentifiers once for every shipped project; the three csproj
copies had to be kept identical by hand, and a missing RID rewrites that
project's lock file on publish.

Spectre.Console was referenced by no code; it returns with the first
command that renders output. Lock files regenerated.
A version component above int.MaxValue made int.Parse throw, so a broken
or hostile server's GET /version response crashed the command instead of
disabling version gating. Also documents TryParse, Parse, ToString and
VersionGate.FormatMessage.
ApiBaseUri accepted any string, so "https://host" or "host/" built a
wrong URL or threw an opaque UriFormatException. It now accepts only a
host or host:port and names the bad value otherwise.

The token provider's docstring credited itself with redirect safety,
which actually comes from the HTTP handler dropping Authorization; the
docstring now says so and a loopback-server test pins the behavior. The
empty-token case is tested alongside null, and the factory's public
members are documented.
Help only answered to --help and -?, so `bellows issue list -h` printed
the stub message and `bellows -h` failed to parse, while gh shows help.
Every command that does not bind -h itself (to --hostname) now gets a
hidden, non-recursive -h help option. The version test compares against
the assembly's informational version instead of a hard-coded 0.1.0.
- CSharpLiteral escapes U+2028/U+2029, which C# treats as line
  terminators and which would have produced uncompilable source.
- The gate generator stops when a newer spec removes an operation, since
  minimum-version gates cannot express removal.
- fetch-spec rejects tags outside SpecChain.Tags, whose provenance
  SOURCES.md would never record.
- The parity manifest rejects non-string entries instead of accepting a
  null exclusion.
The Native AOT binary statically contains MIT and Apache-2.0 code and
gh's MIT-licensed command text. THIRD-PARTY-NOTICES.md lists every
shipped component with its license text verbatim from upstream, and the
CI artifact now carries LICENSE, the notices and LICENSES/ with the
binary. The upload-artifact pin comment is corrected to v4, the tag its
SHA actually resolves to.
README describes bellows, its scaffold status and how to build it.
CLAUDE.md covers the exact SDK pin, src/Directory.Build.props, -h help,
the deferred Spectre.Console and the notices rule. The spec gains a
section listing where the implementation diverged from it and why.
The codegen-drift check uses `git status --porcelain`, which cannot see
ignored files. Template .gitignore rules for Releases/ and Logs/ matched
Kiota request-builder folders, so 10 generated files were regenerated,
compiled locally and were still missing from every commit. A new step
fails the job when any file under Generated/ or any src *.g.cs is
ignored.
docs(audit): record the scaffold audit findings and architecture profile
Some checks failed
CI / lint (pull_request) Failing after 42s
CI / aot-smoke (pull_request) Failing after 9s
CI / build-test (pull_request) Failing after 9s
CI / codegen-drift (pull_request) Failing after 10s
f71732b22a
nitpicker audit of main..feat/scaffold: 22 findings, 19 fixed on this
branch, 1 invalid (fork-PR runner exposure: git.esle.fi is single-user
with registration disabled), 2 open with the reason in each file (branch
protection needs an owner token to verify; the API client is wired in the
auth cycle). arch-profile.md records the layered Cli -> Core -> Forgejo
structure the arch audit validated.
Some checks failed
CI / lint (pull_request) Failing after 42s
CI / aot-smoke (pull_request) Failing after 9s
CI / build-test (pull_request) Failing after 9s
CI / codegen-drift (pull_request) Failing after 10s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/scaffold:feat/scaffold
git switch feat/scaffold

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff feat/scaffold
git switch feat/scaffold
git rebase main
git switch main
git merge --ff-only feat/scaffold
git switch feat/scaffold
git rebase main
git switch main
git merge --no-ff feat/scaffold
git switch main
git merge --squash feat/scaffold
git switch main
git merge --ff-only feat/scaffold
git switch main
git merge feat/scaffold
git push origin main
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ivuorinen/forgejo-cli!1
No description provided.