feat(cli): add yertle nodes list, with the SDK resource behind it - #22
Merged
Conversation
First command on the refactored layout, and the first proof of the merged
CLI+SDK workstream: one endpoint, one implementation, two surfaces.
src/yertle/nodes.py SDK resource — yertle.nodes.list(org_id)
src/yertle/cli/_context.py org resolution: --org / $YERTLE_ORG / all
cli/commands/nodes.py the command, ~20 lines of columns
Pagination: /orgs/{id}/nodes returns 50 at a time and the facade hides it —
list() walks every page and returns one complete list. Callers want the graph,
not page 1. This answers the open question in docs/sdk/OVERVIEW.md, which is
updated to record the decision rather than leaving it implicit; the same shape
should apply to search and any other paginating resource. The loop stops on the
reported total OR a short/empty page, so a total that never catches up cannot
spin forever.
Org resolution is --org, then $YERTLE_ORG, then "all". Defaulting to every org
rather than erroring matches the Go CLI and suits an orienting command. Blank
values are treated as unset rather than malformed. The id shape is validated
here so a typo fails with a sentence naming where to find a real id, instead of
as a ValueError from inside the SDK. It lives in cli/, not shared/, precisely
because it never reads config.json — if a persisted default org is added later
it has to be read in shared/auth.py to keep config access in one place.
The SDK takes an explicit org_id. No ambient default: hidden global state in a
library is miserable to debug, and choosing the org is the CLI's job.
Two things the tests caught rather than review:
- The org column is dropped when the listing is already scoped to one org. Two
36-character uuid columns pushed the title off an 80-column terminal, and
when scoped it is the same value on every row. Short ids will make this
cheaper once the id cache lands for `nodes show` / `tree`.
- test_yertle_run_refuses_unlisted failed on "nodes", which is the guard added
last PR doing its job: widening the CLI is an API change for the SRE agent,
so the allowlist, tool docstring and prompt all move with it.
Coverage: 100% on all three new modules; 133 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJhtswytWsBWUbDxPkMvUT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First command on the refactored layout, and the first proof of the merged CLI+SDK workstream — one endpoint, one implementation, two surfaces.
Pagination — the decision this PR sets
/orgs/{id}/nodesreturns 50 at a time. The facade hides it:list()walks every page and returns one complete list, because callers want the graph, not page 1.docs/sdk/OVERVIEW.mdlisted this as an open question; it's now marked decided rather than left implicit in the code, since this becomes the precedent forsearchand every other paginating resource. If a lazy variant is ever wanted, addnodes.iter()besidelist()rather than changing its contract.The loop stops on the reported
totalor a short/empty page — a total that never catches up can't spin forever. There's a test for exactly that.Org resolution
--org→$YERTLE_ORG→all. Defaulting to every org rather than erroring matches the Go CLI and suits an orienting command:yertle nodes listwith no arguments should show you your world, not a usage error. Blank values are treated as unset, not malformed.Shape is validated in the CLI so a typo fails with a sentence naming where to find a real id, instead of as a
ValueErrorfrom inside the SDK.It lives in
cli/, notshared/— precisely because it never readsconfig.json. If a persisted default org is added later, it has to be read inshared/auth.pyso config access stays in one module (invariant 1). There is no default-org concept in the Python config today;save_credentialswrites onlyapi_urlandtoken.The SDK takes an explicit
org_id. No ambient default — hidden global state in a library is miserable to debug, and picking the org is the CLI's job.Two things the tests caught, not review
The org column is dropped when the listing is already scoped. Two 36-character UUID columns pushed the title clean off an 80-column terminal —
test_nodes_list_table_formatfailed on a missing title, not on anything I'd have noticed by eye. When scoped it's also the same value on every row. Short ids will make this comfortable once the id cache lands fornodes show/tree.test_yertle_run_refuses_unlistedfailed onnodes— the guard added last PR doing its job. Widening the CLI is an API change for the SRE agent, so the allowlist, tool docstring andprompts.pyall moved with it, plus a positive test thatnodes listis actually admitted.Verification
make checkclean. 133 tests (was 111). 100% coverage on all three new modules.Follow-up
IMPLEMENTATION_PLAN.md's Slice 3 checklist lives in the other repo; happy to ticknodes listand note the pagination decision there in a separate docs PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01UJhtswytWsBWUbDxPkMvUT