Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,12 @@ mutants/
# hypothesis property-test cache (local machine paths; never ship)
.hypothesis/


# Raw VHS captures. The tapes write a full-length recording (docs/job-hero.mp4
# is 2.3 MB of mostly dead air) and only the trimmed cut is worth committing —
# scripts/trim_hero.sh turns one into the other. These are untracked AND were
# unignored, so a `git add -A` swept a raw capture into an unrelated test PR
# once (#102). Commit the *-post.* cut deliberately instead.
docs/job-hero.mp4
docs/*-raw.mp4
docs/*-raw.gif
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Logic lives in focused packages; `bambu_cli/bambu.py` is a **thin entrypoint** (

| Module / package | Role |
|------------------|------|
| `bambu.py` | Thin entrypoint: the `plate` console script + a `main` re-export. Nothing else belongs here |
| `printer.py` | `BambuPrinter` — the transport facade over `protocols/` (FTPS + MQTT); build it via `get_printer()` / `RuntimeContext.printer()` |
| `cli.py` | `main()` dispatch and the **only** module holding `sys.exit`; re-exports `build_parser` from `cliparse` |
| `cliparse.py` | The argparse tree (`build_parser`, `get_global_parser`, `JsonArgumentParser`). Split from `cli.py` so domain code can build a namespace without importing the entrypoint |
| `paths.py` | Filesystem path helpers (`expand_path`, `display_path`, `path_for_message`, `exception_for_message`) shared by CLI and domain |
Expand Down Expand Up @@ -126,17 +128,19 @@ Published on PyPI as `platecli`; the installed command is `plate`.
| **Wheel** | Runtime `bambu_cli` package only — no docs, scripts, or tests |
| **Sdist** | Runtime + tests/scripts + **ship docs**: `README.md`, `AGENTS.md`, `SECURITY.md`, `CHANGELOG.md`, `docs/api.md`, `docs/manual.md`, `docs/troubleshooting.md`, `docs/schemas/*` |

**Repo-only (never in sdist/wheel):** `CONTRIBUTING.md`, `docs/quality-roadmap.md`, `docs/test-backlog.md`, `docs/mutation-baseline.md`, `docs/live-printer-smoke.md`, and local agent notes (not in repo). Enforced by `MANIFEST.in` + `tests/package_contents_smoke.py`.
**Repo-only (never in sdist/wheel):** `CONTRIBUTING.md`, `docs/quality-roadmap.md`, `docs/test-backlog.md`, `docs/mutation-baseline.md`, `docs/live-printer-smoke.md`, `docs/releasing.md`, `docs/README.md`, `docs/plans/*`, and local agent notes (not in repo). `MANIFEST.in` ships only the files it lists, and `tests/package_contents_smoke.py` additionally asserts the first six are absent from the sdist (`FORBIDDEN_SDIST_FILES`).

## Quality gates (agents)

| Gate | Command / note |
|------|----------------|
| Default tests | `uv run python -m pytest tests/ -q -m "not live"` — never contacts a printer |
| Coverage (CI) | `--cov-fail-under=83` (CI run `30632442521`, 2026-07-31: Windows 88.09% / Linux 88.51% / macOS 88.33%; A+ target **92%** — see roadmap) |
| Coverage (CI) | `--cov-fail-under=83` (CI run `31044588411` on `5b08720`, 2026-08-05: Windows 88.8% / Linux 3.9 89.3%, 3.12 89.2%, 3.14 89.2% / macOS 89.1%; A+ target **92%** — see roadmap) |
| Lint | `uvx ruff check bambu_cli` + `uvx ruff format --check bambu_cli` |
| Types | `uvx mypy -p bambu_cli` |
| Security lint | `uvx bandit -c pyproject.toml -r bambu_cli -ll` |
| Dependency audit | `pip-audit` over the exported lockfile (blocking high+; CI-only step) |
| Contracts & layers | `python scripts/gen_schemas.py --check` and `python scripts/check_layers.py` — both blocking in the same lint job |
| Smokes (not pytest) | `syntax`, `cli_help`, `ci_workflow`, `python_compat`, `dependency_resolution`, `release_readiness`, `privacy`, `agent_cli` — see [CONTRIBUTING.md](CONTRIBUTING.md). A green pytest says nothing about these. |
| Mutation baseline | `./scripts/run_mutation_baseline.sh` — nightly / `workflow_dispatch` only; [docs/mutation-baseline.md](docs/mutation-baseline.md) |
| Live printer | Opt-in only: `BAMBU_LIVE=1` + real config + `BAMBU_LIVE_SOURCE`. [docs/live-printer-smoke.md](docs/live-printer-smoke.md). Always ask the user before `--confirm` or `BAMBU_LIVE_PRINT_CONFIRM`. |
Expand Down
25 changes: 21 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ uv sync --extra test # test deps (pytest etc.) live in the "test" extra
Plain `uv sync` installs runtime deps only — the test commands below then fail
with `No module named pytest`. CI installs the same set via `uv pip install '.[test]'`.

The `test` extra also pulls in `textual`, so the `plate tui` pilot tests run in the
default suite. The user-facing install is the separate `[tui]` extra
(`pip install 'platecli[tui]'`); Textual is never a runtime dependency.

## Running tests

```bash
Expand Down Expand Up @@ -91,8 +95,17 @@ uvx ruff format --check bambu_cli
uvx mypy -p bambu_cli # full package; check_untyped_defs; no residual excludes
uvx bandit -c pyproject.toml -r bambu_cli -ll
# pip-audit is also blocking in CI (dependency high/critical)

# Also blocking in the same CI job, and cheap to run locally:
python scripts/check_layers.py # import-layer boundaries
uv run --python 3.12 --with pydantic python scripts/gen_schemas.py --check
```

The lint job additionally runs three greps (no test-awareness in production code,
`sys.exit` only in `cli.py`, no `@mockable`) and a `-m "security or contract"`
pytest pass. `gen_schemas.py` is pinned to 3.12 because it needs 3.10+ to evaluate
the contracts' `X | None` annotations — the package itself still runs on 3.9.

CI pins these tool versions (see `.github/workflows/ci.yml`); running them unpinned locally is fine.

A green `pytest` does **not** mean lint/types/security gates are green.
Expand All @@ -106,10 +119,14 @@ Agent/runtime rules: **[AGENTS.md](AGENTS.md)** (ships in sdist).
Threat model: **[SECURITY.md](SECURITY.md)** (ships in sdist).
JSON contracts: **[docs/api.md](docs/api.md)** + **[docs/schemas/](docs/schemas/)** (ship in sdist).

As of the 2026-07 codebase audit: overall **solid A− / A**. Main gaps to A+ / 1.0 are
coverage (~82% vs target 92%), domain→`cli` helper extraction, single-sourced TLS pin
verification, remaining JSON schemas, and a few camera-hardening items documented in
SECURITY.md.
As of 2026-08-05 (0.5.0): overall **solid A− / A**. The 2026-07 audit's four
architecture/contract gaps have since closed — the domain→`cli` helper extraction
(B.4), the single-sourced TLS pin verification (B.5), the remaining JSON schemas
(now *generated* from `bambu_cli/contracts/`, one per `--json` subcommand), and the
camera bind/pin-fallback hardenings. Main gaps to A+ / 1.0 are now coverage
(89.2% measured on CI's Linux legs, CI floor **83**, target 92) and the camera
residuals still listed in SECURITY.md. Do not read "A−/A" as "A+" — see the
scoreboard for what is actually ticked.

## Code conventions

Expand Down
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ plate setup
plate doctor # optional: verify the connection end to end
```

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/DLANSAMA/platecli/main/docs/doctor-dark.gif">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/DLANSAMA/platecli/main/docs/doctor-light.gif">
<img alt="plate doctor: config, MQTT, and FTPS health checks with TLS-pin verification against a real printer" src="https://raw.githubusercontent.com/DLANSAMA/platecli/main/docs/doctor-dark.gif">
</picture>

Now go from a link on the internet to plastic on the bed:

```bash
Expand All @@ -107,27 +113,30 @@ It walks you from a model URL (or local file) to a running print without touchin
### Watch the printer while it works

```bash
pip install 'platecli[tui]'
pip install 'platecli[tui]' # or: pipx install 'platecli[tui]'
# or: uv tool install 'platecli[tui]'
plate tui # or: plate tui --sim to explore it without a printer
```

<sub>Install the extra the same way you installed `plate` — a `pip install` into your
shell's Python does not reach a `pipx` / `uv tool` environment. Already installed
without it? `pipx install --force 'platecli[tui]'` (or `pipx inject platecli textual`).</sub>

`plate tui` is a live view of your printer: state, temperatures, layer and progress, and the AMS trays, on one screen that keeps updating — plus a job monitor that follows a running print to completion. You can start a print from it too, through the same prepare-and-confirm flow the wizard uses, so you never have to leave the screen.

It is a front-end, not new machinery: it slices and builds the `job` request through the same shared code `plate go` runs, so the two cannot drift. Every safety rule holds — a print only ever starts from the confirm dialog, cancelling keeps the sliced file, and leaving the monitor never stops a print. Textual is an optional extra and never a runtime dependency, so `plate go` keeps working with nothing extra installed on SSH, dumb terminals, and with screen readers.

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/DLANSAMA/platecli/main/docs/doctor-dark.gif">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/DLANSAMA/platecli/main/docs/doctor-light.gif">
<img alt="plate doctor: config, MQTT, and FTPS health checks with TLS-pin verification against a real printer" src="https://raw.githubusercontent.com/DLANSAMA/platecli/main/docs/doctor-dark.gif">
</picture>
<p align="center">
<img alt="plate tui: a live printer dashboard — state, temperatures, progress and AMS trays — and the two-column prepare screen it starts prints from" src="https://raw.githubusercontent.com/DLANSAMA/platecli/main/docs/tui.gif">
</p>

## Why platecli

- **One command, whole pipeline** — `plate job <url>` downloads, slices, uploads, and prints in one shot; or run `download` / `slice` / `upload` / `print` individually.
- **Fully local & private** — talks straight to the printer over your LAN; no Bambu cloud account, ever.
- **Deliberate-action gate** — physical commands refuse without `--confirm` (exit `5`), so a typo, a truncated argument list, or a replayed read-only command can't start a print. It is a gate against *accidents*, not an authorization boundary: `plate` cannot tell your `--confirm` from an agent's, so anything you let run `plate` can pass the flag. Sandbox agents accordingly.
- **AI-agent ready** — every command speaks `--json` with published schemas, plus a `--sim` mode for hardware-free automation.
- **Watch it live** — `plate status --monitor` follows a print with a live progress bar until it finishes.
- **Watch it live** — `plate status --monitor` follows a print with a live progress bar until it finishes, or run the full-screen `plate tui` (optional `[tui]` extra) for a dashboard you can also start a print from.
- **Fixes itself findable** — `plate doctor` checks network, FTPS, and MQTT health and tells you exactly what's wrong.
- **Hardened where it counts** — TLS certificate pinning, SSRF-guarded downloads, and size-capped ZIP extraction.

Expand Down Expand Up @@ -155,7 +164,7 @@ you can put in a shell script or hand to an agent, use this.

## Built for AI agents

Every command emits machine-readable `--json` output backed by published [JSON Schemas](https://github.com/DLANSAMA/platecli/tree/main/docs/schemas/), `--sim` provides a full fake printer for development without hardware, and the `--confirm` gate means physical actions never happen by accident. See the [user guide](https://github.com/DLANSAMA/platecli/blob/main/docs/manual.md) and [docs/api.md](https://github.com/DLANSAMA/platecli/blob/main/docs/api.md) for the JSON contracts and stability policy.
Every command emits machine-readable `--json` output backed by published [JSON Schemas](https://github.com/DLANSAMA/platecli/tree/main/docs/schemas/), `--sim` provides a full fake printer for development without hardware, and the `--confirm` gate means physical actions never happen by accident. Two commands are deliberately human-only — the `go` wizard and the `tui` full-screen UI refuse `--json` and a non-TTY stdin with exit `5`; `plate job <url> --confirm` is the machine path that does the same work. See the [user guide](https://github.com/DLANSAMA/platecli/blob/main/docs/manual.md) and [docs/api.md](https://github.com/DLANSAMA/platecli/blob/main/docs/api.md) for the JSON contracts and stability policy.

## Documentation

Expand Down
9 changes: 8 additions & 1 deletion bambu_cli/tui/styles.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,15 @@ AmsPanel {
height: 1fr;
}

/* 62, not 60: the longest radio label is the AMS-detected material
("PLA — easy, rigid, most models (detected in AMS)") at 49 cells, and a
RadioButton adds 4 for its toggle and padding = 53. A 60-wide column leaves
the RadioSet 54 cells of content — one to spare — so the moment the form is
taller than the terminal and this column grows a scrollbar, content drops to
52 and the closing paren is clipped. Two extra columns absorb the scrollbar
instead of silently truncating which material was detected. */
#prepare-inputs {
width: 60;
width: 62;
height: 100%;
padding: 0 2 0 0;
}
Expand Down
Binary file removed docs/job-hero.mp4
Binary file not shown.
2 changes: 1 addition & 1 deletion docs/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Best-match-first, exactly as auto-detection tries them:

