ADR-0021 — Curl-bash installer, CLI rename, and multi-package PyPI release scaffold
- Status: Accepted (build mode), 2026-05-24
- Deciders: Adrian Llopart (TSC)
- Supersedes: —
- Related: ADR-0004 (monorepo), ADR-0010 (inference runner), ADR-0011 (libero ↔ robocasa exclusion), CLAUDE.md §1.13/§1.14/§4
Context
The OpenRAL workspace ships 13 distributable Python packages under
python/*, four optional simulator backends, and a sudo+apt ROS 2 system
bootstrap. The pre-existing installation flow is:
git clone … && cd openral
just bootstrap # apt + uv + ROS 2 Jazzy + libusb
uv sync --all-packages
uv run ral … # CLI never on $PATH
Three pain points fall out of this:
- No frictionless entry. A user who wants to "try the CLI" must clone
the repo, run a sudo-gated bash script, then prefix every command with
uv run. There is no equivalent ofcurl -fsSL https://claude.ai/install.sh | bash. - CLI name mismatch. The console script is
ral, which is a poor discoverability handle (ralis also the Polish word for "moray" —gitgrep collisions are common) and does not match the project name. The ergonomic name isopenral. - No PyPI presence.
openral-core,openral-cli, et al. are not published. The Tier-0 installer therefore has nothing to install from except git, and downstream consumers cannotpip install openral-cli.
Decision
1. Rename the CLI to openral and add an interactive REPL
python/cli/pyproject.toml[project.scripts]ships a single canonical entry point:openral = "openral_cli.main:app". Noralalias, no deprecation banner.- The Typer app uses
invoke_without_command=True. Whenopenralis invoked with no subcommand, the root callback drops into an interactive REPL (openral_cli.main._run_repl): - Prints an ASCII banner + the subtitle "The open-source agentic layer for physical AI".
- Reads lines from
input()(stdlibreadlineenabled when available for arrow-key history — noprompt_toolkitdependency, so the Tier-0 install stays atuv tool install openral-cli). - Each line is
shlex.split-tokenised and re-dispatched through the same Typer app withstandalone_mode=False, so subcommands run bare:sim run --config foo.yamlinside the REPL is equivalent toopenral sim run --config foo.yamlon the shell. exit/quit/:q/ Ctrl-D leave the REPL;help/?runs--help.UsageError,Abort, andSystemExitfrom subcommands are caught so a single bad invocation does not tear down the session.- When
openralis invoked with a subcommand the behaviour is unchanged: single one-shot run, no banner, OTel tracing scope opened per ADR-0010. This keepsopenral skill list | jq …pipelines clean. - All in-repo references (README, docs, Justfile, CLAUDE.md §4) use
openralper CLAUDE.md §1.14.
2. Tiered curl-bash installer (scripts/install.sh)
The installer is explicitly Tier-0 only — it does what curl-bash can honestly do without surprising the user with sudo or 10 GB of CUDA wheels:
| Step | Action | sudo? | Time |
|---|---|---|---|
| 1 | Detect OS / arch, refuse to run as root | no | <1 s |
| 2 | Install uv via curl -LsSf https://astral.sh/uv/install.sh \| sh if missing |
no | ~5 s |
| 3 | uv python install 3.12 (uv-managed CPython, no apt) |
no | ~10 s |
| 4 | uv tool install --python 3.12 openral-cli |
no | ~15 s |
| 5 | Verify ~/.local/bin/openral exists; print PATH hint if needed |
no | <1 s |
| 6 | Print the opt-in openral install <group> menu |
no | <1 s |
Heavier groups (sim, libero, metaworld, maniskill3, simpler-env,
robocasa, rldx) layer in via the new openral install <group>
subcommand, which calls uv pip install --python <tool-venv-python> against
mirrored copies of the workspace [dependency-groups] table. The
sudo-gated ros group re-exec's scripts/bootstrap_ubuntu.sh /
scripts/bootstrap_macos.sh with a clear "this needs sudo" banner.
The libero ↔ robocasa mutual-exclusion declared in the root
[tool.uv].conflicts table (ADR-0011) is enforced inside
openral install as a typed ROSConfigError with a --force escape
hatch.
3. Multi-package PyPI release workflow
.github/workflows/release-pypi.yml is the canonical, single release
workflow (see the 2026-06-17 amendment for the consolidation):
- Two ways to publish, both via PyPI Trusted Publishing
(
pypa/gh-action-pypi-publish@release/v1, OIDC, no long-lived token): workflow_dispatchwith atargetchoice —testpypi(default, no confirmation) orpypi(requiresconfirm=YES).- tag push
v*.*.*— production release to real PyPI; the tag is the deliberate confirmation. - A
resolvejob picks the index + enforces the real-PyPI guard; aprecheckjob runs the same ruff + mypy + schema-drift +mkdocs --strictgate as the PRqualityworkflow, so a release cannot publish a broken tree. - The publish matrix lists every distributable workspace member
(14 packages today, incl.
openral-state-adapter). - The TestPyPI path is usable now (register a TestPyPI trusted publisher, or upload locally with twine). The real-PyPI path remains blocked until:
- Registering each
openral-*name on PyPI under the openral org. - Configuring the Trusted Publisher entry on PyPI for this repo + this workflow file.
Until then the Tier-0 installer ships pointing at PyPI (spec=openral-cli).
Operators bridging the pre-publish gap can override with
OPENRAL_INSTALL_SOURCE=git+https://github.com/OpenRAL/openral.
4. Version pin: all packages stay at 0.1.x
Per the feat/cleaner-cli directive, every python/*/pyproject.toml keeps
version = "0.1.0". The first published release will tag v0.1.0 after
the Trusted Publishing wiring lands.
Consequences
- Positive
openralis on$PATH≤30 s after a curl-bash install, no sudo, no clone.- The installation matrix (sim / libero / robocasa / ros) is explicit and
enforced by typed errors, not by silently-failing
uv pip installs. -
Multi-package release pipeline is defined and reviewable today; flipping it on is a small follow-up PR after PyPI namespace setup.
-
Negative / accepted tradeoffs
- Two installation flows now exist (curl-bash for end users,
just bootstrapfor contributors). CLAUDE.md §4 keepsjust bootstrapcanonical for development; README.md leads with the curl one-liner for discovery. - The REPL adds a stateful interactive path on top of the one-shot surface, but the dispatcher reuses the same Typer app so there is one canonical command tree (no parallel REPL-only command set to keep in sync).
openral installduplicates the[dependency-groups]table from the workspace root pyproject — drift is caught bytests/unit/test_install_command.pywhich loads the root file when present and asserts the two stay in lockstep.
Amendments
- 2026-05-19 — initial Decision. Release workflow ships disabled; will be enabled in a follow-up after PyPI namespace + Trusted Publishing setup.
- 2026-05-23 — §4 reaffirmed. All thirteen workspace packages
(including
openral-core) sit atversion = "0.1.0". A short-livedopenral-core 0.3.0bump that landed alongsideSceneDefaults/TopCameraDefaultswas reverted to0.1.0to keep the entire workspace lockstep until the first PyPI publish. On-diskschema_versionstays at"0.1"per master commit39cb622(drop schema migrators while pre-publish; on-disk shape evolves in place). Net directive: until the first publish, neither package versions nor on-disk schema_version are bumped — both stay at"0.1"/"0.1.0", additions land in place, real-fixture tests prove the shape change. - 2026-05-24 — ADR index renumbering. ADR-0021 retains its number; former collisions renumbered to ADR-0022 (rSkill action vocabulary) and ADR-0023 (data-driven MuJoCo HAL).
- 2026-06-17 — Release-pipeline consolidation. The separate
release.yml(tag-triggered) was dropped: its rootuv buildwas broken (the workspace root has no[build-system], so it fell back to legacy setuptools and errored on multi-package discovery) and its PyPI half duplicatedrelease-pypi.ymlwhile building the wrong artifact (the meta-package). The ghcr runtime-image + cosign path it carried was non-functional (Dockerfile.runtimeCOPYs the gitignored colconinstall/space) and is deferred to a future purpose-builtrelease-image.ymlonce ROS-in-CI infra exists.release-pypi.ymlis now the single source of truth (§3): tag-push trigger live, atargetchoice adds a TestPyPI path, aprecheckgate guards against publishing a broken tree, andopenral-state-adapterwas added to the matrix (13 → 14).