Repository navigation
Blueprints: custom YAML specs, callbacks, and self-contained single-file builds - #56
Conversation
A blueprint is now a pydantic model for a custom YAML schema plus to_manifest(), with pre_validate / post_manifest / post_codegen hooks. References resolve from a .py file, a module:Class path or a cliffy.blueprints entry point, with explicit not-found, ambiguous and invalid-spec errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A callback runs before the commands of the CLI or of one command group, can declare params that become options of the CLI or group, and gets the context as `ctx`. The built-in --version and --aliases options are kept. - `commands` is no longer required, so a callbacks-only CLI is valid. - click: an explicit param no longer renders as `----name`, and an explicit bool option is a flag like it is with typer. - `cli test` uses click's runner for `use_click` manifests; typer's runner rejects a plain click group. - Generated imports are sorted, so the same manifest gives the same code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blueprints: a file selects its blueprint with a top-level `blueprint:` key or with --blueprint on load, run, render, build, dev, test, validate and docs. The reference is stored with a loaded CLI, so `cli update` reuses it and `cli info` shows it. `cli blueprint list` (alias `cli blueprints`) lists installed blueprints and `cli blueprint schema` prints the JSON schema of a spec. Builds: `cli build` applies the blueprint at build time and writes one zipapp that needs neither cliffy nor the blueprint. - Dependencies are installed with the running interpreter's pip in a subprocess; a failure reports pip's reason and writes no file. - The framework the generated code imports (typer or rich-click) is pinned to the installed version unless `requires` names it. Click CLIs used to get typer bundled instead of rich-click. - The archive is reproducible: fixed timestamps and no generation time in the bundled module. - The build reports the Python versions the bundle supports, and the bundled CLI exits with a readable message on any other version. - build_cli() returns a BuildResult instead of a click test Result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- examples/blueprints/tasks.py: a small task-runner spec. - examples/blueprints/mcp_tools.py: an MCP tool server spec. Every tool becomes a command, and `serve` speaks MCP over stdio without extra dependencies. - JSON schemas for both specs, checked against the blueprints in tests. - Tests cover reference resolution (file, module:Class, entry point), not-found / ambiguous / invalid-spec errors, hooks, the CLI commands, and an end-to-end build that executes the artifact in a subprocess from another directory in isolated mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved so that the fixes of fix/audit-bugs-and-security and the blueprint and callback features both survive: - cliffy/builder.py: blueprints' single-file build (BuildResult, pinned framework, reproducible zipapp, `blueprint=`), reading and writing as UTF-8. The script is staged in a TemporaryDirectory, which also covers the cleanup-on-failure fix. get_framework_requirements is superseded by get_build_requirements. - cliffy/cli.py: `--blueprint` on every command and the `blueprint` group next to `ai`; exit codes of the fixes branch kept (a failed build exits 1); `cli update` and `cli docs <loaded>` reuse the stored blueprint. - cliffy/commanders/click.py: callbacks on the root and on groups, with help emitted through py_str. parse_param_list and get_param_list_names are the one param implementation, shared by commands and callbacks (global params, validated names, `--flag/--no-flag` bools). - cliffy/commanders/typer.py: callbacks plus py_str escaping. - cliffy/reloader.py, cliffy/tester.py, cliffy/transformer.py: `blueprint=` threaded through, UTF-8 and context managers kept, include cycle detection next to blueprint resolution. - tests/test_builder.py: all tests of both sides in one file. The tests of the fixes branch patch the pip subprocess instead of shiv's pip and expect the pinned framework requirement. - tests/test_callbacks.py: the click bool option is expected as a `--debug/--no-debug` switch. - cliffy/ai.py: `commands` became optional for callbacks-only CLIs; a model reply without `commands` and without callbacks is still rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Reviewer's GuideThis PR replaces the old blueprint hooks with typed custom YAML specifications that are resolved and transformed into ordinary manifests, adds framework-independent callbacks, and produces reproducible self-contained zipapps while wiring blueprint selection, metadata, validation, testing, documentation, and CLI workflows end to end. Sequence diagram for loading a custom blueprint specsequenceDiagram
participant User
participant CLI
participant Transformer
participant Blueprint as Blueprint module
participant Manifest as CLIManifest
participant Commander
User->>CLI: load or run YAML
CLI->>Transformer: Transformer(manifest, blueprint)
Transformer->>Transformer: find_blueprint_reference
Transformer->>Blueprint: resolve_blueprint
Blueprint-->>Transformer: Blueprint subclass
Transformer->>Blueprint: pre_validate(raw)
Transformer->>Blueprint: model_validate(spec)
Transformer->>Blueprint: to_manifest()
Blueprint-->>Transformer: CLIManifest or mapping
Transformer->>Blueprint: post_manifest(manifest)
Transformer->>Manifest: validate manifest
Transformer->>Commander: generate_cli(manifest)
Commander-->>CLI: generated CLI
Sequence diagram for building a self-contained blueprint CLIsequenceDiagram
participant User
participant CLI
participant Transformer
participant Builder
participant Pip
participant Shiv
participant Zipapp
User->>CLI: build custom YAML spec
CLI->>Transformer: build_cli_from_manifest
Transformer->>Transformer: apply blueprint and generate code
Transformer->>Builder: build_cli(script, requires)
Builder->>Pip: install dependencies into staging
Pip-->>Builder: packages or failure reason
alt dependencies installed
Builder->>Shiv: create reproducible zipapp
Shiv->>Zipapp: write single executable
Zipapp-->>Builder: path and requires_python
Builder-->>CLI: BuildResult(exit_code=0)
else installation fails
Builder-->>CLI: BuildResult(exit_code=1, output)
end
Flow diagram for blueprint-driven CLI generationflowchart LR
YAML[Custom YAML spec] --> Resolve[resolve_blueprint]
Resolve --> Validate[build_manifest]
Validate --> Manifest[CLIManifest]
Manifest --> Generate[TyperCommander or ClickCommander]
Generate --> CLI[Generated CLI]
CLI --> Workflows[load, run, render, test, docs]
CLI --> Build[build_cli]
Build --> Zipapp[Self-contained reproducible zipapp]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 security issue, and 1 other issue
Security issues:
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="examples/blueprints/mcp_tools.py" line_range="53" />
<code_context>
+
+def _handle_message(message):
+ """Answer one JSON-RPC message. Returns None for notifications, which get no reply."""
+ method, params, message_id = message.get("method"), message.get("params") or {}, message.get("id")
+ if message_id is None:
+ return None
</code_context>
<issue_to_address>
**Invalid JSON-RPC messages stop server**
When the MCP peer sends valid JSON whose top-level value is not an object, `_handle_message` calls `.get()` on the decoded value, raising `AttributeError`; `_serve` catches only `ValueError`, so the server exits instead of returning an invalid-request error and continuing to read stdin.
Check that each decoded message is a JSON-RPC object and return an invalid-request response for other JSON values.
Also at `examples/blueprints/mcp_tools.py:82-86`.
</issue_to_address>
### Comment 2
<location path="cliffy/builder.py" line_range="89" />
<code_context>
pip = subprocess.run([*pip_install, "--target", staging, *requirements], capture_output=True, text=True)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and a code-generation or dependency-compatibility defect could leave a loaded CLI or built zipapp behaving incorrectly after this change is reverted. Those generated artifacts can be removed, reloaded, or rebuilt; this diff does not itself make irreversible changes to user data or access.
Blocking findings: examples/blueprints/mcp_tools.py:53, cliffy/builder.py:89
rich colours the help on GitHub Actions, which split '--verbose' across escape sequences and failed the assertion there only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Finishes the blueprint feature and redoes it as a typed spec. A blueprint is a pydantic model for your own YAML schema plus a
to_manifest()method. Because the result is an ordinaryCLIManifest, both commanders,render,test,docsandbuildwork unchanged on files written in a custom spec.cli buildturns such a file into one executable that needs neither cliffy, the blueprint nor the YAML.Stacks on
chore/python-3.11-3.15.What changed
Blueprints (
cliffy/blueprint.py,transformer.py,cli.py)Blueprint(BaseModel, ABC): the fields are the spec,to_manifest()returns aCLIManifestor a mapping. Optional hooks:pre_validate(raw),post_manifest(manifest),post_codegen(code). No class-level state and no prints. Unknown keys are rejected by default.blueprint:key in the file, or--blueprint/-bonload,run,render,build,dev,test,validateanddocs. The option wins. A path in the file is relative to the file; a path on the command line is relative to the working directory..pyfile (tasks.py,tasks.py:Tasks), amodule:Class, or the name of a plugin registered under thecliffy.blueprintsentry-point group.tools[1].parameters[0].type: ...line per problem.cli updatereuses it andcli infoshows it.cli blueprint list(aliascli blueprints) andcli blueprint schema REF [-o file], which prints the JSON schema of a spec for editor completion.Callbacks (
manifest.py, both commanders)callbacks: a list of{run, group, params, invoke_without_command}. A callback runs before the commands of the CLI or of one group (db,db.tables), its params become options of the CLI or group, andctxis available inrun.--versionand--aliasesare kept (the WIP dropped them).groupmatches no command group is a validation error.examples/greet.yaml(callbacks only, no commands) runs unmodified on this branch.Builds (
builder.py)requiresnames it. Click CLIs now getrich-click; the old code always bundledtyper, even foruse_clickmanifests.--reproducible,--no-compile, and the generation timestamp is removed from the bundled module. Two builds of the same input are byte-identical (asserted in a test).--pythonstill sets the shebang. The build reports the supported Python range (>=3.Xfor pure-Python bundles,==3.X.*when a compiled extension is bundled), and the bundled CLI exits with a readable message on an unsupported Python.Examples and docs
examples/blueprints/tasks.py+tasks.yaml: a small task runner.examples/blueprints/mcp_tools.py+text-tools.yaml: an MCP tool server spec. Each tool becomes a command,toolsprints the definitions, andservespeaks MCP over stdio (JSON-RPC, no third-party dependency).*.schema.jsonfor both specs; a test checks they match the blueprints.examples/callbacks.yaml, with manifest tests.docs/blueprints.mdand one nav line inmkdocs.yaml. Callbacks are documented at the end of that page, because it is the only docs file this MR may touch.examples/cliffy_schema.jsonregenerated.Breaking changes
cliffy.blueprint.BlueprintAPI replaced. The hook-only base class (validate_hook,pre_process_hook,load_hook,Blueprint.create) is gone. It was never wired into the CLI onmain.build_cli()andbuild_cli_from_manifest()return aBuildResult(exit_code,output,exception,path,requires_python) instead of aclick.testing.Result.exit_code,outputandexceptionkeep their meaning.commandsis no longer required in a manifest, so a callbacks-only CLI is valid. A manifest without commands used to fail validation.name: --verbosewithshort: -vused to render as'----verbose', '--v', which was unusable. It now renders correctly, and an explicittype: booloption is a flag (is_flag=True), as with Typer. Simple params (--flag: bool) are unchanged.requiresnames it.Fixes outside the brief, needed to make the feature work
cli teston ause_clickmanifest crashed withAttributeError: 'RichGroup' object has no attribute '_add_completion', because typer'sCliRunnerrejects a plain click group.Testernow uses click's runner for click manifests.How verified
macOS arm64, uv 0.13.0, one venv per Python,
uv run --locked --all-extras --group docs.examples/blueprints/text-tools.yaml, checks the archive contains no cliffy, pydantic or blueprint files, then runs the artifact withpython -Ifrom another directory with a stripped environment: tool commands,--version, and an MCPinitialize/tools/list/tools/callsession over stdin.examples/click_hello.yamlandexamples/callbacks.yamlbuild and run.mkdocs buildrenders the new page.🤖 Generated with Claude Code
Summary by Sourcery
Enable custom YAML blueprints, manifest callbacks, and reproducible self-contained CLI builds across the command-line workflow.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests:
Chores: