plain 0.78.1__tar.gz → 0.88.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plain-0.78.1 → plain-0.88.0}/.gitignore +3 -3
- {plain-0.78.1 → plain-0.88.0}/PKG-INFO +2 -1
- {plain-0.78.1 → plain-0.88.0}/plain/CHANGELOG.md +201 -0
- {plain-0.78.1 → plain-0.88.0}/plain/assets/views.py +7 -2
- plain-0.88.0/plain/cli/__init__.py +4 -0
- plain-0.88.0/plain/cli/agent/__init__.py +17 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/agent/docs.py +1 -1
- plain-0.88.0/plain/cli/agent/md.py +106 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/agent/request.py +2 -2
- {plain-0.78.1 → plain-0.88.0}/plain/cli/build.py +1 -1
- {plain-0.78.1 → plain-0.88.0}/plain/cli/changelog.py +3 -1
- {plain-0.78.1 → plain-0.88.0}/plain/cli/chores.py +2 -6
- {plain-0.78.1 → plain-0.88.0}/plain/cli/core.py +61 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/docs.py +1 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/formatting.py +28 -10
- {plain-0.78.1 → plain-0.88.0}/plain/cli/install.py +1 -1
- {plain-0.78.1 → plain-0.88.0}/plain/cli/preflight.py +27 -75
- plain-0.88.0/plain/cli/registry.py +131 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/runtime.py +17 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/scaffold.py +1 -6
- {plain-0.78.1 → plain-0.88.0}/plain/cli/server.py +1 -3
- {plain-0.78.1 → plain-0.88.0}/plain/cli/shell.py +5 -5
- {plain-0.78.1 → plain-0.88.0}/plain/cli/upgrade.py +4 -2
- {plain-0.78.1 → plain-0.88.0}/plain/cli/urls.py +2 -3
- {plain-0.78.1 → plain-0.88.0}/plain/cli/utils.py +1 -1
- {plain-0.78.1 → plain-0.88.0}/plain/csrf/middleware.py +44 -43
- {plain-0.78.1 → plain-0.88.0}/plain/exceptions.py +2 -2
- plain-0.88.0/plain/forms/__init__.py +59 -0
- {plain-0.78.1 → plain-0.88.0}/plain/forms/boundfield.py +1 -1
- {plain-0.78.1 → plain-0.88.0}/plain/forms/fields.py +192 -43
- {plain-0.78.1 → plain-0.88.0}/plain/forms/forms.py +8 -1
- plain-0.88.0/plain/http/README.md +142 -0
- {plain-0.78.1 → plain-0.88.0}/plain/http/__init__.py +5 -3
- plain-0.88.0/plain/http/middleware.py +32 -0
- {plain-0.78.1 → plain-0.88.0}/plain/http/multipartparser.py +2 -2
- {plain-0.78.1 → plain-0.88.0}/plain/http/request.py +20 -6
- {plain-0.78.1 → plain-0.88.0}/plain/http/response.py +8 -6
- {plain-0.78.1 → plain-0.88.0}/plain/internal/files/base.py +1 -1
- {plain-0.78.1 → plain-0.88.0}/plain/internal/files/temp.py +2 -1
- {plain-0.78.1 → plain-0.88.0}/plain/internal/files/uploadedfile.py +10 -7
- {plain-0.78.1 → plain-0.88.0}/plain/internal/files/uploadhandler.py +13 -8
- {plain-0.78.1 → plain-0.88.0}/plain/internal/files/utils.py +4 -1
- {plain-0.78.1 → plain-0.88.0}/plain/internal/handlers/base.py +8 -14
- {plain-0.78.1 → plain-0.88.0}/plain/internal/handlers/exception.py +30 -46
- {plain-0.78.1 → plain-0.88.0}/plain/internal/handlers/wsgi.py +5 -3
- plain-0.88.0/plain/internal/middleware/headers.py +59 -0
- {plain-0.78.1 → plain-0.88.0}/plain/internal/middleware/hosts.py +3 -8
- {plain-0.78.1 → plain-0.88.0}/plain/internal/middleware/https.py +5 -5
- {plain-0.78.1 → plain-0.88.0}/plain/internal/middleware/slash.py +3 -8
- {plain-0.78.1 → plain-0.88.0}/plain/logs/README.md +37 -0
- plain-0.88.0/plain/logs/__init__.py +3 -0
- plain-0.88.0/plain/logs/configure.py +85 -0
- plain-0.88.0/plain/logs/filters.py +15 -0
- {plain-0.78.1 → plain-0.88.0}/plain/logs/formatters.py +1 -1
- plain-0.88.0/plain/preflight/README.md +83 -0
- {plain-0.78.1 → plain-0.88.0}/plain/preflight/results.py +1 -1
- {plain-0.78.1 → plain-0.88.0}/plain/runtime/__init__.py +1 -0
- {plain-0.78.1 → plain-0.88.0}/plain/runtime/global_settings.py +7 -5
- {plain-0.78.1 → plain-0.88.0}/plain/server/arbiter.py +9 -9
- {plain-0.78.1 → plain-0.88.0}/plain/server/http/body.py +2 -2
- {plain-0.78.1 → plain-0.88.0}/plain/server/http/errors.py +7 -2
- {plain-0.78.1 → plain-0.88.0}/plain/server/http/message.py +3 -2
- {plain-0.78.1 → plain-0.88.0}/plain/server/http/parser.py +3 -2
- {plain-0.78.1 → plain-0.88.0}/plain/server/sock.py +19 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/workers/base.py +11 -9
- {plain-0.78.1 → plain-0.88.0}/plain/server/workers/sync.py +5 -3
- {plain-0.78.1 → plain-0.88.0}/plain/server/workers/thread.py +8 -7
- {plain-0.78.1 → plain-0.88.0}/plain/signals/__init__.py +0 -1
- {plain-0.78.1 → plain-0.88.0}/plain/test/client.py +15 -34
- {plain-0.78.1 → plain-0.88.0}/plain/test/exceptions.py +2 -5
- {plain-0.78.1 → plain-0.88.0}/plain/urls/patterns.py +4 -0
- {plain-0.78.1 → plain-0.88.0}/plain/urls/resolvers.py +1 -1
- {plain-0.78.1 → plain-0.88.0}/plain/utils/cache.py +5 -5
- {plain-0.78.1 → plain-0.88.0}/plain/utils/datastructures.py +4 -2
- {plain-0.78.1 → plain-0.88.0}/plain/utils/functional.py +2 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/html.py +12 -7
- {plain-0.78.1 → plain-0.88.0}/plain/utils/regex_helper.py +2 -2
- {plain-0.78.1 → plain-0.88.0}/plain/utils/text.py +2 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/tree.py +8 -5
- {plain-0.78.1 → plain-0.88.0}/plain/validators.py +24 -9
- {plain-0.78.1 → plain-0.88.0}/plain/views/README.md +7 -17
- {plain-0.78.1 → plain-0.88.0}/plain/views/base.py +2 -2
- {plain-0.78.1 → plain-0.88.0}/plain/views/errors.py +3 -2
- {plain-0.78.1 → plain-0.88.0}/plain/views/forms.py +1 -1
- {plain-0.78.1 → plain-0.88.0}/plain/views/objects.py +17 -20
- {plain-0.78.1 → plain-0.88.0}/pyproject.toml +4 -9
- {plain-0.78.1 → plain-0.88.0}/tests/test_csrf.py +19 -20
- {plain-0.78.1 → plain-0.88.0}/tests/test_logs.py +1 -1
- plain-0.78.1/plain/cli/__init__.py +0 -3
- plain-0.78.1/plain/cli/agent/__init__.py +0 -20
- plain-0.78.1/plain/cli/agent/md.py +0 -92
- plain-0.78.1/plain/cli/registry.py +0 -49
- plain-0.78.1/plain/csrf/views.py +0 -34
- plain-0.78.1/plain/forms/__init__.py +0 -8
- plain-0.78.1/plain/http/README.md +0 -30
- plain-0.78.1/plain/internal/middleware/headers.py +0 -34
- plain-0.78.1/plain/logs/__init__.py +0 -3
- plain-0.78.1/plain/logs/configure.py +0 -37
- plain-0.78.1/plain/logs/utils.py +0 -56
- plain-0.78.1/plain/preflight/README.md +0 -61
- {plain-0.78.1 → plain-0.88.0}/LICENSE +0 -0
- {plain-0.78.1 → plain-0.88.0}/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/AGENTS.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/__main__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/assets/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/assets/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/assets/compile.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/assets/finders.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/assets/urls.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/chores/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/chores/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/chores/core.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/chores/registry.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/agent/llmdocs.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/agent/prompt.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/output.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/print.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/settings.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/cli/startup.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/csrf/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/debug.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/forms/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/forms/exceptions.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/http/cookie.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/internal/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/internal/files/locks.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/internal/files/move.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/internal/reloader.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/json.py +0 -0
- /plain-0.78.1/plain/logs/loggers.py → /plain-0.88.0/plain/logs/app.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/logs/debug.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/packages/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/packages/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/packages/config.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/packages/registry.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/paginator.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/preflight/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/preflight/checks.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/preflight/files.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/preflight/registry.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/preflight/security.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/preflight/urls.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/runtime/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/runtime/user_settings.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/runtime/utils.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/LICENSE +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/app.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/config.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/errors.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/glogging.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/http/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/http/unreader.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/http/wsgi.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/pidfile.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/util.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/workers/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/server/workers/workertmp.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/signals/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/signing.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/templates/AGENTS.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/templates/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/templates/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/templates/core.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/test/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/test/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/test/encoding.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/urls/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/urls/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/urls/converters.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/urls/exceptions.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/urls/routers.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/urls/utils.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/README.md +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/crypto.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/dateparse.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/deconstruct.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/decorators.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/duration.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/encoding.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/hashable.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/http.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/inspect.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/ipv6.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/itercompat.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/module_loading.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/safestring.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/timesince.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/utils/timezone.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/views/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/views/exceptions.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/views/redirect.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/views/templates.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/plain/wsgi.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/.gitignore +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/app/.gitignore +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/app/settings.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/app/test/__init__.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/app/urls.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/conftest.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/test_cli.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/test_http_hosts.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/test_runtime.py +0 -0
- {plain-0.78.1 → plain-0.88.0}/tests/test_wsgi.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.88.0
|
|
4
4
|
Summary: A web framework for building products with Python.
|
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
6
7
|
License-File: LICENSE
|
|
7
8
|
Requires-Python: >=3.13
|
|
8
9
|
Requires-Dist: click>=8.0.0
|
|
@@ -1,5 +1,206 @@
|
|
|
1
1
|
# plain changelog
|
|
2
2
|
|
|
3
|
+
## [0.88.0](https://github.com/dropseed/plain/releases/plain@0.88.0) (2025-11-13)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- The `plain.forms` module now uses explicit imports instead of wildcard imports, improving IDE autocomplete and type checking support ([eff36f3](https://github.com/dropseed/plain/commit/eff36f31e8e15f84e11164a44c833aeab096ffbd))
|
|
8
|
+
|
|
9
|
+
### Upgrade instructions
|
|
10
|
+
|
|
11
|
+
- No changes required
|
|
12
|
+
|
|
13
|
+
## [0.87.0](https://github.com/dropseed/plain/releases/plain@0.87.0) (2025-11-12)
|
|
14
|
+
|
|
15
|
+
### What's changed
|
|
16
|
+
|
|
17
|
+
- Internal classes now use abstract base classes with `@abstractmethod` decorators instead of raising `NotImplementedError`, improving type checking and IDE support ([91b329a](https://github.com/dropseed/plain/commit/91b329a8adb477031c4358e638b12f35f19bb85d), [81b5f88](https://github.com/dropseed/plain/commit/81b5f88a4bd39785f6b19c3c00c0ed23a36fb72f), [d2e2423](https://github.com/dropseed/plain/commit/d2e24235f497a92f45d5a21fc83d802897c2dec0), [61e7b5a](https://github.com/dropseed/plain/commit/61e7b5a0c8675aaaf65f0a626ff7959a786dca7f))
|
|
18
|
+
- Updated to latest version of `ty` type checker and fixed type errors and warnings throughout the codebase ([f4dbcef](https://github.com/dropseed/plain/commit/f4dbcefa929058be517cb1d4ab35bd73a89f26b8))
|
|
19
|
+
|
|
20
|
+
### Upgrade instructions
|
|
21
|
+
|
|
22
|
+
- No changes required
|
|
23
|
+
|
|
24
|
+
## [0.86.2](https://github.com/dropseed/plain/releases/plain@0.86.2) (2025-11-11)
|
|
25
|
+
|
|
26
|
+
### What's changed
|
|
27
|
+
|
|
28
|
+
- CLI color output is now enabled in CI environments by checking the `CI` environment variable, matching the behavior of modern tools like uv ([a1500f15ed](https://github.com/dropseed/plain/commit/a1500f15ed))
|
|
29
|
+
|
|
30
|
+
### Upgrade instructions
|
|
31
|
+
|
|
32
|
+
- No changes required
|
|
33
|
+
|
|
34
|
+
## [0.86.1](https://github.com/dropseed/plain/releases/plain@0.86.1) (2025-11-10)
|
|
35
|
+
|
|
36
|
+
### What's changed
|
|
37
|
+
|
|
38
|
+
- The `plain preflight` command now outputs to stderr only when using `--format json`, keeping stdout clean for JSON parsing while avoiding success messages appearing in error logs for text format ([72ebee7729](https://github.com/dropseed/plain/commit/72ebee7729))
|
|
39
|
+
- CLI color handling now follows the CLICOLOR standard with proper priority: `NO_COLOR` > `CLICOLOR_FORCE`/`FORCE_COLOR` > `CLICOLOR` > `isatty` ([c7fea406c5](https://github.com/dropseed/plain/commit/c7fea406c5))
|
|
40
|
+
|
|
41
|
+
### Upgrade instructions
|
|
42
|
+
|
|
43
|
+
- No changes required
|
|
44
|
+
|
|
45
|
+
## [0.86.0](https://github.com/dropseed/plain/releases/plain@0.86.0) (2025-11-10)
|
|
46
|
+
|
|
47
|
+
### What's changed
|
|
48
|
+
|
|
49
|
+
- Log output is now split by severity level: INFO and below go to stdout, WARNING and above go to stderr for proper cloud platform log classification ([52403b15ba](https://github.com/dropseed/plain/commit/52403b15ba))
|
|
50
|
+
- Added `LOG_STREAM` setting to customize log output behavior with options: `"split"` (default), `"stdout"`, or `"stderr"` ([52403b15ba](https://github.com/dropseed/plain/commit/52403b15ba))
|
|
51
|
+
- Log configuration documentation expanded with detailed guidance on output streams and environment variable settings ([52403b15ba](https://github.com/dropseed/plain/commit/52403b15ba))
|
|
52
|
+
|
|
53
|
+
### Upgrade instructions
|
|
54
|
+
|
|
55
|
+
- No changes required (default behavior splits logs to stdout/stderr automatically, but this can be customized via `PLAIN_LOG_STREAM` environment variable if needed)
|
|
56
|
+
|
|
57
|
+
## [0.85.0](https://github.com/dropseed/plain/releases/plain@0.85.0) (2025-11-03)
|
|
58
|
+
|
|
59
|
+
### What's changed
|
|
60
|
+
|
|
61
|
+
- CLI help output now organizes commands into "Common Commands", "Core Commands", and "Package Commands" sections for better discoverability ([73d3a48](https://github.com/dropseed/plain/commit/73d3a48fca))
|
|
62
|
+
- CLI help output has been customized with improved formatting and shortcut indicators showing which commands are shortcuts (e.g., `migrate → models migrate`) ([db882e6](https://github.com/dropseed/plain/commit/db882e6d47))
|
|
63
|
+
- CSRF exception messages now include more detailed context about what was rejected and why (e.g., port mismatches, host mismatches) ([9a8e09c](https://github.com/dropseed/plain/commit/9a8e09c1dc))
|
|
64
|
+
- The `plain agent md` command now saves a combined `AGENTS.md` file to `.plain/` by default when using `plain dev`, making it easier to provide context to coding agents ([786b7a0](https://github.com/dropseed/plain/commit/786b7a0ca1))
|
|
65
|
+
- CLI help text styling has been refined with dimmed descriptions and usage prefixes for improved readability ([d7f7053](https://github.com/dropseed/plain/commit/d7f705398d))
|
|
66
|
+
|
|
67
|
+
### Upgrade instructions
|
|
68
|
+
|
|
69
|
+
- No changes required
|
|
70
|
+
|
|
71
|
+
## [0.84.1](https://github.com/dropseed/plain/releases/plain@0.84.1) (2025-10-31)
|
|
72
|
+
|
|
73
|
+
### What's changed
|
|
74
|
+
|
|
75
|
+
- Added `license = "BSD-3-Clause"` to package metadata in `pyproject.toml` ([8477355](https://github.com/dropseed/plain/commit/8477355e65))
|
|
76
|
+
|
|
77
|
+
### Upgrade instructions
|
|
78
|
+
|
|
79
|
+
- No changes required
|
|
80
|
+
|
|
81
|
+
## [0.84.0](https://github.com/dropseed/plain/releases/plain@0.84.0) (2025-10-29)
|
|
82
|
+
|
|
83
|
+
### What's changed
|
|
84
|
+
|
|
85
|
+
- The `DEFAULT_RESPONSE_HEADERS` setting now supports format string placeholders (e.g., `{request.csp_nonce}`) for dynamic header values instead of requiring a callable function ([5199383128](https://github.com/dropseed/plain/commit/5199383128))
|
|
86
|
+
- Views can now set headers to `None` to explicitly remove default response headers ([5199383128](https://github.com/dropseed/plain/commit/5199383128))
|
|
87
|
+
- Added comprehensive documentation for customizing default response headers including override, remove, and extend patterns ([5199383128](https://github.com/dropseed/plain/commit/5199383128))
|
|
88
|
+
|
|
89
|
+
### Upgrade instructions
|
|
90
|
+
|
|
91
|
+
- If you have `DEFAULT_RESPONSE_HEADERS` configured as a callable function, convert it to a dictionary with format string placeholders:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# Before:
|
|
95
|
+
def DEFAULT_RESPONSE_HEADERS(request):
|
|
96
|
+
nonce = request.csp_nonce
|
|
97
|
+
return {
|
|
98
|
+
"Content-Security-Policy": f"script-src 'self' 'nonce-{nonce}'",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# After:
|
|
102
|
+
DEFAULT_RESPONSE_HEADERS = {
|
|
103
|
+
"Content-Security-Policy": "script-src 'self' 'nonce-{request.csp_nonce}'",
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- If you were overriding default headers to empty strings (`""`) to remove them, change those to `None` instead
|
|
108
|
+
|
|
109
|
+
## [0.83.0](https://github.com/dropseed/plain/releases/plain@0.83.0) (2025-10-29)
|
|
110
|
+
|
|
111
|
+
### What's changed
|
|
112
|
+
|
|
113
|
+
- Added comprehensive Content Security Policy (CSP) documentation explaining how to use nonces with inline scripts and styles ([784f3dd972](https://github.com/dropseed/plain/commit/784f3dd972))
|
|
114
|
+
- The `json_script` utility function now accepts an optional `nonce` parameter for CSP-compliant inline JSON scripts ([784f3dd972](https://github.com/dropseed/plain/commit/784f3dd972))
|
|
115
|
+
|
|
116
|
+
### Upgrade instructions
|
|
117
|
+
|
|
118
|
+
- Any `|json_script` usages need to make sure the second argument is a nonce, not a custom encoder (which is now third)
|
|
119
|
+
|
|
120
|
+
## [0.82.0](https://github.com/dropseed/plain/releases/plain@0.82.0) (2025-10-29)
|
|
121
|
+
|
|
122
|
+
### What's changed
|
|
123
|
+
|
|
124
|
+
- The `DEFAULT_RESPONSE_HEADERS` setting can now be a callable that accepts a request argument, enabling dynamic header generation per request ([cb92905834](https://github.com/dropseed/plain/commit/cb92905834))
|
|
125
|
+
- Added `request.csp_nonce` cached property for generating Content Security Policy nonces ([75071dcc70](https://github.com/dropseed/plain/commit/75071dcc70))
|
|
126
|
+
- Simplified the preflight command by moving `plain preflight check` back to `plain preflight` ([40c2c4560e](https://github.com/dropseed/plain/commit/40c2c4560e))
|
|
127
|
+
|
|
128
|
+
### Upgrade instructions
|
|
129
|
+
|
|
130
|
+
- If you use `plain preflight check`, update to `plain preflight` (the `check` subcommand has been removed for simplicity)
|
|
131
|
+
- If you use `plain preflight check --deploy`, update to `plain preflight --deploy`
|
|
132
|
+
|
|
133
|
+
## [0.81.0](https://github.com/dropseed/plain/releases/plain@0.81.0) (2025-10-22)
|
|
134
|
+
|
|
135
|
+
### What's changed
|
|
136
|
+
|
|
137
|
+
- Removed support for category-specific error template fallbacks like `4xx.html` and `5xx.html` ([9513f7c4fa](https://github.com/dropseed/plain/commit/9513f7c4fa))
|
|
138
|
+
|
|
139
|
+
### Upgrade instructions
|
|
140
|
+
|
|
141
|
+
- If you have `4xx.html` or `5xx.html` error templates, rename them to specific status code templates (e.g., `404.html`, `500.html`) or remove them if you prefer the plain HTTP response fallback
|
|
142
|
+
|
|
143
|
+
## [0.80.0](https://github.com/dropseed/plain/releases/plain@0.80.0) (2025-10-22)
|
|
144
|
+
|
|
145
|
+
### What's changed
|
|
146
|
+
|
|
147
|
+
- CSRF failures now raise `SuspiciousOperation` (HTTP 400) instead of `PermissionDenied` (HTTP 403) ([ad146bde3e](https://github.com/dropseed/plain/commit/ad146bde3e))
|
|
148
|
+
- Error templates can now use category-specific fallbacks like `4xx.html` or `5xx.html` instead of the generic `error.html` ([716cfa3cfc](https://github.com/dropseed/plain/commit/716cfa3cfc))
|
|
149
|
+
- Updated error template documentation with best practices for self-contained `500.html` templates ([55cea3b522](https://github.com/dropseed/plain/commit/55cea3b522))
|
|
150
|
+
|
|
151
|
+
### Upgrade instructions
|
|
152
|
+
|
|
153
|
+
- If you have a `templates/error.html` template, instead create specific error templates for each status code you want to customize (e.g., `400.html`, `403.html`, `404.html`, `500.html`). You can also create category-specific templates like `4xx.html` or `5xx.html` for broader coverage.
|
|
154
|
+
|
|
155
|
+
## [0.79.0](https://github.com/dropseed/plain/releases/plain@0.79.0) (2025-10-22)
|
|
156
|
+
|
|
157
|
+
### What's changed
|
|
158
|
+
|
|
159
|
+
- Response objects now have an `exception` attribute that stores the exception that caused 5xx errors ([0a243ba89c](https://github.com/dropseed/plain/commit/0a243ba89c))
|
|
160
|
+
- Middleware classes now use an abstract base class `HttpMiddleware` with a `process_request()` method ([b960eed6c6](https://github.com/dropseed/plain/commit/b960eed6c6))
|
|
161
|
+
- CSRF middleware now raises `PermissionDenied` instead of rendering a custom `CsrfFailureView` ([d4b93e59b3](https://github.com/dropseed/plain/commit/d4b93e59b3))
|
|
162
|
+
- The `HTTP_ERROR_VIEWS` setting has been removed ([7a4e3a31f4](https://github.com/dropseed/plain/commit/7a4e3a31f4))
|
|
163
|
+
- Standalone `plain-changelog` and `plain-upgrade` executables have been removed in favor of the built-in commands ([07c3a4c540](https://github.com/dropseed/plain/commit/07c3a4c540))
|
|
164
|
+
- Standalone `plain-build` executable has been removed ([99301ea797](https://github.com/dropseed/plain/commit/99301ea797))
|
|
165
|
+
- Removed automatic logging of all HTTP 400+ status codes for cleaner logs ([c2769d7281](https://github.com/dropseed/plain/commit/c2769d7281))
|
|
166
|
+
|
|
167
|
+
### Upgrade instructions
|
|
168
|
+
|
|
169
|
+
- If you have custom middleware, inherit from `HttpMiddleware` and rename your `__call__()` method to `process_request()`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# Before:
|
|
173
|
+
class MyMiddleware:
|
|
174
|
+
def __init__(self, get_response):
|
|
175
|
+
self.get_response = get_response
|
|
176
|
+
|
|
177
|
+
def __call__(self, request):
|
|
178
|
+
response = self.get_response(request)
|
|
179
|
+
return response
|
|
180
|
+
|
|
181
|
+
# After:
|
|
182
|
+
from plain.http import HttpMiddleware
|
|
183
|
+
|
|
184
|
+
class MyMiddleware(HttpMiddleware):
|
|
185
|
+
def process_request(self, request):
|
|
186
|
+
response = self.get_response(request)
|
|
187
|
+
return response
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- Remove any custom `HTTP_ERROR_VIEWS` setting from your configuration - error views are now controlled entirely by exception handlers
|
|
191
|
+
- If you were calling `plain-changelog` or `plain-upgrade` as standalone commands, use `plain changelog` or `plain upgrade` instead
|
|
192
|
+
- If you were calling `plain-build` as a standalone command, use `plain build` instead
|
|
193
|
+
|
|
194
|
+
## [0.78.2](https://github.com/dropseed/plain/releases/plain@0.78.2) (2025-10-20)
|
|
195
|
+
|
|
196
|
+
### What's changed
|
|
197
|
+
|
|
198
|
+
- Updated package metadata to use `[dependency-groups]` instead of `[tool.uv]` for development dependencies, following PEP 735 standard ([1b43a3a272](https://github.com/dropseed/plain/commit/1b43a3a272))
|
|
199
|
+
|
|
200
|
+
### Upgrade instructions
|
|
201
|
+
|
|
202
|
+
- No changes required
|
|
203
|
+
|
|
3
204
|
## [0.78.1](https://github.com/dropseed/plain/releases/plain@0.78.1) (2025-10-17)
|
|
4
205
|
|
|
5
206
|
### What's changed
|
|
@@ -14,6 +14,7 @@ from plain.http import (
|
|
|
14
14
|
ResponseRedirect,
|
|
15
15
|
StreamingResponse,
|
|
16
16
|
)
|
|
17
|
+
from plain.http.response import ResponseHeaders
|
|
17
18
|
from plain.runtime import settings
|
|
18
19
|
from plain.urls import reverse
|
|
19
20
|
from plain.views import View
|
|
@@ -37,7 +38,7 @@ class AssetView(View):
|
|
|
37
38
|
def get_url_path(self) -> str:
|
|
38
39
|
return self.asset_path or self.url_kwargs["path"]
|
|
39
40
|
|
|
40
|
-
def get(self) -> Response | FileResponse:
|
|
41
|
+
def get(self) -> Response | FileResponse | StreamingResponse:
|
|
41
42
|
url_path = self.get_url_path()
|
|
42
43
|
|
|
43
44
|
# Make a trailing slash work, but we don't expect it
|
|
@@ -52,7 +53,11 @@ class AssetView(View):
|
|
|
52
53
|
if redirect_response := self.get_redirect_response(url_path):
|
|
53
54
|
return redirect_response
|
|
54
55
|
|
|
56
|
+
# check_asset_path validates and raises if path is invalid
|
|
57
|
+
# After this point, absolute_path is guaranteed to be a valid str
|
|
55
58
|
self.check_asset_path(absolute_path)
|
|
59
|
+
# Type guard: absolute_path is now str (check_asset_path raises if None/invalid)
|
|
60
|
+
assert absolute_path is not None
|
|
56
61
|
|
|
57
62
|
if encoded_path := self.get_encoded_path(absolute_path):
|
|
58
63
|
absolute_path = encoded_path
|
|
@@ -127,7 +132,7 @@ class AssetView(View):
|
|
|
127
132
|
def get_size(self, path: str) -> int:
|
|
128
133
|
return os.path.getsize(path)
|
|
129
134
|
|
|
130
|
-
def update_headers(self, headers:
|
|
135
|
+
def update_headers(self, headers: ResponseHeaders, path: str) -> ResponseHeaders:
|
|
131
136
|
headers.setdefault("Access-Control-Allow-Origin", "*")
|
|
132
137
|
|
|
133
138
|
# Always vary on Accept-Encoding
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from .docs import docs
|
|
4
|
+
from .md import md
|
|
5
|
+
from .request import request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group("agent")
|
|
9
|
+
def agent() -> None:
|
|
10
|
+
"""Tools for coding agents"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Add commands to the group
|
|
15
|
+
agent.add_command(docs)
|
|
16
|
+
agent.add_command(md)
|
|
17
|
+
agent.add_command(request)
|
|
@@ -16,7 +16,7 @@ from .llmdocs import LLMDocs
|
|
|
16
16
|
help="List available packages",
|
|
17
17
|
)
|
|
18
18
|
def docs(package: str, show_list: bool) -> None:
|
|
19
|
-
"""Show LLM-friendly documentation
|
|
19
|
+
"""Show LLM-friendly documentation for a package"""
|
|
20
20
|
|
|
21
21
|
if show_list:
|
|
22
22
|
# List available packages using same discovery logic as md command
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import pkgutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from plain.runtime import PLAIN_TEMP_PATH
|
|
10
|
+
|
|
11
|
+
from ..output import iterate_markdown
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_packages_with_agents() -> dict[str, Path]:
|
|
15
|
+
"""Get dict mapping package names to AGENTS.md paths."""
|
|
16
|
+
agents_files = {}
|
|
17
|
+
|
|
18
|
+
# Check for plain.* subpackages (including core plain)
|
|
19
|
+
try:
|
|
20
|
+
import plain
|
|
21
|
+
|
|
22
|
+
# Check core plain package (namespace package)
|
|
23
|
+
plain_spec = importlib.util.find_spec("plain")
|
|
24
|
+
if plain_spec and plain_spec.submodule_search_locations:
|
|
25
|
+
# For namespace packages, check all search locations
|
|
26
|
+
for location in plain_spec.submodule_search_locations:
|
|
27
|
+
plain_path = Path(location)
|
|
28
|
+
agents_path = plain_path / "AGENTS.md"
|
|
29
|
+
if agents_path.exists():
|
|
30
|
+
agents_files["plain"] = agents_path
|
|
31
|
+
break # Use the first one found
|
|
32
|
+
|
|
33
|
+
# Check other plain.* subpackages
|
|
34
|
+
if hasattr(plain, "__path__"):
|
|
35
|
+
for importer, modname, ispkg in pkgutil.iter_modules(
|
|
36
|
+
plain.__path__, "plain."
|
|
37
|
+
):
|
|
38
|
+
if ispkg:
|
|
39
|
+
try:
|
|
40
|
+
spec = importlib.util.find_spec(modname)
|
|
41
|
+
if spec and spec.origin:
|
|
42
|
+
package_path = Path(spec.origin).parent
|
|
43
|
+
# Look for AGENTS.md at package root
|
|
44
|
+
agents_path = package_path / "AGENTS.md"
|
|
45
|
+
if agents_path.exists():
|
|
46
|
+
agents_files[modname] = agents_path
|
|
47
|
+
except Exception:
|
|
48
|
+
continue
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
return agents_files
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.command("md")
|
|
56
|
+
@click.option(
|
|
57
|
+
"--save",
|
|
58
|
+
default=None,
|
|
59
|
+
is_flag=False,
|
|
60
|
+
flag_value="PLAIN_TEMP_PATH",
|
|
61
|
+
help="Save combined AGENTS.md from all packages to file (default: .plain/AGENTS.md)",
|
|
62
|
+
)
|
|
63
|
+
def md(save: str | None) -> None:
|
|
64
|
+
"""AGENTS.md from installed Plain packages"""
|
|
65
|
+
|
|
66
|
+
agents_files = _get_packages_with_agents()
|
|
67
|
+
|
|
68
|
+
if not agents_files:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Handle --save flag
|
|
72
|
+
if save:
|
|
73
|
+
# Use PLAIN_TEMP_PATH if flag was used without value
|
|
74
|
+
if save == "PLAIN_TEMP_PATH":
|
|
75
|
+
save_path = PLAIN_TEMP_PATH / "AGENTS.md"
|
|
76
|
+
else:
|
|
77
|
+
save_path = Path(save)
|
|
78
|
+
|
|
79
|
+
# Check if we need to regenerate
|
|
80
|
+
if save_path.exists():
|
|
81
|
+
output_mtime = save_path.stat().st_mtime
|
|
82
|
+
# Check if any source file is newer
|
|
83
|
+
needs_regen = any(
|
|
84
|
+
path.stat().st_mtime > output_mtime for path in agents_files.values()
|
|
85
|
+
)
|
|
86
|
+
if not needs_regen:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Ensure parent directory exists
|
|
90
|
+
save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
|
|
92
|
+
# Generate combined file
|
|
93
|
+
with save_path.open("w") as f:
|
|
94
|
+
for pkg_name in sorted(agents_files.keys()):
|
|
95
|
+
content = agents_files[pkg_name].read_text()
|
|
96
|
+
f.write(content)
|
|
97
|
+
if not content.endswith("\n"):
|
|
98
|
+
f.write("\n")
|
|
99
|
+
f.write("\n")
|
|
100
|
+
else:
|
|
101
|
+
# Display to console
|
|
102
|
+
for pkg in sorted(agents_files.keys()):
|
|
103
|
+
agents_path = agents_files[pkg]
|
|
104
|
+
for line in iterate_markdown(agents_path.read_text()):
|
|
105
|
+
click.echo(line, nl=False)
|
|
106
|
+
print()
|
|
@@ -49,7 +49,7 @@ def request(
|
|
|
49
49
|
content_type: str | None,
|
|
50
50
|
headers: tuple[str, ...],
|
|
51
51
|
) -> None:
|
|
52
|
-
"""Make an HTTP
|
|
52
|
+
"""Make an HTTP test request against the dev database"""
|
|
53
53
|
|
|
54
54
|
try:
|
|
55
55
|
# Only allow in DEBUG mode for security
|
|
@@ -98,7 +98,7 @@ def request(
|
|
|
98
98
|
|
|
99
99
|
# Make the request
|
|
100
100
|
method = method.upper()
|
|
101
|
-
kwargs: dict[str,
|
|
101
|
+
kwargs: dict[str, Any] = {
|
|
102
102
|
"follow": follow,
|
|
103
103
|
}
|
|
104
104
|
if header_dict:
|
|
@@ -34,7 +34,7 @@ from plain.assets.compile import compile_assets, get_compiled_path
|
|
|
34
34
|
help="Compress the assets",
|
|
35
35
|
)
|
|
36
36
|
def build(keep_original: bool, fingerprint: bool, compress: bool) -> None:
|
|
37
|
-
"""Pre-deployment build step
|
|
37
|
+
"""Pre-deployment build step for assets and static files"""
|
|
38
38
|
|
|
39
39
|
if not keep_original and not fingerprint:
|
|
40
40
|
raise click.UsageError(
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
9
|
from .output import style_markdown
|
|
10
|
+
from .runtime import without_runtime_setup
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def parse_version(version_str: str) -> tuple[int, ...]:
|
|
@@ -42,6 +43,7 @@ def compare_versions(v1: str, v2: str) -> int:
|
|
|
42
43
|
return 0
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
@without_runtime_setup
|
|
45
47
|
@click.command("changelog")
|
|
46
48
|
@click.argument("package_label")
|
|
47
49
|
@click.option("--from", "from_version", help="Show entries from this version onwards")
|
|
@@ -49,7 +51,7 @@ def compare_versions(v1: str, v2: str) -> int:
|
|
|
49
51
|
def changelog(
|
|
50
52
|
package_label: str, from_version: str | None, to_version: str | None
|
|
51
53
|
) -> None:
|
|
52
|
-
"""Show changelog
|
|
54
|
+
"""Show changelog for a package"""
|
|
53
55
|
module_name = package_label.replace("-", ".")
|
|
54
56
|
spec = find_spec(module_name)
|
|
55
57
|
if not spec:
|
|
@@ -17,9 +17,7 @@ def chores() -> None:
|
|
|
17
17
|
"--name", default=None, type=str, help="Name of the chore to run", multiple=True
|
|
18
18
|
)
|
|
19
19
|
def list_chores(name: tuple[str, ...]) -> None:
|
|
20
|
-
"""
|
|
21
|
-
List all registered chores.
|
|
22
|
-
"""
|
|
20
|
+
"""List all registered chores"""
|
|
23
21
|
from plain.chores.registry import chores_registry
|
|
24
22
|
|
|
25
23
|
chores_registry.import_modules()
|
|
@@ -51,9 +49,7 @@ def list_chores(name: tuple[str, ...]) -> None:
|
|
|
51
49
|
"--dry-run", is_flag=True, help="Show what would be done without executing"
|
|
52
50
|
)
|
|
53
51
|
def run_chores(name: tuple[str, ...], dry_run: bool) -> None:
|
|
54
|
-
"""
|
|
55
|
-
Run the specified chores.
|
|
56
|
-
"""
|
|
52
|
+
"""Run specified chores"""
|
|
57
53
|
from plain.chores.registry import chores_registry
|
|
58
54
|
|
|
59
55
|
chores_registry.import_modules()
|
|
@@ -140,5 +140,66 @@ class PlainCommandCollection(click.CommandCollection):
|
|
|
140
140
|
self._ensure_registry_loaded()
|
|
141
141
|
return super().list_commands(ctx)
|
|
142
142
|
|
|
143
|
+
def format_commands(self, ctx: Context, formatter: Any) -> None:
|
|
144
|
+
"""Format commands with separate sections for common, core, and package commands."""
|
|
145
|
+
self._ensure_registry_loaded()
|
|
146
|
+
|
|
147
|
+
# Get all commands from both sources, tracking their source
|
|
148
|
+
commands = []
|
|
149
|
+
for source_index, source in enumerate(self.sources):
|
|
150
|
+
for name in source.list_commands(ctx):
|
|
151
|
+
cmd = source.get_command(ctx, name)
|
|
152
|
+
if cmd is not None:
|
|
153
|
+
# source_index 0 = plain_cli (core), 1+ = registry (packages)
|
|
154
|
+
commands.append((name, cmd, source_index))
|
|
155
|
+
|
|
156
|
+
if not commands:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# Get metadata from the registry (for shortcuts)
|
|
160
|
+
shortcuts_metadata = cli_registry.get_shortcuts()
|
|
161
|
+
|
|
162
|
+
# Separate commands into common, core, and package
|
|
163
|
+
common_commands = []
|
|
164
|
+
core_commands = []
|
|
165
|
+
package_commands = []
|
|
166
|
+
|
|
167
|
+
for name, cmd, source_index in commands:
|
|
168
|
+
help_text = cmd.get_short_help_str(limit=200)
|
|
169
|
+
|
|
170
|
+
# Check if command is marked as common via decorator
|
|
171
|
+
is_common = getattr(cmd, "is_common_command", False)
|
|
172
|
+
|
|
173
|
+
if is_common:
|
|
174
|
+
# This is a common command
|
|
175
|
+
# Add arrow notation if it's also a shortcut
|
|
176
|
+
if name in shortcuts_metadata:
|
|
177
|
+
shortcut_for = shortcuts_metadata[name].shortcut_for
|
|
178
|
+
if shortcut_for:
|
|
179
|
+
alias_info = click.style(f"(→ {shortcut_for})", italic=True)
|
|
180
|
+
help_text = f"{help_text} {alias_info}"
|
|
181
|
+
common_commands.append((name, help_text))
|
|
182
|
+
elif source_index == 0:
|
|
183
|
+
# Package command (from registry, inserted at index 0)
|
|
184
|
+
package_commands.append((name, help_text))
|
|
185
|
+
else:
|
|
186
|
+
# Core command (from plain_cli, at index 1)
|
|
187
|
+
core_commands.append((name, help_text))
|
|
188
|
+
|
|
189
|
+
# Write common commands section if any exist
|
|
190
|
+
if common_commands:
|
|
191
|
+
with formatter.section("Common Commands"):
|
|
192
|
+
formatter.write_dl(sorted(common_commands))
|
|
193
|
+
|
|
194
|
+
# Write core commands section if any exist
|
|
195
|
+
if core_commands:
|
|
196
|
+
with formatter.section("Core Commands"):
|
|
197
|
+
formatter.write_dl(sorted(core_commands))
|
|
198
|
+
|
|
199
|
+
# Write package commands section if any exist
|
|
200
|
+
if package_commands:
|
|
201
|
+
with formatter.section("Package Commands"):
|
|
202
|
+
formatter.write_dl(sorted(package_commands))
|
|
203
|
+
|
|
143
204
|
|
|
144
205
|
cli = PlainCommandCollection()
|
|
@@ -10,6 +10,7 @@ from .output import iterate_markdown
|
|
|
10
10
|
@click.option("--open", is_flag=True, help="Open the README in your default editor")
|
|
11
11
|
@click.argument("module", default="")
|
|
12
12
|
def docs(module: str, open: bool) -> None:
|
|
13
|
+
"""Show documentation for a package"""
|
|
13
14
|
if not module:
|
|
14
15
|
raise click.UsageError(
|
|
15
16
|
"You must specify a module. For LLM-friendly docs, use `plain agent docs`."
|
|
@@ -9,17 +9,17 @@ from click.formatting import iter_rows, measure_table, term_len, wrap_text
|
|
|
9
9
|
|
|
10
10
|
class PlainHelpFormatter(click.HelpFormatter):
|
|
11
11
|
def write_heading(self, heading: str) -> None:
|
|
12
|
-
styled_heading = click.style(heading,
|
|
12
|
+
styled_heading = click.style(heading, dim=True)
|
|
13
13
|
self.write(f"{'':>{self.current_indent}}{styled_heading}\n")
|
|
14
14
|
|
|
15
15
|
def write_usage(self, prog: str, args: str = "", prefix: str = "Usage: ") -> None:
|
|
16
|
-
prefix_styled = click.style(prefix,
|
|
16
|
+
prefix_styled = click.style(prefix, dim=True)
|
|
17
17
|
super().write_usage(prog, args, prefix=prefix_styled)
|
|
18
18
|
|
|
19
19
|
def write_dl(
|
|
20
20
|
self,
|
|
21
21
|
rows: list[tuple[str, str]],
|
|
22
|
-
col_max: int =
|
|
22
|
+
col_max: int = 20,
|
|
23
23
|
col_spacing: int = 2,
|
|
24
24
|
) -> None:
|
|
25
25
|
"""Writes a definition list into the buffer. This is how options
|
|
@@ -54,10 +54,15 @@ class PlainHelpFormatter(click.HelpFormatter):
|
|
|
54
54
|
lines = wrapped_text.splitlines()
|
|
55
55
|
|
|
56
56
|
if lines:
|
|
57
|
-
|
|
57
|
+
# Dim the description text
|
|
58
|
+
first_line_styled = click.style(lines[0], dim=True)
|
|
59
|
+
self.write(f"{first_line_styled}\n")
|
|
58
60
|
|
|
59
61
|
for line in lines[1:]:
|
|
60
|
-
|
|
62
|
+
line_styled = click.style(line, dim=True)
|
|
63
|
+
self.write(
|
|
64
|
+
f"{'':>{first_col + self.current_indent}}{line_styled}\n"
|
|
65
|
+
)
|
|
61
66
|
else:
|
|
62
67
|
self.write("\n")
|
|
63
68
|
|
|
@@ -66,11 +71,24 @@ class PlainContext(click.Context):
|
|
|
66
71
|
formatter_class = PlainHelpFormatter
|
|
67
72
|
|
|
68
73
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
74
|
+
# Set a wider max_content_width for help text (default is 80)
|
|
75
|
+
# This allows descriptions to fit more comfortably on one line
|
|
76
|
+
if "max_content_width" not in kwargs:
|
|
77
|
+
kwargs["max_content_width"] = 140
|
|
78
|
+
|
|
69
79
|
super().__init__(*args, **kwargs)
|
|
70
80
|
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
# Follow CLICOLOR standard (http://bixense.com/clicolors/)
|
|
82
|
+
# Priority: NO_COLOR > CLICOLOR_FORCE/FORCE_COLOR > CI detection > CLICOLOR > isatty
|
|
83
|
+
if os.getenv("NO_COLOR") or os.getenv("PYTEST_CURRENT_TEST"):
|
|
84
|
+
self.color = False
|
|
85
|
+
elif os.getenv("CLICOLOR_FORCE") or os.getenv("FORCE_COLOR"):
|
|
86
|
+
self.color = True
|
|
87
|
+
elif os.getenv("CI"):
|
|
88
|
+
# Enable colors in CI/deployment environments even without TTY
|
|
89
|
+
# This matches behavior of modern tools like uv (via Rust's anstyle)
|
|
76
90
|
self.color = True
|
|
91
|
+
elif os.getenv("CLICOLOR"):
|
|
92
|
+
# CLICOLOR=1 means use colors only if TTY (Click's default behavior)
|
|
93
|
+
pass # Let Click handle it with isatty check
|
|
94
|
+
# Otherwise use Click's default behavior (isatty check)
|
|
@@ -24,7 +24,7 @@ def install(
|
|
|
24
24
|
agent_command: str | None = None,
|
|
25
25
|
print_only: bool = False,
|
|
26
26
|
) -> None:
|
|
27
|
-
"""Install Plain packages with
|
|
27
|
+
"""Install Plain packages with agent assistance"""
|
|
28
28
|
# Validate all package names
|
|
29
29
|
invalid_packages = [pkg for pkg in packages if not pkg.startswith("plain")]
|
|
30
30
|
if invalid_packages:
|