create-caspian-app 1.0.13 → 1.0.15
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.
- package/dist/.github/copilot-instructions.md +4 -4
- package/dist/AGENTS.md +3 -3
- package/dist/index.js +1 -1
- package/dist/main.py +29 -54
- package/dist/public/js/main.js +1 -1
- package/dist/public/js/{pp-reactive-v2.js → pp-reactive-v2.min.js} +1 -1
- package/package.json +1 -1
- package/dist/CLAUDE.md +0 -1
|
@@ -87,7 +87,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
|
|
|
87
87
|
- When runtime uploads write into `public/uploads/**`, keep the public-root-relative entry `uploads` in `settings/bs-config.ts` `PUBLIC_IGNORE_DIRS` so `npm run dev` does not reload on each upload.
|
|
88
88
|
- For logout flows, prefer `pp.rpc("signout")` backed by `@rpc(require_auth=True)` from page-level or component-level UI. Use a dedicated signout route only for plain form POST, no-JavaScript fallback, or other full-navigation edge cases.
|
|
89
89
|
- Protect customized `src/lib/auth/auth_config.py` from updater overwrite by adding `./src/lib/auth/auth_config.py` to `excludeFiles` in `caspian.config.json`.
|
|
90
|
-
- Treat `pp-component` on routes, layouts, and components
|
|
90
|
+
- Treat `pp-component` on routes, layouts, and components as compiler-injected by the Python side; do not add it manually in authored templates unless the task is explicitly about runtime internals. Author owned PulsePoint logic as a plain `<script>` inside the component root.
|
|
91
91
|
- `layout()` can be synchronous or async in the installed runtime. Keep async layout work focused on shared layout props or metadata; use `page()` or `@rpc()` when the work belongs to a specific route or user action.
|
|
92
92
|
- Dynamic route params currently reach `page()` as a single positional `dict`, with query params injected by name and `request` injected by keyword when declared.
|
|
93
93
|
- In `layout.py`, return a dict for standard `{{ layout.* }}` props. Use `render_layout(__file__, {...})` only when that layout should consume direct local variables such as `{{ my_class }}` instead of `{{ layout.my_class }}`.
|
|
@@ -106,7 +106,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
|
|
|
106
106
|
### `main.py`
|
|
107
107
|
|
|
108
108
|
- Treat `main.py` as the repo source of truth for FastAPI setup, auth bootstrap, middleware wiring, route registration, cache defaults, and error handlers.
|
|
109
|
-
- `main.py` finalizes every rendered page through `
|
|
109
|
+
- `main.py` finalizes every rendered page through `defer_component_roots(...)`. It wraps each outermost `pp-component` root in an inert `<template pp-component>` so the browser never parses raw `{...}` placeholders as live DOM. Before PulsePoint materializes those roots, it captures and empties their plain component scripts so native browser execution cannot race component-scope evaluation; later morph insertions use the same protection. Because of this deferral, `{...}` is safe in any attribute or position (SVG `d`/`viewBox`/`points`, `src`/`href`, form `value`/date/number/color, table/select text). Do not add per-tag workarounds to dodge browser first-paint validation (static-path `hidden` toggles, `data-*` URL holders, `hidden`-gated `<img src>`, or SSR-resolved initial values). Keep `pp-style` (source-file tooling) and the controlled form-field `value`/`checked`/`defaultvalue`/`<textarea>` rewrites (attribute-vs-property correctness); those exist for reasons deferral does not replace.
|
|
110
110
|
- Treat `main.py` as the source of truth for app-owned WebSocket endpoints, origin validation, idle timeouts, maximum socket message size, JSON message handling, close codes, and broadcast-channel wiring.
|
|
111
111
|
- Treat `main.py` plus imported package-owned helpers such as `casp.runtime_security` as the runtime source of truth for response-header hardening and public-file behavior.
|
|
112
112
|
- Preserve the production effective middleware execution order unless the task explicitly changes request semantics: `SecurityHeadersMiddleware -> PublicFilesMiddleware -> RateLimitMiddleware -> BodySizeLimitMiddleware -> SessionMiddleware -> CSRFMiddleware -> AuthMiddleware -> RPCMiddleware -> route`. In development, `RequestDiagnosticsMiddleware` is outermost. Existing public-file `GET`/`HEAD` requests stop at `PublicFilesMiddleware`; missing paths fall through the remaining stack.
|
|
@@ -179,7 +179,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
|
|
|
179
179
|
- Treat `public/js/pp-reactive-v2.js` as the browser-side PulsePoint runtime source of truth for component execution, hooks, refs, directives, SPA navigation, and `pp.rpc(...)` behavior.
|
|
180
180
|
- Only the built, minified runtime ships to the application. Do not document, reference, or route AI to a TypeScript authoring tree as if the application consumed it.
|
|
181
181
|
- Preserve the current public runtime contract unless the task explicitly changes Caspian frontend behavior.
|
|
182
|
-
- At runtime, component logic is discovered from
|
|
182
|
+
- At runtime, component logic is discovered from a plain, untyped `<script>` inside each `pp-component` root. PulsePoint captures the source before materialization or morph insertion, prevents native execution, and evaluates it in component scope.
|
|
183
183
|
- The current SPA scroll contract is: save scroll positions per history entry, reset window scroll on push navigation, and use `pp-reset-scroll="true"` to opt specific containers into reset behavior. Use `body[pp-reset-scroll="true"]` only when a target route should reset every scrollable surface.
|
|
184
184
|
|
|
185
185
|
### `src/app/**/*.html`
|
|
@@ -194,7 +194,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
|
|
|
194
194
|
- For simple route-level form submissions, collect the submitted fields with `Object.fromEntries(new FormData(event.currentTarget).entries())` inside the `onsubmit` handler and pass that object directly to `pp.rpc(...)`. Use `pp.state(...)` for pending/error/success UI and controlled non-native widgets; use `pp-ref` only when the handler needs imperative element access such as focus, measurement, file input reset, or third-party integration.
|
|
195
195
|
- Preserve standard Jinja template syntax such as `{{ ... }}` in layouts and `pp-*` runtime attributes in rendered HTML.
|
|
196
196
|
- Do not author `pp-component="..."` manually in route or layout templates; the Python render pipeline injects it onto the single root element.
|
|
197
|
-
-
|
|
197
|
+
- Use a plain `<script>` inside the single route or layout root when it owns PulsePoint logic; no custom script type is required.
|
|
198
198
|
- Keep authored route and layout templates to exactly one top-level parent node, the same constraint used for component templates. In source, that parent may be a native HTML element or a single imported `x-*` component tag. If a script is needed, keep it inside that parent instead of as a sibling top-level node. AI must follow this the same way React components return one parent node, otherwise Caspian raises `must have exactly one top-level HTML element so Caspian can inject pp-component`.
|
|
199
199
|
- For dashboard, admin, or grouped sections with multiple child routes, prefer folder-level `layout.html` wrappers in `src/app/**` instead of repeating the same shell in each child route.
|
|
200
200
|
- For grouped shells with independent sidebar and content scrolling, mark the content pane with `pp-reset-scroll="true"` when that pane should start at the top on each child-route navigation. Do not put the attribute on the whole shell when the sidebar or rail should retain its own scroll.
|
package/dist/AGENTS.md
CHANGED
|
@@ -76,7 +76,7 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
|
|
|
76
76
|
- Component-first page composition is the highest-priority authoring rule for this workspace (see `.github/copilot-instructions.md`). Build pages as a short assembly of `x-*` chunk components (top menu, sidebar, header, content sections, cards, forms, footer) and keep each chunk's long markup inside its own focused single-file `html(...)` component, so `src/app/**/index.html` stays small instead of holding a wall of HTML. Plan the chunk breakdown before writing the route, not as a later cleanup pass.
|
|
77
77
|
- **PulsePoint is not React and its templates are not JSX.** This workspace's guidance compares PulsePoint to React in exactly two places — the `pp.*` hook API inside `<script>`, and how components are split by responsibility — and that comparison stops at the markup. Template files are plain HTML. Never generate `{cond && (<div/>)}`, `{cond ? <A/> : <B/>}`, `{list.map(item => (<tr/>))}`, `className`, `htmlFor`, camelCase `onClick`, `style={{...}}`, `dangerouslySetInnerHTML`, or `<>…</>`. Use `hidden="{!cond}"` for conditionals, `<template pp-for="item in list">` with `key="{item.id}"` for lists, and **always quote brace attributes** — `class="{...}"`, never `class={...}`. The unquoted form is invalid HTML: the parser splits the value on spaces into junk attributes, the component root never compiles, and the route serves a blank page with no console error (the body's `opacity: 0` reveal never fires). There is no `pp-if`, `pp-show`, `pp-else`, or `pp-key`. Sanity check before finishing any template: it must still be valid HTML with every `{}` deleted. See `node_modules/caspian-utils/dist/docs/pulsepoint.md` sections "PulsePoint Is Not JSX", "Complete Directive And API Surface", and "Conditional rendering".
|
|
78
78
|
- Split single-file Python components by responsibility, using the same mental model as React components — **for decomposition and single-root shape only, never for syntax** (see the rule above). A page with tabs should usually have one component for the tab shell and separate components for each substantial tab panel. A section with its own form, table, toolbar, or list should usually be its own component with data and options passed by props, not an unrelated block inside a giant Python file.
|
|
79
|
-
- Components may be authored as a single Python file. Import `html` from `casp.component_decorator` and return `html("""...""", **context)` to keep markup, server interpolation, and a PulsePoint `<script>` inline, instead of pairing the `.py` with a same-name `.html` through `render_html(...)`. Inside `html(...)`, `{{ ... }}` is server-side Jinja and `{ ... }` stays for PulsePoint; do not use a Python f-string for the markup. Prefer single-file `html(...)` for small and medium components and keep `render_html(...)` plus a `.html` file for large markup or long scripts. Both forms render identically through
|
|
79
|
+
- Components may be authored as a single Python file. Import `html` from `casp.component_decorator` and return `html("""...""", **context)` to keep markup, server interpolation, and a PulsePoint `<script>` inline, instead of pairing the `.py` with a same-name `.html` through `render_html(...)`. Inside `html(...)`, `{{ ... }}` is server-side Jinja and `{ ... }` stays for PulsePoint; do not use a Python f-string for the markup. Prefer single-file `html(...)` for small and medium components and keep `render_html(...)` plus a `.html` file for large markup or long scripts. Both forms render identically through component transformation and final component-root deferral. See `node_modules/caspian-utils/dist/docs/components.md`.
|
|
80
80
|
- In a prop-receiving single-file Python component, `x-*` attributes arrive as raw string kwargs (including unevaluated strings such as `"{permOpen}"`) and do not become browser `pp.props` automatically. Forward every browser-facing prop onto the single native root with `get_attributes({...}, props)`, render `<root {{ attributes }}>`, and pass `attributes=attributes` into `html(...)`. Props accepted by Python but not re-emitted are silently absent from `pp.props`; no server error or browser warning is raised. Remember that forwarded names are real DOM attributes, so avoid unintended native collisions such as `title` when a component-specific name like `user-name` is appropriate. A named Python parameter is consumed out of `**props`, so it is no longer in the passthrough dict and must be listed explicitly in the `get_attributes` defaults. Forwarding also does not preserve types: a brace expression (`volume="{vol}"`) is evaluated in parent scope and keeps its real type, but a literal server value renders as a string, so `volume="0"` makes `volume === 0` false; a valueless attribute becomes `true`; `None`/`False`/`""` are omitted entirely so the prop reads `undefined` rather than `false`; and JS reserved words such as `class` are dropped from `pp.props`. When an icon toggle, `hidden`, or class binding silently does nothing, verify the prop is on the rendered root before debugging the expression. See `node_modules/caspian-utils/dist/docs/components.md` "Receiving Props In A Python Component" and "Every Prop A Template Reads Must Be Forwarded To The Root."
|
|
81
81
|
- For component-to-component composition, use real Python imports inside single-file `html(...)` components instead of placing `<!-- @import ... -->` inside the returned HTML string. A component's own `x-*` tags resolve from the components imported into its Python module, which disambiguates same-name components across directories. Runtime resolution precedence inside a component's output is inherited ancestor components, then the component's own Python imports, then a local `@import` in that same template, but the authoring pattern for single-file components is Python imports. Slot content (children) resolves in the scope where it was authored, so the component that writes an `x-*` tag in markup must import that component.
|
|
82
82
|
- For first-party HTML interactivity in this workspace, PulsePoint is the required default. Use PulsePoint `on*` event attributes, `pp.state`, refs, effects, directives, and `pp.rpc()` instead of inventing id/data-attribute driven JavaScript with `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or parallel client state. For simple forms, bind `onsubmit` in the HTML, convert named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`, and validate/normalize that payload in Python; do not add `pp-ref` to each input, create a form ref, and attach an effect-managed submit listener just to collect submitted values.
|
|
@@ -98,7 +98,7 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
|
|
|
98
98
|
- Before updating docs, verify runtime-specific claims such as middleware order, route param injection, `layout()` behavior, `StateManager` persistence, safe public-file serving, response header, or session-secret behavior against the current `main.py` and installed `casp` package, especially `.venv/Lib/site-packages/casp/runtime_security.py`, rather than copying older notes.
|
|
99
99
|
- When generating or reviewing `src/app/**/index.html`, `src/app/**/layout.html`, or component HTML templates, treat the single-root rule as a hard requirement: exactly one authored top-level parent element or one imported `x-*` root, with any owned `<script>` kept inside that same root. Do not allow sibling top-level tags, sibling scripts, or stray top-level text, because Caspian injects `pp-component` on that final root and errors if it cannot.
|
|
100
100
|
- When generating or reviewing sign-in flows, do not ask the sign-in page to decide redirect targets by re-implementing `next` support or post-login routing. In this stack, redirect behavior is already owned by the Caspian auth runtime plus `src/lib/auth/auth_config.py`; protected-route guest redirects, auth-route redirects, and the default destination are centralized there, with `default_signin_redirect` defaulting to `/dashboard`.
|
|
101
|
-
- Component markup is server-deferred in an inert `<template>`. `main.py` finalizes every page through `
|
|
101
|
+
- Component markup is server-deferred in an inert `<template>`. `main.py` finalizes every page through `defer_component_roots(...)`, which wraps each outermost `pp-component` root in `<template pp-component="…">`. The browser never parses/validates/fetches `<template>` contents, so raw `{...}` placeholders never reach live DOM at first paint. During `mount()`, PulsePoint captures and empties each plain component `<script>` before materializing `template[pp-component]` into live DOM, then evaluates that captured source in component scope; the same guard applies to scripts introduced by later morphs. Because of this, `{...}` is safe in ANY attribute or position — SVG geometry (`d`, `viewBox`, `points`, `transform`), URL attributes (`src`, `srcset`, `href`, `poster`), form `value`/date/number/color, and text placed directly inside `<table>`/`<select>`. Do NOT add per-tag workarounds to dodge browser first-paint validation: no static-path `hidden` toggles just to avoid binding `d`, no `data-*` URL holders, no gating `<img src>` behind `hidden`, and no SSR-resolving an initial value only to prevent a validation flash. Two compiler transforms still apply for different reasons and stay: `pp-style` (so `.html` source-file HTML/CSS tooling does not choke on `style="{...}"`) and the `<input>`/`<select>`/`checked`/`defaultvalue`/`<textarea>` value rewrites (attribute-vs-property correctness for controlled form fields), not first-paint validation.
|
|
102
102
|
- This workspace has an app-level quality gate for its own Python (`main.py`, `src/**`, `settings/*.py`), added on top of Caspian — the framework itself ships no test runner. One command, `npm run check` (which calls `uv run python settings/check.py`), runs `pyright` (types), `ruff` (lint), and `pytest` (tests) in a single pass and prints each problem as `path:line:col [tool:code] message`, exiting non-zero on failure. Running it is mandatory: after you create, edit, or delete app-owned Python — bug fix, new file, refactor, or feature — run it and get it fully green before treating the change as done, and do not report work as finished on the assumption that it passes. Fix every reported location and re-run until clean. The gate runs four tools: `pyright`, `ruff`, `templates`, and `pytest`.
|
|
103
103
|
- **`templates`** (`settings/check_templates.py`) lints authored markup — `src/**/*.html` plus the triple-quoted markup inside single-file Python components — for JSX and non-existent directives, and **fails the gate** on a hit. It exists because JSX kept reaching routes: `{users.map(user => (<tr/>))}` renders one literal row, and an unquoted `class={...}` is invalid HTML that blanks the entire page with no console error. Rules: `jsx-map`, `jsx-logical`, `jsx-ternary-element`, `unquoted-brace-attr`, `react-attribute`, `camelcase-event`, `jsx-fragment`, `style-object`, `unknown-directive` (`pp-if`/`pp-show`/`pp-else`/`pp-key`/…), `pp-for-placement` (`pp-for` outside `<template>`). `<script>`, `<pre>`/`<code>`, and HTML comments are excluded, so real component JavaScript and docs samples never trip it. Coverage is in `tests/test_check_templates.py`, including a repo-wide clean assertion. Run it alone with `uv run python settings/check_templates.py`.
|
|
104
104
|
- Its boundary: it does not validate Tailwind/`globals.css`, `x-*` tag resolution, single-root violations, or `public/js/**`. Those still surface only at render time — verify front-end changes by loading the affected route in the browser (BrowserSync URL from `./settings/bs-config.json`).
|
|
@@ -136,7 +136,7 @@ If the task generates or edits route, layout, or component HTML templates, check
|
|
|
136
136
|
- File conventions and special route files: read `node_modules/caspian-utils/dist/docs/file-conventions.md` and `node_modules/caspian-utils/dist/docs/routing.md`. Verify against `main.py`, `.venv/Lib/site-packages/casp/layout.py`, `.venv/Lib/site-packages/casp/loading.py`, and `.venv/Lib/site-packages/casp/caspian_config.py`.
|
|
137
137
|
- Feature availability and tooling switches: read `caspian.config.json`. Verify against the current workspace tree, `main.py`, `prisma/**`, and `public/js/**`.
|
|
138
138
|
- Framework internals and core-file lookup: read `node_modules/caspian-utils/dist/docs/core-runtime-map.md`. Verify against `main.py`, `.venv/Lib/site-packages/casp/**`, and the matching feature docs.
|
|
139
|
-
- PulsePoint browser runtime lookup: read `node_modules/caspian-utils/dist/docs/pulsepoint-runtime-map.md` and `node_modules/caspian-utils/dist/docs/pulsepoint.md`. Verify against `public/js/pp-reactive-v2.js`, `main.py`,
|
|
139
|
+
- PulsePoint browser runtime lookup: read `node_modules/caspian-utils/dist/docs/pulsepoint-runtime-map.md` and `node_modules/caspian-utils/dist/docs/pulsepoint.md`. Verify against `public/js/pp-reactive-v2.js`, `main.py`, and `.venv/Lib/site-packages/casp/components_compiler.py`.
|
|
140
140
|
- Library-specific and task-specific rules: read the matching `.github/instructions/**/*.instructions.md` file. Verify against `caspian.config.json`, the current workspace tree, and the owning app and lib files.
|
|
141
141
|
- MCP server layout and launch flow: read `node_modules/caspian-utils/dist/docs/mcp.md`. Verify against `settings/restart-mcp.ts`, `package.json`, and `src/lib/mcp/**`.
|
|
142
142
|
- Routing, layouts, metadata: read `node_modules/caspian-utils/dist/docs/routing.md`. Verify against `main.py` and `.venv/Lib/site-packages/casp/layout.py`.
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
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";let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.html","not-found.html","error.html"],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.html","src/app/index.html"]},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.html","src/app/index.html","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 c=i.replace(/(?<!:)(\/\/+)/g,"/"),o=s.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${o.startsWith("/")?o.substring(1):o}/`}}}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",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 c={...s.scripts};c.browserSync="tsx settings/bs-config.ts",c.dev=`npm-run-all projectName -l -p browserSync ${i.join(" ")}`;let o=["projectName"];n.tailwindcss&&o.unshift("tailwind:build"),n.typescript&&!n.backendOnly&&o.unshift("ts:build"),c.build=`npm-run-all ${o.join(" ")}`,s.scripts=c,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=generateHexEncodedKey(32),c=[];return e.prisma&&c.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')),c.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"'),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="),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&&c.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&&c.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="${i}"`)),c.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"'),buildEnvSection("13. DEVELOPMENT TOOLING\n# Enforced by: main.py cookie scoping, casp/caspian_config.py","# Overrides the dev-only suffix appended to the session and CSRF cookie names,\n# which stops apps on different localhost ports from sharing a session.\n# Resolved from this value, then settings/bs-config.json. Ignored in production.\nCASPIAN_BROWSER_SYNC_PORT=\n\n# Overrides the detected project root used to resolve app paths. Empty uses the\n# working directory. Set only when launching from outside the project folder.\nCASPIAN_ROOT="),buildEnvSection("14. RESERVED - NOT READ BY ANY CODE\n#\n# Kept so the names stay reserved, but nothing reads them today. Changing any\n# of these has no effect. Verified against casp/**, main.py, src/**, settings/**.",`# NOT IMPLEMENTED. Setting this false does not hide anything. Error detail is\n# controlled by APP_ENV alone: client_error_message() returns the exception text\n# in development and a generic message in production, and main.py sends a\n# traceback to error.html only outside production. Change APP_ENV, not this.\nSHOW_ERRORS="true"\n\n# NOT IMPLEMENTED. RPC calls are not signed with this. Actual RPC protection is\n# the session-backed CSRF token (compared with hmac.compare_digest) plus origin\n# validation, both keyed off AUTH_SECRET. Rotating this changes nothing.\nFUNCTION_CALL_SECRET="${s}"\n\n# NOT IMPLEMENTED. No runtime file reads it; date/time helpers use the system\n# timezone. Inert unless your own code calls os.getenv("APP_TIMEZONE").\nAPP_TIMEZONE="UTC"`)),c.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.html"))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),c=resolveTemplateSourcePath(n,"directory"),o=path.join(e,s);if(!c){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(c,o,t)})}function modifyLayoutPHP(e,n){const t=path.join(e,"src","app","layout.html");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.html:"),e)}}async function createOrUpdateEnvFile(e,n){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,n,{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.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.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 c=e.endsWith("\n");return`${e}${c?"\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:"/CLAUDE.md",dest:"/CLAUDE.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"),c=path.join(e,t);if(checkExcludeFiles(c))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(c))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const o=fs.readFileSync(i,"utf8");if("/CLAUDE.md"===n){const e=o.replace(/^\uFEFF/,"");if(!e.startsWith("@AGENTS.md")){const n=`@AGENTS.md\n\n${e.replace(/^\s+/,"")}`;return void fs.writeFileSync(c,n,{flag:"w"})}}if("/AGENTS.md"===n&&updateAnswer?.isUpdate&&fs.existsSync(c)){const e=mergeAgentsCaspianSection(fs.readFileSync(c,"utf8"),o);return void fs.writeFileSync(c,e,{flag:"w"})}fs.writeFileSync(c,o,{flag:"w"})}),await executeCopy(e,s,n),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(n)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,websocket:e.websocket??!1,prisma:e.prisma??!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}}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}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.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||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.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||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"}));const o=await prompts(c,{onCancel:s});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:o.tailwindcss??e.tailwindcss??!1,typescript:o.typescript??e.typescript??!1,mcp:o.mcp??e.mcp??!1,websocket:o.websocket??e.websocket??!1,prisma:o.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),c=s.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>c[e])return 1;if(i[e]<c[e])return-1}const o=t[4]??null,a=s[4]??null;return o&&!a?-1:!o&&a?1:o&&a?o.localeCompare(a):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)}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.1.1","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"6.0.0","chokidar-cli":"3.0.0",cssnano:"8.0.2","npm-run-all":"4.1.5",postcss:"8.5.23","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.3.3",tsx:"4.23.1",typescript:"7.0.2",vite:"8.1.5",vitest:"4.1.10","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,c=bsConfigUrls(s);t.projectName=n.projectName,t.projectRootPath=s,t.bsTarget=c.bsTarget,t.bsPathRewrite=c.bsPathRewrite;const o=await fetchPackageVersion("create-caspian-app");t.version=t.version||o,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.140.0","uvicorn==0.51.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.15.0","slowapi==0.1.10","python-multipart==0.0.32","starsessions==2.2.1","httpx2==2.9.1","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==3.4.4"),e.websocket&&n.push("websockets==16.1.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.15.22","pytest==9.1.1"]}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 c;for(;null!==(c=i.exec(t[1]));){const e=c[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),c=path.join(e,".venv");fs.existsSync(c)?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 o=buildPythonDependencies(n),a=buildPythonDevDependencies(),r=o.map(e=>getPythonRequirementName(e)).filter(e=>null!==e),l=a.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,...o],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,...a],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],c=e.find(e=>e.startsWith("--starter-kit-source=")),o=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let a=null,r=!1;if(t){const s=process.cwd(),c=path.join(s,"caspian.config.json");if(i&&o){r=!0;const s={projectName:t,starterKit:i,starterKitSource:o,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")};a=await getAnswer(s,n)}else if(fs.existsSync(c)){const i=readJsonFile(c);let o=[];i.excludeFiles?.map(e=>{const n=path.join(s,e);fs.existsSync(n)&&o.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:o??[],filePath:s};const r={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)};a=await getAnswer(r,n),null!==a&&(updateAnswer={projectName:t,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,websocket:a.websocket,prisma:a.prisma,typescript:a.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:o??[],filePath:s})}else{const s={projectName:t,starterKit:i,starterKitSource:o,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")};a=await getAnswer(s,n)}if(null===a)return void console.log(chalk.red("Installation cancelled."))}else a=await getAnswer({},n);if(null===a)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync(buildManagedNpmCommand(["uninstall","-g","create-caspian-app"]),{stdio:"inherit"}),execSync(buildManagedNpmCommand(["install","-g","create-caspian-app"]),{stdio:"inherit"})):execSync(buildManagedNpmCommand(["install","-g","create-caspian-app"]),{stdio:"inherit"});const d=process.cwd();let u;if(t)if(r){const n=path.join(d,t);fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,await setupStarterKit(u,a),process.chdir(u);const s=path.join(u,"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),a={...a,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(u,e);fs.existsSync(n)&&t.push(n.replace(/\\/g,"/"))}),updateAnswer={...a,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),n=path.join(d,t),s=path.join(n,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(n)&&fs.existsSync(s)?(u=n,process.chdir(n)):(fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,process.chdir(n))}else fs.mkdirSync(a.projectName,{recursive:!0}),u=path.join(d,a.projectName),process.chdir(a.projectName);let m=[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")];a.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),a.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),a.prisma&&execSync(buildManagedNpmCommand(["install","-g","prisma-client-python@latest"]),{stdio:"inherit"}),a.typescript&&!a.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),a.typescript&&m.push(npmPkg("vitest")),a.starterKit&&!r&&await setupStarterKit(u,a),await installNpmDependencies(u,m,!0);let h=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,a),a.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],n=[],t=e=>{try{const n=path.join(u,"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(u,"src","app",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const n=path.join(u,"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(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"public","js","tailwind-merge.mjs");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${s} was deleted successfully.`));const i=path.join(u,"public","js","bundle-mjs.mjs.map");fs.existsSync(i)&&(fs.unlinkSync(i),console.log(`${i} was deleted successfully.`));const c=path.join(u,"ts","tailwind-merge.ts");fs.existsSync(c)&&(fs.unlinkSync(c),console.log(`${c} was deleted successfully.`));["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano","tailwind-merge"].forEach(n=>{t(n)&&e.push(n)}),n.push("tailwind-merge")}if(a.tailwindcss){const e=path.join(u,"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(u,"settings",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"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(u,"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(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const n=path.join(u,"ts");fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const s=path.join(u,"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(u,i,!0));const c=s(n),o=getPyProjectDependencyNames(u);h=c.filter(e=>o.has(e.toLowerCase())),h.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${h.join(", ")}`))}if(!r||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),n=bsConfigUrls(e),t={projectName:a.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,websocket:a.websocket,prisma:a.prisma,typescript:a.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(t,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,a,h),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
|
|
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";let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.html","not-found.html","error.html"],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.html","src/app/index.html"]},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.html","src/app/index.html","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 c=i.replace(/(?<!:)(\/\/+)/g,"/"),o=s.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${o.startsWith("/")?o.substring(1):o}/`}}}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",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 c={...s.scripts};c.browserSync="tsx settings/bs-config.ts",c.dev=`npm-run-all projectName -l -p browserSync ${i.join(" ")}`;let o=["projectName"];n.tailwindcss&&o.unshift("tailwind:build"),n.typescript&&!n.backendOnly&&o.unshift("ts:build"),c.build=`npm-run-all ${o.join(" ")}`,s.scripts=c,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=generateHexEncodedKey(32),c=[];return e.prisma&&c.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')),c.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"'),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="),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&&c.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&&c.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="${i}"`)),c.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"'),buildEnvSection("13. DEVELOPMENT TOOLING\n# Enforced by: main.py cookie scoping, casp/caspian_config.py","# Overrides the dev-only suffix appended to the session and CSRF cookie names,\n# which stops apps on different localhost ports from sharing a session.\n# Resolved from this value, then settings/bs-config.json. Ignored in production.\nCASPIAN_BROWSER_SYNC_PORT=\n\n# Overrides the detected project root used to resolve app paths. Empty uses the\n# working directory. Set only when launching from outside the project folder.\nCASPIAN_ROOT="),buildEnvSection("14. RESERVED - NOT READ BY ANY CODE\n#\n# Kept so the names stay reserved, but nothing reads them today. Changing any\n# of these has no effect. Verified against casp/**, main.py, src/**, settings/**.",`# NOT IMPLEMENTED. Setting this false does not hide anything. Error detail is\n# controlled by APP_ENV alone: client_error_message() returns the exception text\n# in development and a generic message in production, and main.py sends a\n# traceback to error.html only outside production. Change APP_ENV, not this.\nSHOW_ERRORS="true"\n\n# NOT IMPLEMENTED. RPC calls are not signed with this. Actual RPC protection is\n# the session-backed CSRF token (compared with hmac.compare_digest) plus origin\n# validation, both keyed off AUTH_SECRET. Rotating this changes nothing.\nFUNCTION_CALL_SECRET="${s}"\n\n# NOT IMPLEMENTED. No runtime file reads it; date/time helpers use the system\n# timezone. Inert unless your own code calls os.getenv("APP_TIMEZONE").\nAPP_TIMEZONE="UTC"`)),c.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.html"))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),c=resolveTemplateSourcePath(n,"directory"),o=path.join(e,s);if(!c){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(c,o,t)})}function modifyLayoutPHP(e,n){const t=path.join(e,"src","app","layout.html");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.html:"),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 c=e.endsWith("\n");return`${e}${c?"\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"),c=path.join(e,t);if(checkExcludeFiles(c))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(c))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const o=fs.readFileSync(i,"utf8");if("/AGENTS.md"===n&&updateAnswer?.isUpdate&&fs.existsSync(c)){const e=mergeAgentsCaspianSection(fs.readFileSync(c,"utf8"),o);return void fs.writeFileSync(c,e,{flag:"w"})}fs.writeFileSync(c,o,{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(n)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,websocket:e.websocket??!1,prisma:e.prisma??!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}}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}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.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||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.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||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"}));const o=await prompts(c,{onCancel:s});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:o.tailwindcss??e.tailwindcss??!1,typescript:o.typescript??e.typescript??!1,mcp:o.mcp??e.mcp??!1,websocket:o.websocket??e.websocket??!1,prisma:o.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),c=s.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>c[e])return 1;if(i[e]<c[e])return-1}const o=t[4]??null,a=s[4]??null;return o&&!a?-1:!o&&a?1:o&&a?o.localeCompare(a):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)}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.1.2","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"6.0.0","chokidar-cli":"3.0.0",cssnano:"8.0.2","npm-run-all":"4.1.5",postcss:"8.5.24","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.3.3",tsx:"4.23.1",typescript:"7.0.2",vite:"8.1.5",vitest:"4.1.10","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,c=bsConfigUrls(s);t.projectName=n.projectName,t.projectRootPath=s,t.bsTarget=c.bsTarget,t.bsPathRewrite=c.bsPathRewrite;const o=await fetchPackageVersion("create-caspian-app");t.version=t.version||o,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.140.13","uvicorn==0.51.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.15.0","slowapi==0.1.10","python-multipart==0.0.32","starsessions==2.2.1","httpx2==2.9.1","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==3.4.5"),e.websocket&&n.push("websockets==16.1.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==16.0.0","pytest==9.1.1"]}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 c;for(;null!==(c=i.exec(t[1]));){const e=c[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),c=path.join(e,".venv");fs.existsSync(c)?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 o=buildPythonDependencies(n),a=buildPythonDevDependencies(),r=o.map(e=>getPythonRequirementName(e)).filter(e=>null!==e),l=a.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,...o],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,...a],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],c=e.find(e=>e.startsWith("--starter-kit-source=")),o=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let a=null,r=!1;if(t){const s=process.cwd(),c=path.join(s,"caspian.config.json");if(i&&o){r=!0;const s={projectName:t,starterKit:i,starterKitSource:o,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")};a=await getAnswer(s,n)}else if(fs.existsSync(c)){const i=readJsonFile(c);let o=[];i.excludeFiles?.map(e=>{const n=path.join(s,e);fs.existsSync(n)&&o.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:o??[],filePath:s};const r={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)};a=await getAnswer(r,n),null!==a&&(updateAnswer={projectName:t,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,websocket:a.websocket,prisma:a.prisma,typescript:a.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:o??[],filePath:s})}else{const s={projectName:t,starterKit:i,starterKitSource:o,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")};a=await getAnswer(s,n)}if(null===a)return void console.log(chalk.red("Installation cancelled."))}else a=await getAnswer({},n);if(null===a)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync(buildManagedNpmCommand(["uninstall","-g","create-caspian-app"]),{stdio:"inherit"}),execSync(buildManagedNpmCommand(["install","-g","create-caspian-app"]),{stdio:"inherit"})):execSync(buildManagedNpmCommand(["install","-g","create-caspian-app"]),{stdio:"inherit"});const d=process.cwd();let u;if(t)if(r){const n=path.join(d,t);fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,await setupStarterKit(u,a),process.chdir(u);const s=path.join(u,"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),a={...a,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(u,e);fs.existsSync(n)&&t.push(n.replace(/\\/g,"/"))}),updateAnswer={...a,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),n=path.join(d,t),s=path.join(n,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(n)&&fs.existsSync(s)?(u=n,process.chdir(n)):(fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,process.chdir(n))}else fs.mkdirSync(a.projectName,{recursive:!0}),u=path.join(d,a.projectName),process.chdir(a.projectName);let m=[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")];a.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),a.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),a.prisma&&execSync(buildManagedNpmCommand(["install","-g","prisma-client-python@latest"]),{stdio:"inherit"}),a.typescript&&!a.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),a.typescript&&m.push(npmPkg("vitest")),a.starterKit&&!r&&await setupStarterKit(u,a),await installNpmDependencies(u,m,!0);let h=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,a),a.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],n=[],t=e=>{try{const n=path.join(u,"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(u,"src","app",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const n=path.join(u,"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(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"public","js","tailwind-merge.mjs");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${s} was deleted successfully.`));const i=path.join(u,"public","js","bundle-mjs.mjs.map");fs.existsSync(i)&&(fs.unlinkSync(i),console.log(`${i} was deleted successfully.`));const c=path.join(u,"ts","tailwind-merge.ts");fs.existsSync(c)&&(fs.unlinkSync(c),console.log(`${c} was deleted successfully.`));["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano","tailwind-merge"].forEach(n=>{t(n)&&e.push(n)}),n.push("tailwind-merge")}if(a.tailwindcss){const e=path.join(u,"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(u,"settings",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"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(u,"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(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const n=path.join(u,"ts");fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const s=path.join(u,"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(u,i,!0));const c=s(n),o=getPyProjectDependencyNames(u);h=c.filter(e=>o.has(e.toLowerCase())),h.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${h.join(", ")}`))}if(!r||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),n=bsConfigUrls(e),t={projectName:a.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,websocket:a.websocket,prisma:a.prisma,typescript:a.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(t,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,a,h),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
|
package/dist/main.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from casp.components_compiler import transform_components
|
|
2
|
-
from casp.scripts_type import transform_scripts
|
|
3
2
|
from casp.html_native import (
|
|
4
3
|
mask_escaped_brace_entities,
|
|
5
4
|
parse_fragment,
|
|
@@ -1244,20 +1243,13 @@ def _defer_component_roots_in_soup(
|
|
|
1244
1243
|
soup,
|
|
1245
1244
|
placeholders,
|
|
1246
1245
|
fallback_html: str,
|
|
1247
|
-
soup_is_dirty: bool = False,
|
|
1248
1246
|
) -> str:
|
|
1249
1247
|
"""Shared body of :func:`defer_component_roots`, operating on a parsed soup.
|
|
1250
1248
|
|
|
1251
|
-
Split out so
|
|
1252
|
-
deferral
|
|
1253
|
-
the whole page twice in a row. ``soup_is_dirty`` says the caller already
|
|
1254
|
-
mutated the tree, so the early-exit paths must serialize rather than hand
|
|
1255
|
-
back the untouched source string.
|
|
1249
|
+
Split out so callers that already parsed the document can reuse the
|
|
1250
|
+
component-deferral pass.
|
|
1256
1251
|
"""
|
|
1257
1252
|
def unchanged() -> str:
|
|
1258
|
-
if soup_is_dirty:
|
|
1259
|
-
return restore_escaped_brace_entities(
|
|
1260
|
-
serialize_fragment(soup), placeholders)
|
|
1261
1253
|
return fallback_html
|
|
1262
1254
|
|
|
1263
1255
|
body = soup.body
|
|
@@ -1283,35 +1275,37 @@ def _defer_component_roots_in_soup(
|
|
|
1283
1275
|
root.insert_before(template)
|
|
1284
1276
|
template.append(root.extract())
|
|
1285
1277
|
|
|
1286
|
-
#
|
|
1287
|
-
#
|
|
1288
|
-
#
|
|
1278
|
+
# An HTML parser decodes ``{`` to ``{`` even inside an inert template.
|
|
1279
|
+
# Restore each masked entity into the parsed tree before serialization so
|
|
1280
|
+
# the serializer escapes its ampersand one additional time:
|
|
1281
|
+
#
|
|
1282
|
+
# { -> &#123;
|
|
1283
|
+
#
|
|
1284
|
+
# The browser consumes that outer layer while parsing the response, leaving
|
|
1285
|
+
# the inner entity intact for PulsePoint to mask before expression scanning.
|
|
1286
|
+
# Placeholders outside deferred component templates are restored normally
|
|
1287
|
+
# after serialization.
|
|
1289
1288
|
if placeholders:
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
placeholder
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
)
|
|
1297
|
-
for placeholder, entity in placeholders.items()
|
|
1298
|
-
}
|
|
1289
|
+
def protect_brace_entities(value: str) -> str:
|
|
1290
|
+
protected = value
|
|
1291
|
+
for placeholder, entity in placeholders.items():
|
|
1292
|
+
protected = protected.replace(placeholder, entity)
|
|
1293
|
+
return protected
|
|
1294
|
+
|
|
1299
1295
|
for template in body.select('template[pp-component]'):
|
|
1300
1296
|
for node in list(template.descendants):
|
|
1301
1297
|
if isinstance(node, NavigableString):
|
|
1302
1298
|
original = str(node)
|
|
1303
|
-
content = original
|
|
1304
|
-
for placeholder, marker in brace_markers.items():
|
|
1305
|
-
content = content.replace(placeholder, marker)
|
|
1299
|
+
content = protect_brace_entities(original)
|
|
1306
1300
|
if content != original:
|
|
1307
1301
|
node.replace_with(content)
|
|
1308
1302
|
elif isinstance(node, Tag):
|
|
1309
1303
|
for name, value in node.attrs.items():
|
|
1310
|
-
if
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1304
|
+
if isinstance(value, str):
|
|
1305
|
+
node.attrs[name] = protect_brace_entities(value)
|
|
1306
|
+
elif isinstance(value, list):
|
|
1307
|
+
for index, item in enumerate(value):
|
|
1308
|
+
value[index] = protect_brace_entities(str(item))
|
|
1315
1309
|
|
|
1316
1310
|
return restore_escaped_brace_entities(serialize_fragment(soup), placeholders)
|
|
1317
1311
|
|
|
@@ -1344,32 +1338,13 @@ def _inject_dev_console_bridge(html_output: str) -> str:
|
|
|
1344
1338
|
def finalize_html(html_output: str) -> str:
|
|
1345
1339
|
"""Final full-document transforms applied just before the response.
|
|
1346
1340
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
which meant two full BeautifulSoup round-trips on every response.
|
|
1341
|
+
Injects the development console bridge when enabled, then wraps outermost
|
|
1342
|
+
``pp-component`` roots in inert ``<template>`` elements. Component scripts
|
|
1343
|
+
remain plain ``<script>`` elements: the surrounding template keeps them
|
|
1344
|
+
inert until PulsePoint materializes and mounts the component boundary.
|
|
1352
1345
|
"""
|
|
1353
1346
|
html_output = _inject_dev_console_bridge(html_output)
|
|
1354
|
-
|
|
1355
|
-
if 'pp-component' not in html_output:
|
|
1356
|
-
# Nothing to defer, so fall back to the standalone script transform.
|
|
1357
|
-
return transform_scripts(html_output)
|
|
1358
|
-
|
|
1359
|
-
masked_html, placeholders = mask_escaped_brace_entities(html_output)
|
|
1360
|
-
soup = parse_fragment(masked_html)
|
|
1361
|
-
body = soup.body
|
|
1362
|
-
if body is None:
|
|
1363
|
-
return transform_scripts(html_output)
|
|
1364
|
-
|
|
1365
|
-
tagged_script = False
|
|
1366
|
-
for script in body.find_all('script'):
|
|
1367
|
-
if not script.has_attr('type'):
|
|
1368
|
-
script['type'] = 'text/pp'
|
|
1369
|
-
tagged_script = True
|
|
1370
|
-
|
|
1371
|
-
return _defer_component_roots_in_soup(
|
|
1372
|
-
soup, placeholders, html_output, soup_is_dirty=tagged_script)
|
|
1347
|
+
return defer_component_roots(html_output)
|
|
1373
1348
|
|
|
1374
1349
|
|
|
1375
1350
|
register_routes()
|
package/dist/public/js/main.js
CHANGED