Skip to content

Commit 9cba705

Browse files
committed
Improve plain docs for uninstalled packages
Show all known packages with install status in --list, and show online docs URL when a package isn't installed.
1 parent 88d9424 commit 9cba705

File tree

2 files changed

+118
-35
lines changed

2 files changed

+118
-35
lines changed

plain/plain/agents/.claude/rules/plain.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,52 @@ Use the `/plain-upgrade` skill to upgrade Plain packages.
77

88
## Documentation
99

10-
Run `uv run plain docs --list` to see available packages.
11-
Run `uv run plain docs <package> --source` for detailed API documentation.
10+
Run `uv run plain docs --list` to see all official packages (installed and uninstalled) with descriptions.
11+
Run `uv run plain docs <package> --source` for detailed API documentation (installed packages only).
12+
For uninstalled packages, the CLI shows the install command and an online docs URL.
13+
14+
Online docs URL pattern: `https://plainframework.com/docs/<pip-name>/<module/path>/README.md`
15+
Example: `https://plainframework.com/docs/plain-models/plain/models/README.md`
1216

1317
Examples:
1418

1519
- `uv run plain docs models --source` - Models and database
1620
- `uv run plain docs templates --source` - Jinja2 templates
1721
- `uv run plain docs assets --source` - Static assets
1822

23+
### All official packages
24+
25+
- **plain** — Web framework core
26+
- **plain-admin** — Backend admin interface
27+
- **plain-api** — Class-based API views
28+
- **plain-auth** — User authentication and authorization
29+
- **plain-cache** — Database-backed cache with optional expiration
30+
- **plain-code** — Preconfigured code formatting and linting
31+
- **plain-dev** — Local development server with auto-reload
32+
- **plain-elements** — HTML template components
33+
- **plain-email** — Send email
34+
- **plain-esbuild** — Build JavaScript with esbuild
35+
- **plain-flags** — Feature flags via database models
36+
- **plain-htmx** — HTMX integration for templates and views
37+
- **plain-jobs** — Background jobs with a database-driven queue
38+
- **plain-loginlink** — Link-based authentication
39+
- **plain-models** — Model data and store it in a database
40+
- **plain-oauth** — OAuth provider login
41+
- **plain-observer** — On-page telemetry and observability
42+
- **plain-pages** — Serve static pages, markdown, and assets
43+
- **plain-pageviews** — Client-side pageview tracking
44+
- **plain-passwords** — Password authentication
45+
- **plain-pytest** — Test with pytest
46+
- **plain-redirection** — URL redirection with admin and logging
47+
- **plain-scan** — Test for production best practices
48+
- **plain-sessions** — Database-backed sessions
49+
- **plain-start** — Bootstrap a new project from templates
50+
- **plain-support** — Support forms for your application
51+
- **plain-tailwind** — Tailwind CSS without JavaScript or npm
52+
- **plain-toolbar** — Debug toolbar
53+
- **plain-tunnel** — Remote access to local dev server
54+
- **plain-vendor** — Vendor CDN scripts and styles
55+
1956
## Shell
2057

2158
`uv run plain shell` opens an interactive Python shell with Plain configured and database access.

plain/plain/cli/docs.py

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,74 @@
1+
from __future__ import annotations
2+
13
import importlib.util
2-
import pkgutil
34
from pathlib import Path
45

56
import click
67

78
from .llmdocs import LLMDocs
89
from .output import iterate_markdown
910

