create-caspian-app 1.5.7 → 1.6.0-alpha

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.
@@ -44,11 +44,12 @@ This is the top architectural requirement for this workspace. Treat it as a hard
44
44
  - When `caspian.config.json` has `prisma: true`, all Python-side database reads and writes must go through the generated Prisma Python ORM exposed from `src/lib/prisma/**`. Do not bypass it with ad hoc sqlite/postgres drivers, hand-written fetch helpers, JSON files as active stores, browser-side database fetches, or custom HTTP endpoints that reinvent the ORM. Use raw SQL only through Prisma as a narrow fallback when the generated ORM cannot express the query clearly.
45
45
  - Treat `src/lib/prisma/__init__.py`, `src/lib/prisma/db.py`, `src/lib/prisma/models.py`, and `settings/prisma-schema.json` as generated outputs owned by `npx ppy generate`; do not create or hand-edit them manually.
46
46
  - Treat `package.json` scripts as opt-in operations. Do not run `npm run dev`, `npm run build`, `npm run static`, `npm run static:serve`, or other npm scripts unless the user explicitly asks, the task genuinely requires that exact script, or deployment preparation needs `npm run build`.
47
- - This workspace supports static HTML export (SSG, like Next.js `output: export`) as an app-owned build convention, not a shipped Caspian feature and not gated by a `caspian.config.json` flag. `npm run static` runs `npm run build && uv run python settings/build-static.py`; keep it composed on `npm run build` (Tailwind **plus** `projectName`) so `settings/files-list.json` is regenerated before `settings/build-static.py` walks that route index — do not reduce it to `tailwind:build` only, or a newly added route/component can be exported from a stale index. `npm run static:serve` runs `settings/serve-static.py`, which serves only `static/`, binds loopback `127.0.0.1` by default, and auto-selects a free port by walking upward from a preferred default (8000, overridable via `PORT`; `HOST`/`PORT_TRIES` also apply) so an occupied port never aborts the preview. Read the port the serve command prints; do not assume 8000 or read `settings/bs-config.json` for it (that file is the dev BrowserSync source of truth, not the static preview). Pre-render a dynamic route by exporting `static_paths` from its `index.py`; auth-gated, non-200, and non-HTML routes are skipped by design. Warn users that `pp.rpc()`, auth, WebSockets, streaming, and per-request server data are inert in a static export. See `node_modules/caspian-utils/dist/docs/static-export.md`.
47
+ - This workspace supports static HTML export (SSG, like Next.js `output: export`) as an app-owned build convention, not a shipped Caspian feature and not gated by a `caspian.config.json` flag. `npm run static` runs `npm run build && uv run python settings/build-static.py`; keep it composed on `npm run build` (`css:build` **plus** `projectName`) so `settings/files-list.json` is regenerated before `settings/build-static.py` walks that route index — do not reduce it to `css:build` only, or a newly added route/component can be exported from a stale index. `npm run static:serve` runs `settings/serve-static.py`, which serves only `static/`, binds loopback `127.0.0.1` by default, and auto-selects a free port by walking upward from a preferred default (8000, overridable via `PORT`; `HOST`/`PORT_TRIES` also apply) so an occupied port never aborts the preview. Read the port the serve command prints; do not assume 8000 or read `settings/bs-config.json` for it (that file is the dev BrowserSync source of truth, not the static preview). Pre-render a dynamic route by exporting `static_paths` from its `index.py`; auth-gated, non-200, and non-HTML routes are skipped by design. Warn users that `pp.rpc()`, auth, WebSockets, streaming, and per-request server data are inert in a static export. See `node_modules/caspian-utils/dist/docs/static-export.md`.
48
48
  - Use `npm run build` for deployment prep or an explicit build request, not as the default validation step for routine route, feature, or documentation edits.
49
49
  - **This workspace has an app-level quality gate, and running it is mandatory — not optional.** Any time you create, edit, or delete app-owned Python (`main.py`, `src/**`) — whether fixing a bug, adding a new file, refactoring, or implementing a feature — you must run `npm run check` (which calls `uv run python settings/check.py`) and get it fully green before treating the change as done. **A change is not complete while the gate reports anything.** It type checks with `pyright`, lints with `ruff`, and runs `pytest` in one pass, prints each problem as `path:line:col [tool:code] message`, and exits non-zero; fix every reported location and re-run until it passes clean. Do not report work as finished, hand it back, or move on to the next task on the assumption that it passes — actually run it and confirm green output first. Write app-owned Python to pass type checking (annotate parameters and returns, avoid untyped `Any` drift) and add or extend tests in `tests/` for the behavior you change. See `### tests/**/*.py and settings/check.py`.
50
50
  - **Know the gate's boundary: `npm run check` validates Python only** (`pyright` + `ruff` + `pytest` over `main.py`, `src/**`, and `settings/*.py`). It does **not** validate authored markup (the templates inside `index.py`, `layout.py`, and component `.py` files), `globals.css`/Tailwind, or `public/js/**` — a broken template, an invalid `x-*` tag, a single-root violation, or a PulsePoint error will pass the gate and only surface at render time. So a green gate means "the Python is sound," not "the page works." When you change templates, components, styles, or browser JS, verify them by actually loading the affected route in the browser (use the BrowserSync URL from `./settings/bs-config.json`) and checking it renders without console errors — do not treat a passing `npm run check` as proof that front-end work is correct.
51
51
  - **To see browser-side errors, run `npm run logs` — never start a second `npm run dev`.** The dev terminal usually belongs to the developer, so its stdout is invisible to you. `npm run dev` starts by deleting `.casp/` and `caches/`, so launching your own copy corrupts the running server's state, takes different ports, and rewrites `settings/bs-config.json`. Instead, PulsePoint's browser errors are appended to `.casp/browser-log.jsonl` and rendered by `npm run logs` (also printed at the end of every `npm run check`, where it never affects the exit code). Read its verdicts literally: `CLEAN` means that route was opened and rendered without error; a route **absent from the listing was never opened**, which is no signal rather than a pass; and `dev server is NOT running` means the entries are leftover history, not current state. After a fix, reload and exercise the affected route, then re-run `npm run logs`; verify the behavior and absence of fresh errors or relevant warnings. **What counts as exercising it depends on the status:** a mount error is cleared by a reload, but `NEEDS RECHECK` can mean an interaction error or an error carried across a source change. Repeat the affected interaction (click/submit). The reporter does not observe successful clicks, so historical recheck entries can remain; report the retest evidence and remaining history separately instead of claiming the digest is `CLEAN`. **Prefer the digest over raw `.casp/browser-log.jsonl`:** the file includes history and is compacted on source changes. A later matching load can retire earlier mount errors, but interaction and carried entries can still need rechecking. If inspecting raw events to correlate a retest, use session, route, page id, and timestamp; a later event does not unconditionally clear every earlier error. Never delete logs or restart the server just to make the status look clean. `UNCONFIRMED` means an error with no matching load in this log (a tab left open across a dev restart); ask for a reload before treating it as live. Details in `AGENTS.md`.
52
+ - `src/app/globals.css` is the only stylesheet you edit. It compiles to `public/css/styles.css` in every project, with or without Tailwind (`caspian.config.json` decides whether `postcss.config.js` loads the Tailwind plugin). Never edit `public/css/styles.css`.
52
53
  - Let the running dev stack own generated outputs such as `public/css/styles.css`, `settings/component-map.json`, `settings/files-list.json`, `__pycache__/`, and `.pyc` files. Treat those as generated artifacts rather than authored source.
53
54
  - Never treat `__pycache__/` directories or `.pyc` files as files to edit, regenerate on purpose, or keep in the final diff.
54
55
  - Treat `settings/component-map.json` and `settings/files-list.json` as generated outputs owned by `settings/component-map.ts` and `settings/files-list.ts`; inspect them when needed, but do not hand-edit them.
@@ -176,7 +177,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
176
177
 
177
178
  - Treat these as the app-owned static-export tooling (not shipped `casp` runtime). `settings/build-static.py` is the SSG exporter; `settings/serve-static.py` is the preview server. Document behavior against these files, `package.json`, and `settings/project-name.ts`, not against a `casp` module.
178
179
  - `settings/build-static.py` boots the real app via Starlette `TestClient` and iterates `get_files_index()` (which reads `settings/files-list.json`) to render every static route to `static/<route>/index.html`, then mirrors the complete `public/**` tree into `static/`. Keep its "warn & skip" scope policy: dynamic routes need `static_paths` in their `index.py`, and auth-gated / non-200 / non-HTML routes are reported and skipped rather than written broken. Preserve `APP_ENV=development` so the build needs no production secrets.
179
- - Keep `npm run static` composed as `npm run build && uv run python settings/build-static.py` so `projectName` regenerates `settings/files-list.json` and `settings/component-map.json` before the exporter walks the route index. Do not change it to run only `tailwind:build`.
180
+ - Keep `npm run static` composed as `npm run build && uv run python settings/build-static.py` so `projectName` regenerates `settings/files-list.json` and `settings/component-map.json` before the exporter walks the route index. Do not change it to run only `css:build`.
180
181
  - `settings/serve-static.py` must keep its robustness and safety contract: serve only `static/`, bind loopback `127.0.0.1` by default (network exposure only via `HOST=0.0.0.0`), auto-select a free port by genuinely binding upward from the preferred start port (default 8000; `PORT`/`PORT_TRIES` overrides) with `SO_REUSEADDR` disabled so an occupied Windows port truly fails instead of silently colliding, fail fast when `static/` is unbuilt, and shut down cleanly on Ctrl+C. Do not reintroduce a hardcoded-port `python -m http.server` one-liner.
181
182
  - Treat `static/` as generated output; do not hand-edit exported HTML. Fix the source route, component, or asset and re-export.
182
183
 
package/dist/AGENTS.md CHANGED
@@ -401,7 +401,7 @@ If the task generates or edits route, layout, or component HTML templates, check
401
401
  - Validation: read `node_modules/caspian-utils/dist/docs/validation.md`. Verify against `.venv/Lib/site-packages/casp/validate.py`.
402
402
  - Dates, times, "today", or date-range queries: read `node_modules/caspian-utils/dist/docs/core-runtime-map.md` "Application time (`casp.app_time`)". Verify against `.venv/Lib/site-packages/casp/app_time.py` and the `APP_TIMEZONE` resolution in `main.py`. **Never write a bare `datetime.now()` in `src/**`or`main.py`** — it returns the server's local wall clock, which silently disagrees with the UTC timestamps `src/lib/prisma/models.py`writes. Use`app_time.now()`/`today()`for the current moment,`to_app_time(...)`to display a stored value, and`day_bounds_utc(...)`with`gte`/`lt` to query a calendar day. Session expiry and cache TTLs stay on UTC and must not be routed through this module.
403
403
  - Database and seed flow: read `node_modules/caspian-utils/dist/docs/database.md` — start at "Two Generators, One Schema" for the required command order after schema changes (`npx prisma migrate dev` or `npx prisma db push`, then always `npx ppy generate`; `npx prisma generate` is Node-client-only and never a substitute). Verify against `prisma/schema.prisma`, `prisma/seed.ts`, and `src/lib/prisma/**`.
404
- - Static export (SSG) or previewing a static build: read `node_modules/caspian-utils/dist/docs/static-export.md`. Verify against `package.json` (`static`, `static:serve`), `settings/build-static.py`, `settings/serve-static.py`, and `settings/project-name.ts`. This is an app-owned convention, not a shipped Caspian feature and not gated by a `caspian.config.json` flag. `npm run static` = `npm run build && uv run python settings/build-static.py`, so it regenerates `settings/files-list.json` (via `projectName`) before the exporter walks that route index; do not reduce it back to `tailwind:build` only. `npm run static:serve` runs `settings/serve-static.py`, which auto-selects a free port from a preferred default (8000) and binds loopback `127.0.0.1` — read the port it prints, not `settings/bs-config.json` (that is the dev BrowserSync source of truth, not the static preview).
404
+ - Static export (SSG) or previewing a static build: read `node_modules/caspian-utils/dist/docs/static-export.md`. Verify against `package.json` (`static`, `static:serve`), `settings/build-static.py`, `settings/serve-static.py`, and `settings/project-name.ts`. This is an app-owned convention, not a shipped Caspian feature and not gated by a `caspian.config.json` flag. `npm run static` = `npm run build && uv run python settings/build-static.py`, so it regenerates `settings/files-list.json` (via `projectName`) before the exporter walks that route index; do not reduce it back to `css:build` only. `npm run static:serve` runs `settings/serve-static.py`, which auto-selects a free port from a preferred default (8000) and binds loopback `127.0.0.1` — read the port it prints, not `settings/bs-config.json` (that is the dev BrowserSync source of truth, not the static preview).
405
405
  - Testing, type checking, linting, or the quality gate: read `tests/README.md` and `settings/check.py`. This is a workspace-adopted convention, not a shipped Caspian feature, so it is documented in the workspace files (this section plus `.github/copilot-instructions.md`), not in the packaged docs. Verify against `pyproject.toml` (`[dependency-groups]`, `[tool.pyright]`, `[tool.ruff]`, `[tool.pytest.ini_options]`) and the `package.json` `check` script. The single command is `npm run check`.
406
406
  - Formatting code or markup: read `tests/README.md` "Formatting" and `settings/format.py`. App-owned tooling, not a shipped Caspian feature. The single command is `npm run format` (`npm run format:check` to report only); `npm run check:fix` runs it first, before the ruff fixes and the gate. It formats markup with **djLint** and Python with **`ruff format`**, in that order — reformatting a template changes how many lines its literal spans, which changes how ruff wraps the enclosing `html(...)` call, so ruff must run last for a single pass to converge. The house style is `html(r"""` on one line with the markup starting on the next; `ruff format` explodes that shape whenever the call has arguments besides the template, so `format.py` rejoins the opening afterwards and iterates the pair to a fixed point. A bare `ruff format` or an IDE format-on-save will re-split them — rerun `npm run format` rather than editing call sites by hand. Prettier is not usable on this markup: it has no Jinja awareness and de-indents `{% for %}` blocks to column 0. Verify against `settings/format.py`, `settings/_markup_equivalence.py`, and `tests/test_format.py`.