| Platform | Binary | `profiles/BBL` |
|---|---|---|
| Linux | `orca-slicer` / `OrcaSlicer` / `orcaslicer` on `$PATH`, then `/usr/bin/orca-slicer`, `/usr/local/bin/orca-slicer`, `/opt/OrcaSlicer/orca-slicer`, `/var/lib/flatpak/exports/bin/io.github.softfever.OrcaSlicer`, `~/.local/share/flatpak/exports/bin/io.github.softfever.OrcaSlicer`, `~/Applications/OrcaSlicer.AppImage`, `~/tools/OrcaSlicer.AppImage` | `/usr/share/OrcaSlicer/resources/profiles/BBL`, `/opt/OrcaSlicer/resources/profiles/BBL`, `~/tools/squashfs-root/resources/profiles/BBL` |
| Linux | `orca-slicer` / `OrcaSlicer` / `orcaslicer` on `$PATH`, then `/usr/bin/orca-slicer`, `/usr/local/bin/orca-slicer`, `/opt/OrcaSlicer/orca-slicer`, the Flatpak exports (`{/var/lib,~/.local/share}/flatpak/exports/bin/<app-id>` for **both** app ids — current `com.orcaslicer.OrcaSlicer` first, legacy `io.github.softfever.OrcaSlicer` after), `~/Applications/OrcaSlicer.AppImage`, `~/tools/OrcaSlicer.AppImage` | `/usr/share/OrcaSlicer/resources/profiles/BBL`, `/opt/OrcaSlicer/resources/profiles/BBL`, the Flatpak app trees (`{/var/lib,~/.local/share}/flatpak/app/<app-id>/current/active/files/share/OrcaSlicer/resources/profiles/BBL`, both app ids — these are inferred, see the note above), `~/tools/squashfs-root/resources/profiles/BBL` |
| macOS | `/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer`, `~/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer` | the matching `.../Contents/Resources/profiles/BBL` |
| Windows | Each of the three directories is probed for `orca-slicer.exe` first (current installer), then `OrcaSlicer.exe` (older builds): `%PROGRAMFILES%\OrcaSlicer\`, `%LOCALAPPDATA%\Programs\OrcaSlicer\`, `%PROGRAMFILES(X86)%\OrcaSlicer\` | the matching `...\OrcaSlicer\resources\profiles\BBL` |

Expand Down
4 changes: 2 additions & 2 deletions docs/mutation-baseline.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Mutation testing baseline (Phase 3)

**Baseline date:** 2026-07-09 (scores below; re-run before changing the floor)
**Doc refresh:** 2026-07-17 (scope/floor unchanged)
**Baseline date:** 2026-07-09 (original widened baseline; retained below for comparison)
**Current measurement:** 2026-08-04 — **50.7%**, floor raised 40 → **48** (re-run before changing the floor again)
**Tool:** mutmut 3.6.0
**Reproduce:** `./scripts/run_mutation_baseline.sh`
**CI:** `.github/workflows/mutation.yml` — `workflow_dispatch` + nightly `schedule` only
Expand Down
13 changes: 9 additions & 4 deletions docs/plans/interactive-mode-plan.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Implementation plan: interactive mode (`plate go`)

**Status:** Draft for implementation — hand-off document for the implementing agent.
**Prerequisite:** Ship **0.4.0** first (current `main` is `0.4.0.dev0` with an active
Unreleased changelog). Interactive mode targets **0.5.0** on a stable base. Do not
start Phase 1 until the 0.4.0 tag exists (see `docs/releasing.md`).
**Status:** **Implemented — `plate go` shipped in 0.4.0.** Kept as the design record
and rationale; it is no longer a to-do list, and the "current version" statements
below are frozen at the time of writing. For current behaviour read
[the manual](../manual.md#guided-mode-plate-go), not this file.

*(Historical, as written:)* **Prerequisite:** Ship **0.4.0** first (`main` was
`0.4.0.dev0` with an active Unreleased changelog). Interactive mode targeted
**0.5.0** on a stable base; do not start Phase 1 until the 0.4.0 tag exists
(see `docs/releasing.md`).

## 1. Goal

Expand Down
Loading