Summary
plain shell initializes the runtime by setting PYTHONSTARTUP to a startup file that calls plain.runtime.setup(). Python only honors PYTHONSTARTUP in interactive sessions, so when stdin is a pipe (heredoc, redirected file, etc.) the startup file is silently skipped, the package registry never loads, and any from app.<pkg>.models import ... raises PackageRegistryNotReady.
The failure is opaque — there's no hint that the issue is "you ran this non-interactively." plain shell -c \"...\" and plain run <script> work because they explicitly prepend import plain.runtime; plain.runtime.setup() before the user code.
Repro
In any Plain project with at least one local package:
```sh
uv run plain shell <<'PY'
from app.users.models import User
print(User.query.count())
PY
```
Yields:
```
Traceback (most recent call last):
File "", line 1, in
File ".../plain/postgres/registry.py", line 214, in register_model
model_class.model_options.package_label,
...
plain.exceptions.PackageRegistryNotReady: Packages aren't loaded yet.
```
Workarounds: `plain shell -c "..."` or `plain run script.py`.
Suggested fix
Either:
- In `plain/cli/shell.py`, detect non-TTY stdin (`sys.stdin.isatty() is False`) and route to the same `-c`-style path that prepends `plain.runtime.setup()` (e.g. read stdin and `exec` it after setup, like `plain run` does for files).
- Or, at minimum, emit a clearer error when `PackageRegistryNotReady` is raised from a CLI entry — pointing users at `plain run` / `plain shell -c`.
(1) is the friendlier behavior and matches what most users probably expect from `plain shell <<EOF ... EOF`.
Environment
- plain 0.139.0
- Python 3.13.3
- macOS 25.4.0
Surfaced while smoke-testing new models in a fresh `plain-start` app — piped a heredoc into `plain shell`, hit `PackageRegistryNotReady`, took a minute to realize `PYTHONSTARTUP` was the culprit. Logging it as a small UX gap, not a bug.
Summary
plain shellinitializes the runtime by settingPYTHONSTARTUPto a startup file that callsplain.runtime.setup(). Python only honorsPYTHONSTARTUPin interactive sessions, so when stdin is a pipe (heredoc, redirected file, etc.) the startup file is silently skipped, the package registry never loads, and anyfrom app.<pkg>.models import ...raisesPackageRegistryNotReady.The failure is opaque — there's no hint that the issue is "you ran this non-interactively."
plain shell -c \"...\"andplain run <script>work because they explicitly prependimport plain.runtime; plain.runtime.setup()before the user code.Repro
In any Plain project with at least one local package:
```sh
uv run plain shell <<'PY'
from app.users.models import User
print(User.query.count())
PY
```
Yields:
```
Traceback (most recent call last):
File "", line 1, in
File ".../plain/postgres/registry.py", line 214, in register_model
model_class.model_options.package_label,
...
plain.exceptions.PackageRegistryNotReady: Packages aren't loaded yet.
```
Workarounds: `plain shell -c "..."` or `plain run script.py`.
Suggested fix
Either:
(1) is the friendlier behavior and matches what most users probably expect from `plain shell <<EOF ... EOF`.
Environment
Surfaced while smoke-testing new models in a fresh `plain-start` app — piped a heredoc into `plain shell`, hit `PackageRegistryNotReady`, took a minute to realize `PYTHONSTARTUP` was the culprit. Logging it as a small UX gap, not a bug.