407
407
 
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
2
- import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),PACKAGE_ROOT=path.resolve(__dirname,".."),OPTIONAL_TEMPLATE_FILES=new Set([".python-version",".prettierrc"]),OPTIONAL_TEMPLATE_DIRECTORIES=new Set([".github",".vscode"]),CASPIAN_SECTION_START="\x3c!-- caspian:start --\x3e",CASPIAN_SECTION_END="\x3c!-- caspian:end --\x3e",SELF_UPDATE_GUARD_ENV="CREATE_CASPIAN_APP_SELF_UPDATED";let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.py","not-found.py","error.py"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1,websocket:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.py","src/app/index.py"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,websocket:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.py","src/app/index.py","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1,websocket:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0,websocket:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const n=e.indexOf("\\htdocs\\");if(-1===n)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,n+8).replace(/\\/g,"\\\\"),s=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let i=`http://localhost/${s}`;i=i.endsWith("/")?i.slice(0,-1):i;const a=i.replace(/(?<!:)(\/\/+)/g,"/"),c=s.replace(/\/\/+/g,"/");return{bsTarget:`${a}/`,bsPathRewrite:{"^/":`/${c.startsWith("/")?c.substring(1):c}/`}}}async function updatePackageJson(e,n){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const s=JSON.parse(fs.readFileSync(t,"utf8"));s.scripts={...s.scripts,projectName:"tsx settings/project-name.ts",format:"uv run python settings/format.py","format:check":"uv run python settings/format.py --check",check:"uv run python settings/check.py","check:fix":"uv run python settings/fix.py",logs:"uv run python settings/browser_log.py",static:"npm run build && uv run python settings/build-static.py","static:serve":"uv run python settings/serve-static.py"};let i=[];n.tailwindcss&&(s.scripts={...s.scripts,tailwind:"tsx settings/run-postcss.ts watch","tailwind:build":"tsx settings/run-postcss.ts build"},i.push("tailwind")),n.typescript&&!n.backendOnly&&(s.scripts={...s.scripts,"ts:watch":"vite build --watch","ts:watch:dev":"tsx settings/run-vite-watch.ts","ts:build":"vite build"},i.push("ts:watch:dev")),n.mcp&&(s.scripts={...s.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let a={...s.scripts};a.browserSync="tsx settings/bs-config.ts",a.dev=`npm-run-all projectName -l -p browserSync ${i.join(" ")}`;let c=["projectName"];n.tailwindcss&&c.unshift("tailwind:build"),n.typescript&&!n.backendOnly&&c.unshift("ts:build"),a.build=`npm-run-all ${c.join(" ")}`,s.scripts=a,s.type="module",fs.writeFileSync(t,JSON.stringify(s,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function buildEnvSection(e,n){return`# =============================================================================\n${e.split("\n").map(e=>e.startsWith("#")?e:`# ${e}`).join("\n")}\n# =============================================================================\n\n${n.trimEnd()}`}function buildCaspianEnvContent(e){const n=generateAuthSecret(),t=generateHexEncodedKey(8),s=generateHexEncodedKey(32),i=[];return e.prisma&&i.push(buildEnvSection("1. DATABASE\n# Enforced by: prisma/schema.prisma, src/lib/prisma/db.py",'# Connection string. Prisma reads this directly from .env.\n# Format reference: https://pris.ly/d/connection-strings\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"\n\n# Connection-pool limit. Defaults: SQLite 5; MySQL and PostgreSQL 20.\n# Use 5 for local development; production does not need this unless you want to\n# limit the pool.\nDB_POOL_SIZE=5\n\n# Seconds idle before the client re-probes its connection. Default 30.\nPRISMA_CONN_PROBE_IDLE_SECONDS=30\n\n# Warn on queries that cause a full table scan. 0/false silences it. Default 1.\nPRISMA_WARN_FULL_SCAN=1')),i.push(buildEnvSection("2. APPLICATION RUNTIME\n# Enforced by: casp/runtime_security.py is_production_environment()",'# Environment selector, resolved FAIL-CLOSED: only an explicit development\n# value (dev, development, local, staging, test, testing) enables the\n# development relaxations. Unset or misspelled counts as production.\n#\n# Production turns on: HTTPS-only session cookie, Secure CSRF cookie, HSTS,\n# generic error messages, mandatory AUTH_SECRET, mandatory MCP_AUTH_TOKEN, and\n# it removes the localhost origin bypass and the WebSocket same-origin fallback.\n#\n# This single value gates most of the security posture. Set it deliberately.\nAPP_ENV="development"\n\n# Calendar timezone for the application, as an IANA name (e.g. "UTC",\n# "America/New_York", "America/Santo_Domingo"). Read by casp/app_time.py and\n# resolved once at boot in main.py.\n#\n# This sets which wall-clock DAY an instant belongs to: what casp.app_time.now()\n# and today() answer, how a stored timestamp reads back to a user, and the\n# boundaries a "today\'s totals" query uses. Timestamps are still STORED in UTC.\n#\n# It deliberately does NOT affect absolute time -- session expiry (casp/auth.py)\n# and cache TTLs (casp/cache_handler.py) stay on UTC, so changing this can never\n# extend a session or a cache entry.\n#\n# An unrecognized name raises InvalidAppTimezoneError at startup rather than\n# silently falling back to UTC. Empty or unset means UTC.\nAPP_TIMEZONE="UTC"'),buildEnvSection("3. PUBLIC URL, CORS, AND ORIGIN VALIDATION\n# Enforced by: casp/rpc.py origin checks, main.py CORS layer",'# Canonical public origin. Leave empty when the browser URL and the app runtime\n# URL match. Set it when they differ, i.e. behind an ingress, reverse proxy,\n# load balancer, gateway, edge network, or TLS terminator.\nAPP_BASE_URL=""\n\n# Extra browser origins allowed to call protected endpoints such as RPC. Use\n# when one deployment is reachable from more than one public origin.\n# Comma-separated, no spaces.\nCORS_ALLOWED_ORIGINS=""\n\n# Trust Forwarded/X-Forwarded-* headers. Enable ONLY when every request passes\n# through infrastructure that strips client-supplied forwarded headers before\n# setting its own, because a direct client can otherwise forge them.\n#\n# Affects two things: which origin RPC accepts, and which address the rate\n# limiter buckets on. Left false, both use the direct request instead.\nTRUST_FORWARDED_HEADERS="false"\n\n# Allow cookies/Authorization on cross-origin requests. Keep true only when\n# credentialed cross-origin requests are actually required.\nCORS_ALLOW_CREDENTIALS="true"\n\n# CORS preflight response fields.\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\n\n# Preflight cache duration in seconds.\nCORS_MAX_AGE="86400"'),buildEnvSection("4. AUTHENTICATION AND SESSIONS\n# Enforced by: casp/auth.py, main.py SessionMiddleware\n# Route privacy and RBAC live in src/lib/auth/auth_config.py, not here.",`# Session signing secret. Unique and strong per app and per environment.\n# In production the app refuses to start when this is missing or left on a\n# placeholder ("change-me"/"changeme"); in development it falls back.\nAUTH_SECRET="${n}"\n\n# Session cookie name. Use a unique value when several apps share a parent\n# domain, or their sessions overwrite each other.\nAUTH_COOKIE_NAME="${t}"\n\n# Session lifetime in hours (SessionMiddleware max_age).\nSESSION_LIFETIME_HOURS="7"`),buildEnvSection("5. OAUTH PROVIDERS\n# Enforced by: casp/auth.py; routes served by main.py AuthMiddleware",'# Google and GitHub sign-in are already wired: AuthMiddleware serves\n# /api/auth/signin/{google,github} and /api/auth/callback/{google,github}.\n# Link a button at those paths, do not hand-roll OAuth.\n#\n# A provider with no client id is skipped SILENTLY: the redirect returns None\n# and the button appears dead, with no error and no log. Empty means disabled.\n\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\n\n# Must match the redirect URI registered in Google Cloud Console exactly.\n# Google is skipped unless BOTH the client id and this value are set.\nGOOGLE_REDIRECT_URI=\n\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\n\n# Callback the GitHub authorize request asks for. A GitHub App accepts up to 10\n# redirect URIs, but a request that omits this parameter always lands on the\n# FIRST one registered on the App -- which is why adding a localhost URI next to\n# the production one has no effect on its own. Register both on the App, then\n# let each environment name the one it wants here. Must match a registered URI\n# character for character. Leave empty to derive it from APP_BASE_URL as\n# "<APP_BASE_URL>/api/auth/callback/github", and empty with no APP_BASE_URL to\n# send no redirect_uri at all (GitHub\'s first-registered-URI default).\nGITHUB_REDIRECT_URI='),buildEnvSection("6. REQUEST SECURITY\n# Enforced by: main.py BodySizeLimitMiddleware, RequestDiagnosticsMiddleware",'# Max size of the whole HTTP request body in MB. Caps the entire body (file +\n# form fields + encoding overhead), so usable file size is a bit below this.\n# Middleware rejects oversized requests before the route runs. Raise if valid\n# uploads are blocked. Default 16.\nMAX_CONTENT_LENGTH_MB="16"\n\n# Seconds before a stalled route returns 504. Streaming paths (/mcp) are exempt\n# so long-lived transports are not cut mid-response. Default 20.\nCASPIAN_REQUEST_TIMEOUT_SECONDS=20'),buildEnvSection("7. SECURITY HEADERS\n# Enforced by: casp/runtime_security.py, main.py SecurityHeadersMiddleware","# Replaces the built-in Content-Security-Policy wholesale. Empty keeps the\n# default, which already permits the app's own assets.\n#\n# Any replacement MUST keep 'unsafe-eval' and 'unsafe-inline' in script-src:\n# the PulsePoint runtime compiles component templates with new Function(), so\n# removing them stops every page from rendering. Set this only to widen the\n# policy, e.g. for a CDN, analytics host, or an external frame embedder.\n#\n# Outside production, connect-src also allows http(s)/ws on localhost and\n# 127.0.0.1 on any port, because BrowserSync serves the proxied page on one port\n# while its injected live-reload client polls the BrowserSync server on another.\n# That is a separate origin, so 'self' does not cover it. Those entries are\n# omitted from a production policy. Setting an override here replaces BOTH, so\n# an override used in development must include the loopback sources itself or\n# live reload stops working.\n#\n# img-src and media-src admit remote content by scheme, so posters, avatars, CDN\n# thumbnails, and video load without per-project configuration. Plain http: is\n# development-only. Set an override here to pin them to named origins instead.\n#\n# Default: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';\n# style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:;\n# media-src 'self' data: blob: https:; font-src 'self' data:;\n# connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';\n# form-action 'self'; frame-ancestors 'self'\nCONTENT_SECURITY_POLICY="),buildEnvSection("8. RATE LIMITING\n# Enforced by: main.py RateLimitMiddleware, casp/rpc.py RPCRateLimiter\n# Buckets are per client address; see TRUST_FORWARDED_HEADERS for which one.",'# Per-IP cap on page requests, applied before session decryption and rendering.\n# Static assets (/css, /js, /assets, /favicon.ico) and /health are exempt, so a\n# page load does not spend its own budget on its assets. Empty disables it.\n# Default 200/minute.\nRATE_LIMIT_PAGES=200/minute\n\n# Fallback limit for @rpc() actions that declare no limits= of their own.\nRATE_LIMIT_RPC="60 per minute"\n\n# Limit applied to @rpc(require_auth=True) actions that declare no limits=.\n# Tighten this and the per-action limits on sign-in and other credential paths.\nRATE_LIMIT_AUTH="60 per minute"\n\n# Configured for slowapi\'s Limiter. Note that slowapi\'s middleware is not in\n# the stack, so this value is inert today; page limiting is RATE_LIMIT_PAGES.\nRATE_LIMIT_DEFAULT="200 per minute"\n\n# In-memory bucket ceiling and sweep interval for the limiter store.\n# Defaults 10000 buckets, swept every 60 seconds.\nRATE_LIMIT_MAX_BUCKETS=10000\nRATE_LIMIT_CLEANUP_INTERVAL=60')),e.websocket&&i.push(buildEnvSection("9. WEBSOCKETS\n# Enforced by: src/lib/websocket/websocket_security.py, main.py channel loop\n# Only active when caspian.config.json has websocket: true.","# Browser origins allowed to open a socket (anti-CSWSH). Falls back to\n# CORS_ALLOWED_ORIGINS then APP_BASE_URL when empty.\n#\n# REQUIRED IN PRODUCTION. The convenience same-origin fallback is derived from\n# the client-supplied Host header, so it is development-only: without an\n# explicit list a spoofed Host plus matching Origin would validate itself.\n#\n# The HTTP middleware stack skips websocket scopes, so this and the socket\n# guard are the only checks a handshake passes. Comma-separated, no spaces.\nWEBSOCKET_ALLOWED_ORIGINS=\n\n# Seconds a socket may stay silent before the server closes it. Default 120.\nWEBSOCKET_IDLE_TIMEOUT_SECONDS=120\n\n# Max size of one inbound socket message in bytes. Oversized closes with 1009.\n# Default 4096.\nMAX_WEBSOCKET_MESSAGE_BYTES=4096\n\n# Simultaneous connections per pool; authenticated and guest pools are counted\n# separately. Refused connections close with 1013 during the handshake. Every\n# open socket is a live task and a broadcast target. Default 200.\nMAX_WEBSOCKET_CONNECTIONS=200\n\n# Per-connection send budget: messages allowed per rolling window. Each\n# accepted message fans out to the whole pool, so this bounds how much\n# broadcast one connection can generate. Defaults 20 per 10 seconds.\nMAX_WEBSOCKET_MESSAGES_PER_WINDOW=20\nWEBSOCKET_RATE_WINDOW_SECONDS=10")),e.mcp&&i.push(buildEnvSection("10. MCP ENDPOINT\n# Enforced by: main.py MCPAuthMiddleware; tools in src/lib/mcp/mcp_server.py\n# Only active when caspian.config.json has mcp: true.",`# Bearer token required to call /mcp. The MCP app is mounted outside the page\n# routing tree, so AuthMiddleware does NOT protect it, and its tools enumerate\n# the workspace file inventory and component map.\n#\n# generated -> every request needs "Authorization: Bearer <token>"\n#\n# REQUIRED IN PRODUCTION for the endpoint to work at all.\nMCP_AUTH_TOKEN="${s}"`)),i.push(buildEnvSection("11. CACHE\n# Enforced by: casp/cache_handler.py, main.py is_request_cacheable()",'# Master switch for serving pages from the disk cache.\n#\n# Entries are keyed on the URI alone, with no session component, so an\n# authenticated render is never cached: is_request_cacheable() gates both the\n# read and the write, and a route\'s Cache(...) cannot override it.\nCACHE_ENABLED="false"\n\n# Default cache lifetime in seconds, used when a route sets no ttl.\nCACHE_TTL="600"'),buildEnvSection("12. SERVER PROCESS\n# Enforced by: main.py __main__, settings/serve-static.py",'# Uvicorn workers are separate OS processes for the same FastAPI app. More\n# workers can increase throughput under concurrent load, but they do not make a\n# single request faster and they duplicate memory, connection pools, and any\n# in-process state. Keep at 1 unless the app is designed for multi-process\n# coordination and testing shows a real concurrency bottleneck.\nUVICORN_WORKERS="1"')),i.join("\n\n")}function copyRecursiveSync(e,n,t){const s=fs.existsSync(e),i=s&&fs.statSync(e);if(s&&i&&i.isDirectory()){const s=n.toLowerCase();if(!t.mcp&&s.includes("src\\lib\\mcp"))return;if(!t.websocket&&s.includes("src\\lib\\websocket"))return;if((!t.typescript||t.backendOnly)&&(s.endsWith("\\ts")||s.includes("\\ts\\")))return;if((!t.typescript||t.backendOnly)&&(s.endsWith("\\vite-plugins")||s.includes("\\vite-plugins\\")||s.includes("\\vite-plugins")))return;if(t.backendOnly&&s.includes("public\\js")||t.backendOnly&&s.includes("public\\css")||t.backendOnly&&s.includes("public\\assets"))return;const i=n.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),fs.readdirSync(e).forEach(s=>{copyRecursiveSync(path.join(e,s),path.join(n,s),t)})}else{if(checkExcludeFiles(n))return;const s=n.replace(/\\/g,"/").toLowerCase();if(s.endsWith("/settings/run-vite-watch.ts")&&(!t.typescript||t.backendOnly))return;if(s.endsWith("/ts/tailwind-merge.ts")&&(!t.typescript||t.backendOnly||!t.tailwindcss))return;if(!t.tailwindcss&&(n.includes("globals.css")||n.includes("styles.css")))return;if(!t.mcp&&n.includes("restart-mcp.ts"))return;if(!t.websocket&&n.includes("src\\lib\\websocket"))return;if(t.backendOnly&&nonBackendFiles.some(e=>n.includes(e)))return;if(t.backendOnly&&n.includes("layout.py"))return;if(t.tailwindcss&&n.includes("index.css"))return;if(!t.prisma&&n.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,n,0)}}async function executeCopy(e,n,t){n.forEach(({src:n,dest:s})=>{const i=normalizeTemplatePath(n),a=resolveTemplateSourcePath(n,"directory"),c=path.join(e,s);if(!a){if(OPTIONAL_TEMPLATE_DIRECTORIES.has(i))return void console.log(chalk.gray(`Optional template directory not found, skipping: ${i}`));throw new Error(`Template directory not found: ${i}. The package may be incomplete.`)}copyRecursiveSync(a,c,t)})}function modifyLayoutPHP(e,n){const t=path.join(e,"src","app","layout.py");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),s="";n.backendOnly||(n.tailwindcss||(s='\n <link href="/css/index.css" rel="stylesheet" />'),s+='\n <script type="module" src="/js/main.js"><\/script>');let i="";n.backendOnly||(i=n.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${s}`:s),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.py:"),e)}}async function createOrUpdateEnvFile(e,n){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,n,{flag:"w"})}function ensureClaudeMd(e){const n=path.join(e,"CLAUDE.md");if(checkExcludeFiles(n))return;const t="@AGENTS.md";if(!fs.existsSync(n))return void fs.writeFileSync(n,`${t}\n`,{flag:"w"});const s=fs.readFileSync(n,"utf8").replace(/^\uFEFF/,"");if(s.trimStart().startsWith(t))return;const i=`${t}\n\n${s.trimStart()}`;fs.writeFileSync(n,i,{flag:"w"})}function writeTailwindMainJs(e){const n=path.join(e,"public","js","main.js");checkExcludeFiles(n)||(fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,'import "/js/pp-reactive-v2.min.js";\nimport { twMerge } from "/js/tailwind-merge.mjs";\n\nconst pp = (globalThis).pp;\n\nglobalThis.twMerge = twMerge;\n\nif (document.readyState !== "loading") {\n pp?.mount?.();\n} else {\n document.addEventListener(\n "DOMContentLoaded",\n () => pp?.mount?.(),\n { once: true },\n );\n}\n',{flag:"w"}))}function copyTailwindMergeBundle(e){const n=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs"),t=path.join(e,"public","js","tailwind-merge.mjs"),s=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs.map"),i=path.join(e,"public","js","bundle-mjs.mjs.map");if(!checkExcludeFiles(t)){if(!fs.existsSync(n))throw new Error(`tailwind-merge bundle not found at ${n}`);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.copyFileSync(n,t),!checkExcludeFiles(i)&&fs.existsSync(s)&&fs.copyFileSync(s,i)}}function writeTailwindTypeScriptMain(e){const n=path.join(e,"ts","main.ts");checkExcludeFiles(n)||(fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,'import "/js/pp-reactive-v2.min.js";\n\n// The following global names have already been declared elsewhere in the project:\n// - pp: Used for the Reactive Core functionality.\n\n// Imports goes here --Start\nimport { createGlobalSingleton } from "./global-functions.js";\nimport { mergeTailwindClasses } from "./tailwind-merge.js";\n\ncreateGlobalSingleton("twMerge", mergeTailwindClasses);\n\n\n// Imports goes here --End\n\nconst pp = (globalThis as any).pp;\n\nif (document.readyState !== "loading") {\n\tpp?.mount?.();\n} else {\n\tdocument.addEventListener(\n\t\t"DOMContentLoaded",\n\t\t() => pp?.mount?.(),\n\t\t{ once: true },\n\t);\n}\n',{flag:"w"}))}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const n=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(n)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const t=e.replace(/\\/g,"/");return n.endsWith("/"+t)||n===t})}function normalizeTemplatePath(e){return e.replace(/^[\\/]+/,"")}function resolveTemplateSourcePath(e,n){const t=normalizeTemplatePath(e),s=[path.join(__dirname,t),path.join(PACKAGE_ROOT,t)];for(const e of s){if(!fs.existsSync(e))continue;const t=fs.statSync(e);if("file"===n&&t.isFile())return e;if("directory"===n&&t.isDirectory())return e}return null}function extractCaspianSection(e){const n=e.indexOf(CASPIAN_SECTION_START);if(-1===n)return null;const t=e.indexOf(CASPIAN_SECTION_END,n);return-1===t?null:e.slice(t>n?n:0,t+20)}function mergeAgentsCaspianSection(e,n){const t=extractCaspianSection(n);if(!t)return e;const s=e.indexOf(CASPIAN_SECTION_START),i=e.indexOf(CASPIAN_SECTION_END,s);if(-1!==s&&-1!==i){return`${e.slice(0,s)}${t}${e.slice(i+20)}`}const a=e.endsWith("\n");return`${e}${a?"\n":"\n\n"}${t}\n`}async function createDirectoryStructure(e,n){const t=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/AGENTS.md",dest:"/AGENTS.md"},{src:"/.python-version",dest:"/.python-version"}];n.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),n.typescript&&!n.backendOnly&&t.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const s=[{src:"/settings",dest:"/settings"},{src:"/tests",dest:"/tests"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"},{src:"/.vscode",dest:"/.vscode"}];n.typescript&&!n.backendOnly&&s.push({src:"/ts",dest:"/ts"}),t.forEach(({src:n,dest:t})=>{const s=normalizeTemplatePath(n),i=resolveTemplateSourcePath(n,"file"),a=path.join(e,t);if(checkExcludeFiles(a))return;if(!i){if(OPTIONAL_TEMPLATE_FILES.has(s))return void console.log(chalk.gray(`Optional template file not found, skipping: ${s}`));throw new Error(`Template file not found: ${s}. The package may be incomplete.`)}if("/pyproject.toml"===n&&updateAnswer?.isUpdate&&fs.existsSync(a))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const c=fs.readFileSync(i,"utf8");if("/AGENTS.md"===n&&updateAnswer?.isUpdate&&fs.existsSync(a)){const e=mergeAgentsCaspianSection(fs.readFileSync(a,"utf8"),c);return void fs.writeFileSync(a,e,{flag:"w"})}fs.writeFileSync(a,c,{flag:"w"})}),await executeCopy(e,s,n),ensureClaudeMd(e),n.tailwindcss&&!n.backendOnly&&(n.typescript?writeTailwindTypeScriptMain(e):(copyTailwindMergeBundle(e),writeTailwindMainJs(e))),await updatePackageJson(e,n),!n.tailwindcss&&n.backendOnly||modifyLayoutPHP(e,n),await createOrUpdateEnvFile(e,buildCaspianEnvContent(n))}async function getAnswer(e={},n=!1){if(e.starterKit){const n=e.starterKit;let t=null;if(STARTER_KITS[n]&&(t=STARTER_KITS[n]),t){const s={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,prisma:t.features.prisma??!1,mcp:t.features.mcp??!1,websocket:t.features.websocket??!1,typescript:t.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(s.backendOnly=!0),i.includes("--tailwindcss")&&(s.tailwindcss=!0),i.includes("--mcp")&&(s.mcp=!0),i.includes("--websocket")&&(s.websocket=!0),i.includes("--prisma")&&(s.prisma=!0),i.includes("--typescript")&&(s.typescript=!0),s}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,websocket:!1,typescript:!1},s=process.argv.slice(2);return s.includes("--backend-only")&&(t.backendOnly=!0),s.includes("--tailwindcss")&&(t.tailwindcss=!0),s.includes("--mcp")&&(t.mcp=!0),s.includes("--websocket")&&(t.websocket=!0),s.includes("--prisma")&&(t.prisma=!0),s.includes("--typescript")&&(t.typescript=!0),t}}if(n)return{projectName:e.projectName??"my-app",starterKit:e.starterKit,starterKitSource:e.starterKitSource,backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,websocket:e.websocket??!1,prisma:e.prisma??!1};const t=[];e.projectName||t.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||t.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const s=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(t,{onCancel:s}),a=[];i.backendOnly??e.backendOnly??!1?(e.mcp||a.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||a.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||a.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||a.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||a.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||a.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||a.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||a.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"}));const c=await prompts(a,{onCancel:s});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:c.tailwindcss??e.tailwindcss??!1,typescript:c.typescript??e.typescript??!1,mcp:c.mcp??e.mcp??!1,websocket:c.websocket??e.websocket??!1,prisma:c.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,n,t=!1){console.log("Uninstalling Node dependencies:"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=buildManagedNpmCommand(["uninstall",t?"--save-dev":"--save",...n]);execSync(s,{stdio:"inherit",cwd:e})}function buildManagedNpmCommand(e){return`npm ${e.join(" ")} --ignore-scripts=false --min-release-age=0 --audit=false`}function fetchPackageVersion(e){return new Promise((n,t)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let s="";e.on("data",e=>s+=e),e.on("end",()=>{try{const e=JSON.parse(s);n(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}})}).on("error",e=>t(e))})}const readJsonFile=e=>{const n=fs.readFileSync(e,"utf8");return JSON.parse(n)};function compareVersions(e,n){const t=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),s=n.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!t||!s)return e.localeCompare(n);const i=t.slice(1,4).map(Number),a=s.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>a[e])return 1;if(i[e]<a[e])return-1}const c=t[4]??null,o=s[4]??null;return c&&!o?-1:!c&&o?1:c&&o?c.localeCompare(o):0}function getInstalledPackageInfo(e){try{const n=execSync(buildManagedNpmCommand(["list","-g",e,"--depth=0"])).toString(),t=n.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return t?{version:t[1],isLinked:n.includes(`${e}@`)&&n.includes("->")}:(console.error(`Package ${e} is not installed`),{version:null,isLinked:!1})}catch(e){return console.error(e instanceof Error?e.message:String(e)),{version:null,isLinked:!1}}}function isRunningFromNpxCache(e){const n=path.resolve(e).toLowerCase(),t=`${path.sep}_npx${path.sep}`.toLowerCase();return n.includes(t)}function resolveGlobalCliEntryPoint(){const e=[__filename];try{const n=execSync("npm root -g").toString().trim();n&&e.push(path.join(n,"create-caspian-app","dist","index.js"))}catch{}for(const n of e)if(n&&fs.existsSync(n))return n;return null}async function ensureLatestCliVersion(e){const n=await fetchPackageVersion("create-caspian-app");if("1"===process.env[SELF_UPDATE_GUARD_ENV])return n;if(isRunningFromNpxCache(__dirname))return console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")),n;const t=getInstalledPackageInfo("create-caspian-app");if(t.isLinked)return console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")),n;if(!(!t.version||-1===compareVersions(t.version,n)))return n;execSync(buildManagedNpmCommand(["install","-g",`create-caspian-app@${n}`]),{stdio:"inherit"});const s=resolveGlobalCliEntryPoint();if(!s)throw new Error(`create-caspian-app was updated to ${n}, but the updated CLI could not be located. Please run the command again.`);console.log(chalk.gray(`Restarting with create-caspian-app@${n}...`));const i=spawnSync(process.execPath,[s,...e],{stdio:"inherit",env:{...process.env,[SELF_UPDATE_GUARD_ENV]:"1"}});if(i.error)throw i.error;process.exit(i.status??1)}async function installNpmDependencies(e,n,t=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync(buildManagedNpmCommand(["init","-y"]),{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=buildManagedNpmCommand(["install",...t?["--save-dev"]:[],...n]);execSync(s,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.3.3","@types/browser-sync":"2.29.1","@types/node":"26.4.1","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"6.0.0","chokidar-cli":"3.0.0",cssnano:"9.0.2","npm-run-all":"4.1.5",postcss:"8.5.28","postcss-cli":"12.0.0",prompts:"2.4.2",tailwindcss:"4.3.3",tsx:"4.23.13",typescript:"7.0.2",vite:"8.2.2",vitest:"5.0.0","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.19","caspian-utils":"0.2.x","tailwind-merge":"3.6.0"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(n){const t=n;if("win32"===globalThis.process?.platform&&("EPERM"===t.code||"EACCES"===t.code)){try{spawnSync("cmd",["/c","attrib","-R","-H","-S","/S","/D",`${e}\\*`],{stdio:"ignore"})}catch{}return void spawnSync("cmd",["/c","rd","/s","/q",e],{stdio:"ignore"})}throw n}}async function setupStarterKit(e,n){if(!n.starterKit)return;let t=null;if(STARTER_KITS[n.starterKit]?t=STARTER_KITS[n.starterKit]:n.starterKitSource&&(t={id:n.starterKit,name:`Custom Starter Kit (${n.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:n.starterKitSource}}),t){if(console.log(chalk.green(`Setting up ${t.name}...`)),t.source)try{const s=t.source.branch?`git clone -b ${t.source.branch} --depth 1 ${t.source.url} "${e}"`:`git clone --depth 1 ${t.source.url} "${e}"`;execSync(s,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const i=path.join(e,"caspian.config.json");if(fs.existsSync(i))try{const t=JSON.parse(fs.readFileSync(i,"utf8")),s=e,a=bsConfigUrls(s);t.projectName=n.projectName,t.projectRootPath=s,t.bsTarget=a.bsTarget,t.bsPathRewrite=a.bsPathRewrite;const c=await fetchPackageVersion("create-caspian-app");t.version=t.version||c,fs.writeFileSync(i,JSON.stringify(t,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}t.customSetup&&await t.customSetup(e,n),console.log(chalk.green(`✓ ${t.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${n.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\n🚀 Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const n=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(n)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const t=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");t&&console.log(chalk.magenta(` Features: ${t}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,n,t){const s=spawnSync(e,n,{cwd:t,stdio:"inherit",shell:!1,encoding:"utf8"});if(s.error)throw s.error;if(0!==s.status)throw new Error(`Command failed (${e} ${n.join(" ")}), exit=${s.status}`)}function tryRunCmd(e,n,t){const s=spawnSync(e,n,{cwd:t,stdio:"ignore",shell:!1,encoding:"utf8"});return!s.error&&0===s.status}function tryInstallUv(e){console.log(chalk.blue("uv not found. Attempting to install uv..."));const n=[{cmd:"py",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python3",args:["-m","pip","install","--upgrade","uv"]}];for(const t of n)if(tryRunCmd(t.cmd,t.args,e))return!0;return!1}function resolveUvCommand(e){const n=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const t of n)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;if(tryInstallUv(e))for(const t of n)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;throw new Error("Could not find or install uv. Install uv and ensure `uv`, `py`, or `python` is available in PATH.")}function buildPythonDependencies(e){const n=["fastapi==0.141.1","uvicorn==0.52.4","python-dotenv==1.2.3","tzdata==2026.3","jinja2==3.1.6","beautifulsoup4==4.15.0","slowapi==0.1.10","python-multipart==0.0.32","starsessions==2.2.1","httpx2==2.12.0","werkzeug==3.1.8","cuid2==2.0.1","nanoid==2.0.0","python-ulid==4.0.1","cuid==0.4","caspian-utils~=0.4"];return e.mcp&&n.push("fastmcp==4.0.3"),e.websocket&&n.push("websockets==17.1"),e.prisma&&(n.push("psycopg2-binary==2.9.12"),n.push("asyncpg==0.31.0"),n.push("aiosqlite==0.22.1"),n.push("aiomysql==0.3.2")),n}function buildPythonDevDependencies(){return["pyright==1.1.411","ruff==0.16.6","pytest==9.1.1","djlint==1.45.2"]}function getPythonRequirementName(e){const n=e.trim().match(/^([A-Za-z0-9._-]+)/);return n?.[1]??null}function getPyProjectDependencyNames(e){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))return new Set;const t=fs.readFileSync(n,"utf8").replace(/\r\n/g,"\n").match(/^[ \t]*dependencies[ \t]*=[ \t]*\[([\s\S]*?)\]/m);if(!t)return new Set;const s=new Set,i=/"([^"]+)"/g;let a;for(;null!==(a=i.exec(t[1]));){const e=a[1].trim().match(/^([A-Za-z0-9._-]+)/)?.[1];e&&s.add(e.toLowerCase())}return s}function ensurePyProjectExists(e){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))throw new Error(`pyproject.toml not found at: ${n}`);let t=fs.readFileSync(n,"utf8");t=t.replace(/\r\n/g,"\n"),t.includes("package = false")||(t=t.includes("[tool.uv]")?t.replace("[tool.uv]","[tool.uv]\npackage = false"):`${t.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(n,t,"utf8")}async function ensurePythonVenvAndDeps(e,n,t=[]){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: syncing dependencies with uv")),console.log(chalk.green("=========================\n")),console.log(chalk.blue("Preparing pyproject.toml...")),ensurePyProjectExists(e);const s=path.join(e,"requirements.txt");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(chalk.gray("Removed legacy requirements.txt")));const i=resolveUvCommand(e),a=path.join(e,".venv");fs.existsSync(a)?console.log(chalk.blue("Existing .venv detected. Reusing it so uv sync can update dependencies without replacing the environment.")):(console.log(chalk.blue("Creating the virtual environment with uv...")),runCmd(i.cmd,[...i.argsPrefix,"venv",".venv"],e));const c=buildPythonDependencies(n),o=buildPythonDevDependencies(),r=c.map(e=>getPythonRequirementName(e)).filter(e=>null!==e),l=o.map(e=>getPythonRequirementName(e)).filter(e=>null!==e);t.length>0&&(console.log(chalk.blue("Removing obsolete Python dependencies via uv remove...")),runCmd(i.cmd,[...i.argsPrefix,"remove",...t],e));const p=r.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...p,...c],e);const d=l.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dev dependencies via uv add --dev...")),runCmd(i.cmd,[...i.argsPrefix,"add","--dev",...d,...o],e),console.log(chalk.blue("Syncing dependencies...")),runCmd(i.cmd,[...i.argsPrefix,"sync"],e),console.log(chalk.green("\n✓ uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),n=e.includes("-y");let t=e[0];const s=e.find(e=>e.startsWith("--starter-kit=")),i=s?.split("=")[1],a=e.find(e=>e.startsWith("--starter-kit-source=")),c=a?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();const o=await ensureLatestCliVersion(e);let r=null,l=!1;if(t){const s=process.cwd(),a=path.join(s,"caspian.config.json");if(i&&c){l=!0;const s={projectName:t,starterKit:i,starterKitSource:c,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};r=await getAnswer(s,n)}else if(fs.existsSync(a)){const i=readJsonFile(a);let c=[];i.excludeFiles?.map(e=>{const n=path.join(s,e);fs.existsSync(n)&&c.push(n.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,websocket:i.websocket??!1,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:c??[],filePath:s};const o={projectName:t,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp,websocket:e.includes("--websocket")||(i.websocket??!1)};r=await getAnswer(o,n),null!==r&&(updateAnswer={projectName:t,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,websocket:r.websocket,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:c??[],filePath:s})}else{const s={projectName:t,starterKit:i,starterKitSource:c,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};r=await getAnswer(s,n)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},n);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const p=process.cwd();let d;if(t)if(l){const n=path.join(p,t);fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),d=n,await setupStarterKit(d,r),process.chdir(d);const s=path.join(d,"caspian.config.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));e.includes("--backend-only")&&(n.backendOnly=!0),e.includes("--tailwindcss")&&(n.tailwindcss=!0),e.includes("--typescript")&&(n.typescript=!0),e.includes("--mcp")&&(n.mcp=!0),e.includes("--websocket")&&(n.websocket=!0),e.includes("--prisma")&&(n.prisma=!0),r={...r,backendOnly:n.backendOnly,tailwindcss:n.tailwindcss,typescript:n.typescript,mcp:n.mcp,websocket:n.websocket??!1,prisma:n.prisma};let t=[];n.excludeFiles?.map(e=>{const n=path.join(d,e);fs.existsSync(n)&&t.push(n.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:t??[],filePath:d}}}else{const e=path.join(p,"caspian.config.json"),n=path.join(p,t),s=path.join(n,"caspian.config.json");fs.existsSync(e)?d=p:fs.existsSync(n)&&fs.existsSync(s)?(d=n,process.chdir(n)):(fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),d=n,process.chdir(n))}else fs.mkdirSync(r.projectName,{recursive:!0}),d=path.join(p,r.projectName),process.chdir(r.projectName);let u=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python"),npmPkg("caspian-utils")];r.prisma&&u.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&u.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),r.prisma&&execSync(buildManagedNpmCommand(["install","-g","prisma-client-python@latest"]),{stdio:"inherit"}),r.typescript&&!r.backendOnly&&u.push(npmPkg("vite"),npmPkg("fast-glob")),r.typescript&&u.push(npmPkg("vitest")),r.starterKit&&!l&&await setupStarterKit(d,r),await installNpmDependencies(d,u,!0);let m=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(d,r),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],n=[],t=e=>{try{const n=path.join(d,"package.json");if(fs.existsSync(n)){const t=JSON.parse(fs.readFileSync(n,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const n=path.join(d,"src","app",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const n=path.join(d,"src","app",e);fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const n=path.join(d,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const s=path.join(d,"public","js","tailwind-merge.mjs");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${s} was deleted successfully.`));const i=path.join(d,"public","js","bundle-mjs.mjs.map");fs.existsSync(i)&&(fs.unlinkSync(i),console.log(`${i} was deleted successfully.`));const a=path.join(d,"ts","tailwind-merge.ts");fs.existsSync(a)&&(fs.unlinkSync(a),console.log(`${a} was deleted successfully.`));["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano","tailwind-merge"].forEach(n=>{t(n)&&e.push(n)}),n.push("tailwind-merge")}if(r.tailwindcss){const e=path.join(d,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(n){console.warn(chalk.yellow(`Failed to delete ${e}: ${n}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const n=path.join(d,"settings",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const e=path.join(d,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),n.push("fastmcp")}if(!updateAnswer.websocket){const e=path.join(d,"src","lib","websocket");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("WebSocket folder was deleted successfully.")),n.push("websockets")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(n=>{t(n)&&e.push(n)}),n.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts",path.join("settings","run-vite-watch.ts")].forEach(e=>{const n=path.join(d,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const n=path.join(d,"ts");fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const s=path.join(d,"settings","vite-plugins");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(n=>{t(n)&&e.push(n)})}const s=e=>Array.from(new Set(e)),i=s(e);i.length>0&&(console.log(`Uninstalling npm packages: ${i.join(", ")}`),await uninstallNpmDependencies(d,i,!0));const a=s(n),c=getPyProjectDependencyNames(d);m=a.filter(e=>c.has(e.toLowerCase())),m.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${m.join(", ")}`))}if(!l||!fs.existsSync(path.join(d,"caspian.config.json"))){const e=d.replace(/\\/g,"\\"),n=bsConfigUrls(e),t={projectName:r.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,websocket:r.websocket,prisma:r.prisma,typescript:r.typescript,version:o,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(d,"caspian.config.json"),JSON.stringify(t,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(d,r,m),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(d.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
1
+ #!/usr/bin/env node
2
+ import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),PACKAGE_ROOT=path.resolve(__dirname,".."),OPTIONAL_TEMPLATE_FILES=new Set([".python-version",".prettierrc"]),OPTIONAL_TEMPLATE_DIRECTORIES=new Set([".github",".vscode"]),CASPIAN_SECTION_START="\x3c!-- caspian:start --\x3e",CASPIAN_SECTION_END="\x3c!-- caspian:end --\x3e",SELF_UPDATE_GUARD_ENV="CREATE_CASPIAN_APP_SELF_UPDATED";let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.py","not-found.py","error.py"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1,websocket:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.py","src/app/index.py"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,websocket:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.py","src/app/index.py","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1,websocket:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0,websocket:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const n=e.indexOf("\\htdocs\\");if(-1===n)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,n+8).replace(/\\/g,"\\\\"),s=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let i=`http://localhost/${s}`;i=i.endsWith("/")?i.slice(0,-1):i;const a=i.replace(/(?<!:)(\/\/+)/g,"/"),c=s.replace(/\/\/+/g,"/");return{bsTarget:`${a}/`,bsPathRewrite:{"^/":`/${c.startsWith("/")?c.substring(1):c}/`}}}async function updatePackageJson(e,n){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const s=JSON.parse(fs.readFileSync(t,"utf8"));s.scripts={...s.scripts,projectName:"tsx settings/project-name.ts",format:"uv run python settings/format.py","format:check":"uv run python settings/format.py --check",check:"uv run python settings/check.py","check:fix":"uv run python settings/fix.py",logs:"uv run python settings/browser_log.py",static:"npm run build && uv run python settings/build-static.py","static:serve":"uv run python settings/serve-static.py"};let i=[];n.backendOnly||(s.scripts={...s.scripts,css:"tsx settings/run-postcss.ts watch","css:build":"tsx settings/run-postcss.ts build"},i.push("css")),delete s.scripts.tailwind,delete s.scripts["tailwind:build"],n.typescript&&!n.backendOnly&&(s.scripts={...s.scripts,"ts:watch":"vite build --watch","ts:watch:dev":"tsx settings/run-vite-watch.ts","ts:build":"vite build"},i.push("ts:watch:dev")),n.mcp&&(s.scripts={...s.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let a={...s.scripts};a.browserSync="tsx settings/bs-config.ts",a.dev=`npm-run-all projectName -l -p browserSync ${i.join(" ")}`;let c=["projectName"];n.backendOnly||c.unshift("css:build"),n.typescript&&!n.backendOnly&&c.unshift("ts:build"),a.build=`npm-run-all ${c.join(" ")}`,s.scripts=a,s.type="module",fs.writeFileSync(t,JSON.stringify(s,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function buildEnvSection(e,n){return`# =============================================================================\n${e.split("\n").map(e=>e.startsWith("#")?e:`# ${e}`).join("\n")}\n# =============================================================================\n\n${n.trimEnd()}`}function buildCaspianEnvContent(e){const n=generateAuthSecret(),t=generateHexEncodedKey(8),s=generateHexEncodedKey(32),i=[];return e.prisma&&i.push(buildEnvSection("1. DATABASE\n# Enforced by: prisma/schema.prisma, src/lib/prisma/db.py",'# Connection string. Prisma reads this directly from .env.\n# Format reference: https://pris.ly/d/connection-strings\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"\n\n# Connection-pool limit. Defaults: SQLite 5; MySQL and PostgreSQL 20.\n# Use 5 for local development; production does not need this unless you want to\n# limit the pool.\nDB_POOL_SIZE=5\n\n# Seconds idle before the client re-probes its connection. Default 30.\nPRISMA_CONN_PROBE_IDLE_SECONDS=30\n\n# Warn on queries that cause a full table scan. 0/false silences it. Default 1.\nPRISMA_WARN_FULL_SCAN=1')),i.push(buildEnvSection("2. APPLICATION RUNTIME\n# Enforced by: casp/runtime_security.py is_production_environment()",'# Environment selector, resolved FAIL-CLOSED: only an explicit development\n# value (dev, development, local, staging, test, testing) enables the\n# development relaxations. Unset or misspelled counts as production.\n#\n# Production turns on: HTTPS-only session cookie, Secure CSRF cookie, HSTS,\n# generic error messages, mandatory AUTH_SECRET, mandatory MCP_AUTH_TOKEN, and\n# it removes the localhost origin bypass and the WebSocket same-origin fallback.\n#\n# This single value gates most of the security posture. Set it deliberately.\nAPP_ENV="development"\n\n# Calendar timezone for the application, as an IANA name (e.g. "UTC",\n# "America/New_York", "America/Santo_Domingo"). Read by casp/app_time.py and\n# resolved once at boot in main.py.\n#\n# This sets which wall-clock DAY an instant belongs to: what casp.app_time.now()\n# and today() answer, how a stored timestamp reads back to a user, and the\n# boundaries a "today\'s totals" query uses. Timestamps are still STORED in UTC.\n#\n# It deliberately does NOT affect absolute time -- session expiry (casp/auth.py)\n# and cache TTLs (casp/cache_handler.py) stay on UTC, so changing this can never\n# extend a session or a cache entry.\n#\n# An unrecognized name raises InvalidAppTimezoneError at startup rather than\n# silently falling back to UTC. Empty or unset means UTC.\nAPP_TIMEZONE="UTC"'),buildEnvSection("3. PUBLIC URL, CORS, AND ORIGIN VALIDATION\n# Enforced by: casp/rpc.py origin checks, main.py CORS layer",'# Canonical public origin. Leave empty when the browser URL and the app runtime\n# URL match. Set it when they differ, i.e. behind an ingress, reverse proxy,\n# load balancer, gateway, edge network, or TLS terminator.\nAPP_BASE_URL=""\n\n# Extra browser origins allowed to call protected endpoints such as RPC. Use\n# when one deployment is reachable from more than one public origin.\n# Comma-separated, no spaces.\nCORS_ALLOWED_ORIGINS=""\n\n# Trust Forwarded/X-Forwarded-* headers. Enable ONLY when every request passes\n# through infrastructure that strips client-supplied forwarded headers before\n# setting its own, because a direct client can otherwise forge them.\n#\n# Affects two things: which origin RPC accepts, and which address the rate\n# limiter buckets on. Left false, both use the direct request instead.\nTRUST_FORWARDED_HEADERS="false"\n\n# Allow cookies/Authorization on cross-origin requests. Keep true only when\n# credentialed cross-origin requests are actually required.\nCORS_ALLOW_CREDENTIALS="true"\n\n# CORS preflight response fields.\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\n\n# Preflight cache duration in seconds.\nCORS_MAX_AGE="86400"'),buildEnvSection("4. AUTHENTICATION AND SESSIONS\n# Enforced by: casp/auth.py, main.py SessionMiddleware\n# Route privacy and RBAC live in src/lib/auth/auth_config.py, not here.",`# Session signing secret. Unique and strong per app and per environment.\n# In production the app refuses to start when this is missing or left on a\n# placeholder ("change-me"/"changeme"); in development it falls back.\nAUTH_SECRET="${n}"\n\n# Session cookie name. Use a unique value when several apps share a parent\n# domain, or their sessions overwrite each other.\nAUTH_COOKIE_NAME="${t}"\n\n# Session lifetime in hours (SessionMiddleware max_age).\nSESSION_LIFETIME_HOURS="7"`),buildEnvSection("5. OAUTH PROVIDERS\n# Enforced by: casp/auth.py; routes served by main.py AuthMiddleware",'# Google and GitHub sign-in are already wired: AuthMiddleware serves\n# /api/auth/signin/{google,github} and /api/auth/callback/{google,github}.\n# Link a button at those paths, do not hand-roll OAuth.\n#\n# A provider with no client id is skipped SILENTLY: the redirect returns None\n# and the button appears dead, with no error and no log. Empty means disabled.\n\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\n\n# Must match the redirect URI registered in Google Cloud Console exactly.\n# Google is skipped unless BOTH the client id and this value are set.\nGOOGLE_REDIRECT_URI=\n\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\n\n# Callback the GitHub authorize request asks for. A GitHub App accepts up to 10\n# redirect URIs, but a request that omits this parameter always lands on the\n# FIRST one registered on the App -- which is why adding a localhost URI next to\n# the production one has no effect on its own. Register both on the App, then\n# let each environment name the one it wants here. Must match a registered URI\n# character for character. Leave empty to derive it from APP_BASE_URL as\n# "<APP_BASE_URL>/api/auth/callback/github", and empty with no APP_BASE_URL to\n# send no redirect_uri at all (GitHub\'s first-registered-URI default).\nGITHUB_REDIRECT_URI='),buildEnvSection("6. REQUEST SECURITY\n# Enforced by: main.py BodySizeLimitMiddleware, RequestDiagnosticsMiddleware",'# Max size of the whole HTTP request body in MB. Caps the entire body (file +\n# form fields + encoding overhead), so usable file size is a bit below this.\n# Middleware rejects oversized requests before the route runs. Raise if valid\n# uploads are blocked. Default 16.\nMAX_CONTENT_LENGTH_MB="16"\n\n# Seconds before a stalled route returns 504. Streaming paths (/mcp) are exempt\n# so long-lived transports are not cut mid-response. Default 20.\nCASPIAN_REQUEST_TIMEOUT_SECONDS=20'),buildEnvSection("7. SECURITY HEADERS\n# Enforced by: casp/runtime_security.py, main.py SecurityHeadersMiddleware","# Replaces the built-in Content-Security-Policy wholesale. Empty keeps the\n# default, which already permits the app's own assets.\n#\n# Any replacement MUST keep 'unsafe-eval' and 'unsafe-inline' in script-src:\n# the PulsePoint runtime compiles component templates with new Function(), so\n# removing them stops every page from rendering. Set this only to widen the\n# policy, e.g. for a CDN, analytics host, or an external frame embedder.\n#\n# Outside production, connect-src also allows http(s)/ws on localhost and\n# 127.0.0.1 on any port, because BrowserSync serves the proxied page on one port\n# while its injected live-reload client polls the BrowserSync server on another.\n# That is a separate origin, so 'self' does not cover it. Those entries are\n# omitted from a production policy. Setting an override here replaces BOTH, so\n# an override used in development must include the loopback sources itself or\n# live reload stops working.\n#\n# img-src and media-src admit remote content by scheme, so posters, avatars, CDN\n# thumbnails, and video load without per-project configuration. Plain http: is\n# development-only. Set an override here to pin them to named origins instead.\n#\n# Default: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';\n# style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:;\n# media-src 'self' data: blob: https:; font-src 'self' data:;\n# connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';\n# form-action 'self'; frame-ancestors 'self'\nCONTENT_SECURITY_POLICY="),buildEnvSection("8. RATE LIMITING\n# Enforced by: main.py RateLimitMiddleware, casp/rpc.py RPCRateLimiter\n# Buckets are per client address; see TRUST_FORWARDED_HEADERS for which one.",'# Per-IP cap on page requests, applied before session decryption and rendering.\n# Static assets (/css, /js, /assets, /favicon.ico) and /health are exempt, so a\n# page load does not spend its own budget on its assets. Empty disables it.\n# Default 200/minute.\nRATE_LIMIT_PAGES=200/minute\n\n# Fallback limit for @rpc() actions that declare no limits= of their own.\nRATE_LIMIT_RPC="60 per minute"\n\n# Limit applied to @rpc(require_auth=True) actions that declare no limits=.\n# Tighten this and the per-action limits on sign-in and other credential paths.\nRATE_LIMIT_AUTH="60 per minute"\n\n# Configured for slowapi\'s Limiter. Note that slowapi\'s middleware is not in\n# the stack, so this value is inert today; page limiting is RATE_LIMIT_PAGES.\nRATE_LIMIT_DEFAULT="200 per minute"\n\n# In-memory bucket ceiling and sweep interval for the limiter store.\n# Defaults 10000 buckets, swept every 60 seconds.\nRATE_LIMIT_MAX_BUCKETS=10000\nRATE_LIMIT_CLEANUP_INTERVAL=60')),e.websocket&&i.push(buildEnvSection("9. WEBSOCKETS\n# Enforced by: src/lib/websocket/websocket_security.py, main.py channel loop\n# Only active when caspian.config.json has websocket: true.","# Browser origins allowed to open a socket (anti-CSWSH). Falls back to\n# CORS_ALLOWED_ORIGINS then APP_BASE_URL when empty.\n#\n# REQUIRED IN PRODUCTION. The convenience same-origin fallback is derived from\n# the client-supplied Host header, so it is development-only: without an\n# explicit list a spoofed Host plus matching Origin would validate itself.\n#\n# The HTTP middleware stack skips websocket scopes, so this and the socket\n# guard are the only checks a handshake passes. Comma-separated, no spaces.\nWEBSOCKET_ALLOWED_ORIGINS=\n\n# Seconds a socket may stay silent before the server closes it. Default 120.\nWEBSOCKET_IDLE_TIMEOUT_SECONDS=120\n\n# Max size of one inbound socket message in bytes. Oversized closes with 1009.\n# Default 4096.\nMAX_WEBSOCKET_MESSAGE_BYTES=4096\n\n# Simultaneous connections per pool; authenticated and guest pools are counted\n# separately. Refused connections close with 1013 during the handshake. Every\n# open socket is a live task and a broadcast target. Default 200.\nMAX_WEBSOCKET_CONNECTIONS=200\n\n# Per-connection send budget: messages allowed per rolling window. Each\n# accepted message fans out to the whole pool, so this bounds how much\n# broadcast one connection can generate. Defaults 20 per 10 seconds.\nMAX_WEBSOCKET_MESSAGES_PER_WINDOW=20\nWEBSOCKET_RATE_WINDOW_SECONDS=10")),e.mcp&&i.push(buildEnvSection("10. MCP ENDPOINT\n# Enforced by: main.py MCPAuthMiddleware; tools in src/lib/mcp/mcp_server.py\n# Only active when caspian.config.json has mcp: true.",`# Bearer token required to call /mcp. The MCP app is mounted outside the page\n# routing tree, so AuthMiddleware does NOT protect it, and its tools enumerate\n# the workspace file inventory and component map.\n#\n# generated -> every request needs "Authorization: Bearer <token>"\n#\n# REQUIRED IN PRODUCTION for the endpoint to work at all.\nMCP_AUTH_TOKEN="${s}"`)),i.push(buildEnvSection("11. CACHE\n# Enforced by: casp/cache_handler.py, main.py is_request_cacheable()",'# Master switch for serving pages from the disk cache.\n#\n# Entries are keyed on the URI alone, with no session component, so an\n# authenticated render is never cached: is_request_cacheable() gates both the\n# read and the write, and a route\'s Cache(...) cannot override it.\nCACHE_ENABLED="false"\n\n# Default cache lifetime in seconds, used when a route sets no ttl.\nCACHE_TTL="600"'),buildEnvSection("12. SERVER PROCESS\n# Enforced by: main.py __main__, settings/serve-static.py",'# Uvicorn workers are separate OS processes for the same FastAPI app. More\n# workers can increase throughput under concurrent load, but they do not make a\n# single request faster and they duplicate memory, connection pools, and any\n# in-process state. Keep at 1 unless the app is designed for multi-process\n# coordination and testing shows a real concurrency bottleneck.\nUVICORN_WORKERS="1"')),i.join("\n\n")}function copyRecursiveSync(e,n,t){const s=fs.existsSync(e),i=s&&fs.statSync(e);if(s&&i&&i.isDirectory()){const s=n.toLowerCase();if(!t.mcp&&s.includes("src\\lib\\mcp"))return;if(!t.websocket&&s.includes("src\\lib\\websocket"))return;if((!t.typescript||t.backendOnly)&&(s.endsWith("\\ts")||s.includes("\\ts\\")))return;if((!t.typescript||t.backendOnly)&&(s.endsWith("\\vite-plugins")||s.includes("\\vite-plugins\\")||s.includes("\\vite-plugins")))return;if(t.backendOnly&&s.includes("public\\js")||t.backendOnly&&s.includes("public\\css")||t.backendOnly&&s.includes("public\\assets"))return;const i=n.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),fs.readdirSync(e).forEach(s=>{copyRecursiveSync(path.join(e,s),path.join(n,s),t)})}else{if(checkExcludeFiles(n))return;const s=n.replace(/\\/g,"/").toLowerCase();if(s.endsWith("/settings/run-vite-watch.ts")&&(!t.typescript||t.backendOnly))return;if(s.endsWith("/ts/tailwind-merge.ts")&&(!t.typescript||t.backendOnly||!t.tailwindcss))return;if(t.backendOnly&&n.includes("globals."))return;if(t.tailwindcss&&n.endsWith("globals.plain.css"))return;if(!t.tailwindcss&&n.endsWith("globals.css"))return;if(!t.mcp&&n.includes("restart-mcp.ts"))return;if(!t.websocket&&n.includes("src\\lib\\websocket"))return;if(t.backendOnly&&nonBackendFiles.some(e=>n.includes(e)))return;if(t.backendOnly&&n.includes("layout.py"))return;if(!t.prisma&&n.includes("prisma-schema-config.json"))return;if(n.endsWith("globals.plain.css")&&(n=n.replace("globals.plain.css","globals.css")),n.endsWith("globals.css")&&fs.existsSync(n))return;fs.copyFileSync(e,n,0)}}async function executeCopy(e,n,t){n.forEach(({src:n,dest:s})=>{const i=normalizeTemplatePath(n),a=resolveTemplateSourcePath(n,"directory"),c=path.join(e,s);if(!a){if(OPTIONAL_TEMPLATE_DIRECTORIES.has(i))return void console.log(chalk.gray(`Optional template directory not found, skipping: ${i}`));throw new Error(`Template directory not found: ${i}. The package may be incomplete.`)}copyRecursiveSync(a,c,t)})}function modifyLayoutPHP(e,n){const t=path.join(e,"src","app","layout.py");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),s="";n.backendOnly||(s=' <link href="/css/styles.css" rel="stylesheet" />\n <script type="module" src="/js/main.js"><\/script>'),e=e.replace("</head>",`${s}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.py:"),e)}}const TAILWIND_STYLESHEET_HEADER='@import "tailwindcss" source(none);\n@source "../";\n@source "../../ts";\n';function syncGlobalsStylesheetForTailwind(e,n){const t=path.join(e,"src","app","globals.css");if(checkExcludeFiles(t))return;if(!fs.existsSync(t))return;const s=fs.readFileSync(t,"utf8"),i=/^\s*@import\s+["']tailwindcss["']/m.test(s);if(n){if(i)return;return fs.writeFileSync(t,`${TAILWIND_STYLESHEET_HEADER}\n${s}`,{flag:"w"}),void console.log(chalk.green("Added the Tailwind directives to src/app/globals.css. Your own rules were kept."))}if(!i)return;const a=s.split(/\r?\n/).filter(e=>!/^\s*@import\s+["']tailwindcss["']/.test(e)&&!/^\s*@source\s/.test(e)).join("\n").replace(/^\n+/,"");fs.writeFileSync(t,a,{flag:"w"}),console.log(chalk.green("Removed the Tailwind directives from src/app/globals.css. Your own rules were kept.")),/@apply\s|@theme\s|@variant\s|@custom-variant\s/.test(a)&&console.log(chalk.yellow("Warning: src/app/globals.css still uses Tailwind-only at-rules (@apply/@theme/@variant).\n Without Tailwind these do nothing -- rewrite them as plain CSS.")),console.log(chalk.yellow("Note: Tailwind utility classes in your markup no longer resolve.\n Define the classes you still use in src/app/globals.css."))}async function createOrUpdateEnvFile(e,n){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,n,{flag:"w"})}function ensureClaudeMd(e){const n=path.join(e,"CLAUDE.md");if(checkExcludeFiles(n))return;const t="@AGENTS.md";if(!fs.existsSync(n))return void fs.writeFileSync(n,`${t}\n`,{flag:"w"});const s=fs.readFileSync(n,"utf8").replace(/^\uFEFF/,"");if(s.trimStart().startsWith(t))return;const i=`${t}\n\n${s.trimStart()}`;fs.writeFileSync(n,i,{flag:"w"})}function writeTailwindMainJs(e){const n=path.join(e,"public","js","main.js");checkExcludeFiles(n)||(fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,'import "/js/pp-reactive-v2.min.js";\nimport { twMerge } from "/js/tailwind-merge.mjs";\n\nconst pp = (globalThis).pp;\n\nglobalThis.twMerge = twMerge;\n\nif (document.readyState !== "loading") {\n pp?.mount?.();\n} else {\n document.addEventListener(\n "DOMContentLoaded",\n () => pp?.mount?.(),\n { once: true },\n );\n}\n',{flag:"w"}))}function copyTailwindMergeBundle(e){const n=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs"),t=path.join(e,"public","js","tailwind-merge.mjs"),s=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs.map"),i=path.join(e,"public","js","bundle-mjs.mjs.map");if(!checkExcludeFiles(t)){if(!fs.existsSync(n))throw new Error(`tailwind-merge bundle not found at ${n}`);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.copyFileSync(n,t),!checkExcludeFiles(i)&&fs.existsSync(s)&&fs.copyFileSync(s,i)}}function writeTailwindTypeScriptMain(e){const n=path.join(e,"ts","main.ts");checkExcludeFiles(n)||(fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,'import "/js/pp-reactive-v2.min.js";\n\n// The following global names have already been declared elsewhere in the project:\n// - pp: Used for the Reactive Core functionality.\n\n// Imports goes here --Start\nimport { createGlobalSingleton } from "./global-functions.js";\nimport { mergeTailwindClasses } from "./tailwind-merge.js";\n\ncreateGlobalSingleton("twMerge", mergeTailwindClasses);\n\n\n// Imports goes here --End\n\nconst pp = (globalThis as any).pp;\n\nif (document.readyState !== "loading") {\n\tpp?.mount?.();\n} else {\n\tdocument.addEventListener(\n\t\t"DOMContentLoaded",\n\t\t() => pp?.mount?.(),\n\t\t{ once: true },\n\t);\n}\n',{flag:"w"}))}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const n=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(n)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const t=e.replace(/\\/g,"/");return n.endsWith("/"+t)||n===t})}function normalizeTemplatePath(e){return e.replace(/^[\\/]+/,"")}function resolveTemplateSourcePath(e,n){const t=normalizeTemplatePath(e),s=[path.join(__dirname,t),path.join(PACKAGE_ROOT,t)];for(const e of s){if(!fs.existsSync(e))continue;const t=fs.statSync(e);if("file"===n&&t.isFile())return e;if("directory"===n&&t.isDirectory())return e}return null}function extractCaspianSection(e){const n=e.indexOf(CASPIAN_SECTION_START);if(-1===n)return null;const t=e.indexOf(CASPIAN_SECTION_END,n);return-1===t?null:e.slice(t>n?n:0,t+20)}function mergeAgentsCaspianSection(e,n){const t=extractCaspianSection(n);if(!t)return e;const s=e.indexOf(CASPIAN_SECTION_START),i=e.indexOf(CASPIAN_SECTION_END,s);if(-1!==s&&-1!==i){return`${e.slice(0,s)}${t}${e.slice(i+20)}`}const a=e.endsWith("\n");return`${e}${a?"\n":"\n\n"}${t}\n`}async function createDirectoryStructure(e,n){const t=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/AGENTS.md",dest:"/AGENTS.md"},{src:"/.python-version",dest:"/.python-version"}];n.backendOnly||t.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),n.typescript&&!n.backendOnly&&t.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const s=[{src:"/settings",dest:"/settings"},{src:"/tests",dest:"/tests"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"},{src:"/.vscode",dest:"/.vscode"}];n.typescript&&!n.backendOnly&&s.push({src:"/ts",dest:"/ts"}),t.forEach(({src:n,dest:t})=>{const s=normalizeTemplatePath(n),i=resolveTemplateSourcePath(n,"file"),a=path.join(e,t);if(checkExcludeFiles(a))return;if(!i){if(OPTIONAL_TEMPLATE_FILES.has(s))return void console.log(chalk.gray(`Optional template file not found, skipping: ${s}`));throw new Error(`Template file not found: ${s}. The package may be incomplete.`)}if("/pyproject.toml"===n&&updateAnswer?.isUpdate&&fs.existsSync(a))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const c=fs.readFileSync(i,"utf8");if("/AGENTS.md"===n&&updateAnswer?.isUpdate&&fs.existsSync(a)){const e=mergeAgentsCaspianSection(fs.readFileSync(a,"utf8"),c);return void fs.writeFileSync(a,e,{flag:"w"})}fs.writeFileSync(a,c,{flag:"w"})}),await executeCopy(e,s,n),ensureClaudeMd(e),n.tailwindcss&&!n.backendOnly&&(n.typescript?writeTailwindTypeScriptMain(e):(copyTailwindMergeBundle(e),writeTailwindMainJs(e))),await updatePackageJson(e,n),!n.tailwindcss&&n.backendOnly||modifyLayoutPHP(e,n),await createOrUpdateEnvFile(e,buildCaspianEnvContent(n))}async function getAnswer(e={},n=!1){if(e.starterKit){const n=e.starterKit;let t=null;if(STARTER_KITS[n]&&(t=STARTER_KITS[n]),t){const s={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,prisma:t.features.prisma??!1,mcp:t.features.mcp??!1,websocket:t.features.websocket??!1,typescript:t.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(s.backendOnly=!0),i.includes("--tailwindcss")&&(s.tailwindcss=!0),i.includes("--mcp")&&(s.mcp=!0),i.includes("--websocket")&&(s.websocket=!0),i.includes("--prisma")&&(s.prisma=!0),i.includes("--typescript")&&(s.typescript=!0),s}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,websocket:!1,typescript:!1},s=process.argv.slice(2);return s.includes("--backend-only")&&(t.backendOnly=!0),s.includes("--tailwindcss")&&(t.tailwindcss=!0),s.includes("--mcp")&&(t.mcp=!0),s.includes("--websocket")&&(t.websocket=!0),s.includes("--prisma")&&(t.prisma=!0),s.includes("--typescript")&&(t.typescript=!0),t}}if(n)return{projectName:e.projectName??"my-app",starterKit:e.starterKit,starterKitSource:e.starterKitSource,backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,websocket:e.websocket??!1,prisma:e.prisma??!1};const t=[];e.projectName||t.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||t.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const s=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(t,{onCancel:s}),a=[];i.backendOnly??e.backendOnly??!1?(e.mcp||a.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||a.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||a.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||a.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||a.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||a.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||a.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||a.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"}));const c=await prompts(a,{onCancel:s});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:c.tailwindcss??e.tailwindcss??!1,typescript:c.typescript??e.typescript??!1,mcp:c.mcp??e.mcp??!1,websocket:c.websocket??e.websocket??!1,prisma:c.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,n,t=!1){console.log("Uninstalling Node dependencies:"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=buildManagedNpmCommand(["uninstall",t?"--save-dev":"--save",...n]);execSync(s,{stdio:"inherit",cwd:e})}function buildManagedNpmCommand(e){return`npm ${e.join(" ")} --ignore-scripts=false --min-release-age=0 --audit=false`}function fetchPackageVersion(e){return new Promise((n,t)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let s="";e.on("data",e=>s+=e),e.on("end",()=>{try{const e=JSON.parse(s);n(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}})}).on("error",e=>t(e))})}const readJsonFile=e=>{const n=fs.readFileSync(e,"utf8");return JSON.parse(n)};function compareVersions(e,n){const t=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),s=n.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!t||!s)return e.localeCompare(n);const i=t.slice(1,4).map(Number),a=s.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>a[e])return 1;if(i[e]<a[e])return-1}const c=t[4]??null,o=s[4]??null;return c&&!o?-1:!c&&o?1:c&&o?c.localeCompare(o):0}function getInstalledPackageInfo(e){try{const n=execSync(buildManagedNpmCommand(["list","-g",e,"--depth=0"])).toString(),t=n.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return t?{version:t[1],isLinked:n.includes(`${e}@`)&&n.includes("->")}:(console.error(`Package ${e} is not installed`),{version:null,isLinked:!1})}catch(e){return console.error(e instanceof Error?e.message:String(e)),{version:null,isLinked:!1}}}function isRunningFromNpxCache(e){const n=path.resolve(e).toLowerCase(),t=`${path.sep}_npx${path.sep}`.toLowerCase();return n.includes(t)}function resolveGlobalCliEntryPoint(){const e=[__filename];try{const n=execSync("npm root -g").toString().trim();n&&e.push(path.join(n,"create-caspian-app","dist","index.js"))}catch{}for(const n of e)if(n&&fs.existsSync(n))return n;return null}async function ensureLatestCliVersion(e){const n=await fetchPackageVersion("create-caspian-app");if("1"===process.env[SELF_UPDATE_GUARD_ENV])return n;if(isRunningFromNpxCache(__dirname))return console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")),n;const t=getInstalledPackageInfo("create-caspian-app");if(t.isLinked)return console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")),n;if(!(!t.version||-1===compareVersions(t.version,n)))return n;execSync(buildManagedNpmCommand(["install","-g",`create-caspian-app@${n}`]),{stdio:"inherit"});const s=resolveGlobalCliEntryPoint();if(!s)throw new Error(`create-caspian-app was updated to ${n}, but the updated CLI could not be located. Please run the command again.`);console.log(chalk.gray(`Restarting with create-caspian-app@${n}...`));const i=spawnSync(process.execPath,[s,...e],{stdio:"inherit",env:{...process.env,[SELF_UPDATE_GUARD_ENV]:"1"}});if(i.error)throw i.error;process.exit(i.status??1)}async function installNpmDependencies(e,n,t=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync(buildManagedNpmCommand(["init","-y"]),{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=buildManagedNpmCommand(["install",...t?["--save-dev"]:[],...n]);execSync(s,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.3.3","@types/browser-sync":"2.29.1","@types/node":"26.4.1","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"6.0.0","chokidar-cli":"3.0.0",cssnano:"9.0.2","npm-run-all":"4.1.5",postcss:"8.5.28","postcss-cli":"12.0.0",prompts:"2.4.2",tailwindcss:"4.3.3",tsx:"4.23.13",typescript:"7.0.2",vite:"8.2.2",vitest:"5.0.0","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.19","caspian-utils":"0.2.x","tailwind-merge":"3.6.0"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(n){const t=n;if("win32"===globalThis.process?.platform&&("EPERM"===t.code||"EACCES"===t.code)){try{spawnSync("cmd",["/c","attrib","-R","-H","-S","/S","/D",`${e}\\*`],{stdio:"ignore"})}catch{}return void spawnSync("cmd",["/c","rd","/s","/q",e],{stdio:"ignore"})}throw n}}async function setupStarterKit(e,n){if(!n.starterKit)return;let t=null;if(STARTER_KITS[n.starterKit]?t=STARTER_KITS[n.starterKit]:n.starterKitSource&&(t={id:n.starterKit,name:`Custom Starter Kit (${n.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:n.starterKitSource}}),t){if(console.log(chalk.green(`Setting up ${t.name}...`)),t.source)try{const s=t.source.branch?`git clone -b ${t.source.branch} --depth 1 ${t.source.url} "${e}"`:`git clone --depth 1 ${t.source.url} "${e}"`;execSync(s,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const i=path.join(e,"caspian.config.json");if(fs.existsSync(i))try{const t=JSON.parse(fs.readFileSync(i,"utf8")),s=e,a=bsConfigUrls(s);t.projectName=n.projectName,t.projectRootPath=s,t.bsTarget=a.bsTarget,t.bsPathRewrite=a.bsPathRewrite;const c=await fetchPackageVersion("create-caspian-app");t.version=t.version||c,fs.writeFileSync(i,JSON.stringify(t,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}t.customSetup&&await t.customSetup(e,n),console.log(chalk.green(`✓ ${t.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${n.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\n🚀 Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const n=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(n)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const t=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");t&&console.log(chalk.magenta(` Features: ${t}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,n,t){const s=spawnSync(e,n,{cwd:t,stdio:"inherit",shell:!1,encoding:"utf8"});if(s.error)throw s.error;if(0!==s.status)throw new Error(`Command failed (${e} ${n.join(" ")}), exit=${s.status}`)}function tryRunCmd(e,n,t){const s=spawnSync(e,n,{cwd:t,stdio:"ignore",shell:!1,encoding:"utf8"});return!s.error&&0===s.status}function tryInstallUv(e){console.log(chalk.blue("uv not found. Attempting to install uv..."));const n=[{cmd:"py",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python3",args:["-m","pip","install","--upgrade","uv"]}];for(const t of n)if(tryRunCmd(t.cmd,t.args,e))return!0;return!1}function resolveUvCommand(e){const n=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const t of n)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;if(tryInstallUv(e))for(const t of n)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;throw new Error("Could not find or install uv. Install uv and ensure `uv`, `py`, or `python` is available in PATH.")}function buildPythonDependencies(e){const n=["fastapi==0.141.1","uvicorn==0.52.4","python-dotenv==1.2.3","tzdata==2026.3","jinja2==3.1.6","beautifulsoup4==4.15.0","slowapi==0.1.10","python-multipart==0.0.32","starsessions==2.2.1","httpx2==2.12.0","werkzeug==3.1.8","cuid2==2.0.1","nanoid==2.0.0","python-ulid==4.0.1","cuid==0.4","caspian-utils~=0.4"];return e.mcp&&n.push("fastmcp==4.0.3"),e.websocket&&n.push("websockets==17.1"),e.prisma&&(n.push("psycopg2-binary==2.9.12"),n.push("asyncpg==0.31.0"),n.push("aiosqlite==0.22.1"),n.push("aiomysql==0.3.2"),n.push("cryptography==50.0.1")),n}function buildPythonDevDependencies(){return["pyright==1.1.411","ruff==0.16.6","pytest==9.1.1","djlint==1.45.2"]}function getPythonRequirementName(e){const n=e.trim().match(/^([A-Za-z0-9._-]+)/);return n?.[1]??null}function getPyProjectDependencyNames(e){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))return new Set;const t=fs.readFileSync(n,"utf8").replace(/\r\n/g,"\n").match(/^[ \t]*dependencies[ \t]*=[ \t]*\[([\s\S]*?)\]/m);if(!t)return new Set;const s=new Set,i=/"([^"]+)"/g;let a;for(;null!==(a=i.exec(t[1]));){const e=a[1].trim().match(/^([A-Za-z0-9._-]+)/)?.[1];e&&s.add(e.toLowerCase())}return s}function ensurePyProjectExists(e){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))throw new Error(`pyproject.toml not found at: ${n}`);let t=fs.readFileSync(n,"utf8");t=t.replace(/\r\n/g,"\n"),t.includes("package = false")||(t=t.includes("[tool.uv]")?t.replace("[tool.uv]","[tool.uv]\npackage = false"):`${t.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(n,t,"utf8")}async function ensurePythonVenvAndDeps(e,n,t=[]){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: syncing dependencies with uv")),console.log(chalk.green("=========================\n")),console.log(chalk.blue("Preparing pyproject.toml...")),ensurePyProjectExists(e);const s=path.join(e,"requirements.txt");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(chalk.gray("Removed legacy requirements.txt")));const i=resolveUvCommand(e),a=path.join(e,".venv");fs.existsSync(a)?console.log(chalk.blue("Existing .venv detected. Reusing it so uv sync can update dependencies without replacing the environment.")):(console.log(chalk.blue("Creating the virtual environment with uv...")),runCmd(i.cmd,[...i.argsPrefix,"venv",".venv"],e));const c=buildPythonDependencies(n),o=buildPythonDevDependencies(),r=c.map(e=>getPythonRequirementName(e)).filter(e=>null!==e),l=o.map(e=>getPythonRequirementName(e)).filter(e=>null!==e);t.length>0&&(console.log(chalk.blue("Removing obsolete Python dependencies via uv remove...")),runCmd(i.cmd,[...i.argsPrefix,"remove",...t],e));const p=r.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...p,...c],e);const d=l.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dev dependencies via uv add --dev...")),runCmd(i.cmd,[...i.argsPrefix,"add","--dev",...d,...o],e),console.log(chalk.blue("Syncing dependencies...")),runCmd(i.cmd,[...i.argsPrefix,"sync"],e),console.log(chalk.green("\n✓ uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),n=e.includes("-y");let t=e[0];const s=e.find(e=>e.startsWith("--starter-kit=")),i=s?.split("=")[1],a=e.find(e=>e.startsWith("--starter-kit-source=")),c=a?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();const o=await ensureLatestCliVersion(e);let r=null,l=!1;if(t){const s=process.cwd(),a=path.join(s,"caspian.config.json");if(i&&c){l=!0;const s={projectName:t,starterKit:i,starterKitSource:c,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};r=await getAnswer(s,n)}else if(fs.existsSync(a)){const i=readJsonFile(a);let c=[];i.excludeFiles?.map(e=>{const n=path.join(s,e);fs.existsSync(n)&&c.push(n.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,websocket:i.websocket??!1,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:c??[],filePath:s};const o={projectName:t,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp,websocket:e.includes("--websocket")||(i.websocket??!1)};r=await getAnswer(o,n),null!==r&&(updateAnswer={projectName:t,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,websocket:r.websocket,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:c??[],filePath:s})}else{const s={projectName:t,starterKit:i,starterKitSource:c,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};r=await getAnswer(s,n)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},n);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const p=process.cwd();let d;if(t)if(l){const n=path.join(p,t);fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),d=n,await setupStarterKit(d,r),process.chdir(d);const s=path.join(d,"caspian.config.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));e.includes("--backend-only")&&(n.backendOnly=!0),e.includes("--tailwindcss")&&(n.tailwindcss=!0),e.includes("--typescript")&&(n.typescript=!0),e.includes("--mcp")&&(n.mcp=!0),e.includes("--websocket")&&(n.websocket=!0),e.includes("--prisma")&&(n.prisma=!0),r={...r,backendOnly:n.backendOnly,tailwindcss:n.tailwindcss,typescript:n.typescript,mcp:n.mcp,websocket:n.websocket??!1,prisma:n.prisma};let t=[];n.excludeFiles?.map(e=>{const n=path.join(d,e);fs.existsSync(n)&&t.push(n.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:t??[],filePath:d}}}else{const e=path.join(p,"caspian.config.json"),n=path.join(p,t),s=path.join(n,"caspian.config.json");fs.existsSync(e)?d=p:fs.existsSync(n)&&fs.existsSync(s)?(d=n,process.chdir(n)):(fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),d=n,process.chdir(n))}else fs.mkdirSync(r.projectName,{recursive:!0}),d=path.join(p,r.projectName),process.chdir(r.projectName);let u=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python"),npmPkg("caspian-utils")];r.prisma&&u.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.backendOnly||u.push(npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("cssnano")),r.tailwindcss&&u.push(npmPkg("tailwindcss"),npmPkg("@tailwindcss/postcss"),npmPkg("tailwind-merge")),r.prisma&&execSync(buildManagedNpmCommand(["install","-g","prisma-client-python@latest"]),{stdio:"inherit"}),r.typescript&&!r.backendOnly&&u.push(npmPkg("vite"),npmPkg("fast-glob")),r.typescript&&u.push(npmPkg("vitest")),r.starterKit&&!l&&await setupStarterKit(d,r),await installNpmDependencies(d,u,!0);let m=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(d,r),updateAnswer?.isUpdate&&!r.backendOnly&&syncGlobalsStylesheetForTailwind(d,r.tailwindcss),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],n=[],t=e=>{try{const n=path.join(d,"package.json");if(fs.existsSync(n)){const t=JSON.parse(fs.readFileSync(n,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const n=path.join(d,"src","app",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const n=path.join(d,"src","app",e);fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){const s=path.join(d,"public","js","tailwind-merge.mjs");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${s} was deleted successfully.`));const i=path.join(d,"public","js","bundle-mjs.mjs.map");fs.existsSync(i)&&(fs.unlinkSync(i),console.log(`${i} was deleted successfully.`));const a=path.join(d,"ts","tailwind-merge.ts");fs.existsSync(a)&&(fs.unlinkSync(a),console.log(`${a} was deleted successfully.`));["tailwindcss","@tailwindcss/postcss","tailwind-merge"].forEach(n=>{t(n)&&e.push(n)}),n.push("tailwind-merge")}if(r.tailwindcss){const e=path.join(d,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(n){console.warn(chalk.yellow(`Failed to delete ${e}: ${n}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const n=path.join(d,"settings",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const e=path.join(d,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),n.push("fastmcp")}if(!updateAnswer.websocket){const e=path.join(d,"src","lib","websocket");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("WebSocket folder was deleted successfully.")),n.push("websockets")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(n=>{t(n)&&e.push(n)}),n.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts",path.join("settings","run-vite-watch.ts")].forEach(e=>{const n=path.join(d,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const n=path.join(d,"ts");fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const s=path.join(d,"settings","vite-plugins");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(n=>{t(n)&&e.push(n)})}const s=e=>Array.from(new Set(e)),i=s(e);i.length>0&&(console.log(`Uninstalling npm packages: ${i.join(", ")}`),await uninstallNpmDependencies(d,i,!0));const a=s(n),c=getPyProjectDependencyNames(d);m=a.filter(e=>c.has(e.toLowerCase())),m.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${m.join(", ")}`))}if(!l||!fs.existsSync(path.join(d,"caspian.config.json"))){const e=d.replace(/\\/g,"\\"),n=bsConfigUrls(e),t={projectName:r.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,websocket:r.websocket,prisma:r.prisma,typescript:r.typescript,version:o,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(d,"caspian.config.json"),JSON.stringify(t,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(d,r,m),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(d.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
@@ -1,8 +1,18 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ // Tailwind is optional. The CSS pipeline is not: src/app/globals.css always
4
+ // compiles to public/css/styles.css. Tailwind, when enabled, is just one more
5
+ // plugin in that pipeline.
6
+ const { tailwindcss } = JSON.parse(
7
+ readFileSync(new URL("./caspian.config.json", import.meta.url), "utf8"),
8
+ );
9
+
10
+ // Skip minification while watching so devtools shows readable CSS.
1
11
  const isWatchMode = process.env.PP_POSTCSS_MODE === "watch";
2
12
 
3
13
  export default {
4
14
  plugins: {
5
- "@tailwindcss/postcss": {},
15
+ ...(tailwindcss ? { "@tailwindcss/postcss": {} } : {}),
6
16
  ...(isWatchMode ? {} : { cssnano: {} }),
7
17
  },
8
18
  };
@@ -130,7 +130,7 @@ async function cleanupStaleWatcher(): Promise<void> {
130
130
  }
131
131
 
132
132
  console.warn(
133
- `[tailwind] Found stale PostCSS watcher (PID ${pid}), stopping it before restart.`,
133
+ `[css] Found stale PostCSS watcher (PID ${pid}), stopping it before restart.`,
134
134
  );
135
135
  await killProcessTree(pid);
136
136
  await rmAsync(watcherPidFile, { force: true });
@@ -185,31 +185,35 @@ function createWatchers(rebuildWorker: DebouncedWorker): ClosableWatcher[] {
185
185
  rebuildWorker.schedule(relPath);
186
186
  };
187
187
 
188
+ // Tailwind scans markup for class names, so any authored file can change the
189
+ // generated CSS. Plain CSS has no such scan: only stylesheets matter.
188
190
  const watchers: ClosableWatcher[] = [
189
191
  createSrcWatcher(join(process.cwd(), "src", "**", "*"), {
190
- exts: [".css", ".html", ".js", ".py"],
192
+ exts: caspianConfig.tailwindcss
193
+ ? [".css", ".html", ".js", ".py"]
194
+ : [".css"],
191
195
  ignored: WATCH_IGNORES,
192
196
  awaitWriteFinish: DEFAULT_AWF,
193
- logPrefix: "tailwind:src",
197
+ logPrefix: "css:src",
194
198
  onEvent: scheduleRebuild,
195
199
  }),
196
200
  createSrcWatcher(join(process.cwd(), "postcss.config.js"), {
197
201
  exts: [".js"],
198
202
  ignored: WATCH_IGNORES,
199
203
  awaitWriteFinish: DEFAULT_AWF,
200
- logPrefix: "tailwind:config",
204
+ logPrefix: "css:config",
201
205
  onEvent: scheduleRebuild,
202
206
  }),
203
207
  ];
204
208
 
205
209
  const tsRoot = join(process.cwd(), "ts");
206
- if (caspianConfig.typescript && existsSync(tsRoot)) {
210
+ if (caspianConfig.tailwindcss && caspianConfig.typescript && existsSync(tsRoot)) {
207
211
  watchers.push(
208
212
  createSrcWatcher(join(tsRoot, "**", "*"), {
209
213
  exts: [".js", ".jsx", ".ts", ".tsx"],
210
214
  ignored: WATCH_IGNORES,
211
215
  awaitWriteFinish: DEFAULT_AWF,
212
- logPrefix: "tailwind:ts",
216
+ logPrefix: "css:ts",
213
217
  onEvent: scheduleRebuild,
214
218
  }),
215
219
  );
@@ -235,13 +239,13 @@ async function runWatchMode(): Promise<void> {
235
239
 
236
240
  if (exitCode !== 0) {
237
241
  console.error(
238
- `[tailwind] PostCSS exited with code ${exitCode}. Watching for the next change...`,
242
+ `[css] PostCSS exited with code ${exitCode}. Watching for the next change...`,
239
243
  );
240
244
  }
241
245
  } catch (error) {
242
246
  console.error(error);
243
247
  }
244
- }, 150, "tailwind");
248
+ }, 150, "css");
245
249
 
246
250
  sourceWatchers.push(...createWatchers(rebuildWorker));
247
251
 
@@ -250,7 +254,7 @@ async function runWatchMode(): Promise<void> {
250
254
 
251
255
  if (exitCode !== 0) {
252
256
  console.error(
253
- `[tailwind] Initial PostCSS build exited with code ${exitCode}. Watching for the next change...`,
257
+ `[css] Initial PostCSS build exited with code ${exitCode}. Watching for the next change...`,
254
258
  );
255
259
  }
256
260
  } catch (error) {
@@ -5,17 +5,26 @@
5
5
  :root {
6
6
  --background: #ffffff;
7
7
  --foreground: #171717;
8
+ --border: oklch(92.8% 0.006 264.531);
9
+ --muted: oklch(96.7% 0.003 264.542);
10
+ --muted-foreground: oklch(55.1% 0.027 264.364);
8
11
  }
9
12
 
10
13
  @theme inline {
11
14
  --color-background: var(--background);
12
15
  --color-foreground: var(--foreground);
16
+ --color-border: var(--border);
17
+ --color-muted: var(--muted);
18
+ --color-muted-foreground: var(--muted-foreground);
13
19
  }
14
20
 
15
21
  @media (prefers-color-scheme: dark) {
16
22
  :root {
17
23
  --background: #0a0a0a;
18
24
  --foreground: #ededed;
25
+ --border: oklch(100% 0 0 / 12%);
26
+ --muted: oklch(26.9% 0 0);
27
+ --muted-foreground: oklch(70.8% 0 0);
19
28
  }
20
29
  }
21
30
 
@@ -0,0 +1,502 @@
1
+ /*
2
+ * globals.css - the only stylesheet you edit.
3
+ *
4
+ * `npm run dev` and `npm run build` compile this file to public/css/styles.css,
5
+ * which the layout links. Never edit public/css/styles.css: it is generated
6
+ * and overwritten on every build.
7
+ *
8
+ * This project was created without Tailwind CSS, so the rules below are plain
9
+ * CSS that you own. They cover the starter pages; add, rename or delete
10
+ * whatever you like.
11
+ */
12
+
13
+ /* ---------------------------------------------------------------- tokens */
14
+
15
+ :root {
16
+ --background: #ffffff;
17
+ --foreground: #171717;
18
+ --border: oklch(92.8% 0.006 264.531);
19
+ --muted: oklch(96.7% 0.003 264.542);
20
+ --muted-foreground: oklch(55.1% 0.027 264.364);
21
+
22
+ --gray-50: oklch(98.5% 0.002 247.839);
23
+ --gray-100: oklch(96.7% 0.003 264.542);
24
+ --gray-200: oklch(92.8% 0.006 264.531);
25
+ --gray-500: oklch(55.1% 0.027 264.364);
26
+ --gray-600: oklch(44.6% 0.03 256.802);
27
+ --gray-700: oklch(37.3% 0.034 259.733);
28
+ --gray-800: oklch(27.8% 0.033 256.848);
29
+ --gray-900: oklch(21% 0.034 264.665);
30
+ --red-600: oklch(57.7% 0.245 27.325);
31
+ --blue-600: oklch(54.6% 0.245 262.881);
32
+ --indigo-600: oklch(51.1% 0.262 276.966);
33
+ --indigo-700: oklch(45.7% 0.24 277.023);
34
+
35
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
36
+ "Liberation Mono", "Courier New", monospace;
37
+ --transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
38
+ }
39
+
40
+ @media (prefers-color-scheme: dark) {
41
+ :root {
42
+ --background: #0a0a0a;
43
+ --foreground: #ededed;
44
+ --border: oklch(100% 0 0 / 12%);
45
+ --muted: oklch(26.9% 0 0);
46
+ --muted-foreground: oklch(70.8% 0 0);
47
+ }
48
+ }
49
+
50
+ /* ------------------------------------------------------------- base reset */
51
+
52
+ *,
53
+ ::before,
54
+ ::after {
55
+ box-sizing: border-box;
56
+ border: 0 solid var(--border);
57
+ }
58
+
59
+ html {
60
+ -webkit-text-size-adjust: 100%;
61
+ line-height: 1.5;
62
+ }
63
+
64
+ body {
65
+ margin: 0;
66
+ background: var(--background);
67
+ color: var(--foreground);
68
+ font-family: Arial, Helvetica, sans-serif;
69
+ }
70
+
71
+ h1,
72
+ h2,
73
+ h3,
74
+ p,
75
+ figure,
76
+ pre {
77
+ margin: 0;
78
+ font-size: inherit;
79
+ font-weight: inherit;
80
+ }
81
+
82
+ code,
83
+ pre {
84
+ font-family: var(--font-mono);
85
+ font-size: 1em;
86
+ }
87
+
88
+ a {
89
+ color: inherit;
90
+ text-decoration: inherit;
91
+ }
92
+
93
+ button {
94
+ margin: 0;
95
+ padding: 0;
96
+ background: transparent;
97
+ color: inherit;
98
+ font: inherit;
99
+ cursor: pointer;
100
+ }
101
+
102
+ img,
103
+ svg {
104
+ display: block;
105
+ vertical-align: middle;
106
+ }
107
+
108
+ img {
109
+ max-width: 100%;
110
+ height: auto;
111
+ }
112
+
113
+ /* ---------------------------------------------------------------- layout */
114
+
115
+ .container {
116
+ width: 100%;
117
+ }
118
+ .flex {
119
+ display: flex;
120
+ }
121
+ .grid {
122
+ display: grid;
123
+ }
124
+ .hidden {
125
+ display: none;
126
+ }
127
+ .inline-block {
128
+ display: inline-block;
129
+ }
130
+
131
+ .flex-col {
132
+ flex-direction: column;
133
+ }
134
+ .flex-wrap {
135
+ flex-wrap: wrap;
136
+ }
137
+ .flex-1 {
138
+ flex: 1 1 0%;
139
+ }
140
+
141
+ .items-start {
142
+ align-items: flex-start;
143
+ }
144
+ .items-center {
145
+ align-items: center;
146
+ }
147
+ .justify-center {
148
+ justify-content: center;
149
+ }
150
+ .justify-between {
151
+ justify-content: space-between;
152
+ }
153
+
154
+ .gap-px {
155
+ gap: 1px;
156
+ }
157
+ .gap-2 {
158
+ gap: 0.5rem;
159
+ }
160
+ .gap-4 {
161
+ gap: 1rem;
162
+ }
163
+ .gap-16 {
164
+ gap: 4rem;
165
+ }
166
+
167
+ .min-h-screen {
168
+ min-height: 100vh;
169
+ }
170
+ .mx-auto {
171
+ margin-inline: auto;
172
+ }
173
+ .overflow-auto {
174
+ overflow: auto;
175
+ }
176
+ .object-contain {
177
+ object-fit: contain;
178
+ }
179
+
180
+ /* --------------------------------------------------------------- spacing */
181
+
182
+ .mt-2 {
183
+ margin-top: 0.5rem;
184
+ }
185
+ .mt-3 {
186
+ margin-top: 0.75rem;
187
+ }
188
+ .mt-4 {
189
+ margin-top: 1rem;
190
+ }
191
+ .mt-5 {
192
+ margin-top: 1.25rem;
193
+ }
194
+ .mt-6 {
195
+ margin-top: 1.5rem;
196
+ }
197
+ .mt-8 {
198
+ margin-top: 2rem;
199
+ }
200
+ .mb-2 {
201
+ margin-bottom: 0.5rem;
202
+ }
203
+
204
+ .p-4 {
205
+ padding: 1rem;
206
+ }
207
+ .p-6 {
208
+ padding: 1.5rem;
209
+ }
210
+
211
+ .px-2 {
212
+ padding-inline: 0.5rem;
213
+ }
214
+ .px-3 {
215
+ padding-inline: 0.75rem;
216
+ }
217
+ .px-4 {
218
+ padding-inline: 1rem;
219
+ }
220
+ .px-5 {
221
+ padding-inline: 1.25rem;
222
+ }
223
+ .px-6 {
224
+ padding-inline: 1.5rem;
225
+ }
226
+
227
+ .py-1 {
228
+ padding-block: 0.25rem;
229
+ }
230
+ .py-1\.5 {
231
+ padding-block: 0.375rem;
232
+ }
233
+ .py-2 {
234
+ padding-block: 0.5rem;
235
+ }
236
+ .py-3 {
237
+ padding-block: 0.75rem;
238
+ }
239
+ .py-8 {
240
+ padding-block: 2rem;
241
+ }
242
+ .py-10 {
243
+ padding-block: 2.5rem;
244
+ }
245
+
246
+ /* ---------------------------------------------------------------- sizing */
247
+
248
+ .size-5 {
249
+ width: 1.25rem;
250
+ height: 1.25rem;
251
+ }
252
+ .size-20 {
253
+ width: 5rem;
254
+ height: 5rem;
255
+ }
256
+ .h-10 {
257
+ height: 2.5rem;
258
+ }
259
+ .w-10 {
260
+ width: 2.5rem;
261
+ }
262
+ .max-w-md {
263
+ max-width: 28rem;
264
+ }
265
+ .max-w-2xl {
266
+ max-width: 42rem;
267
+ }
268
+
269
+ /* ------------------------------------------------------------ typography */
270
+
271
+ .font-mono {
272
+ font-family: var(--font-mono);
273
+ }
274
+ .font-medium {
275
+ font-weight: 500;
276
+ }
277
+ .font-semibold {
278
+ font-weight: 600;
279
+ }
280
+ .font-bold {
281
+ font-weight: 700;
282
+ }
283
+ .font-black {
284
+ font-weight: 900;
285
+ }
286
+
287
+ .text-xs {
288
+ font-size: 0.75rem;
289
+ line-height: 1.33333;
290
+ }
291
+ .text-\[11px\] {
292
+ font-size: 11px;
293
+ }
294
+ .text-sm {
295
+ font-size: 0.875rem;
296
+ line-height: 1.42857;
297
+ }
298
+ .text-2xl {
299
+ font-size: 1.5rem;
300
+ line-height: 1.33333;
301
+ }
302
+ .text-5xl {
303
+ font-size: 3rem;
304
+ line-height: 1;
305
+ }
306
+ .text-9xl {
307
+ font-size: 8rem;
308
+ line-height: 1;
309
+ }
310
+
311
+ .leading-relaxed {
312
+ line-height: 1.625;
313
+ }
314
+ .tracking-tight {
315
+ letter-spacing: -0.025em;
316
+ }
317
+ .tracking-\[0\.18em\] {
318
+ letter-spacing: 0.18em;
319
+ }
320
+ .tracking-\[-0\.04em\] {
321
+ letter-spacing: -0.04em;
322
+ }
323
+ .uppercase {
324
+ text-transform: uppercase;
325
+ }
326
+ .text-center {
327
+ text-align: center;
328
+ }
329
+ .text-pretty {
330
+ text-wrap: pretty;
331
+ }
332
+
333
+ /* ---------------------------------------------------------------- colors */
334
+
335
+ .bg-background {
336
+ background-color: var(--background);
337
+ }
338
+ .bg-border {
339
+ background-color: var(--border);
340
+ }
341
+ .bg-transparent {
342
+ background-color: transparent;
343
+ }
344
+ .bg-white {
345
+ background-color: #fff;
346
+ }
347
+ .bg-gray-50 {
348
+ background-color: var(--gray-50);
349
+ }
350
+ .bg-gray-100 {
351
+ background-color: var(--gray-100);
352
+ }
353
+ .bg-blue-600 {
354
+ background-color: var(--blue-600);
355
+ }
356
+ .bg-indigo-600 {
357
+ background-color: var(--indigo-600);
358
+ }
359
+
360
+ .text-foreground {
361
+ color: var(--foreground);
362
+ }
363
+ .text-muted-foreground {
364
+ color: var(--muted-foreground);
365
+ }
366
+ .text-black {
367
+ color: #000;
368
+ }
369
+ .text-white {
370
+ color: #fff;
371
+ }
372
+ .text-red-600 {
373
+ color: var(--red-600);
374
+ }
375
+ .text-gray-200 {
376
+ color: var(--gray-200);
377
+ }
378
+ .text-gray-500 {
379
+ color: var(--gray-500);
380
+ }
381
+ .text-gray-600 {
382
+ color: var(--gray-600);
383
+ }
384
+ .text-gray-700 {
385
+ color: var(--gray-700);
386
+ }
387
+ .text-gray-800 {
388
+ color: var(--gray-800);
389
+ }
390
+ .text-gray-900 {
391
+ color: var(--gray-900);
392
+ }
393
+
394
+ /* --------------------------------------------------------------- borders */
395
+
396
+ .border {
397
+ border-width: 1px;
398
+ }
399
+ .border-border {
400
+ border-color: var(--border);
401
+ }
402
+ .border-gray-200 {
403
+ border-color: var(--gray-200);
404
+ }
405
+ .rounded {
406
+ border-radius: 0.25rem;
407
+ }
408
+ .shadow-sm {
409
+ box-shadow:
410
+ 0 1px 3px 0 rgb(0 0 0 / 0.1),
411
+ 0 1px 2px -1px rgb(0 0 0 / 0.1);
412
+ }
413
+
414
+ /* ------------------------------------------------------ motion and focus */
415
+
416
+ .transition-colors {
417
+ transition:
418
+ color var(--transition),
419
+ background-color var(--transition),
420
+ border-color var(--transition);
421
+ }
422
+
423
+ .transition-transform {
424
+ transition: transform var(--transition);
425
+ }
426
+
427
+ .focus\:outline-none:focus {
428
+ outline: none;
429
+ }
430
+ .focus\:ring:focus {
431
+ box-shadow: 0 0 0 3px rgb(59 130 246 / 0.5);
432
+ }
433
+
434
+ .hover\:text-foreground:hover {
435
+ color: var(--foreground);
436
+ }
437
+ .hover\:bg-gray-200:hover {
438
+ background-color: var(--gray-200);
439
+ }
440
+ .hover\:bg-indigo-700:hover {
441
+ background-color: var(--indigo-700);
442
+ }
443
+ .hover\:bg-muted\/50:hover {
444
+ background-color: color-mix(in oklab, var(--muted) 50%, transparent);
445
+ }
446
+
447
+ .group:hover .group-hover\:translate-x-1 {
448
+ transform: translateX(0.25rem);
449
+ }
450
+
451
+ @media (prefers-reduced-motion: reduce) {
452
+ .motion-reduce\:transform-none {
453
+ transform: none;
454
+ }
455
+ }
456
+
457
+ /* ----------------------------------------------------------- breakpoints */
458
+
459
+ @media (width >= 40rem) {
460
+ .container {
461
+ max-width: 40rem;
462
+ }
463
+ .sm\:inline {
464
+ display: inline;
465
+ }
466
+ .sm\:grid-cols-2 {
467
+ grid-template-columns: repeat(2, minmax(0, 1fr));
468
+ }
469
+ .sm\:text-4xl {
470
+ font-size: 2.25rem;
471
+ line-height: 1.11111;
472
+ }
473
+ }
474
+
475
+ @media (width >= 48rem) {
476
+ .container {
477
+ max-width: 48rem;
478
+ }
479
+ .md\:size-24 {
480
+ width: 6rem;
481
+ height: 6rem;
482
+ }
483
+ .md\:px-12 {
484
+ padding-inline: 3rem;
485
+ }
486
+ .md\:py-12 {
487
+ padding-block: 3rem;
488
+ }
489
+ .md\:text-7xl {
490
+ font-size: 4.5rem;
491
+ line-height: 1;
492
+ }
493
+ }
494
+
495
+ @media (width >= 64rem) {
496
+ .container {
497
+ max-width: 64rem;
498
+ }
499
+ .lg\:grid-cols-4 {
500
+ grid-template-columns: repeat(4, minmax(0, 1fr));
501
+ }
502
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-caspian-app",
3
- "version": "1.5.7",
3
+ "version": "1.6.0-alpha",
4
4
  "description": "Scaffold a new Caspian project (FastAPI-powered reactive Python framework).",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer properties;@layer theme, base, components, utilities;@layer theme{:host,:root{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-600:oklch(57.7% 0.245 27.325);--color-green-400:oklch(79.2% 0.209 151.711);--color-cyan-300:oklch(86.5% 0.127 207.078);--color-cyan-400:oklch(78.9% 0.154 211.53);--color-blue-500:oklch(62.3% 0.214 259.815);--color-blue-600:oklch(54.6% 0.245 262.881);--color-indigo-600:oklch(51.1% 0.262 276.966);--color-indigo-700:oklch(45.7% 0.24 277.023);--color-purple-400:oklch(71.4% 0.203 305.504);--color-gray-50:oklch(98.5% 0.002 247.839);--color-gray-100:oklch(96.7% 0.003 264.542);--color-gray-200:oklch(92.8% 0.006 264.531);--color-gray-500:oklch(55.1% 0.027 264.364);--color-gray-600:oklch(44.6% 0.03 256.802);--color-gray-700:oklch(37.3% 0.034 259.733);--color-gray-800:oklch(27.8% 0.033 256.848);--color-gray-900:oklch(21% 0.034 264.665);--color-zinc-800:oklch(27.4% 0.006 286.033);--color-neutral-800:oklch(26.9% 0 0);--color-black:#000;--color-white:#fff;--spacing:0.25rem;--container-sm:24rem;--container-lg:32rem;--container-2xl:42rem;--container-5xl:64rem;--text-xs:0.75rem;--text-xs--line-height:1.33333;--text-sm:0.875rem;--text-sm--line-height:1.42857;--text-base:1rem;--text-base--line-height:1.5;--text-lg:1.125rem;--text-lg--line-height:1.55556;--text-xl:1.25rem;--text-xl--line-height:1.4;--text-2xl:1.5rem;--text-2xl--line-height:1.33333;--text-4xl:2.25rem;--text-4xl--line-height:1.11111;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--tracking-tight:-0.025em;--tracking-widest:0.1em;--radius-xs:0.125rem;--radius-sm:0.25rem;--radius-md:0.375rem;--radius-lg:0.5rem;--radius-xl:0.75rem;--ease-in:cubic-bezier(0.4,0,1,1);--ease-out:cubic-bezier(0,0,0.2,1);--ease-in-out:cubic-bezier(0.4,0,0.2,1);--blur-sm:8px;--blur-2xl:40px;--default-transition-duration:150ms;--default-transition-timing-function:cubic-bezier(0.4,0,0.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,::backdrop,::file-selector-button,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);tab-size:4;-webkit-tap-highlight-color:transparent}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-size:1em;font-variation-settings:var(--default-mono-font-variation-settings,normal)}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}menu,ol,ul{list-style:none}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}::file-selector-button,button,input,optgroup,select,textarea{background-color:transparent;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::placeholder{color:currentcolor;@supports (color:color-mix(in lab,red,red)){color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}::file-selector-button,button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.sr-only{border-width:0;clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;white-space:nowrap;width:1px}.absolute,.sr-only{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-y-0{inset-block:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.top-4{top:calc(var(--spacing)*4)}.top-\[50\%\]{top:50%}.right-0{right:calc(var(--spacing)*0)}.right-4{right:calc(var(--spacing)*4)}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.left-\[50\%\]{left:50%}.-z-10{z-index:-10}.-z-20{z-index:-20}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%;@media (width >= 40rem){max-width:40rem}@media (width >= 48rem){max-width:48rem}@media (width >= 64rem){max-width:64rem}@media (width >= 80rem){max-width:80rem}@media (width >= 96rem){max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-16{margin-top:calc(var(--spacing)*16)}.mt-auto{margin-top:auto}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-32{margin-bottom:calc(var(--spacing)*32)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.hidden\!{display:none!important}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-4{height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}.size-8{height:calc(var(--spacing)*8);width:calc(var(--spacing)*8)}.size-9{height:calc(var(--spacing)*9);width:calc(var(--spacing)*9)}.size-10{height:calc(var(--spacing)*10);width:calc(var(--spacing)*10)}.h-5{height:calc(var(--spacing)*5)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-24{height:calc(var(--spacing)*24)}.h-48{height:calc(var(--spacing)*48)}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-\[300px\]{max-height:300px}.min-h-screen{min-height:100vh}.w-3\/4{width:75%}.w-5{width:calc(var(--spacing)*5)}.w-10{width:calc(var(--spacing)*10)}.w-24{width:calc(var(--spacing)*24)}.w-60{width:calc(var(--spacing)*60)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-\[85vw\]{max-width:85vw}.max-w-\[calc\(100\%-2rem\)\]{max-width:calc(100% - 2rem)}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.translate-x-\[-50\%\]{--tw-translate-x:-50%}.translate-x-\[-50\%\],.translate-y-\[-20\%\]{translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-\[-20\%\]{--tw-translate-y:-20%}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.scroll-py-1{scroll-padding-block:calc(var(--spacing)*1)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}.gap-5{gap:calc(var(--spacing)*5)}.gap-6{gap:calc(var(--spacing)*6)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xs{border-radius:var(--radius-xs)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.border-transparent{border-color:transparent}.bg-background{background-color:var(--background)}.bg-background\/80{background-color:var(--background);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--background) 80%,transparent)}}.bg-black\/50{background-color:color-mix(in srgb,#000 50%,transparent);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-transparent{background-color:transparent}.bg-white{background-color:var(--color-white)}.bg-linear-to-t{--tw-gradient-position:to top;@supports (background-image:linear-gradient(in lab,red,red)){--tw-gradient-position:to top in oklab}background-image:linear-gradient(var(--tw-gradient-stops))}.from-background{--tw-gradient-from:var(--background);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-background{--tw-gradient-via:var(--background);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-via) var(--tw-gradient-via-position),var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.object-contain{object-fit:contain}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-10{padding-block:calc(var(--spacing)*10)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-24{padding-top:calc(var(--spacing)*24)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-9xl{font-size:var(--text-9xl);line-height:var(--tw-leading,var(--text-9xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.whitespace-nowrap{white-space:nowrap}.text-\[\#053b69\]{color:#053b69}.text-black{color:var(--color-black)}.text-blue-500{color:var(--color-blue-500)}.text-foreground{color:var(--foreground)}.text-gray-200{color:var(--color-gray-200)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:50%}.opacity-70{opacity:70%}.opacity-\[0\.15\]{opacity:.15}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,rgba(0,0,0,.1)),0 4px 6px -4px var(--tw-shadow-color,rgba(0,0,0,.1))}.shadow-lg,.shadow-sm{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,rgba(0,0,0,.1)),0 1px 2px -1px var(--tw-shadow-color,rgba(0,0,0,.1))}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,rgba(0,0,0,.05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline-hidden{--tw-outline-style:none;outline-style:none;@media (forced-colors:active){outline:2px solid transparent;outline-offset:2px}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[color\,box-shadow\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-all{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-colors{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-opacity{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-transform{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.duration-200{--tw-duration:200ms;transition-duration:.2s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-hover\:translate-x-1{&:is(:where(.group):hover *){@media (hover:hover){--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x) var(--tw-translate-y)}}}.group-hover\:text-cyan-400{&:is(:where(.group):hover *){@media (hover:hover){color:var(--color-cyan-400)}}}.group-hover\:text-green-400{&:is(:where(.group):hover *){@media (hover:hover){color:var(--color-green-400)}}}.group-hover\:text-purple-400{&:is(:where(.group):hover *){@media (hover:hover){color:var(--color-purple-400)}}}.group-data-\[disabled\=true\]\:pointer-events-none{&:is(:where(.group)[data-disabled=true] *){pointer-events:none}}.group-data-\[disabled\=true\]\:opacity-50{&:is(:where(.group)[data-disabled=true] *){opacity:50%}}.peer-disabled\:cursor-not-allowed{&:is(:where(.peer):disabled~*){cursor:not-allowed}}.peer-disabled\:opacity-50{&:is(:where(.peer):disabled~*){opacity:50%}}.file\:inline-flex{&::file-selector-button{display:inline-flex}}.file\:h-7{&::file-selector-button{height:calc(var(--spacing)*7)}}.file\:border-0{&::file-selector-button{border-style:var(--tw-border-style);border-width:0}}.file\:bg-transparent{&::file-selector-button{background-color:transparent}}.file\:text-sm{&::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.file\:font-medium{&::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}}.file\:text-foreground{&::file-selector-button{color:var(--foreground)}}.before\:absolute{&:before{content:var(--tw-content);position:absolute}}.before\:h-75{&:before{content:var(--tw-content);height:calc(var(--spacing)*75)}}.before\:w-120{&:before{content:var(--tw-content);width:calc(var(--spacing)*120)}}.before\:max-w-full{&:before{content:var(--tw-content);max-width:100%}}.before\:-translate-x-1\/2{&:before{content:var(--tw-content);--tw-translate-x:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}}.before\:rounded-full{&:before{border-radius:calc(infinity*1px);content:var(--tw-content)}}.before\:bg-linear-to-br{&:before{content:var(--tw-content);--tw-gradient-position:to bottom right;@supports (background-image:linear-gradient(in lab,red,red)){--tw-gradient-position:to bottom right in oklab}background-image:linear-gradient(var(--tw-gradient-stops))}}.before\:from-transparent{&:before{content:var(--tw-content);--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-to) var(--tw-gradient-to-position))}}.before\:opacity-10{&:before{content:var(--tw-content);opacity:10%}}.before\:blur-2xl{&:before{content:var(--tw-content);--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.before\:content-\[\'\'\]{&:before{--tw-content:"";content:var(--tw-content)}}.after\:absolute{&:after{content:var(--tw-content);position:absolute}}.after\:-z-20{&:after{content:var(--tw-content);z-index:-20}}.after\:h-45{&:after{content:var(--tw-content);height:calc(var(--spacing)*45)}}.after\:w-60{&:after{content:var(--tw-content);width:calc(var(--spacing)*60)}}.after\:max-w-full{&:after{content:var(--tw-content);max-width:100%}}.after\:translate-x-1\/3{&:after{content:var(--tw-content);--tw-translate-x:33.33333%;translate:var(--tw-translate-x) var(--tw-translate-y)}}.after\:bg-linear-to-t{&:after{content:var(--tw-content);--tw-gradient-position:to top;@supports (background-image:linear-gradient(in lab,red,red)){--tw-gradient-position:to top in oklab}background-image:linear-gradient(var(--tw-gradient-stops))}}.after\:from-cyan-300{&:after{content:var(--tw-content);--tw-gradient-from:var(--color-cyan-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-to) var(--tw-gradient-to-position))}}.after\:opacity-20{&:after{content:var(--tw-content);opacity:20%}}.after\:blur-2xl{&:after{content:var(--tw-content);--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.after\:content-\[\'\'\]{&:after{--tw-content:"";content:var(--tw-content)}}.hover\:bg-gray-200{&:hover{@media (hover:hover){background-color:var(--color-gray-200)}}}.hover\:bg-indigo-700{&:hover{@media (hover:hover){background-color:var(--color-indigo-700)}}}.hover\:underline{&:hover{@media (hover:hover){text-decoration-line:underline}}}.hover\:opacity-100{&:hover{@media (hover:hover){opacity:100%}}}.hover\:backdrop-blur-sm{&:hover{@media (hover:hover){--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}}}.focus\:ring{&:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:ring-2{&:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:ring-offset-2{&:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}}.focus\:outline-hidden{&:focus{--tw-outline-style:none;outline-style:none;@media (forced-colors:active){outline:2px solid transparent;outline-offset:2px}}}.focus\:outline-none{&:focus{--tw-outline-style:none;outline-style:none}}.focus-visible\:ring-\[3px\]{&:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.disabled\:pointer-events-none{&:disabled{pointer-events:none}}.disabled\:cursor-not-allowed{&:disabled{cursor:not-allowed}}.disabled\:opacity-50{&:disabled{opacity:50%}}.has-\[\>svg\]\:px-2\.5{&:has(>svg){padding-inline:calc(var(--spacing)*2.5)}}.has-\[\>svg\]\:px-3{&:has(>svg){padding-inline:calc(var(--spacing)*3)}}.has-\[\>svg\]\:px-4{&:has(>svg){padding-inline:calc(var(--spacing)*4)}}.data-\[disabled\=true\]\:pointer-events-none{&[data-disabled=true]{pointer-events:none}}.data-\[disabled\=true\]\:opacity-50{&[data-disabled=true]{opacity:50%}}.data-\[state\=closed\]\:duration-300{&[data-state=closed]{--tw-duration:300ms;transition-duration:.3s}}.data-\[state\=open\]\:duration-500{&[data-state=open]{--tw-duration:500ms;transition-duration:.5s}}.motion-reduce\:transform-none{@media (prefers-reduced-motion:reduce){transform:none}}.sm\:max-w-lg{@media (width >= 40rem){max-width:var(--container-lg)}}.sm\:max-w-sm{@media (width >= 40rem){max-width:var(--container-sm)}}.sm\:flex-row{@media (width >= 40rem){flex-direction:row}}.sm\:justify-end{@media (width >= 40rem){justify-content:flex-end}}.sm\:text-left{@media (width >= 40rem){text-align:left}}.sm\:text-4xl{@media (width >= 40rem){font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}}.md\:h-32{@media (width >= 48rem){height:calc(var(--spacing)*32)}}.md\:w-32{@media (width >= 48rem){width:calc(var(--spacing)*32)}}.md\:max-w-125{@media (width >= 48rem){max-width:calc(var(--spacing)*125)}}.md\:grid-cols-2{@media (width >= 48rem){grid-template-columns:repeat(2,minmax(0,1fr))}}.md\:gap-6{@media (width >= 48rem){gap:calc(var(--spacing)*6)}}.md\:px-24{@media (width >= 48rem){padding-inline:calc(var(--spacing)*24)}}.md\:py-12{@media (width >= 48rem){padding-block:calc(var(--spacing)*12)}}.md\:text-2xl{@media (width >= 48rem){font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}.md\:text-6xl{@media (width >= 48rem){font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}}.md\:text-base{@media (width >= 48rem){font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}}.md\:text-sm{@media (width >= 48rem){font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.lg\:static{@media (width >= 64rem){position:static}}.lg\:mb-0{@media (width >= 64rem){margin-bottom:calc(var(--spacing)*0)}}.lg\:flex{@media (width >= 64rem){display:flex}}.lg\:h-auto{@media (width >= 64rem){height:auto}}.lg\:w-auto{@media (width >= 64rem){width:auto}}.lg\:w-full{@media (width >= 64rem){width:100%}}.lg\:max-w-5xl{@media (width >= 64rem){max-width:var(--container-5xl)}}.lg\:grid-cols-4{@media (width >= 64rem){grid-template-columns:repeat(4,minmax(0,1fr))}}.lg\:rounded-xl{@media (width >= 64rem){border-radius:var(--radius-xl)}}.lg\:border{@media (width >= 64rem){border-bottom-width:1px;border-left-width:1px;border-right-width:1px;border-style:var(--tw-border-style);border-top-width:1px}}.lg\:bg-none{@media (width >= 64rem){background-image:none}}.lg\:p-0{@media (width >= 64rem){padding:calc(var(--spacing)*0)}}.lg\:p-4{@media (width >= 64rem){padding:calc(var(--spacing)*4)}}.lg\:pt-0{@media (width >= 64rem){padding-top:calc(var(--spacing)*0)}}.lg\:text-left{@media (width >= 64rem){text-align:left}}.dark\:border-neutral-800{@media (prefers-color-scheme:dark){border-color:var(--color-neutral-800)}}.dark\:bg-zinc-800\/30{@media (prefers-color-scheme:dark){background-color:color-mix(in srgb,oklch(27.4% .006 286.033) 30%,transparent);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-zinc-800) 30%,transparent)}}}.dark\:from-inherit{@media (prefers-color-scheme:dark){--tw-gradient-from:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-to) var(--tw-gradient-to-position))}}.lg\:dark\:bg-zinc-800\/30{@media (width >= 64rem){@media (prefers-color-scheme:dark){background-color:color-mix(in srgb,oklch(27.4% .006 286.033) 30%,transparent);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-zinc-800) 30%,transparent)}}}}.\[\&_\[data-slot\=command-group-heading\]\]\:px-2{& [data-slot=command-group-heading]{padding-inline:calc(var(--spacing)*2)}}.\[\&_\[data-slot\=command-group-heading\]\]\:py-1\.5{& [data-slot=command-group-heading]{padding-block:calc(var(--spacing)*1.5)}}.\[\&_\[data-slot\=command-group-heading\]\]\:text-xs{& [data-slot=command-group-heading]{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}.\[\&_\[data-slot\=command-group-heading\]\]\:font-medium{& [data-slot=command-group-heading]{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}}.\[\&_\[data-slot\=command-group\]\]\:px-2{& [data-slot=command-group]{padding-inline:calc(var(--spacing)*2)}}.\[\&_\[data-slot\=command-group\]\:not\(\[hidden\]\)_\~_\[data-slot\=command-group\]\]\:pt-0{& [data-slot=command-group]:not([hidden])~[data-slot=command-group]{padding-top:calc(var(--spacing)*0)}}.\[\&_\[data-slot\=command-input-wrapper\]\]\:h-12{& [data-slot=command-input-wrapper]{height:calc(var(--spacing)*12)}}.\[\&_\[data-slot\=command-input-wrapper\]_svg\]\:h-5{& [data-slot=command-input-wrapper] svg{height:calc(var(--spacing)*5)}}.\[\&_\[data-slot\=command-input-wrapper\]_svg\]\:w-5{& [data-slot=command-input-wrapper] svg{width:calc(var(--spacing)*5)}}.\[\&_\[data-slot\=command-input\]\]\:h-12{& [data-slot=command-input]{height:calc(var(--spacing)*12)}}.\[\&_\[data-slot\=command-item\]\]\:px-2{& [data-slot=command-item]{padding-inline:calc(var(--spacing)*2)}}.\[\&_\[data-slot\=command-item\]\]\:py-3{& [data-slot=command-item]{padding-block:calc(var(--spacing)*3)}}.\[\&_\[data-slot\=command-item\]_svg\]\:h-5{& [data-slot=command-item] svg{height:calc(var(--spacing)*5)}}.\[\&_\[data-slot\=command-item\]_svg\]\:w-5{& [data-slot=command-item] svg{width:calc(var(--spacing)*5)}}.\[\&_svg\]\:pointer-events-none{& svg{pointer-events:none}}.\[\&_svg\]\:shrink-0{& svg{flex-shrink:0}}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4{& svg:not([class*=size-]){height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}}}:root{--background:#fff;--foreground:#171717}@media (prefers-color-scheme:dark){:root{--background:#0a0a0a;--foreground:#ededed}}body{background:var(--background);color:var(--foreground);font-family:Arial,Helvetica,sans-serif}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";initial-value:"";inherits:false}@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}
@@ -1 +0,0 @@
1
- /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer properties;@layer theme, base, components, utilities;@layer theme{:host,:root{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-600:oklch(57.7% 0.245 27.325);--color-green-400:oklch(79.2% 0.209 151.711);--color-cyan-300:oklch(86.5% 0.127 207.078);--color-cyan-400:oklch(78.9% 0.154 211.53);--color-blue-500:oklch(62.3% 0.214 259.815);--color-blue-600:oklch(54.6% 0.245 262.881);--color-indigo-600:oklch(51.1% 0.262 276.966);--color-indigo-700:oklch(45.7% 0.24 277.023);--color-purple-400:oklch(71.4% 0.203 305.504);--color-gray-50:oklch(98.5% 0.002 247.839);--color-gray-100:oklch(96.7% 0.003 264.542);--color-gray-200:oklch(92.8% 0.006 264.531);--color-gray-500:oklch(55.1% 0.027 264.364);--color-gray-600:oklch(44.6% 0.03 256.802);--color-gray-700:oklch(37.3% 0.034 259.733);--color-gray-800:oklch(27.8% 0.033 256.848);--color-gray-900:oklch(21% 0.034 264.665);--color-zinc-800:oklch(27.4% 0.006 286.033);--color-neutral-800:oklch(26.9% 0 0);--color-black:#000;--color-white:#fff;--spacing:0.25rem;--container-sm:24rem;--container-lg:32rem;--container-2xl:42rem;--container-5xl:64rem;--text-xs:0.75rem;--text-xs--line-height:1.33333;--text-sm:0.875rem;--text-sm--line-height:1.42857;--text-base:1rem;--text-base--line-height:1.5;--text-lg:1.125rem;--text-lg--line-height:1.55556;--text-xl:1.25rem;--text-xl--line-height:1.4;--text-2xl:1.5rem;--text-2xl--line-height:1.33333;--text-4xl:2.25rem;--text-4xl--line-height:1.11111;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--tracking-tight:-0.025em;--tracking-widest:0.1em;--radius-xs:0.125rem;--radius-sm:0.25rem;--radius-md:0.375rem;--radius-lg:0.5rem;--radius-xl:0.75rem;--ease-in:cubic-bezier(0.4,0,1,1);--ease-out:cubic-bezier(0,0,0.2,1);--ease-in-out:cubic-bezier(0.4,0,0.2,1);--blur-sm:8px;--blur-2xl:40px;--default-transition-duration:150ms;--default-transition-timing-function:cubic-bezier(0.4,0,0.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,::backdrop,::file-selector-button,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);tab-size:4;-webkit-tap-highlight-color:transparent}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-size:1em;font-variation-settings:var(--default-mono-font-variation-settings,normal)}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}menu,ol,ul{list-style:none}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}::file-selector-button,button,input,optgroup,select,textarea{background-color:transparent;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::placeholder{color:currentcolor;@supports (color:color-mix(in lab,red,red)){color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}::file-selector-button,button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.sr-only{border-width:0;clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;white-space:nowrap;width:1px}.absolute,.sr-only{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-y-0{inset-block:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.top-4{top:calc(var(--spacing)*4)}.top-\[50\%\]{top:50%}.right-0{right:calc(var(--spacing)*0)}.right-4{right:calc(var(--spacing)*4)}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.left-\[50\%\]{left:50%}.-z-10{z-index:-10}.-z-20{z-index:-20}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%;@media (width >= 40rem){max-width:40rem}@media (width >= 48rem){max-width:48rem}@media (width >= 64rem){max-width:64rem}@media (width >= 80rem){max-width:80rem}@media (width >= 96rem){max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-16{margin-top:calc(var(--spacing)*16)}.mt-auto{margin-top:auto}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-32{margin-bottom:calc(var(--spacing)*32)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.hidden\!{display:none!important}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-4{height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}.size-8{height:calc(var(--spacing)*8);width:calc(var(--spacing)*8)}.size-9{height:calc(var(--spacing)*9);width:calc(var(--spacing)*9)}.size-10{height:calc(var(--spacing)*10);width:calc(var(--spacing)*10)}.h-5{height:calc(var(--spacing)*5)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-24{height:calc(var(--spacing)*24)}.h-48{height:calc(var(--spacing)*48)}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-\[300px\]{max-height:300px}.min-h-screen{min-height:100vh}.w-3\/4{width:75%}.w-5{width:calc(var(--spacing)*5)}.w-10{width:calc(var(--spacing)*10)}.w-24{width:calc(var(--spacing)*24)}.w-60{width:calc(var(--spacing)*60)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-\[85vw\]{max-width:85vw}.max-w-\[calc\(100\%-2rem\)\]{max-width:calc(100% - 2rem)}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.translate-x-\[-50\%\]{--tw-translate-x:-50%}.translate-x-\[-50\%\],.translate-y-\[-20\%\]{translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-\[-20\%\]{--tw-translate-y:-20%}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.scroll-py-1{scroll-padding-block:calc(var(--spacing)*1)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}.gap-5{gap:calc(var(--spacing)*5)}.gap-6{gap:calc(var(--spacing)*6)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xs{border-radius:var(--radius-xs)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.border-transparent{border-color:transparent}.bg-background{background-color:var(--background)}.bg-background\/80{background-color:var(--background);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--background) 80%,transparent)}}.bg-black\/50{background-color:color-mix(in srgb,#000 50%,transparent);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-transparent{background-color:transparent}.bg-white{background-color:var(--color-white)}.bg-linear-to-t{--tw-gradient-position:to top;@supports (background-image:linear-gradient(in lab,red,red)){--tw-gradient-position:to top in oklab}background-image:linear-gradient(var(--tw-gradient-stops))}.from-background{--tw-gradient-from:var(--background);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-background{--tw-gradient-via:var(--background);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-via) var(--tw-gradient-via-position),var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.object-contain{object-fit:contain}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-10{padding-block:calc(var(--spacing)*10)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-24{padding-top:calc(var(--spacing)*24)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-9xl{font-size:var(--text-9xl);line-height:var(--tw-leading,var(--text-9xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.whitespace-nowrap{white-space:nowrap}.text-\[\#053b69\]{color:#053b69}.text-black{color:var(--color-black)}.text-blue-500{color:var(--color-blue-500)}.text-foreground{color:var(--foreground)}.text-gray-200{color:var(--color-gray-200)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:50%}.opacity-70{opacity:70%}.opacity-\[0\.15\]{opacity:.15}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,rgba(0,0,0,.1)),0 4px 6px -4px var(--tw-shadow-color,rgba(0,0,0,.1))}.shadow-lg,.shadow-sm{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,rgba(0,0,0,.1)),0 1px 2px -1px var(--tw-shadow-color,rgba(0,0,0,.1))}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,rgba(0,0,0,.05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline-hidden{--tw-outline-style:none;outline-style:none;@media (forced-colors:active){outline:2px solid transparent;outline-offset:2px}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-\[color\,box-shadow\]{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-all{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-colors{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-opacity{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-transform{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.duration-200{--tw-duration:200ms;transition-duration:.2s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-hover\:translate-x-1{&:is(:where(.group):hover *){@media (hover:hover){--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x) var(--tw-translate-y)}}}.group-hover\:text-cyan-400{&:is(:where(.group):hover *){@media (hover:hover){color:var(--color-cyan-400)}}}.group-hover\:text-green-400{&:is(:where(.group):hover *){@media (hover:hover){color:var(--color-green-400)}}}.group-hover\:text-purple-400{&:is(:where(.group):hover *){@media (hover:hover){color:var(--color-purple-400)}}}.group-data-\[disabled\=true\]\:pointer-events-none{&:is(:where(.group)[data-disabled=true] *){pointer-events:none}}.group-data-\[disabled\=true\]\:opacity-50{&:is(:where(.group)[data-disabled=true] *){opacity:50%}}.peer-disabled\:cursor-not-allowed{&:is(:where(.peer):disabled~*){cursor:not-allowed}}.peer-disabled\:opacity-50{&:is(:where(.peer):disabled~*){opacity:50%}}.file\:inline-flex{&::file-selector-button{display:inline-flex}}.file\:h-7{&::file-selector-button{height:calc(var(--spacing)*7)}}.file\:border-0{&::file-selector-button{border-style:var(--tw-border-style);border-width:0}}.file\:bg-transparent{&::file-selector-button{background-color:transparent}}.file\:text-sm{&::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.file\:font-medium{&::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}}.file\:text-foreground{&::file-selector-button{color:var(--foreground)}}.before\:absolute{&:before{content:var(--tw-content);position:absolute}}.before\:h-75{&:before{content:var(--tw-content);height:calc(var(--spacing)*75)}}.before\:w-120{&:before{content:var(--tw-content);width:calc(var(--spacing)*120)}}.before\:max-w-full{&:before{content:var(--tw-content);max-width:100%}}.before\:-translate-x-1\/2{&:before{content:var(--tw-content);--tw-translate-x:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}}.before\:rounded-full{&:before{border-radius:calc(infinity*1px);content:var(--tw-content)}}.before\:bg-linear-to-br{&:before{content:var(--tw-content);--tw-gradient-position:to bottom right;@supports (background-image:linear-gradient(in lab,red,red)){--tw-gradient-position:to bottom right in oklab}background-image:linear-gradient(var(--tw-gradient-stops))}}.before\:from-transparent{&:before{content:var(--tw-content);--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-to) var(--tw-gradient-to-position))}}.before\:opacity-10{&:before{content:var(--tw-content);opacity:10%}}.before\:blur-2xl{&:before{content:var(--tw-content);--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.before\:content-\[\'\'\]{&:before{--tw-content:"";content:var(--tw-content)}}.after\:absolute{&:after{content:var(--tw-content);position:absolute}}.after\:-z-20{&:after{content:var(--tw-content);z-index:-20}}.after\:h-45{&:after{content:var(--tw-content);height:calc(var(--spacing)*45)}}.after\:w-60{&:after{content:var(--tw-content);width:calc(var(--spacing)*60)}}.after\:max-w-full{&:after{content:var(--tw-content);max-width:100%}}.after\:translate-x-1\/3{&:after{content:var(--tw-content);--tw-translate-x:33.33333%;translate:var(--tw-translate-x) var(--tw-translate-y)}}.after\:bg-linear-to-t{&:after{content:var(--tw-content);--tw-gradient-position:to top;@supports (background-image:linear-gradient(in lab,red,red)){--tw-gradient-position:to top in oklab}background-image:linear-gradient(var(--tw-gradient-stops))}}.after\:from-cyan-300{&:after{content:var(--tw-content);--tw-gradient-from:var(--color-cyan-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-to) var(--tw-gradient-to-position))}}.after\:opacity-20{&:after{content:var(--tw-content);opacity:20%}}.after\:blur-2xl{&:after{content:var(--tw-content);--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.after\:content-\[\'\'\]{&:after{--tw-content:"";content:var(--tw-content)}}.hover\:bg-gray-200{&:hover{@media (hover:hover){background-color:var(--color-gray-200)}}}.hover\:bg-indigo-700{&:hover{@media (hover:hover){background-color:var(--color-indigo-700)}}}.hover\:underline{&:hover{@media (hover:hover){text-decoration-line:underline}}}.hover\:opacity-100{&:hover{@media (hover:hover){opacity:100%}}}.hover\:backdrop-blur-sm{&:hover{@media (hover:hover){--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}}}.focus\:ring{&:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:ring-2{&:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:ring-offset-2{&:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}}.focus\:outline-hidden{&:focus{--tw-outline-style:none;outline-style:none;@media (forced-colors:active){outline:2px solid transparent;outline-offset:2px}}}.focus\:outline-none{&:focus{--tw-outline-style:none;outline-style:none}}.focus-visible\:ring-\[3px\]{&:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.disabled\:pointer-events-none{&:disabled{pointer-events:none}}.disabled\:cursor-not-allowed{&:disabled{cursor:not-allowed}}.disabled\:opacity-50{&:disabled{opacity:50%}}.has-\[\>svg\]\:px-2\.5{&:has(>svg){padding-inline:calc(var(--spacing)*2.5)}}.has-\[\>svg\]\:px-3{&:has(>svg){padding-inline:calc(var(--spacing)*3)}}.has-\[\>svg\]\:px-4{&:has(>svg){padding-inline:calc(var(--spacing)*4)}}.data-\[disabled\=true\]\:pointer-events-none{&[data-disabled=true]{pointer-events:none}}.data-\[disabled\=true\]\:opacity-50{&[data-disabled=true]{opacity:50%}}.data-\[state\=closed\]\:duration-300{&[data-state=closed]{--tw-duration:300ms;transition-duration:.3s}}.data-\[state\=open\]\:duration-500{&[data-state=open]{--tw-duration:500ms;transition-duration:.5s}}.motion-reduce\:transform-none{@media (prefers-reduced-motion:reduce){transform:none}}.sm\:max-w-lg{@media (width >= 40rem){max-width:var(--container-lg)}}.sm\:max-w-sm{@media (width >= 40rem){max-width:var(--container-sm)}}.sm\:flex-row{@media (width >= 40rem){flex-direction:row}}.sm\:justify-end{@media (width >= 40rem){justify-content:flex-end}}.sm\:text-left{@media (width >= 40rem){text-align:left}}.sm\:text-4xl{@media (width >= 40rem){font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}}.md\:h-32{@media (width >= 48rem){height:calc(var(--spacing)*32)}}.md\:w-32{@media (width >= 48rem){width:calc(var(--spacing)*32)}}.md\:max-w-125{@media (width >= 48rem){max-width:calc(var(--spacing)*125)}}.md\:grid-cols-2{@media (width >= 48rem){grid-template-columns:repeat(2,minmax(0,1fr))}}.md\:gap-6{@media (width >= 48rem){gap:calc(var(--spacing)*6)}}.md\:px-24{@media (width >= 48rem){padding-inline:calc(var(--spacing)*24)}}.md\:py-12{@media (width >= 48rem){padding-block:calc(var(--spacing)*12)}}.md\:text-2xl{@media (width >= 48rem){font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}.md\:text-6xl{@media (width >= 48rem){font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}}.md\:text-base{@media (width >= 48rem){font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}}.md\:text-sm{@media (width >= 48rem){font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.lg\:static{@media (width >= 64rem){position:static}}.lg\:mb-0{@media (width >= 64rem){margin-bottom:calc(var(--spacing)*0)}}.lg\:flex{@media (width >= 64rem){display:flex}}.lg\:h-auto{@media (width >= 64rem){height:auto}}.lg\:w-auto{@media (width >= 64rem){width:auto}}.lg\:w-full{@media (width >= 64rem){width:100%}}.lg\:max-w-5xl{@media (width >= 64rem){max-width:var(--container-5xl)}}.lg\:grid-cols-4{@media (width >= 64rem){grid-template-columns:repeat(4,minmax(0,1fr))}}.lg\:rounded-xl{@media (width >= 64rem){border-radius:var(--radius-xl)}}.lg\:border{@media (width >= 64rem){border-bottom-width:1px;border-left-width:1px;border-right-width:1px;border-style:var(--tw-border-style);border-top-width:1px}}.lg\:bg-none{@media (width >= 64rem){background-image:none}}.lg\:p-0{@media (width >= 64rem){padding:calc(var(--spacing)*0)}}.lg\:p-4{@media (width >= 64rem){padding:calc(var(--spacing)*4)}}.lg\:pt-0{@media (width >= 64rem){padding-top:calc(var(--spacing)*0)}}.lg\:text-left{@media (width >= 64rem){text-align:left}}.dark\:border-neutral-800{@media (prefers-color-scheme:dark){border-color:var(--color-neutral-800)}}.dark\:bg-zinc-800\/30{@media (prefers-color-scheme:dark){background-color:color-mix(in srgb,oklch(27.4% .006 286.033) 30%,transparent);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-zinc-800) 30%,transparent)}}}.dark\:from-inherit{@media (prefers-color-scheme:dark){--tw-gradient-from:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from) var(--tw-gradient-from-position),var(--tw-gradient-to) var(--tw-gradient-to-position))}}.lg\:dark\:bg-zinc-800\/30{@media (width >= 64rem){@media (prefers-color-scheme:dark){background-color:color-mix(in srgb,oklch(27.4% .006 286.033) 30%,transparent);@supports (color:color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-zinc-800) 30%,transparent)}}}}.\[\&_\[data-slot\=command-group-heading\]\]\:px-2{& [data-slot=command-group-heading]{padding-inline:calc(var(--spacing)*2)}}.\[\&_\[data-slot\=command-group-heading\]\]\:py-1\.5{& [data-slot=command-group-heading]{padding-block:calc(var(--spacing)*1.5)}}.\[\&_\[data-slot\=command-group-heading\]\]\:text-xs{& [data-slot=command-group-heading]{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}.\[\&_\[data-slot\=command-group-heading\]\]\:font-medium{& [data-slot=command-group-heading]{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}}.\[\&_\[data-slot\=command-group\]\]\:px-2{& [data-slot=command-group]{padding-inline:calc(var(--spacing)*2)}}.\[\&_\[data-slot\=command-group\]\:not\(\[hidden\]\)_\~_\[data-slot\=command-group\]\]\:pt-0{& [data-slot=command-group]:not([hidden])~[data-slot=command-group]{padding-top:calc(var(--spacing)*0)}}.\[\&_\[data-slot\=command-input-wrapper\]\]\:h-12{& [data-slot=command-input-wrapper]{height:calc(var(--spacing)*12)}}.\[\&_\[data-slot\=command-input-wrapper\]_svg\]\:h-5{& [data-slot=command-input-wrapper] svg{height:calc(var(--spacing)*5)}}.\[\&_\[data-slot\=command-input-wrapper\]_svg\]\:w-5{& [data-slot=command-input-wrapper] svg{width:calc(var(--spacing)*5)}}.\[\&_\[data-slot\=command-input\]\]\:h-12{& [data-slot=command-input]{height:calc(var(--spacing)*12)}}.\[\&_\[data-slot\=command-item\]\]\:px-2{& [data-slot=command-item]{padding-inline:calc(var(--spacing)*2)}}.\[\&_\[data-slot\=command-item\]\]\:py-3{& [data-slot=command-item]{padding-block:calc(var(--spacing)*3)}}.\[\&_\[data-slot\=command-item\]_svg\]\:h-5{& [data-slot=command-item] svg{height:calc(var(--spacing)*5)}}.\[\&_\[data-slot\=command-item\]_svg\]\:w-5{& [data-slot=command-item] svg{width:calc(var(--spacing)*5)}}.\[\&_svg\]\:pointer-events-none{& svg{pointer-events:none}}.\[\&_svg\]\:shrink-0{& svg{flex-shrink:0}}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4{& svg:not([class*=size-]){height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}}}:root{--background:#fff;--foreground:#171717}@media (prefers-color-scheme:dark){:root{--background:#0a0a0a;--foreground:#ededed}}body{background:var(--background);color:var(--foreground);font-family:Arial,Helvetica,sans-serif}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";initial-value:"";inherits:false}@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}