Skip to content

refactor(cli): split into a command package and route commands through the SDK - #21

Merged
albertcmiller1 merged 1 commit into
mainfrom
slice-3/cli-package-layout
Sep 2, 2026
Merged

refactor(cli): split into a command package and route commands through the SDK#21
albertcmiller1 merged 1 commit into
mainfrom
slice-3/cli-package-layout

Conversation

@albertcmiller1

Copy link
Copy Markdown
Contributor

Prepares the CLI to absorb the rest of Slice 3. No new commands — layout and layering only, so the next six land into a shape that can hold them.

Why now

main.py was 321 lines, and half of it was private helpers pooled above the app. Those helpers were never CLI-generic — they're per-command presentation that had nowhere else to go: _source_checkout belongs to version, _mask_token/_source_label to auth status, _web_url_for to login. Six more commands into that file and the split only gets more expensive.

Layout

cli/main.py       composition root — builds the app, mounts commands, nothing else
cli/_render.py    Format enum, Column, render(), display_path()
cli/_errors.py    api_errors() — wire/credential failures → sentences
cli/commands/     version.py, login.py, auth.py, orgs.py

_render.py is the one that had to come first: every command branches on --format identically, and six copies is exactly the near-copy duplication make hygiene watches. Commands now declare columns and hand them over.

The layering change

main.py imported list_organizations_orgs_get from yertle_client and hand-rolled the response check — which src/yertle/orgs.py already does. Two implementations of "list orgs" in one package.

That was about to multiply. IMPLEMENTATION_PLAN.md tracks Slice 3 (nodes list, tree, rag) and "SDK wider surface" (yertle.nodes.*, yertle.search) as separate workstreams hitting identical endpoints. Commands now call the SDK, so each CLI command ships the SDK function it needs rather than the two surfaces being built twice — and the SDK gets exercised by the CLI instead of sitting parallel and untested.

test_cli_calls_the_sdk_not_the_wire_layer enforces it. I checked it fails on a reintroduced wire import rather than passing vacuously:

AssertionError: CLI commands must call the SDK (`yertle.orgs`, `yertle.nodes`, …)
  src/yertle/cli/commands/orgs.py:7 imports yertle_client.api.organizations

Wire types (yertle_client.models, .errors) stay allowed — rendering and error handling legitimately need them.

Two intentional behavior changes

Both were already logged as Slice 3 cleanups; flagging them because the rest of the diff is structural.

  1. --format is an enum, so --format xml now errors instead of silently falling through to the table branch. Help shows [table|json].
  2. _is_edge_rejection is deleted. It detected API Gateway's JWT authorizer — removed 2026-08-31 in the in-app auth migration — and told users PATs only work against a locally-run backend, which has been the opposite of true since. A test now asserts that advice can't come back.

Also

  • CLAUDE.md gains the CLI extension surface. It was the only subpackage without one (SDK resources and SRE tools both have theirs), which is how the CLI drifted into a single 321-line file.
  • Tests mirror the source: tests/cli/. The shared default-client reset moved to tests/conftest.py rather than being copied into a second file.
  • tests/cli/test_orgs.py mocks the wire layer, not yertle.orgs.list — patching the SDK would pass even if the CLI stopped calling it.

Verification

make check clean. 111 tests (was 100), coverage on src/yertle/cli 98%. yertle --help, yertle orgs --help, and yertle orgs list --help all render as before.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UJhtswytWsBWUbDxPkMvUT

…h the SDK

Prepares the CLI to absorb the rest of Slice 3. main.py was 321 lines with
half of it private helpers pooled above the app, and every helper was really
per-command presentation: _source_checkout belongs to version, _mask_token
and _source_label to auth status, _web_url_for to login. Six more commands
into that file and the split only gets more expensive.

Layout, one module per noun group:

  cli/main.py           composition root — builds the app, mounts commands
  cli/_render.py        Format enum, Column, render(), display_path()
  cli/_errors.py        api_errors() — wire/credential failures to sentences
  cli/commands/         version, login, auth, orgs

The bigger change is layering. cli/main.py imported
list_organizations_orgs_get from yertle_client and hand-rolled the response
check, which src/yertle/orgs.py already does — two implementations of "list
orgs" in one package. That would have multiplied: IMPLEMENTATION_PLAN.md has
Slice 3 (nodes list, tree, rag) and "SDK wider surface" (yertle.nodes.*,
yertle.search) as separate workstreams hitting identical endpoints. Commands
now call the SDK, so each CLI command ships the SDK function it needs instead
of the two surfaces being built twice, and the SDK gets exercised by the CLI
rather than sitting parallel and untested.

test_cli_calls_the_sdk_not_the_wire_layer enforces it — verified it fails on
a reintroduced wire import rather than passing vacuously. CLAUDE.md gains the
CLI extension surface, which was the only subpackage without one, and that is
how this drifted into a single file in the first place.

Two intentional behavior changes, both previously logged:

- --format is a Format enum, so `--format xml` errors instead of silently
  falling through to the table branch. Help now shows [table|json].
- _is_edge_rejection is deleted. It detected API Gateway's JWT authorizer,
  removed 2026-08-31 in the in-app auth migration, and told users PATs only
  work against a local backend — the opposite of true since that migration.
  This was the folded-in cleanup queued in Slice 3.

Coverage on src/yertle/cli is 98%; 111 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJhtswytWsBWUbDxPkMvUT
@albertcmiller1
albertcmiller1 merged commit 63f71bd into main Sep 2, 2026
3 checks passed
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.

1 participant