11+
# All known official Plain packages: pip name -> short description
12+
KNOWN_PACKAGES = {
13+
"plain": "Web framework core",
14+
"plain-admin": "Backend admin interface",
15+
"plain-api": "Class-based API views",
16+
"plain-auth": "User authentication and authorization",
17+
"plain-cache": "Database-backed cache with optional expiration",
18+
"plain-code": "Preconfigured code formatting and linting",
19+
"plain-dev": "Local development server with auto-reload",
20+
"plain-elements": "HTML template components",
21+
"plain-email": "Send email",
22+
"plain-esbuild": "Build JavaScript with esbuild",
23+
"plain-flags": "Feature flags via database models",
24+
"plain-htmx": "HTMX integration for templates and views",
25+
"plain-jobs": "Background jobs with a database-driven queue",
26+
"plain-loginlink": "Link-based authentication",
27+
"plain-models": "Model data and store it in a database",
28+
"plain-oauth": "OAuth provider login",
29+
"plain-observer": "On-page telemetry and observability",
30+
"plain-pages": "Serve static pages, markdown, and assets",
31+
"plain-pageviews": "Client-side pageview tracking",
32+
"plain-passwords": "Password authentication",
33+
"plain-pytest": "Test with pytest",
34+
"plain-redirection": "URL redirection with admin and logging",
35+
"plain-scan": "Test for production best practices",
36+
"plain-sessions": "Database-backed sessions",
37+
"plain-start": "Bootstrap a new project from templates",
38+
"plain-support": "Support forms for your application",
39+
"plain-tailwind": "Tailwind CSS without JavaScript or npm",
40+
"plain-toolbar": "Debug toolbar",
41+
"plain-tunnel": "Remote access to local dev server",
42+
"plain-vendor": "Vendor CDN scripts and styles",
43+
}
44+
45+
46+
def _normalize_module(module: str) -> str:
47+
"""Normalize a module string to dotted form (e.g. plain-models -> plain.models)."""
48+
module = module.replace("-", ".")
49+
if not module.startswith("plain"):
50+
module = f"plain.{module}"
51+
return module
52+
53+
54+
def _pip_package_name(module: str) -> str:
55+
"""Convert a dotted module name to a pip package name (e.g. plain.models -> plain-models)."""
56+
return module.replace(".", "-")
57+
58+
59+
def _is_installed(module: str) -> bool:
60+
"""Check if a dotted module name is installed."""
61+
try:
62+
return importlib.util.find_spec(module) is not None
63+
except (ModuleNotFoundError, ValueError):
64+
return False
65+
66+
67+
def _online_docs_url(pip_name: str) -> str:
68+
"""Return the online documentation URL for a package."""
69+
module = pip_name.replace("-", ".")
70+
return f"https://plainframework.com/docs/{pip_name}/{module.replace('.', '/')}/"
71+
1072

1173
@click.command()
1274
@click.option("--open", is_flag=True, help="Open the README in your default editor")
@@ -16,49 +78,33 @@
1678
def docs(module: str, open: bool, source: bool, show_list: bool) -> None:
1779
"""Show documentation for a package"""
1880
if show_list:
19-
# List available packages
20-
available_packages = []
21-
try:
22-
import plain
23-
24-
# Check core plain package (namespace package)
25-
plain_spec = importlib.util.find_spec("plain")
26-
if plain_spec and plain_spec.submodule_search_locations:
27-
available_packages.append("plain")
28-
29-
# Check other plain.* subpackages
30-
if hasattr(plain, "__path__"):
31-
for importer, modname, ispkg in pkgutil.iter_modules(
32-
plain.__path__, "plain."
33-
):
34-
if ispkg:
35-
available_packages.append(modname)
36-
except Exception:
37-
pass
38-
39-
if available_packages:
40-
for pkg in sorted(available_packages):
41-
click.echo(f"- {pkg}")
42-
else:
43-
click.echo("No packages found.")
81+
for pip_name in sorted(KNOWN_PACKAGES):
82+
description = KNOWN_PACKAGES[pip_name]
83+
dotted = pip_name.replace("-", ".")
84+
installed = _is_installed(dotted)
85+
status = " (installed)" if installed else ""
86+
click.echo(f" {pip_name}{status}{description}")
4487
return
4588

4689
if not module:
4790
raise click.UsageError(
4891
"You must specify a module. Use --list to see available packages."
4992
)
5093

51-
# Convert hyphens to dots (e.g., plain-models -> plain.models)
52-
module = module.replace("-", ".")
53-
54-
# Automatically prefix if we need to
55-
if not module.startswith("plain"):
56-
module = f"plain.{module}"
94+
module = _normalize_module(module)
5795

5896
# Get the module path
5997
spec = importlib.util.find_spec(module)
6098
if not spec or not spec.origin:
61-
raise click.UsageError(f"Module {module} not found")
99+
pip_name = _pip_package_name(module)
100+
if pip_name in KNOWN_PACKAGES:
101+
msg = (
102+
f"{module} is not installed.\n\n"
103+
f" Online docs: {_online_docs_url(pip_name)}"
104+
)
105+
else:
106+
msg = f"Module {module} not found. Use --list to see available packages."
107+
raise click.UsageError(msg)
62108

63109
module_path = Path(spec.origin).parent
64110

0 commit comments

Comments
 (0)