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,14 @@
|
|
|
1
|
+
"""ASGI config for {{ cookiecutter.project_name }}.
|
|
2
|
+
|
|
3
|
+
Exposes the ASGI callable as a module-level variable named ``application``.
|
|
4
|
+
This is what gunicorn runs in production (with uvicorn workers); see the
|
|
5
|
+
Dockerfile ``prod`` target.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from django.core.asgi import get_asgi_application
|
|
11
|
+
|
|
12
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
13
|
+
|
|
14
|
+
application = get_asgi_application()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Celery entry point for {{ cookiecutter.project_name }}.
|
|
2
|
+
|
|
3
|
+
Used by ``celery -A config worker`` and ``celery -A config beat``. The app
|
|
4
|
+
is configured from Django settings (every ``CELERY_*`` name), so override
|
|
5
|
+
Celery behaviour in config/settings.py, or pass keyword overrides to
|
|
6
|
+
``create_celery_app()`` here.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from karni.celery import create_celery_app
|
|
12
|
+
|
|
13
|
+
# create_celery_app() requires DJANGO_SETTINGS_MODULE to be set already;
|
|
14
|
+
# setdefault here covers the `celery -A config ...` entry points.
|
|
15
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
16
|
+
|
|
17
|
+
app = create_celery_app()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Django settings for {{ cookiecutter.project_name }}.
|
|
2
|
+
|
|
3
|
+
karni_settings() fills this module with Karni's defaults — INSTALLED_APPS,
|
|
4
|
+
middleware, DRF, Celery, Redis cache, storage, logging, security headers,
|
|
5
|
+
CORS and Sentry — all read from environment variables (see .env.example).
|
|
6
|
+
|
|
7
|
+
Overriding: anything assigned *after* the call wins, and any name defined
|
|
8
|
+
*before* the call is never overwritten by Karni.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from karni.conf import karni_settings
|
|
12
|
+
|
|
13
|
+
karni_settings(globals())
|
|
14
|
+
|
|
15
|
+
# --- Project apps ------------------------------------------------------------
|
|
16
|
+
INSTALLED_APPS += [ # noqa: F821
|
|
17
|
+
# "myapp",
|
|
18
|
+
] # karni:apps
|
|
19
|
+
|
|
20
|
+
# --- Project middleware (runs after Karni's stack) ----------------------------
|
|
21
|
+
MIDDLEWARE += [ # noqa: F821
|
|
22
|
+
# "myapp.middleware.MyMiddleware",
|
|
23
|
+
] # karni:middleware
|
|
24
|
+
|
|
25
|
+
# --- Project overrides -------------------------------------------------------
|
|
26
|
+
# Any Django/Karni setting assigned here beats the Karni default, e.g.:
|
|
27
|
+
# REST_FRAMEWORK["PAGE_SIZE"] = 25
|
|
28
|
+
# CELERY_TASK_TIME_LIMIT = 600
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""URL configuration for {{ cookiecutter.project_name }}.
|
|
2
|
+
|
|
3
|
+
Routes: Django admin, the OpenAPI schema and Swagger UI (drf-spectacular),
|
|
4
|
+
Karni's auth endpoints (/api/auth/), the auto-routed API (/api/), Karni's
|
|
5
|
+
health endpoints (/health/ liveness, /health/ready/ readiness) and, last,
|
|
6
|
+
the welcome page at "/" while DEBUG is on. Add project routes to
|
|
7
|
+
``urlpatterns`` below; to replace a single Karni endpoint, declare your
|
|
8
|
+
``path(...)`` for it BEFORE the include — first match wins. Your own
|
|
9
|
+
``path("", ...)`` replaces the welcome page the same way.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from django.conf import settings
|
|
13
|
+
from django.contrib import admin
|
|
14
|
+
from django.urls import include, path
|
|
15
|
+
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
|
16
|
+
|
|
17
|
+
from karni.welcome import media_urls, welcome_urls
|
|
18
|
+
|
|
19
|
+
urlpatterns = [
|
|
20
|
+
# KARNI_ADMIN_PATH (env, default "admin/"): move the admin off the guessable path.
|
|
21
|
+
path(settings.KARNI_ADMIN_PATH, admin.site.urls),
|
|
22
|
+
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
|
|
23
|
+
path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
|
|
24
|
+
path("api/auth/", include("karni.accounts.urls")),
|
|
25
|
+
path("api/", include("karni.api.urls")),
|
|
26
|
+
path("health/", include("karni.health.urls")),
|
|
27
|
+
# Uploads under MEDIA_URL while DEBUG is on and storage is local (dev only).
|
|
28
|
+
*media_urls(),
|
|
29
|
+
# Only while DEBUG is on, and only until a route above answers "/".
|
|
30
|
+
*welcome_urls(),
|
|
31
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""WSGI config for {{ cookiecutter.project_name }}.
|
|
2
|
+
|
|
3
|
+
Exposes the WSGI callable as a module-level variable named ``application``.
|
|
4
|
+
The production stack serves ASGI (config/asgi.py) by default; this module is
|
|
5
|
+
kept for WSGI-only hosts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from django.core.wsgi import get_wsgi_application
|
|
11
|
+
|
|
12
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
13
|
+
|
|
14
|
+
application = get_wsgi_application()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Development stack for {{ cookiecutter.project_name }}.
|
|
2
|
+
#
|
|
3
|
+
# cp .env.example .env && docker compose up --build
|
|
4
|
+
#
|
|
5
|
+
# django runs migrations before serving, so worker and beat (which need the
|
|
6
|
+
# django-celery-beat / django-celery-results tables) wait on django's
|
|
7
|
+
# healthcheck, not just on db/redis.
|
|
8
|
+
# KARNI_SRC (from .env) optionally installs a vendored karni checkout — see README.md.
|
|
9
|
+
|
|
10
|
+
services:
|
|
11
|
+
django:
|
|
12
|
+
build:
|
|
13
|
+
context: .
|
|
14
|
+
target: dev
|
|
15
|
+
args:
|
|
16
|
+
KARNI_SRC: ${KARNI_SRC:-}
|
|
17
|
+
command: sh -c "python manage.py migrate --noinput && python manage.py runserver 0.0.0.0:8000"
|
|
18
|
+
ports:
|
|
19
|
+
- "8000:8000"
|
|
20
|
+
env_file: .env
|
|
21
|
+
volumes:
|
|
22
|
+
- .:/app
|
|
23
|
+
- app_data:/data
|
|
24
|
+
depends_on:
|
|
25
|
+
db:
|
|
26
|
+
condition: service_healthy
|
|
27
|
+
redis:
|
|
28
|
+
condition: service_healthy
|
|
29
|
+
healthcheck:
|
|
30
|
+
# Liveness endpoint only responds once migrations have finished and
|
|
31
|
+
# runserver is up — downstream services key off this.
|
|
32
|
+
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')"]
|
|
33
|
+
interval: 5s
|
|
34
|
+
timeout: 5s
|
|
35
|
+
retries: 30
|
|
36
|
+
start_period: 30s
|
|
37
|
+
|
|
38
|
+
db:
|
|
39
|
+
image: postgres:16
|
|
40
|
+
environment:
|
|
41
|
+
POSTGRES_USER: karni
|
|
42
|
+
POSTGRES_PASSWORD: karni
|
|
43
|
+
POSTGRES_DB: karni
|
|
44
|
+
volumes:
|
|
45
|
+
- postgres_data:/var/lib/postgresql/data
|
|
46
|
+
healthcheck:
|
|
47
|
+
test: ["CMD", "pg_isready", "-U", "karni", "-d", "karni"]
|
|
48
|
+
interval: 5s
|
|
49
|
+
timeout: 5s
|
|
50
|
+
retries: 10
|
|
51
|
+
|
|
52
|
+
redis:
|
|
53
|
+
image: redis:7
|
|
54
|
+
healthcheck:
|
|
55
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
56
|
+
interval: 5s
|
|
57
|
+
timeout: 5s
|
|
58
|
+
retries: 10
|
|
59
|
+
|
|
60
|
+
worker:
|
|
61
|
+
build:
|
|
62
|
+
context: .
|
|
63
|
+
target: dev
|
|
64
|
+
args:
|
|
65
|
+
KARNI_SRC: ${KARNI_SRC:-}
|
|
66
|
+
command: celery -A config worker -l info
|
|
67
|
+
env_file: .env
|
|
68
|
+
volumes:
|
|
69
|
+
- .:/app
|
|
70
|
+
- app_data:/data
|
|
71
|
+
depends_on:
|
|
72
|
+
db:
|
|
73
|
+
condition: service_healthy
|
|
74
|
+
redis:
|
|
75
|
+
condition: service_healthy
|
|
76
|
+
django:
|
|
77
|
+
condition: service_healthy
|
|
78
|
+
|
|
79
|
+
beat:
|
|
80
|
+
build:
|
|
81
|
+
context: .
|
|
82
|
+
target: dev
|
|
83
|
+
args:
|
|
84
|
+
KARNI_SRC: ${KARNI_SRC:-}
|
|
85
|
+
# --pidfile= disables the pidfile; a stale one in the bind mount would
|
|
86
|
+
# otherwise block restarts.
|
|
87
|
+
command: celery -A config beat -l info --pidfile=
|
|
88
|
+
env_file: .env
|
|
89
|
+
volumes:
|
|
90
|
+
- .:/app
|
|
91
|
+
depends_on:
|
|
92
|
+
db:
|
|
93
|
+
condition: service_healthy
|
|
94
|
+
redis:
|
|
95
|
+
condition: service_healthy
|
|
96
|
+
django:
|
|
97
|
+
condition: service_healthy
|
|
98
|
+
|
|
99
|
+
frontend:
|
|
100
|
+
build:
|
|
101
|
+
context: ./frontend
|
|
102
|
+
dockerfile: Dockerfile.dev
|
|
103
|
+
# Runs `npm install`, then Vite (see frontend/Dockerfile.dev).
|
|
104
|
+
environment:
|
|
105
|
+
KARNI_API_PROXY: http://django:8000
|
|
106
|
+
ports:
|
|
107
|
+
- "5173:5173"
|
|
108
|
+
volumes:
|
|
109
|
+
- ./frontend:/app
|
|
110
|
+
# Container-built node_modules (Linux binaries), never the host's.
|
|
111
|
+
- frontend_node_modules:/app/node_modules
|
|
112
|
+
{%- if cookiecutter.karni_ui.startswith("file:") %}
|
|
113
|
+
# The local @karni/ui checkout that package.json's "file:" spec names,
|
|
114
|
+
# mounted read-only where that spec resolves inside the container (the
|
|
115
|
+
# same absolute path, or /app/<relative path> for a project inside the
|
|
116
|
+
# Karni checkout) so npm resolves it here too.
|
|
117
|
+
- type: bind
|
|
118
|
+
source: {{ cookiecutter.karni_ui[5:] | tojson }}
|
|
119
|
+
target: {{ cookiecutter.karni_ui_mount | tojson }}
|
|
120
|
+
read_only: true
|
|
121
|
+
{%- endif %}
|
|
122
|
+
|
|
123
|
+
volumes:
|
|
124
|
+
postgres_data:
|
|
125
|
+
app_data:
|
|
126
|
+
frontend_node_modules:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# A local @karni/ui ("file:<path>" in package.json, written by `karni new`
|
|
2
|
+
# from a Karni checkout) installs as a packed COPY, exactly like the npm
|
|
3
|
+
# release: its dependencies and peers land in this node_modules, so there is
|
|
4
|
+
# one React and one QueryClient, never the checkout's own node_modules.
|
|
5
|
+
# The copy is a snapshot: after rebuilding the checkout (npm run build there),
|
|
6
|
+
# refresh it with `rm -rf node_modules/@karni/ui && npm install`.
|
|
7
|
+
install-links=true
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Dev-only image for the Vite dev server. docker-compose bind-mounts the
|
|
2
|
+
# source over /app and keeps node_modules in a named volume.
|
|
3
|
+
#
|
|
4
|
+
# Dependencies install when the container STARTS, not at image build: a local
|
|
5
|
+
# @karni/ui ("file:<path>" in package.json, written by `karni new` from a Karni
|
|
6
|
+
# checkout) lives outside this build context and is only reachable through
|
|
7
|
+
# the read-only mount docker-compose.yml adds at that same path. Installing on
|
|
8
|
+
# start also keeps the node_modules volume in sync with package.json.
|
|
9
|
+
#
|
|
10
|
+
# --no-package-lock: the host's package-lock.json records a local @karni/ui
|
|
11
|
+
# relative to the host's frontend/ directory, which does not hold from /app;
|
|
12
|
+
# the container resolves from package.json and never rewrites the host lock.
|
|
13
|
+
FROM node:20-alpine
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
COPY . .
|
|
16
|
+
EXPOSE 5173
|
|
17
|
+
CMD ["sh", "-c", "npm install --no-package-lock --no-audit --no-fund && exec npm run dev -- --host"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// ESLint flat config (ESLint >= 9) for the frontend.
|
|
2
|
+
//
|
|
3
|
+
// The Karni enforcement rules come from @karni/ui's bundled plugin:
|
|
4
|
+
// karni/no-raw-elements (compose ResourceTable/ResourceForm/Input instead of
|
|
5
|
+
// raw elements), karni/max-page-lines (pages cap at 50 physical lines),
|
|
6
|
+
// karni/no-deep-imports (never import @karni/ui/dist or /src),
|
|
7
|
+
// karni/no-direct-http (fetch/axios only under src/api/), plus a required
|
|
8
|
+
// reason on every disable directive and errors on unused directives.
|
|
9
|
+
import karni from "@karni/ui/eslint-plugin";
|
|
10
|
+
import js from "@eslint/js";
|
|
11
|
+
import globals from "globals";
|
|
12
|
+
import tseslint from "typescript-eslint";
|
|
13
|
+
|
|
14
|
+
export default tseslint.config(
|
|
15
|
+
// src/api/ is generated by `karni types` (and is the allowed home of raw
|
|
16
|
+
// HTTP) — lint what you write, not what Karni generates.
|
|
17
|
+
{ ignores: ["dist/", "node_modules/", "src/api/**"] },
|
|
18
|
+
js.configs.recommended,
|
|
19
|
+
...tseslint.configs.recommended,
|
|
20
|
+
...karni.configs.recommended,
|
|
21
|
+
{
|
|
22
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
23
|
+
languageOptions: {
|
|
24
|
+
globals: { ...globals.browser },
|
|
25
|
+
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
files: ["*.config.js", "*.config.ts"],
|
|
30
|
+
languageOptions: { globals: { ...globals.node } },
|
|
31
|
+
},
|
|
32
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{ cookiecutter.project_name }}</title>
|
|
7
|
+
<!-- public/favicon.svg is a placeholder: replace it with your own mark. -->
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{ cookiecutter.project_slug }}-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"dup": "jscpd --config .jscpd.json"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@hookform/resolvers": "^3.9.0",
|
|
15
|
+
"@karni/ui": {{ cookiecutter.karni_ui | tojson }},
|
|
16
|
+
"@tanstack/react-query": "^5.56.0",
|
|
17
|
+
"@tanstack/react-router": "^1.130.0",
|
|
18
|
+
"@tanstack/react-table": "^8.20.5",
|
|
19
|
+
"react": "^18.3.1",
|
|
20
|
+
"react-dom": "^18.3.1",
|
|
21
|
+
"react-hook-form": "^7.53.0",
|
|
22
|
+
"zod": "^3.23.8"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@eslint/js": "^9.39.5",
|
|
26
|
+
"@types/react": "^18.3.5",
|
|
27
|
+
"@types/react-dom": "^18.3.0",
|
|
28
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
29
|
+
"autoprefixer": "^10.4.20",
|
|
30
|
+
"eslint": "^9.39.5",
|
|
31
|
+
"globals": "^15.15.0",
|
|
32
|
+
"jscpd": "5.4.0",
|
|
33
|
+
"openapi-typescript": "^7.4.0",
|
|
34
|
+
"postcss": "^8.4.45",
|
|
35
|
+
"tailwindcss": "~3.4.19",
|
|
36
|
+
"typescript": "^5.5.4",
|
|
37
|
+
"typescript-eslint": "^8.71.0",
|
|
38
|
+
"vite": "^6.4.4"
|
|
39
|
+
},
|
|
40
|
+
"overrides": {
|
|
41
|
+
"postcss-selector-parser": "^7.1.7"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<!-- Placeholder icon. Replace this file with your own mark (keep the name, or
|
|
2
|
+
change the <link rel="icon"> in index.html). It is deliberately plain. -->
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="{{ cookiecutter.project_name }}">
|
|
4
|
+
<rect width="32" height="32" rx="7" fill="#64748B"/>
|
|
5
|
+
<circle cx="16" cy="16" r="6" fill="#FFFFFF"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AuthProvider, KarniProvider, Toaster, createKarniClient } from "@karni/ui";
|
|
2
|
+
import { RouterProvider } from "@tanstack/react-router";
|
|
3
|
+
import { router } from "./routes";
|
|
4
|
+
|
|
5
|
+
// One client for the whole app: every @karni/ui hook and component reaches
|
|
6
|
+
// the Django API through it (same origin; the Vite dev server proxies /api).
|
|
7
|
+
const client = createKarniClient({ baseUrl: "/api" });
|
|
8
|
+
|
|
9
|
+
export default function App() {
|
|
10
|
+
return (
|
|
11
|
+
<KarniProvider client={client}>
|
|
12
|
+
<AuthProvider>
|
|
13
|
+
<RouterProvider router={router} />
|
|
14
|
+
<Toaster />
|
|
15
|
+
</AuthProvider>
|
|
16
|
+
</KarniProvider>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev health probe. src/api/ is the one home of raw HTTP
|
|
3
|
+
* (karni/no-direct-http allows it); everywhere else, data access goes through
|
|
4
|
+
* the typed hooks and client that `karni types` generates into this
|
|
5
|
+
* directory.
|
|
6
|
+
*/
|
|
7
|
+
export type Health = "ok" | "fail" | "unreachable";
|
|
8
|
+
|
|
9
|
+
export async function fetchHealth(): Promise<Health> {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch("/health/ready/");
|
|
12
|
+
const data: unknown = await response.json();
|
|
13
|
+
const status =
|
|
14
|
+
typeof data === "object" && data !== null
|
|
15
|
+
? (data as { status?: unknown }).status
|
|
16
|
+
: undefined;
|
|
17
|
+
return status === "ok" ? "ok" : "fail";
|
|
18
|
+
} catch {
|
|
19
|
+
return "unreachable";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* Tailwind layers. The --karni-* tokens come from @karni/ui/styles.css
|
|
2
|
+
(imported first in main.tsx); override any token below to retheme. */
|
|
3
|
+
@tailwind base;
|
|
4
|
+
@tailwind components;
|
|
5
|
+
@tailwind utilities;
|
|
6
|
+
|
|
7
|
+
@layer base {
|
|
8
|
+
body {
|
|
9
|
+
@apply bg-background text-foreground antialiased;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The app shell: @karni/ui's <Layout>, with one nav entry per static route in
|
|
2
|
+
// routes.tsx, in registration order — pages from `karni make page` show up
|
|
3
|
+
// here on their own.
|
|
4
|
+
// Detail/edit routes ($id) and "new" forms are reached from their list page.
|
|
5
|
+
import { Layout, type LayoutNavItem } from "@karni/ui";
|
|
6
|
+
import { Outlet, useRouter, useRouterState } from "@tanstack/react-router";
|
|
7
|
+
|
|
8
|
+
function label(path: string): string {
|
|
9
|
+
const name = path.split("/").filter(Boolean).at(-1) ?? "home";
|
|
10
|
+
return name.replace(/[-_]+/g, " ").replace(/^./, (first) => first.toUpperCase());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function AppLayout() {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
|
16
|
+
const navItems: LayoutNavItem[] = Object.keys(router.routesByPath)
|
|
17
|
+
.filter((path) => !path.includes("$") && !path.endsWith("/new"))
|
|
18
|
+
.map((href) => ({
|
|
19
|
+
href,
|
|
20
|
+
label: label(href),
|
|
21
|
+
active: href === pathname || (href !== "/" && pathname.startsWith(`${href}/`)),
|
|
22
|
+
}));
|
|
23
|
+
return (
|
|
24
|
+
<Layout
|
|
25
|
+
navItems={navItems}
|
|
26
|
+
// Your brand goes here: swap the placeholder square for your mark
|
|
27
|
+
// (an <img src="/favicon.svg">, an inline SVG, or a component).
|
|
28
|
+
sidebarHeader={
|
|
29
|
+
<span className="flex items-center gap-2 px-3 text-lg font-semibold">
|
|
30
|
+
<span aria-hidden="true" className="h-6 w-6 rounded-md bg-slate-500" />
|
|
31
|
+
{{ cookiecutter.project_name }}
|
|
32
|
+
</span>
|
|
33
|
+
}
|
|
34
|
+
// Client-side navigation: intercept the default link's click (it
|
|
35
|
+
// still closes the mobile drawer); `contents` keeps the nav grid.
|
|
36
|
+
renderNavItem={(item, { defaultNode }) => (
|
|
37
|
+
<span
|
|
38
|
+
className="contents"
|
|
39
|
+
onClickCapture={(event) => {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
router.history.push(item.href);
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{defaultNode}
|
|
45
|
+
</span>
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
<Outlet />
|
|
49
|
+
</Layout>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "@karni/ui/styles.css";
|
|
2
|
+
import "./index.css";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import ReactDOM from "react-dom/client";
|
|
5
|
+
import App from "./App";
|
|
6
|
+
|
|
7
|
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<App />
|
|
10
|
+
</React.StrictMode>,
|
|
11
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Page } from "@karni/ui";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { fetchHealth, type Health } from "../api/health";
|
|
4
|
+
|
|
5
|
+
export default function HomePage() {
|
|
6
|
+
const [health, setHealth] = useState<Health | "checking">("checking");
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
void fetchHealth().then(setHealth);
|
|
10
|
+
}, []);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Page
|
|
14
|
+
title="{{ cookiecutter.project_name }}"
|
|
15
|
+
description="Vite + React dev server. Requests to /api and /health are proxied to Django."
|
|
16
|
+
>
|
|
17
|
+
<p className="text-sm">
|
|
18
|
+
Backend readiness:{" "}
|
|
19
|
+
<code className="rounded bg-muted px-1.5 py-0.5">{health}</code>
|
|
20
|
+
</p>
|
|
21
|
+
<p className="text-sm">
|
|
22
|
+
<a className="font-medium underline underline-offset-4" href="/api/docs/">
|
|
23
|
+
API docs
|
|
24
|
+
</a>
|
|
25
|
+
</p>
|
|
26
|
+
</Page>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Routes (TanStack Router, code-based). `karni make page` registers each new
|
|
2
|
+
// page through the two marker comments below — keep them. Hand-written routes
|
|
3
|
+
// are fine too: import the page and add a createRoute(...) entry to `routes`.
|
|
4
|
+
// Every route renders inside the app shell (./layout.tsx), whose navigation
|
|
5
|
+
// lists the static routes registered here.
|
|
6
|
+
import { createRootRoute, createRoute, createRouter } from "@tanstack/react-router";
|
|
7
|
+
import AppLayout from "./layout";
|
|
8
|
+
import HomePage from "./pages/home";
|
|
9
|
+
// karni:page-imports
|
|
10
|
+
|
|
11
|
+
const root = createRootRoute({ component: AppLayout });
|
|
12
|
+
|
|
13
|
+
const routes = [
|
|
14
|
+
createRoute({ getParentRoute: () => root, path: "/", component: HomePage }),
|
|
15
|
+
// karni:routes
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const router = createRouter({ routeTree: root.addChildren(routes) });
|
|
19
|
+
|
|
20
|
+
declare module "@tanstack/react-router" {
|
|
21
|
+
interface Register {
|
|
22
|
+
router: typeof router;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import karniPreset from "@karni/ui/tailwind-preset";
|
|
2
|
+
import type { Config } from "tailwindcss";
|
|
3
|
+
|
|
4
|
+
// Tailwind v3 + the @karni/ui preset: maps bg-primary, text-muted-foreground,
|
|
5
|
+
// border, rounded-lg, ... onto the --karni-* tokens from @karni/ui/styles.css.
|
|
6
|
+
export default {
|
|
7
|
+
presets: [karniPreset],
|
|
8
|
+
content: [
|
|
9
|
+
"./src/**/*.{ts,tsx}",
|
|
10
|
+
// So Tailwind sees the classes used inside @karni/ui:
|
|
11
|
+
"./node_modules/@karni/ui/dist/**/*.js",
|
|
12
|
+
],
|
|
13
|
+
} satisfies Config;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true,
|
|
12
|
+
"noFallthroughCasesInSwitch": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"types": ["vite/client"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import react from "@vitejs/plugin-react";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
// API and health requests from the dev server are proxied to Django:
|
|
5
|
+
// `karni serve --local` (runserver on 127.0.0.1:8000) by default;
|
|
6
|
+
// docker-compose sets KARNI_API_PROXY=http://django:8000 for its container.
|
|
7
|
+
const target = process.env.KARNI_API_PROXY ?? "http://127.0.0.1:8000";
|
|
8
|
+
const proxy = {
|
|
9
|
+
"/api": { target, changeOrigin: true },
|
|
10
|
+
"/health": { target, changeOrigin: true },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
plugins: [react()],
|
|
15
|
+
server: { host: true, port: 5173, proxy },
|
|
16
|
+
// `vite preview` serves the production build the same way (e2e suites, smoke checks).
|
|
17
|
+
preview: { host: true, port: 4173, proxy },
|
|
18
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Django's command-line utility for administrative tasks."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
"""Run administrative tasks."""
|
|
10
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
11
|
+
try:
|
|
12
|
+
from django.core.management import execute_from_command_line
|
|
13
|
+
except ImportError as exc:
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"Couldn't import Django. Are you sure it's installed and "
|
|
16
|
+
"available on your PYTHONPATH environment variable? Did you "
|
|
17
|
+
"forget to activate a virtual environment?"
|
|
18
|
+
) from exc
|
|
19
|
+
execute_from_command_line(sys.argv)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|