karni-cli 0.3.1__py3-none-any.whl
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.
- karni_cli/__init__.py +3 -0
- karni_cli/_agent/__init__.py +82 -0
- karni_cli/_agent/__main__.py +44 -0
- karni_cli/_agent/check.py +658 -0
- karni_cli/_agent/history.py +328 -0
- karni_cli/_agent/history_chronicle.py +246 -0
- karni_cli/_agent/overrides.py +194 -0
- karni_cli/_agent/rules.py +673 -0
- karni_cli/_changelog/0.2.0.toml +304 -0
- karni_cli/_changelog/0.3.0.toml +25 -0
- karni_cli/_changelog/__init__.py +248 -0
- karni_cli/_errorcodes/__init__.py +1171 -0
- karni_cli/_errorcodes/__main__.py +34 -0
- karni_cli/_errorcodes/links.py +217 -0
- karni_cli/_templates/project/cookiecutter.json +8 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/.dockerignore +7 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/.env.example +85 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/.gitignore +10 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/Caddyfile +22 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/Makefile +52 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/README.md +169 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/__init__.py +16 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/asgi.py +14 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/celery.py +17 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/settings.py +28 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/urls.py +31 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/config/wsgi.py +14 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +126 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/.dockerignore +2 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/.jscpd.json +9 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/.npmrc +7 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/Dockerfile.dev +17 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/eslint.config.js +32 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/index.html +14 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/package.json +43 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/postcss.config.js +6 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/public/favicon.svg +6 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/App.tsx +18 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/api/health.ts +21 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/index.css +11 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/layout.tsx +51 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/main.tsx +11 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/pages/home.tsx +28 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/routes.tsx +24 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/src/vite-env.d.ts +1 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/tailwind.config.ts +13 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/tsconfig.json +19 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/frontend/vite.config.ts +18 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/manage.py +23 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/pyproject.toml +31 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/tests/__init__.py +0 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/tests/test_api_schema.py +17 -0
- karni_cli/_templates/project/{{cookiecutter.project_slug}}/tests/test_smoke.py +11 -0
- karni_cli/adapters.py +373 -0
- karni_cli/agent_adapters.toml +160 -0
- karni_cli/agent_data.py +260 -0
- karni_cli/brand.py +55 -0
- karni_cli/clidoc.py +254 -0
- karni_cli/commands/__init__.py +0 -0
- karni_cli/commands/add.py +144 -0
- karni_cli/commands/beat.py +36 -0
- karni_cli/commands/check.py +101 -0
- karni_cli/commands/config.py +81 -0
- karni_cli/commands/context.py +171 -0
- karni_cli/commands/createsuperuser.py +28 -0
- karni_cli/commands/doctor.py +136 -0
- karni_cli/commands/explain.py +60 -0
- karni_cli/commands/make.py +55 -0
- karni_cli/commands/manage_passthrough.py +28 -0
- karni_cli/commands/mcp.py +20 -0
- karni_cli/commands/migrate.py +23 -0
- karni_cli/commands/new.py +106 -0
- karni_cli/commands/routes.py +20 -0
- karni_cli/commands/rules.py +139 -0
- karni_cli/commands/run_tests.py +38 -0
- karni_cli/commands/schedule.py +40 -0
- karni_cli/commands/seed.py +34 -0
- karni_cli/commands/serve.py +50 -0
- karni_cli/commands/shell.py +28 -0
- karni_cli/commands/sync_roles.py +32 -0
- karni_cli/commands/types.py +72 -0
- karni_cli/commands/upgrade.py +231 -0
- karni_cli/commands/worker.py +63 -0
- karni_cli/config.py +142 -0
- karni_cli/environment.py +29 -0
- karni_cli/errorcodes.py +377 -0
- karni_cli/errordoc.py +227 -0
- karni_cli/generators/__init__.py +67 -0
- karni_cli/generators/apitest.py +206 -0
- karni_cli/generators/app.py +103 -0
- karni_cli/generators/command.py +98 -0
- karni_cli/generators/component.py +115 -0
- karni_cli/generators/core.py +319 -0
- karni_cli/generators/event.py +57 -0
- karni_cli/generators/factory.py +88 -0
- karni_cli/generators/fields.py +343 -0
- karni_cli/generators/introspect.py +263 -0
- karni_cli/generators/listener.py +168 -0
- karni_cli/generators/middleware.py +95 -0
- karni_cli/generators/migration.py +82 -0
- karni_cli/generators/model.py +235 -0
- karni_cli/generators/module.py +255 -0
- karni_cli/generators/naming.py +100 -0
- karni_cli/generators/page.py +373 -0
- karni_cli/generators/pysource.py +304 -0
- karni_cli/generators/schedule.py +174 -0
- karni_cli/generators/seeder.py +86 -0
- karni_cli/generators/serializer.py +165 -0
- karni_cli/generators/service.py +68 -0
- karni_cli/generators/task.py +84 -0
- karni_cli/generators/viewset.py +179 -0
- karni_cli/history.py +59 -0
- karni_cli/local_state.py +73 -0
- karni_cli/main.py +104 -0
- karni_cli/mcp_server.py +103 -0
- karni_cli/project.py +248 -0
- karni_cli/rules_render.py +86 -0
- karni_cli/scaffold.py +197 -0
- karni_cli/settings_edit.py +128 -0
- karni_cli/tips.py +53 -0
- karni_cli/typegen.py +101 -0
- karni_cli/upgrades/__init__.py +28 -0
- karni_cli/upgrades/changelog.py +123 -0
- karni_cli/upgrades/codemod.py +125 -0
- karni_cli/upgrades/engine.py +224 -0
- karni_cli/upgrades/legacy_agent_files.py +138 -0
- karni_cli/upgrades/python.py +670 -0
- karni_cli/upgrades/transforms/rename-export.cjs +96 -0
- karni_cli/upgrades/typescript.py +160 -0
- karni_cli/upgrades/v0_2/__init__.py +1 -0
- karni_cli/upgrades/v0_2/agent_guidance.py +269 -0
- karni_cli/upgrades/v1_0/__init__.py +5 -0
- karni_cli/upgrades/v1_0/all_objects_to_without.py +18 -0
- karni_cli/upgrades/v1_0/all_with_deleted_to_without.py +13 -0
- karni_cli/upgrades/v1_0/deleted_only_to_without_dead.py +13 -0
- karni_cli/upgrades/v1_0/object_permission_classes_to_access.py +16 -0
- karni_cli/upgrades/v1_0/related_lookups_to_optimize.py +15 -0
- karni_cli/upgrades/v1_0/scoped_related_lookups_to_optimize.py +15 -0
- karni_cli/upgrades/v1_0/soft_delete_manager_mixin_to_scoped.py +14 -0
- karni_cli-0.3.1.dist-info/METADATA +191 -0
- karni_cli-0.3.1.dist-info/RECORD +145 -0
- karni_cli-0.3.1.dist-info/WHEEL +4 -0
- karni_cli-0.3.1.dist-info/entry_points.txt +2 -0
- karni_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""``karni doctor`` — a human-readable report on the environment karni sees.
|
|
2
|
+
|
|
3
|
+
One line per fact: the project, how project commands would run (``.venv`` or
|
|
4
|
+
a running compose service), Node/npm, the ``@karni/ui`` manifest behind
|
|
5
|
+
``karni context``'s COMPONENTS/HOOKS, whether the agent adapters are fresh,
|
|
6
|
+
and which history providers are installed / detect a store. It reports and
|
|
7
|
+
never changes anything; the exit status is 0 unless the CLI itself fails.
|
|
8
|
+
|
|
9
|
+
It may end with ONE tip (see :mod:`karni_cli.tips`); ``-q`` suppresses it.
|
|
10
|
+
This is human output — never parse it; agents use ``karni context``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
|
|
21
|
+
from karni_cli import agent_data, tips
|
|
22
|
+
from karni_cli import history as history_mod
|
|
23
|
+
from karni_cli import project as project_mod
|
|
24
|
+
from karni_cli.adapters import sync
|
|
25
|
+
from karni_cli.commands.context import find_manifest
|
|
26
|
+
from karni_cli.project import CliError, find_project
|
|
27
|
+
|
|
28
|
+
_WIDTH = 12
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _line(label: str, text: str) -> str:
|
|
32
|
+
return f"{label:<{_WIDTH}}{text}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def tool_version(name: str) -> str | None:
|
|
36
|
+
"""``<name> --version`` output, or ``None`` when absent or failing."""
|
|
37
|
+
executable = shutil.which(name)
|
|
38
|
+
if executable is None:
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
proc = subprocess.run(
|
|
42
|
+
[executable, "--version"],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=10,
|
|
46
|
+
check=False,
|
|
47
|
+
stdin=subprocess.DEVNULL,
|
|
48
|
+
)
|
|
49
|
+
except (OSError, subprocess.SubprocessError):
|
|
50
|
+
return None
|
|
51
|
+
out = proc.stdout.strip().splitlines()
|
|
52
|
+
return out[0] if proc.returncode == 0 and out else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _display(path: Path, project: Path) -> str:
|
|
56
|
+
"""``path`` relative to the project when it lives inside it, else as given.
|
|
57
|
+
|
|
58
|
+
The quickstart's layout keeps the venv one level above the project.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
return str(path.relative_to(project))
|
|
62
|
+
except ValueError:
|
|
63
|
+
return str(path)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _interpreter(project: Path, service: str) -> str:
|
|
67
|
+
python = project_mod.venv_python(project)
|
|
68
|
+
if python is not None:
|
|
69
|
+
return f"{_display(python, project)} (project commands run here)"
|
|
70
|
+
if project_mod.compose_service_running(project, service):
|
|
71
|
+
return f'docker compose service "{service}" (running)'
|
|
72
|
+
return (
|
|
73
|
+
"none — create .venv (python -m venv .venv && .venv/bin/pip install -e .), activate a "
|
|
74
|
+
"virtualenv with the project's dependencies, or start the stack (docker compose up -d)"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _node() -> str:
|
|
79
|
+
node = tool_version("node")
|
|
80
|
+
if node is None:
|
|
81
|
+
return "not found — needed for the frontend and `karni types`"
|
|
82
|
+
npm = tool_version("npm")
|
|
83
|
+
return f"{node} (npm {npm})" if npm else f"{node} (npm not found)"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _manifest(project: Path) -> str:
|
|
87
|
+
found = find_manifest(project, None)
|
|
88
|
+
if found is None:
|
|
89
|
+
return "@karni/ui manifest absent — `npm install` in frontend/ (COMPONENTS/HOOKS hidden)"
|
|
90
|
+
return f"@karni/ui manifest: {_display(found, project)}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _adapters(project: Path) -> str:
|
|
94
|
+
try:
|
|
95
|
+
result = sync(project, agent_data.load(project), check=True)
|
|
96
|
+
except CliError as exc:
|
|
97
|
+
return f"cannot check: {exc}"
|
|
98
|
+
if result.stale:
|
|
99
|
+
return f"stale ({len(result.stale)} files) — run `karni rules sync`"
|
|
100
|
+
return f"up to date ({len(result.unchanged)} files)"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _history(project: Path) -> list[str]:
|
|
104
|
+
found = history_mod.discovered()
|
|
105
|
+
if found is None:
|
|
106
|
+
return ["unavailable (karni.agent.history not found)"]
|
|
107
|
+
detected = history_mod.detected_names(project)
|
|
108
|
+
lines = (
|
|
109
|
+
[f"{name}: store detected — `karni context --with-history FILE`" for name in detected]
|
|
110
|
+
if detected
|
|
111
|
+
else [f"no store detected (installed: {', '.join(found.names) or 'none'})"]
|
|
112
|
+
)
|
|
113
|
+
lines += [f"skipped provider {error}" for error in found.errors]
|
|
114
|
+
return lines
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def doctor(
|
|
118
|
+
service: str = typer.Option("django", "--service", envvar="KARNI_COMPOSE_SERVICE"),
|
|
119
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Do not print the optional tip."),
|
|
120
|
+
) -> None:
|
|
121
|
+
project = find_project()
|
|
122
|
+
if project is None:
|
|
123
|
+
typer.echo(_line("project", "not found (no manage.py here or above)"))
|
|
124
|
+
typer.echo(_line("node", _node()))
|
|
125
|
+
return
|
|
126
|
+
typer.echo(_line("project", str(project)))
|
|
127
|
+
typer.echo(_line("interpreter", _interpreter(project, service)))
|
|
128
|
+
typer.echo(_line("node", _node()))
|
|
129
|
+
typer.echo(_line("ui", _manifest(project)))
|
|
130
|
+
typer.echo(_line("adapters", _adapters(project)))
|
|
131
|
+
for i, text in enumerate(_history(project)):
|
|
132
|
+
typer.echo(_line("history" if i == 0 else "", text))
|
|
133
|
+
tip = tips.history_tip(project, quiet=quiet)
|
|
134
|
+
if tip:
|
|
135
|
+
typer.echo("")
|
|
136
|
+
typer.echo(tip)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""``karni explain CODE`` — what an error code means and how to fix it, offline.
|
|
2
|
+
|
|
3
|
+
karni explain E104 a Karni error or system check (E/W codes)
|
|
4
|
+
karni explain C101 a karni command error (C codes)
|
|
5
|
+
karni explain PAY004 an installed module's code (asks the project interpreter)
|
|
6
|
+
karni explain K106 an agent rule (same as karni rules --explain K106)
|
|
7
|
+
|
|
8
|
+
Works without Django and without a project; every error Karni prints starts
|
|
9
|
+
with its code, so an agent can run this instead of searching the docs.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from karni_cli import agent_data
|
|
20
|
+
from karni_cli.errorcodes import lookup
|
|
21
|
+
from karni_cli.project import CliError, find_project
|
|
22
|
+
from karni_cli.rules_render import render_explain
|
|
23
|
+
|
|
24
|
+
_RULE = re.compile(r"^[KP]\d{3}$")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render_entry(entry: dict[str, str]) -> str:
|
|
28
|
+
return "\n".join(
|
|
29
|
+
[
|
|
30
|
+
f"{entry['code']} {entry['title']} ({entry['kind']})",
|
|
31
|
+
"",
|
|
32
|
+
f"Why: {entry['why']}",
|
|
33
|
+
f"Fix: {entry['fix']}",
|
|
34
|
+
f"From: {entry['source']}",
|
|
35
|
+
f"Docs: {entry['url']}",
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def explain(
|
|
41
|
+
code: str = typer.Argument(..., help="An error code such as E104, W101, C101, PAY004 or K106."),
|
|
42
|
+
as_json: bool = typer.Option(False, "--json", help="Machine-readable output."),
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Explain an error code: why Karni objects and the exact fix."""
|
|
45
|
+
wanted = code.strip().upper()
|
|
46
|
+
if _RULE.match(wanted):
|
|
47
|
+
rule = agent_data.load(find_project()).rule(wanted)
|
|
48
|
+
if rule is None:
|
|
49
|
+
raise CliError(f"No rule {wanted!r} — `karni rules` lists the codes.", code="C204")
|
|
50
|
+
typer.echo(json.dumps(rule, indent=2) if as_json else render_explain(rule))
|
|
51
|
+
return
|
|
52
|
+
entry = lookup(wanted)
|
|
53
|
+
if entry is None:
|
|
54
|
+
raise CliError(
|
|
55
|
+
f"Unknown code {code!r}. Codes look like E104, W101, C101, PAY004 or K106 and "
|
|
56
|
+
"start every Karni error message; a module's codes are found inside a project "
|
|
57
|
+
"that installs it. docs/errors.md lists every code.",
|
|
58
|
+
code="C204",
|
|
59
|
+
)
|
|
60
|
+
typer.echo(json.dumps(entry, indent=2) if as_json else render_entry(entry))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""``karni make <type>`` — artisan-style generators (and the ``make:<type>`` aliases).
|
|
2
|
+
|
|
3
|
+
Every generator writes project code only, never overwrites without
|
|
4
|
+
``--force``, previews with ``--dry-run``, prints each created/modified file,
|
|
5
|
+
and edits existing files only through marker comments or AST-located
|
|
6
|
+
appends. ``karni make --list`` prints them all; the generators themselves
|
|
7
|
+
live in :mod:`karni_cli.generators`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from karni_cli.generators import GENERATORS
|
|
15
|
+
|
|
16
|
+
make_app = typer.Typer(
|
|
17
|
+
help=(
|
|
18
|
+
"Generate project code: apps, models, viewsets, tasks, schedules, listeners, pages "
|
|
19
|
+
"... (`karni make --list`). Never overwrites without --force; --dry-run previews."
|
|
20
|
+
),
|
|
21
|
+
no_args_is_help=False,
|
|
22
|
+
add_completion=False,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def list_text() -> str:
|
|
27
|
+
width = max(len(g.name) for g in GENERATORS) + 2
|
|
28
|
+
return "\n".join(f"{g.name:<{width}}{g.summary}" for g in GENERATORS)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@make_app.callback(invoke_without_command=True)
|
|
32
|
+
def make(
|
|
33
|
+
ctx: typer.Context,
|
|
34
|
+
list_: bool = typer.Option(
|
|
35
|
+
False, "--list", help="Print every generator with a one-line summary."
|
|
36
|
+
),
|
|
37
|
+
) -> None:
|
|
38
|
+
if ctx.invoked_subcommand is not None:
|
|
39
|
+
return
|
|
40
|
+
if list_:
|
|
41
|
+
typer.echo(list_text())
|
|
42
|
+
return
|
|
43
|
+
typer.echo(ctx.get_help())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
for _generator in GENERATORS:
|
|
47
|
+
make_app.command(_generator.name, short_help=_generator.summary)(_generator.command)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register_aliases(app: typer.Typer) -> None:
|
|
51
|
+
"""``karni make:<type>`` — hidden Laravel-style aliases for every generator."""
|
|
52
|
+
for generator in GENERATORS:
|
|
53
|
+
app.command(f"make:{generator.name}", hidden=True, short_help=generator.summary)(
|
|
54
|
+
generator.command
|
|
55
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""``karni manage`` — raw ``manage.py`` passthrough in the project environment.
|
|
2
|
+
|
|
3
|
+
The escape hatch for any Django/third-party management command without a
|
|
4
|
+
dedicated runner. Put ``--`` before arguments that clash with this command's
|
|
5
|
+
own options (``--docker``, ``--service``, ``--help``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from karni_cli import project as project_mod
|
|
13
|
+
from karni_cli.project import require_project
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def manage(
|
|
17
|
+
args: list[str] | None = typer.Argument(
|
|
18
|
+
None, help="The manage.py command and its arguments, e.g. `showmigrations events`."
|
|
19
|
+
),
|
|
20
|
+
docker: bool | None = typer.Option(None, "--docker/--no-docker"),
|
|
21
|
+
service: str = typer.Option("django", "--service", envvar="KARNI_COMPOSE_SERVICE"),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Run any management command — wraps `manage.py ARGS` (raw passthrough)."""
|
|
24
|
+
project = require_project()
|
|
25
|
+
proc = project_mod.run_manage(
|
|
26
|
+
project, list(args or []), docker=docker, service=service, capture=False, interactive=True
|
|
27
|
+
)
|
|
28
|
+
raise typer.Exit(proc.returncode)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""``karni mcp`` — run the Karni MCP server on stdio (``pip install 'karni-cli[mcp]'``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from karni_cli.project import find_project
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def mcp(
|
|
13
|
+
project: Path | None = typer.Option(
|
|
14
|
+
None, "--project", help="Project root (default: found from the working directory)."
|
|
15
|
+
),
|
|
16
|
+
) -> None:
|
|
17
|
+
from karni_cli.mcp_server import build_server # lazy: the MCP SDK is an extra
|
|
18
|
+
|
|
19
|
+
root = project.resolve() if project is not None else find_project()
|
|
20
|
+
build_server(root).run()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""``karni migrate`` — apply database migrations through the project interpreter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from karni_cli import project as project_mod
|
|
8
|
+
from karni_cli.project import require_project
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def migrate(
|
|
12
|
+
args: list[str] | None = typer.Argument(
|
|
13
|
+
None, help="Passed to manage.py migrate: [app_label] [migration_name] --plan --fake …"
|
|
14
|
+
),
|
|
15
|
+
docker: bool | None = typer.Option(None, "--docker/--no-docker"),
|
|
16
|
+
service: str = typer.Option("django", "--service", envvar="KARNI_COMPOSE_SERVICE"),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Apply migrations — wraps `manage.py migrate [ARGS]`."""
|
|
19
|
+
project = require_project()
|
|
20
|
+
proc = project_mod.run_manage(
|
|
21
|
+
project, ["migrate", *(args or [])], docker=docker, service=service, capture=False
|
|
22
|
+
)
|
|
23
|
+
raise typer.Exit(proc.returncode)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""``karni new <name>`` — scaffold a project from templates/project.
|
|
2
|
+
|
|
3
|
+
The template carries no agent guidance files: they are generated afterwards
|
|
4
|
+
by the same code as ``karni rules sync`` (AGENTS.md, CLAUDE.md, Cursor,
|
|
5
|
+
Windsurf, Copilot, Cline, Aider, Junie adapters + slash commands).
|
|
6
|
+
|
|
7
|
+
@karni/ui is resolved like the template: ``KARNI_UI_SRC`` > the monorepo
|
|
8
|
+
checkout's ``packages/karni-ui`` > the npm release. A local source becomes
|
|
9
|
+
``"@karni/ui": "file:<abs path>"`` in frontend/package.json (installed as a
|
|
10
|
+
packed copy via the scaffold's ``.npmrc``), and its ``dist/`` is built here
|
|
11
|
+
when missing — package.json ``exports`` point into dist/.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
|
|
20
|
+
from karni_cli import agent_data, scaffold
|
|
21
|
+
from karni_cli.adapters import sync
|
|
22
|
+
from karni_cli.brand import print_mark
|
|
23
|
+
from karni_cli.scaffold import resolve_template, validate_name
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def write_agent_adapters(project: Path) -> int:
|
|
27
|
+
"""Generate the agent adapter files into a freshly rendered project."""
|
|
28
|
+
result = sync(project, agent_data.load(project))
|
|
29
|
+
return len(result.written)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def prepare_ui_source(source: Path) -> None:
|
|
33
|
+
"""Make sure a local @karni/ui has the dist/ a ``file:`` install ships."""
|
|
34
|
+
rebuild = f"(cd {source} && npm install && npm run build)"
|
|
35
|
+
if not scaffold.ui_dist_missing(source):
|
|
36
|
+
if scaffold.ui_dist_stale(source):
|
|
37
|
+
typer.echo(
|
|
38
|
+
f"warning: {source}/dist is older than its sources — rebuild it with "
|
|
39
|
+
f"{rebuild} before `npm install` in frontend/.",
|
|
40
|
+
err=True,
|
|
41
|
+
)
|
|
42
|
+
return
|
|
43
|
+
typer.echo(f"Building @karni/ui in {source} (dist/ is missing; file: installs ship dist/)...")
|
|
44
|
+
failure = scaffold.build_ui_dist(source)
|
|
45
|
+
if failure is not None:
|
|
46
|
+
typer.echo(
|
|
47
|
+
f"warning: could not build @karni/ui: {failure}\n"
|
|
48
|
+
f"Build it yourself before `npm install` in frontend/: {rebuild}",
|
|
49
|
+
err=True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _baseline_release() -> str:
|
|
54
|
+
"""The newest release this karni-cli's changelog knows: what the scaffold is written for."""
|
|
55
|
+
from karni_cli.upgrades import changelog
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
return changelog.newest_release(changelog.load())
|
|
59
|
+
except Exception: # noqa: BLE001 - a missing changelog never blocks `karni new`
|
|
60
|
+
from karni_cli import __version__
|
|
61
|
+
|
|
62
|
+
return __version__
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def new(
|
|
66
|
+
name: str = typer.Argument(..., help="Project name (slug is derived from it)"),
|
|
67
|
+
output_dir: Path = typer.Option(Path("."), "--output-dir", "-o"),
|
|
68
|
+
domain: str = typer.Option("example.com", "--domain", help="Production domain for Caddy"),
|
|
69
|
+
) -> None:
|
|
70
|
+
from cookiecutter.main import cookiecutter # lazy: heavy import
|
|
71
|
+
|
|
72
|
+
slug = validate_name(name)
|
|
73
|
+
print_mark()
|
|
74
|
+
template = resolve_template()
|
|
75
|
+
ui_source = scaffold.resolve_ui_source()
|
|
76
|
+
if ui_source is not None:
|
|
77
|
+
prepare_ui_source(ui_source)
|
|
78
|
+
cookiecutter(
|
|
79
|
+
str(template),
|
|
80
|
+
no_input=True,
|
|
81
|
+
extra_context={
|
|
82
|
+
"project_name": name,
|
|
83
|
+
"domain": domain,
|
|
84
|
+
"karni_ui": (ui_spec := scaffold.ui_spec(ui_source, output_dir / slug)),
|
|
85
|
+
"karni_ui_mount": scaffold.ui_mount_target(ui_spec),
|
|
86
|
+
"karni_baseline": _baseline_release(),
|
|
87
|
+
},
|
|
88
|
+
output_dir=str(output_dir),
|
|
89
|
+
)
|
|
90
|
+
adapters = write_agent_adapters(output_dir / slug)
|
|
91
|
+
typer.echo(f"Created {output_dir / slug} ({adapters} agent adapter files)")
|
|
92
|
+
if ui_source is not None:
|
|
93
|
+
typer.echo(f"@karni/ui: {ui_spec} (local checkout)")
|
|
94
|
+
else:
|
|
95
|
+
typer.echo(
|
|
96
|
+
f"@karni/ui: {scaffold.UI_REGISTRY_SPEC} from npm — until it is published, "
|
|
97
|
+
"set KARNI_UI_SRC=/path/to/Karni/packages/karni-ui and re-run karni new."
|
|
98
|
+
)
|
|
99
|
+
typer.echo("Next steps:")
|
|
100
|
+
typer.echo(f" cd {slug}")
|
|
101
|
+
typer.echo(" cp .env.example .env")
|
|
102
|
+
typer.echo(" docker compose up -d")
|
|
103
|
+
typer.echo(
|
|
104
|
+
" (cd frontend && npm install) # editor, lint, type-check and `karni check` on the host"
|
|
105
|
+
)
|
|
106
|
+
typer.echo(" karni check")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""``karni routes`` — every URL the project serves (``manage.py karni_routes``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from karni_cli import project as project_mod
|
|
8
|
+
from karni_cli.project import require_project
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def routes(
|
|
12
|
+
as_json: bool = typer.Option(False, "--json", help="Print the routes as a JSON list."),
|
|
13
|
+
docker: bool | None = typer.Option(None, "--docker/--no-docker"),
|
|
14
|
+
service: str = typer.Option("django", "--service", envvar="KARNI_COMPOSE_SERVICE"),
|
|
15
|
+
) -> None:
|
|
16
|
+
"""List every URL: methods, name, view, model, access — wraps `manage.py karni_routes`."""
|
|
17
|
+
project = require_project()
|
|
18
|
+
args = ["karni_routes", *(["--json"] if as_json else [])]
|
|
19
|
+
proc = project_mod.run_manage(project, args, docker=docker, service=service, capture=False)
|
|
20
|
+
raise typer.Exit(proc.returncode)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""``karni rules`` — the conventions, file checks, and adapter sync.
|
|
2
|
+
|
|
3
|
+
karni rules conventions (code + one line each)
|
|
4
|
+
karni rules --explain K101 full statement, rationale, fix, enforcement
|
|
5
|
+
karni rules --procedure new-model the steps of a standard procedure
|
|
6
|
+
karni rules --check FILE... static violations with fixes (exit 1 if any)
|
|
7
|
+
karni rules --json the raw data (rules, procedures)
|
|
8
|
+
karni rules sync [--check] [--force]
|
|
9
|
+
regenerate AGENTS.md, CLAUDE.md, ... (CI: --check)
|
|
10
|
+
|
|
11
|
+
Works without Django and, except ``sync``, without a project; inside one,
|
|
12
|
+
``karni.rules.py`` overrides and ``.karni/modules/`` guidance apply.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
from karni_cli import agent_data
|
|
23
|
+
from karni_cli.adapters import module_fragments, sync
|
|
24
|
+
from karni_cli.project import CliError, find_project, require_project
|
|
25
|
+
from karni_cli.rules_render import (
|
|
26
|
+
render_explain,
|
|
27
|
+
render_findings,
|
|
28
|
+
render_procedure,
|
|
29
|
+
render_prompts_doc,
|
|
30
|
+
render_rules,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
rules_app = typer.Typer(
|
|
34
|
+
help="Karni's conventions for humans and AI agents (no Django needed).",
|
|
35
|
+
no_args_is_help=False,
|
|
36
|
+
add_completion=False,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def rules_text(project: Path | None) -> str:
|
|
41
|
+
data = agent_data.load(project)
|
|
42
|
+
return render_rules(data, module_fragments(project) if project else {})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_text(paths: list[Path], project: Path | None) -> tuple[str, int]:
|
|
46
|
+
"""Rendered findings and their count for ``paths``."""
|
|
47
|
+
missing = [str(p) for p in paths if not p.is_file()]
|
|
48
|
+
if missing:
|
|
49
|
+
raise CliError(f"No such file: {', '.join(missing)}", code="C010")
|
|
50
|
+
findings = agent_data.check(paths, project)
|
|
51
|
+
if not findings:
|
|
52
|
+
return f"ok: no violations in {len(paths)} file(s)", 0
|
|
53
|
+
return render_findings(findings), len(findings)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@rules_app.callback(invoke_without_command=True)
|
|
57
|
+
def rules(
|
|
58
|
+
ctx: typer.Context,
|
|
59
|
+
check: list[Path] | None = typer.Option(
|
|
60
|
+
None, "--check", help="Statically check FILE (repeatable); exit 1 on violations."
|
|
61
|
+
),
|
|
62
|
+
explain: str | None = typer.Option(None, "--explain", help="Show one rule in full."),
|
|
63
|
+
procedure: str | None = typer.Option(None, "--procedure", help="Show a procedure's steps."),
|
|
64
|
+
as_json: bool = typer.Option(False, "--json", help="Machine-readable output."),
|
|
65
|
+
prompts_doc: bool = typer.Option(False, "--prompts-doc", hidden=True),
|
|
66
|
+
) -> None:
|
|
67
|
+
if ctx.invoked_subcommand is not None:
|
|
68
|
+
return
|
|
69
|
+
project = find_project()
|
|
70
|
+
if check:
|
|
71
|
+
if as_json:
|
|
72
|
+
typer.echo(json.dumps(agent_data.check(list(check), project), indent=2))
|
|
73
|
+
return
|
|
74
|
+
text, count = check_text(list(check), project)
|
|
75
|
+
typer.echo(text)
|
|
76
|
+
raise typer.Exit(1 if count else 0)
|
|
77
|
+
|
|
78
|
+
data = agent_data.load(project)
|
|
79
|
+
if explain:
|
|
80
|
+
rule = data.rule(explain)
|
|
81
|
+
if rule is None:
|
|
82
|
+
raise CliError(f"No rule {explain!r} — `karni rules` lists the codes.", code="C204")
|
|
83
|
+
typer.echo(json.dumps(rule, indent=2) if as_json else render_explain(rule))
|
|
84
|
+
return
|
|
85
|
+
if procedure:
|
|
86
|
+
found = data.procedure(procedure)
|
|
87
|
+
if found is None:
|
|
88
|
+
names = ", ".join(p["name"] for p in data.procedures)
|
|
89
|
+
raise CliError(f"No procedure {procedure!r} (available: {names}).", code="C204")
|
|
90
|
+
typer.echo(json.dumps(found, indent=2) if as_json else render_procedure(found))
|
|
91
|
+
return
|
|
92
|
+
if prompts_doc:
|
|
93
|
+
typer.echo(render_prompts_doc(data))
|
|
94
|
+
return
|
|
95
|
+
if as_json:
|
|
96
|
+
typer.echo(json.dumps(data.raw, indent=2))
|
|
97
|
+
return
|
|
98
|
+
typer.echo(render_rules(data, module_fragments(project) if project else {}))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@rules_app.command("sync")
|
|
102
|
+
def sync_command(
|
|
103
|
+
check: bool = typer.Option(
|
|
104
|
+
False, "--check", help="Only compare; exit 1 if any adapter is stale (for CI)."
|
|
105
|
+
),
|
|
106
|
+
force: bool = typer.Option(
|
|
107
|
+
False, "--force", help="Overwrite hand-written files (backed up under .karni/backup/)."
|
|
108
|
+
),
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Regenerate the agent adapter files from Karni's rule registry."""
|
|
111
|
+
project = require_project()
|
|
112
|
+
data = agent_data.load(project)
|
|
113
|
+
result = sync(project, data, check=check, force=force)
|
|
114
|
+
if check:
|
|
115
|
+
if result.stale:
|
|
116
|
+
for rel in result.stale:
|
|
117
|
+
typer.echo(f"stale: {rel}", err=True)
|
|
118
|
+
typer.echo("Agent adapters are stale — run `karni rules sync`.", err=True)
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
typer.echo(f"Agent adapters up to date ({len(result.unchanged)} files).")
|
|
121
|
+
return
|
|
122
|
+
for rel in result.backed_up:
|
|
123
|
+
typer.echo(f"backed up: {rel}")
|
|
124
|
+
for rel in result.written:
|
|
125
|
+
typer.echo(f"wrote {rel}")
|
|
126
|
+
for rel in result.removed:
|
|
127
|
+
typer.echo(f"removed {rel}")
|
|
128
|
+
if result.blocked:
|
|
129
|
+
for rel in result.blocked:
|
|
130
|
+
typer.echo(f"skipped {rel}: not generated by karni (hand-written?)", err=True)
|
|
131
|
+
typer.echo(
|
|
132
|
+
"Move project-specific guidance into karni.rules.py (`karni upgrade` does this "
|
|
133
|
+
"for Karni's old scaffold files), then re-run; or `--force` to overwrite with a "
|
|
134
|
+
"backup.",
|
|
135
|
+
err=True,
|
|
136
|
+
)
|
|
137
|
+
raise typer.Exit(1)
|
|
138
|
+
if not result.written and not result.removed:
|
|
139
|
+
typer.echo(f"Agent adapters up to date ({len(result.unchanged)} files).")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""``karni test`` — the project's test suite (pytest) through the project interpreter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from karni_cli import project as project_mod
|
|
8
|
+
from karni_cli.project import CliError, require_project
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test(
|
|
12
|
+
args: list[str] | None = typer.Argument(
|
|
13
|
+
None, help="Passed to pytest: paths, -k EXPR, -x, --lf … (use -- before --help)."
|
|
14
|
+
),
|
|
15
|
+
docker: bool | None = typer.Option(None, "--docker/--no-docker"),
|
|
16
|
+
service: str = typer.Option("django", "--service", envvar="KARNI_COMPOSE_SERVICE"),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Run the test suite — wraps `python -m pytest [ARGS]`."""
|
|
19
|
+
project = require_project()
|
|
20
|
+
probe = project_mod.run_python(
|
|
21
|
+
project, ["-c", "import pytest"], docker=docker, service=service, capture=True
|
|
22
|
+
)
|
|
23
|
+
if probe.returncode != 0 and "No module named" in (probe.stderr or ""):
|
|
24
|
+
raise CliError(
|
|
25
|
+
"pytest is not installed in the project interpreter, so `karni test` cannot run.\n"
|
|
26
|
+
"Install the project's dev group there: pip install pytest pytest-django "
|
|
27
|
+
"(or `uv sync`), then run karni test again.",
|
|
28
|
+
code="C011",
|
|
29
|
+
)
|
|
30
|
+
proc = project_mod.run_python(
|
|
31
|
+
project,
|
|
32
|
+
["-m", "pytest", *(args or [])],
|
|
33
|
+
docker=docker,
|
|
34
|
+
service=service,
|
|
35
|
+
capture=False,
|
|
36
|
+
interactive=True,
|
|
37
|
+
)
|
|
38
|
+
raise typer.Exit(proc.returncode)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""``karni schedule list|run`` — the merged beat schedules (``manage.py karni_schedules``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from karni_cli import project as project_mod
|
|
8
|
+
from karni_cli.project import require_project
|
|
9
|
+
|
|
10
|
+
schedule_app = typer.Typer(
|
|
11
|
+
help="Celery beat schedules — wraps `manage.py karni_schedules`.",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@schedule_app.command("list")
|
|
17
|
+
def list_schedules(
|
|
18
|
+
as_json: bool = typer.Option(False, "--json", help="Print the schedules as a JSON list."),
|
|
19
|
+
docker: bool | None = typer.Option(None, "--docker/--no-docker"),
|
|
20
|
+
service: str = typer.Option("django", "--service", envvar="KARNI_COMPOSE_SERVICE"),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""List every schedule: name, task, timing, source — wraps `manage.py karni_schedules`."""
|
|
23
|
+
project = require_project()
|
|
24
|
+
args = ["karni_schedules", *(["--json"] if as_json else [])]
|
|
25
|
+
proc = project_mod.run_manage(project, args, docker=docker, service=service, capture=False)
|
|
26
|
+
raise typer.Exit(proc.returncode)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@schedule_app.command("run")
|
|
30
|
+
def run_schedule(
|
|
31
|
+
name: str = typer.Argument(..., help="The schedule name (see `karni schedule list`)."),
|
|
32
|
+
docker: bool | None = typer.Option(None, "--docker/--no-docker"),
|
|
33
|
+
service: str = typer.Option("django", "--service", envvar="KARNI_COMPOSE_SERVICE"),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Run one schedule's task now, synchronously — wraps `manage.py karni_schedules --run NAME`."""
|
|
36
|
+
project = require_project()
|
|
37
|
+
proc = project_mod.run_manage(
|
|
38
|
+
project, ["karni_schedules", "--run", name], docker=docker, service=service, capture=False
|
|
39
|
+
)
|
|
40
|
+
raise typer.Exit(proc.returncode)
|