feat(cli): flow commands — the whole cycle without touching Python (#34) - #106
Merged
Conversation
Twenty new commands take the CLI from four to the full cycle: project create / list, schema apply / list, ingest, batch approve / start / complete / promote, job list / next / progress / start / mark / complete, release publish / list / verify, export, format list and backfill-thumbnails. Every one calls the SDK in-process — no HTTP hop and no token dance — and every one takes --json. Also lands `visionset init`, closing #104: the first acceptance criterion here is a scripted shell test driving the cycle via CLI only, and without a command-line way to create a workspace that is unsatisfiable. examples/cli_end_to_end.sh walks init → export using nothing but `visionset`, asserts the --json shapes and one deliberate refusal, and runs twice in CI. It needs no ffmpeg, no jq and no server. Two kernel reads, no migration: ProjectService.get_by_name (case-insensitive, like the index) and ReleaseService.get_by_tag (case-sensitive, like the index). They live there because the two rules are opposites and a surface re-deriving either from prose would eventually get one wrong. --json shapes agree key-for-key with the REST wire models, gated by tests/cli/test_json_contract.py over fifteen resources — a test may import both packages where the packages themselves may not.
JArmandoAnaya
added a commit
that referenced
this pull request
Aug 21, 2026
…) (#106) Twenty new commands take the CLI from four to the full cycle: project create / list, schema apply / list, ingest, batch approve / start / complete / promote, job list / next / progress / start / mark / complete, release publish / list / verify, export, format list and backfill-thumbnails. Every one calls the SDK in-process — no HTTP hop and no token dance — and every one takes --json. Also lands `visionset init`, closing #104: the first acceptance criterion here is a scripted shell test driving the cycle via CLI only, and without a command-line way to create a workspace that is unsatisfiable. examples/cli_end_to_end.sh walks init → export using nothing but `visionset`, asserts the --json shapes and one deliberate refusal, and runs twice in CI. It needs no ffmpeg, no jq and no server. Two kernel reads, no migration: ProjectService.get_by_name (case-insensitive, like the index) and ReleaseService.get_by_tag (case-sensitive, like the index). They live there because the two rules are opposites and a surface re-deriving either from prose would eventually get one wrong. --json shapes agree key-for-key with the REST wire models, gated by tests/cli/test_json_contract.py over fifteen resources — a test may import both packages where the packages themselves may not.
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.
Closes #34. Also closes #104 (
visionset init), absorbed here because #34's first acceptancecriterion — "scripted shell test drives the full cycle via CLI only" — is unsatisfiable without a
command-line way to create a workspace.
What lands
Twenty commands, taking the CLI from four to the full cycle. Every one calls the SDK in-process
— no HTTP hop, no token dance — and every one takes
--json.Twenty-three of the twenty-four are exactly one service call.
ingestis the one that is two(
register_images/register_videodispatched onis_dir(), theningest) and its module says so.Ledger
FORMAT_VERSIONstays 11;VERSIONstays0.0.1.dev0.schema applyis JSON via the stdlib; PyYAML was considered and declined.ERROR_RULES.openapi.jsonandfrontend/ui-core/src/generated/api.tsare both byte-identical — the CLItouches no route. Both drift gates verified locally.
ProjectService.get_by_name/require_project_namedandReleaseService.get_by_tag.Decisions worth reviewing
Two new kernel reads rather than resolution in the CLI. A project name is unique
case-insensitively; a release tag is case-sensitive. Those are opposite rules, each enforced
by an index, and a surface re-deriving either from prose is a second spelling free to drift.
TokenService.get_by_nameis the precedent. #35 wants both.--jsonshapes agree key-for-key with the REST wire models, gated bytests/cli/test_json_contract.pyover fifteen resources — key-set parity and a round-trip throughthe wire model, which catches encoding drift a key comparison cannot see. That test imports both
visionset.cliandvisionset.server, whichtests/may do and the packages may not. Projectionsare hand-written in
cli/_json.py, nevermodel_dump():Asset.uri,Source.pathandBatch.asset_idsstay unpublished, exactly as on the wire.release verifyexits 1 when the answer is no. Not a refusal — the check ran and found damage.EXIT_ANSWER_IS_NOis a secondFinalin_errors.pywith the same value asEXIT_DOMAIN_ERRORand its own docstring, so the second meaning of code 1 is written down rather than inferred. It is
grep's anddiff's convention and the only wayverify && train.shmeans anything.job markandjob nextare additions the issue does not list, and without them "the full cyclewithout touching Python" is false — nothing else settles an asset, and a batch cannot complete until
every asset has. The wart is stated out loud in
docs/jobs.md, in the example and in a test:--progress annotatedrecords that somebody labeled an asset while the CLI writes no labels, so arelease driven this way carries
annotation_count: 0.Deliberately out, each argued in the docs rather than omitted:
batch create/membership editing(a batch is born from an ingest — #29's reason);
BySegmentspartitioning (the only caller holdingan exact partition is a program, and it is the one partition that can be wrong);
project rename/deleteand a dataset group (administration and curation, not flow — this PR adds zeronew destructive commands);
ingest --resume(re-running the same line is idempotent, so the remedyneeds no new vocabulary).
The traps that shaped the code
VisionSetErrorand would print a traceback:--fps <= 0(bare
ValueError), a missing path (FileNotFoundError), a non-directory (NotADirectoryError).Each is refused by Click at exit 2 before the call. The general rule — mirror every domain
Field(gt=…)bound in the Typer option — also covers--jobs-of(min=1, becauseBySize.sizeis
gt=0) and--split(parsed into aSplitRecipeinside atry).min=but not Click'smin_open, so agt=0bound cannot be expressed as anoption constraint;
--fpsis checked in the body.schema applyparses through the domain models, soLabelClass/Attributevalidators do therefusing and nothing is restated. Both non-domain failures (bad JSON, bad shape) are exit 2 — the
CLI's 422, the same call the server makes by moving them into request parsing.
__init__.pyanywhere,tests/cli/test_batches.pybesidetests/server/test_batches.pyis a collection error, not twomodules — hence
test_<noun>_commands.py. Private helpers are exempt (_flow.pytwice) becausethey are imported by full dotted path. Written into
docs/cli.md.--help, preserving declaration order within eachkind rather than interleaving. Registration is still in cycle order; the comment in
main.pysayswhat actually happens.
dummyis the only installed exporter and it writes nothing, sofile_count: 0is an exportthat ran. The issue's
--format yolodoes not exist and appears nowhere. The lossy gate is testedagainst an exporter registered for the test, because no installed one declares itself lossy.
The example
examples/cli_end_to_end.sh—set -euo pipefail, driving onlyvisionsetandpython3. Noffmpeg (stills only), no jq (the always-printed header and id-first column order are what
tail -n +2 | awk '{print $1}'needs), no server. It asserts the--jsonenvelope, tag, assetcount and split recipe — which is acceptance criterion 2 — and ends with a deliberate refusal it
also asserts.
tests/examples/test_cli_end_to_end.pyruns it by subprocess, which is the onlything that proves
[project.scripts]still works; a fourth CI step runs it standalone.MCP parity list for #35
visionset initgets no tool — creating a workspace is the agent's sandbox boundary, the sameargument that kept token administration off MCP in #25. Every flow command maps onto a tool already
recorded in PRs #99–#102, with two additions this task's surface implies:
backfill_thumbnailsand
list_formats(the latter already listed by #30).release verifymaps toverify_release, also already listed. Nothing new is invented here.Checks