create-caspian-app 0.3.0-rc.8 ā 0.4.0-rc.0
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 +28 -2
- package/dist/AGENTS.md +13 -2
- package/dist/caspian.js +1 -1
- package/dist/index.js +1 -1
- package/dist/main.py +209 -3
- package/dist/public/js/pp-reactive-v2.js +1 -1
- package/dist/pyproject.toml +1 -1
- package/dist/settings/bs-config.json +6 -6
- package/dist/settings/bs-config.ts +132 -62
- package/dist/settings/component-map.ts +6 -6
- package/dist/settings/python-server.ts +8 -18
- package/dist/settings/utils.ts +3 -3
- package/dist/src/lib/mcp/mcp_server.py +8 -8
- package/dist/src/lib/websocket/websocket_security.py +84 -0
- package/package.json +1 -1
|
@@ -10,16 +10,28 @@
|
|
|
10
10
|
- When packaged docs need to point AI from a feature guide to the controlling runtime file, prefer `node_modules/caspian-utils/dist/docs/core-runtime-map.md` instead of duplicating the full module map in multiple pages.
|
|
11
11
|
- When packaged docs need to point AI from a PulsePoint feature or directive to the controlling browser behavior, prefer `node_modules/caspian-utils/dist/docs/pulsepoint-runtime-map.md` instead of duplicating the full browser feature map in multiple pages.
|
|
12
12
|
|
|
13
|
+
## Component-First Page Composition (Highest Priority)
|
|
14
|
+
|
|
15
|
+
This is the top architectural requirement for this workspace. Treat it as a hard rule that outranks convenience, and apply it before writing any route, layout, or page markup.
|
|
16
|
+
|
|
17
|
+
- Build pages from components, not from one large block of HTML. A route's `src/app/**/index.html` should read like a short composition of `x-*` component tags, not a wall of markup. When a page would otherwise carry a long stretch of HTML, that markup must move into a component instead of living in the page.
|
|
18
|
+
- Separate every page into meaningful chunks and give each chunk its own component. Typical chunks are a top menu / topbar, header, sidebar / nav rail, hero, toolbar, content sections, cards, lists, forms, footer, and any repeated block. Each chunk owns its own long markup inside its component file, so the page content stays small and readable.
|
|
19
|
+
- Default to single-file Python components authored with inline `html(...)` (import `html` from `casp.component_decorator`, return `html("""...""", **context)`). Keep the chunk's markup, server interpolation, and any PulsePoint `<script>` inside that one Python file. Move to a thin `.py` plus same-name `.html` via `render_html(...)` only when the markup is very large or the script carries heavy logic.
|
|
20
|
+
- Put these page-chunk components in `src/components/` (or a route-local component folder when they are truly single-route), import them into the route with a top-level `<!-- @import ... -->` directive or a real Python import, and render them as kebab-cased `x-*` tags. Keep the single-root contract in both the page and each component.
|
|
21
|
+
- When the user asks to build or extend a page, plan the chunk breakdown first (for example: top menu component, sidebar component, content section component), create those components, then assemble them in the route. Do not start by pasting a full HTML page into `index.html` and only later consider extraction; component-first is the starting point, not a cleanup step.
|
|
22
|
+
- If you find an existing page with a large inline HTML body, prefer splitting it into chunk components as part of the work rather than adding more inline markup to it.
|
|
23
|
+
|
|
13
24
|
## Global Rules
|
|
14
25
|
|
|
15
26
|
- Use this decision order: `caspian.config.json` for optional feature enablement, app runtime and app-owned code for current project behavior, matching workspace instruction files under `.github/instructions/**/*.instructions.md` for task-specific implementation guidance, installed `casp` runtime for framework internals, and packaged markdown docs for Caspian feature discovery and task routing.
|
|
16
27
|
- As the app grows, prefer `src/components/` for reusable application UI and reserve `src/lib/` for reusable non-UI code such as services, validators, adapters, and shared helpers.
|
|
17
|
-
- Read `./caspian.config.json` almost immediately before making feature, tooling, scaffolding, or file-placement decisions. Treat it as the workspace feature gate for flags such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, and `componentScanDirs`.
|
|
28
|
+
- Read `./caspian.config.json` almost immediately before making feature, tooling, scaffolding, or file-placement decisions. Treat it as the workspace feature gate for flags such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, `websocket`, and `componentScanDirs`.
|
|
18
29
|
- Treat `caspian.config.json` as the single source of truth for whether optional Caspian features are enabled in the current workspace. Use feature-specific docs, files, and commands only after the matching flag is confirmed as enabled.
|
|
19
30
|
- If a feature is disabled and the user wants it, ask whether they want to enable it first, then update `caspian.config.json` and follow `npx casp update project` so framework-managed files align with the new feature set.
|
|
20
31
|
- When `.github/instructions/**/*.instructions.md` files exist, treat them as workspace-local file instructions for specific libraries, component systems, icon sets, integrations, and implementation rules. Read the matching instruction before deciding how to implement work in that area, but do not let it override `caspian.config.json`, app code, or installed runtime behavior.
|
|
21
32
|
- Treat `node_modules/caspian-utils/dist/docs/**` as packaged Caspian docs that teach AI how Caspian features work and where to look next. Their presence does not mean the feature is enabled in the current project.
|
|
22
33
|
- Use `node_modules/caspian-utils/dist/docs/pulsepoint-runtime-map.md` for fast PulsePoint feature lookup before editing browser-side behavior or generating advanced PulsePoint patterns.
|
|
34
|
+
- Use `node_modules/caspian-utils/dist/docs/websockets.md` before changing FastAPI WebSocket endpoints, socket origin checks, socket auth/session behavior, broadcast managers, or native browser `WebSocket` clients.
|
|
23
35
|
- For current repo behavior, trust `main.py`, `src/lib/**`, `public/js/**`, `prisma/**`, and `src/app/**` over generic Caspian docs.
|
|
24
36
|
- For framework internals, trust `.venv/Lib/site-packages/casp/**` over generic or older upstream guidance.
|
|
25
37
|
- When packaged docs conflict with project code or installed runtime, the project code, `caspian.config.json`, and installed runtime win. Keep the packaged docs feature-oriented and point AI back to the project files that decide actual enablement and behavior.
|
|
@@ -46,7 +58,13 @@
|
|
|
46
58
|
- Treat imperative DOM APIs and `pp-ref` element reads as narrow escape hatches for third-party widgets, browser APIs that require direct DOM access, focus/measurement/media/canvas behavior, or one-off integration code. When they are needed, keep them inside the owning PulsePoint component script, usually behind `pp.ref(...)` and `pp.effect(...)`, so PulsePoint still owns the component state and event flow.
|
|
47
59
|
- When `caspian.config.json` has `tailwindcss: true`, treat Python `merge_classes(...)` plus browser `twMerge(...)` as the only Tailwind class-merging contract: `merge_classes(...)` emits frontend-ready `{twMerge(...)}` expressions, and authored PulsePoint attribute expressions or scripts may call global `twMerge(...)` directly.
|
|
48
60
|
- Treat Caspian component usage as HTML-first in the current runtime: import Python components with `<!-- @import ... -->` and render them as kebab-cased `x-*` tags such as `<x-button />` or `<x-command-dialog />`.
|
|
61
|
+
- Components may be authored single-file. Return `html("""...""", **context)` (import `html` from `casp.component_decorator`) to keep markup, server interpolation, and a PulsePoint `<script>` inline instead of a same-name `.html` via `render_html(...)`. Inside `html(...)`, `{{ ... }}` is server-side Jinja and `{ ... }` is left for PulsePoint; never use a Python f-string for the markup. Autoescaping is on, so `{{ value }}` is safe for user text and trusted HTML needs `Markup(...)` or `| safe`; a `children` value is auto-safe. Prefer single-file `html(...)` for small and medium components and keep `render_html(...)` plus a `.html` file for large markup or long scripts.
|
|
62
|
+
- Inside single-file components, prefer real Python imports over `<!-- @import ... -->` for child components: a component's own `x-*` tags resolve from the components imported into its Python module, which disambiguates same-name components across directories. Resolution precedence is inherited ancestor components, then the component's own Python imports, then a local `@import`. Slot content resolves in the scope where it was authored, so the template that writes an `x-*` tag must import that component.
|
|
49
63
|
- For CRUD operations and any browser-initiated reads from the backend, use route or backend `@rpc()` actions on the server and `pp.rpc(...)` from PulsePoint code on the client unless the user explicitly asks for another integration pattern.
|
|
64
|
+
- Google and GitHub OAuth ship pre-registered in this starter: `main.py` already calls `Auth.set_providers(GithubProvider(), GoogleProvider())`, and `AuthMiddleware` already handles the `signin/{google,github}` and `callback/{google,github}` paths under `api_auth_prefix` (default `/api/auth`). To add social sign-in, point a link or button at `/api/auth/signin/google` or `/api/auth/signin/github` and set the provider credentials in `.env` (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`). Do not hand-roll an OAuth flow, manual `httpx`/`authlib` token exchange, or custom callback routes; reuse the shipped providers and let `auth.auth_providers(...)` own redirect, callback, and sign-in.
|
|
65
|
+
- For one-way streaming output, including AI/LLM/chat token streams, use Caspian's shipped RPC streaming: write a generator `@rpc()` action that `yield`s chunks (the runtime wraps generators as SSE via `casp.streaming.SSE`) and consume it with `pp.rpc(name, args, { onStream, onStreamComplete, onStreamError })`. When bridging a Python LLM/SDK stream, `async for` over the provider's stream inside the `@rpc()` action and `yield` each token. Do not reinvent one-way streaming with raw `fetch`/`ReadableStream`, `EventSource`, or a WebSocket; reserve WebSockets for genuinely bidirectional channels per the WebSocket rules above.
|
|
66
|
+
- For live bidirectional channels, first confirm `caspian.config.json` has `websocket: true`, then use app-owned FastAPI WebSocket endpoints in `main.py` plus native browser `WebSocket` clients inside the owning PulsePoint route template. Do not replace normal CRUD, form submits, uploads, or one-way progress streams with WebSockets.
|
|
67
|
+
- When `caspian.config.json` has `websocket: true`, WebSocket endpoint paths are project-defined in `main.py`; do not assume any default socket path or route folder exists in every Caspian project. Keep shared socket helpers under `src/lib/websocket/**` when session extraction, auth payload validation, connection tracking, or broadcast behavior is reused.
|
|
50
68
|
- For route creation, keep page markup in `src/app/**/index.html`. If a route is UI-only, `index.html` alone is sufficient. Add `src/app/**/index.py` only as a companion when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side behavior. Keep shared section wrappers in `layout.html` and use `layout.py` only for shared props or metadata. Do not place route HTML in `index.py` or layout HTML in `layout.py`; use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
|
|
51
69
|
- Keep route-specific logic in that route's `index.py`. Move code into `src/lib/**` only when it is genuinely reusable across routes, components, integrations, or features; do not extract one-route orchestration just to make it look generic.
|
|
52
70
|
- Treat the single-root template contract as a hard requirement, not a style preference: every authored route, layout, and component HTML file must have exactly one parent HTML element or one imported `x-*` component tag as its root. Do not leave sibling top-level markup, and do not place a `<script>` after the root element. If a script is needed, keep it inside that same root.
|
|
@@ -77,11 +95,13 @@
|
|
|
77
95
|
### `main.py`
|
|
78
96
|
|
|
79
97
|
- Treat `main.py` as the repo source of truth for FastAPI setup, auth bootstrap, middleware wiring, route registration, cache defaults, and error handlers.
|
|
98
|
+
- 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.
|
|
80
99
|
- When the app factors response-header hardening or safe static-file behavior into app-owned helpers, treat `main.py` plus those imported helpers as the runtime source of truth together.
|
|
81
100
|
- Preserve the effective middleware execution order unless the task explicitly changes request semantics: `SecurityHeadersMiddleware -> SessionMiddleware -> CSRFMiddleware -> AuthMiddleware -> RPCMiddleware`.
|
|
82
101
|
- Do not move normal file upload or file-manager behavior into `main.py`; keep those actions in the owning route `index.py` and shared helpers in `src/lib/**`.
|
|
83
102
|
- Document route param behavior exactly as implemented here.
|
|
84
103
|
- Do not use `main.py` alone to infer whether optional features are enabled; confirm that in `caspian.config.json` first.
|
|
104
|
+
- Before changing WebSocket behavior, verify `cfg.websocket`, the app's endpoint registration, origin allow-list logic, session/auth checks, idle timeout, maximum message size, close codes, and connection cleanup. HTTP-only middleware does not automatically protect `scope["type"] == "websocket"` connections.
|
|
85
105
|
|
|
86
106
|
### `src/lib/**/*.py`
|
|
87
107
|
|
|
@@ -92,12 +112,14 @@
|
|
|
92
112
|
- When `caspian.config.json` has `mcp: true`, keep app-owned MCP tools in `src/lib/mcp/mcp_server.py` and keep the default FastMCP config in `src/lib/mcp/fastmcp.json`. If those locations change, update `settings/restart-mcp.ts` and the MCP docs together.
|
|
93
113
|
- Keep auth policy in `src/lib/auth/auth_config.py`. Keep auth bootstrap and middleware order changes in `main.py`.
|
|
94
114
|
- Do not recreate or customize `src/lib/security/runtime_security.py` for normal application work. Runtime security helpers are package-owned in `casp.runtime_security`; app-specific policy should live in app-owned config or route/helper code instead.
|
|
115
|
+
- Keep reusable WebSocket helpers under `src/lib/websocket/**` when they are shared across socket endpoints or route clients. Common shared helpers include session extraction, authenticated payload checks, origin utilities, connection managers, payload normalization, and broadcast fan-out.
|
|
95
116
|
|
|
96
117
|
### `src/components/**/*.py`
|
|
97
118
|
|
|
98
|
-
- Keep `src/components/` as the default home for reusable application UI components.
|
|
119
|
+
- Keep `src/components/` as the default home for reusable application UI components and for the page chunks produced by component-first composition (top menus, sidebars, headers, content sections, cards, lists, forms, footers).
|
|
99
120
|
- Move shared cards, forms, shells, navigation, and other reusable rendered building blocks here once they are used across routes or features.
|
|
100
121
|
- Keep route-owned markup in `src/app/**`, and keep non-UI helpers or services in `src/lib/**`.
|
|
122
|
+
- Author components as a single Python file with inline `html(...)` by default for small and medium UI, or as a `.py` plus same-name `.html` via `render_html(...)` for large markup or long scripts. Keep the single-root rule in both forms. Resolve child `x-*` tags from real Python imports in single-file components rather than `<!-- @import ... -->`. See `node_modules/caspian-utils/dist/docs/components.md`.
|
|
101
123
|
|
|
102
124
|
### `public/js/main.js`
|
|
103
125
|
|
|
@@ -114,6 +136,7 @@
|
|
|
114
136
|
|
|
115
137
|
### `src/app/**/*.html`
|
|
116
138
|
|
|
139
|
+
- Compose pages from components first (see "Component-First Page Composition"). Keep `index.html` a short assembly of `x-*` chunk components (top menu, sidebar, content sections, cards, forms, footer, and other repeated blocks) instead of a long inline HTML body. When a route would carry a long stretch of markup, move that markup into a single-file `html(...)` component and render it as an `x-*` tag here.
|
|
117
140
|
- Keep route templates and layouts server-rendered first, with PulsePoint enhancement as the default interactive layer.
|
|
118
141
|
- Keep visible page and layout markup in `index.html` and `layout.html`. Treat `index.py` and `layout.py` as backend companions for metadata, `page()` or `layout()`, `@rpc()` actions, auth checks, caching, redirects, and other server-side preparation, not as places to author visible HTML.
|
|
119
142
|
- When a route renders UI, author that markup in the route's `index.html` even if the route also has an `index.py` companion.
|
|
@@ -128,6 +151,8 @@
|
|
|
128
151
|
- 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.
|
|
129
152
|
- 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.
|
|
130
153
|
- For upload managers and similar interactive lists, prefer `pp.state(...)` plus `pp-for` over manual DOM painting so rerenders keep the list stable.
|
|
154
|
+
- For route-owned WebSocket clients, use PulsePoint state, refs, and cleanup effects around the native `WebSocket`. Keep the socket object in `pp.ref(...)`, close it on component disposal, and keep socket event listeners inside the owning route template script.
|
|
155
|
+
- Do not assume WebSocket clients live in a dashboard or any fixed route. Put the browser client in whichever route owns that live experience, pass first-render socket values from the matching `index.py`, and use route auth policy plus WebSocket endpoint auth checks intentionally for public, private, or mixed channels.
|
|
131
156
|
- Do not assume React, Vue, JSX-first component syntax, HTMX, or another frontend runtime unless the user explicitly requests one.
|
|
132
157
|
|
|
133
158
|
### `prisma/**`
|
|
@@ -159,6 +184,7 @@
|
|
|
159
184
|
- These files are the packaged Caspian documentation layer, not the runtime and not the source of current workspace state.
|
|
160
185
|
- Use them to help AI answer three questions: which Caspian feature applies, which project files should be inspected next, and which workflow is appropriate once the feature is confirmed as enabled.
|
|
161
186
|
- Use `node_modules/caspian-utils/dist/docs/file-conventions.md` when deciding what belongs in `index.html`, `index.py`, `layout.html`, `layout.py`, `loading.html`, `not-found.html`, or `error.html`.
|
|
187
|
+
- Use `node_modules/caspian-utils/dist/docs/websockets.md` when deciding how to document or implement app-owned FastAPI WebSockets, browser `WebSocket` clients, origin checks, auth/session checks, message contracts, and the choice between WebSockets, RPC, and SSE.
|
|
162
188
|
- Verify behavior claims in this order:
|
|
163
189
|
1. `caspian.config.json`, then `main.py`, `src/lib/**`, `public/js/**`, `prisma/**`, `src/app/**`
|
|
164
190
|
2. `.venv/Lib/site-packages/casp/**`
|
package/dist/AGENTS.md
CHANGED
|
@@ -39,7 +39,7 @@ If the task is about framework internals, prefer the installed `casp` package.
|
|
|
39
39
|
|
|
40
40
|
If packaged docs differ from the project or installed runtime, the project and runtime win. Keep the packaged docs reusable across Caspian projects and move project-specific clarifications into this file or `.github/copilot-instructions.md`.
|
|
41
41
|
|
|
42
|
-
Before making feature, tooling, or scaffolding decisions, read `caspian.config.json` almost immediately. Treat it as the workspace feature gate for flags such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, and `componentScanDirs`.
|
|
42
|
+
Before making feature, tooling, or scaffolding decisions, read `caspian.config.json` almost immediately. Treat it as the workspace feature gate for flags such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, `websocket`, and `componentScanDirs`.
|
|
43
43
|
|
|
44
44
|
Treat `caspian.config.json` as the single source of truth for whether an optional Caspian feature is enabled in the current workspace. Use feature-specific docs only after the matching flag is confirmed as enabled. If a feature is disabled and the user wants it, ask whether they want to enable it first, then follow the Caspian update workflow to refresh framework-managed files.
|
|
45
45
|
|
|
@@ -64,9 +64,17 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
|
|
|
64
64
|
- Workspace file instructions live under `.github/instructions/**/*.instructions.md` when the repo needs task- or library-specific AI guidance that should not be always-on.
|
|
65
65
|
- Use `node_modules/caspian-utils/dist/docs/core-runtime-map.md` when a behavior is controlled by `main.py`, package-owned runtime helpers such as `.venv/Lib/site-packages/casp/runtime_security.py`, or other `.venv/Lib/site-packages/casp/**` files and the owning file is not obvious yet.
|
|
66
66
|
- Use `node_modules/caspian-utils/dist/docs/pulsepoint-runtime-map.md` when a behavior is controlled by the shipped PulsePoint browser runtime and the task names state, effects, refs, context, portals, directives, `pp.rpc`, uploads, streaming, SPA navigation, or scroll restoration.
|
|
67
|
+
- Use `node_modules/caspian-utils/dist/docs/websockets.md` when the task names WebSockets, live bidirectional channels, socket origin checks, socket auth/session behavior, broadcast managers, or native browser `WebSocket` clients.
|
|
67
68
|
- Use `node_modules/caspian-utils/dist/docs/file-conventions.md` when the task asks what belongs in `index.html`, `index.py`, `layout.html`, `layout.py`, `loading.html`, `not-found.html`, or `error.html`.
|
|
68
69
|
- When `caspian.config.json` has `prisma: true`, database reads and writes from Python routes, layouts, RPC actions, upload flows, auth flows, and helpers must use the generated Prisma Python ORM in `src/lib/prisma/**`. Do not create a separate database fetch layer with raw drivers, hand-written SQL helpers, JSON manifests, app-specific HTTP fetches, or browser-side data fetches to replace the ORM. Use raw SQL only as a narrow Prisma ORM fallback when the generated client cannot express a query clearly.
|
|
70
|
+
- 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 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.
|
|
71
|
+
- 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 `transform_components(...)` then `transform_scripts(...)`. See `node_modules/caspian-utils/dist/docs/components.md`.
|
|
72
|
+
- For component-to-component composition, prefer real Python imports over `<!-- @import ... -->` comments inside single-file components. A component's own `x-*` tags resolve from the components imported into its Python module, which disambiguates same-name components across directories. 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. 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.
|
|
69
73
|
- 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.
|
|
74
|
+
- When `caspian.config.json` has `websocket: true`, WebSocket behavior is app-owned in `main.py`. Do not assume fixed route names or endpoint paths across Caspian projects; define project-specific socket paths in the app, keep shared socket helpers in `src/lib/websocket/**` when needed, and let each route that needs a socket client pass its own `websocket_path` or `websocket_url` into its template.
|
|
75
|
+
- WebSocket pages may be public, authenticated, role-gated, or mixed depending on the app. Keep route-specific browser UI in that route's `src/app/**/index.html`, keep first-render socket URL/path values in the matching `index.py`, and keep reusable session/auth/connection/broadcast helpers out of route files only when they are shared.
|
|
76
|
+
- For WebSocket clients, use PulsePoint for component state and lifecycle but native `new WebSocket(...)` for the transport. Keep the socket in `pp.ref(...)`, close it in a cleanup effect, and keep direct socket event listeners inside the owning route template script.
|
|
77
|
+
- Before changing socket security, verify the app's origin allow-list logic, session extraction, authenticated payload checks, idle timeout, message-size limits, and close codes in the running code. HTTP route privacy and `AuthMiddleware` do not by themselves protect WebSocket scopes.
|
|
70
78
|
- Keep route-specific backend logic in that route's `src/app/**/index.py`, including first-render data loading, route-owned `@rpc()` actions, auth checks, redirects, and validation. Move logic to `src/lib/**` only when it is shared by more than one route, feature, component, or integration.
|
|
71
79
|
- For grouped-subtree SPA navigation UX, the current browser runtime keeps unmarked shell scrollers stable and uses `pp-reset-scroll="true"` on the content pane that should reset. Check `pulsepoint.md`, `routing.md`, and `public/js/pp-reactive-v2.js` before changing that behavior.
|
|
72
80
|
- 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.
|
|
@@ -89,7 +97,10 @@ If the task generates or edits route, layout, or component HTML templates, check
|
|
|
89
97
|
- Routing, layouts, metadata: read `node_modules/caspian-utils/dist/docs/routing.md`. Verify against `main.py` and `.venv/Lib/site-packages/casp/layout.py`.
|
|
90
98
|
- SPA navigation and scroll restoration: read `pulsepoint.md`, `routing.md`, and `core-runtime-map.md`. Verify against `public/js/pp-reactive-v2.js`, `src/app/**/layout.html`, and `main.py`.
|
|
91
99
|
- Auth, sessions, RBAC, providers: read `node_modules/caspian-utils/dist/docs/auth.md`. Verify against `src/lib/auth/auth_config.py`, `main.py`, `.venv/Lib/site-packages/casp/runtime_security.py`, and `.venv/Lib/site-packages/casp/auth.py`.
|
|
100
|
+
- Social login (Google or GitHub sign-in): treat it as shipped. `main.py` already registers both providers via `Auth.set_providers(...)`, and `AuthMiddleware` already serves `/api/auth/signin/{google,github}` and `/api/auth/callback/{google,github}`. Link a button at the signin path and set `.env` credentials instead of hand-rolling OAuth. Read `node_modules/caspian-utils/dist/docs/auth.md` "OAuth Providers" and verify against `main.py` plus `src/lib/auth/auth_config.py`.
|
|
92
101
|
- RPC, data loading, streaming, uploads: read `node_modules/caspian-utils/dist/docs/fetch-data.md` and `node_modules/caspian-utils/dist/docs/pulsepoint.md`. Verify against `.venv/Lib/site-packages/casp/rpc.py`, `public/js/pp-reactive-v2.js`, and `main.py`.
|
|
102
|
+
- AI/LLM/chat or any one-way streaming output: use the shipped RPC streaming path (generator `@rpc()` that `yield`s chunks, consumed by `pp.rpc(..., { onStream })`); for an LLM SDK, `async for ... yield`. Read `node_modules/caspian-utils/dist/docs/fetch-data.md` "Streaming Responses" and `node_modules/caspian-utils/dist/docs/pulsepoint.md`. Verify against `.venv/Lib/site-packages/casp/streaming.py`, `.venv/Lib/site-packages/casp/rpc.py`, `public/js/pp-reactive-v2.js`, and `main.py`. Do not reinvent with raw `fetch`/`ReadableStream`, `EventSource`, or WebSockets.
|
|
103
|
+
- WebSockets and live channels: first confirm `caspian.config.json` has `websocket: true`, then read `node_modules/caspian-utils/dist/docs/websockets.md`. Verify against `main.py`, `src/lib/websocket/**` when present, the owning `src/app/**` route files, and `settings/bs-config.json`.
|
|
93
104
|
- File uploads and managers: read `node_modules/caspian-utils/dist/docs/file-uploads.md` and `node_modules/caspian-utils/dist/docs/fetch-data.md`. Verify against `src/app/**`, `src/lib/**`, `prisma/**`, and `settings/bs-config.ts`.
|
|
94
105
|
- Server state: read `node_modules/caspian-utils/dist/docs/state.md`. Verify against `.venv/Lib/site-packages/casp/state_manager.py` and `main.py`.
|
|
95
106
|
- Page caching: read `node_modules/caspian-utils/dist/docs/cache.md`. Verify against `.venv/Lib/site-packages/casp/cache_handler.py` and `main.py`.
|
|
@@ -110,7 +121,7 @@ If the task generates or edits route, layout, or component HTML templates, check
|
|
|
110
121
|
- When `caspian.config.json` has `tailwindcss: true`, document Tailwind class handling as the current contract: Python `merge_classes(...)` emits frontend `{twMerge(...)}` expressions and browser `twMerge(...)` resolves conflicts.
|
|
111
122
|
- Keep repo-specific clarifications in this file or `.github/copilot-instructions.md` rather than embedding them in the packaged docs unless the behavior is truly framework-wide.
|
|
112
123
|
- Keep `index.md` and cross-links aligned so AI can discover the right task doc quickly.
|
|
113
|
-
- Continue validating `file-conventions.md`, `routing.md`, `components.md`, `auth.md`, `fetch-data.md`, `cache.md`, `pulsepoint.md`, `validation.md`, `database.md`, and `mcp.md` against the installed `casp` runtime before changing behavior claims.
|
|
124
|
+
- Continue validating `file-conventions.md`, `routing.md`, `components.md`, `auth.md`, `fetch-data.md`, `websockets.md`, `cache.md`, `pulsepoint.md`, `validation.md`, `database.md`, and `mcp.md` against the installed `casp` runtime before changing behavior claims.
|
|
114
125
|
|
|
115
126
|
## Maintenance Checklist
|
|
116
127
|
|
package/dist/caspian.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import chalk from"chalk";import{spawn}from"child_process";import fs from"fs";import path from"path";import prompts from"prompts";const args=process.argv.slice(2),isNonInteractive=args.includes("-y"),resolveNpxInvocation=e=>"win32"===process.platform?{command:process.env.ComSpec||process.env.COMSPEC||"cmd.exe",args:["/d","/s","/c","npx.cmd",...e]}:{command:"npx",args:e},readJsonFile=e=>{const o=fs.readFileSync(e,"utf8");return JSON.parse(o)},executeCommand=(e,o=[],n={})=>new Promise((a,r)=>{const t=spawn(e,o,{stdio:"inherit",shell:!1,...n});t.on("error",e=>{console.error(`Execution error: ${e.message}`),r(e)}),t.on("close",e=>{0===e?a():r(new Error(`Process exited with code ${e}`))})});async function getAnswer(e){const o=[{type:"toggle",name:"shouldProceed",message:`This command will update the ${chalk.blue("create-caspian-app")} package and overwrite all default files. ${chalk.blue("Do you want to proceed")}?`,initial:!1,active:"Yes",inactive:"No"}];e||o.push({type:e=>e?"text":null,name:"versionTag",message:`Enter version tag (e.g., ${chalk.cyan("latest")}, ${chalk.cyan("v4-alpha")}, ${chalk.cyan("1.2.3")}), or press Enter for ${chalk.green("latest")}:`,initial:"latest",validate:e=>!(!e||""===e.trim())||"Version tag cannot be empty"});const n=await prompts(o,{onCancel:()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)}});if(0===Object.keys(n).length)return null;let a="latest";return e?a=e:n.versionTag&&(a=n.versionTag),{shouldProceed:n.shouldProceed,versionTag:a}}const commandsToExecute={update:"npx casp update project"},normalizeVersionTag=e=>{const o=e.startsWith("@")?e.slice(1):e;if(!o.trim())throw new Error("Version tag cannot be empty.");return o},addVersionTag=(e,o)=>{if(e&&e!==o)throw new Error(`Conflicting version tags provided: ${e} and ${o}`);return o},parseCliArgs=e=>{const o=[];let n;for(let a=0;a<e.length;a+=1){const r=e[a];if("-y"!==r){if("--tag"===r||"--version"===r){const o=e[a+1];if(!o||o.startsWith("-"))throw new Error(`Missing value for ${r}.`);n=addVersionTag(n,normalizeVersionTag(o)),a+=1;continue}r.startsWith("--tag=")?n=addVersionTag(n,normalizeVersionTag(r.slice(6))):r.startsWith("--version=")?n=addVersionTag(n,normalizeVersionTag(r.slice(10))):o.push(r)}}if("update"===o[0]&&"project"===o[1]){const e=o.slice(2);if(e.length>1)throw new Error(`Too many arguments for update project: ${e.join(" ")}`);return 1===e.length&&(n=addVersionTag(n,normalizeVersionTag(e[0]))),{formattedCommand:commandsToExecute.update,versionTag:n}}return{formattedCommand:`npx casp ${o.join(" ")}`,versionTag:n}},main=async()=>{if(0===args.length)return console.log("No command provided."),console.log("\nUsage:"),console.log(` ${chalk.cyan("npx casp update project")} - Update to latest version`),console.log(` ${chalk.cyan("npx casp update project beta")} - Update to specific tag`),console.log(` ${chalk.cyan("npx casp update project --tag beta")} - Update to a specific tag via named option`),console.log(` ${chalk.cyan("npx casp update project 1.2.3")} - Update to specific version`),void console.log(` ${chalk.cyan("npx casp update project -y")} - Update without prompts (non-interactive)`);let e;try{e=parseCliArgs(args)}catch(e){return void(e instanceof Error?console.error(chalk.red(e.message)):console.error(chalk.red("Failed to parse command arguments.")))}const{formattedCommand:o,versionTag:n}=e;if(!Object.values(commandsToExecute).includes(o))return console.log("Command not recognized or not allowed."),console.log("\nAvailable commands:"),console.log(` ${chalk.cyan("update project")} - Update project files`),console.log(` ${chalk.cyan("update project beta")} - Update to a specific tag`),console.log(` ${chalk.cyan("update project --tag beta")} - PowerShell-safe tagged update`),void console.log(` ${chalk.cyan("-y")} - Non-interactive mode (skip prompts)`);if(o===commandsToExecute.update)try{let e;if(isNonInteractive)e={shouldProceed:!0,versionTag:n||"latest"},console.log(chalk.blue("Running in non-interactive mode..."));else if(e=await getAnswer(n),!e?.shouldProceed)return void console.log(chalk.red("Operation cancelled by the user."));const o=process.cwd(),a=path.join(o,"caspian.config.json");if(!fs.existsSync(a))return void console.error(chalk.red("The configuration file 'caspian.config.json' was not found in the current directory."));const r=readJsonFile(a),t=e.versionTag||"latest",s=`create-caspian-app@${t}`;console.log(chalk.blue(`\nUpdating to: ${chalk.green(s)}\n`));const c=[r.projectName];r.backendOnly&&c.push("--backend-only"),r.tailwindcss&&c.push("--tailwindcss"),r.prisma&&c.push("--prisma"),r.mcp&&c.push("--mcp"),r.typescript&&c.push("--typescript"),isNonInteractive&&c.push("-y"),console.log("Executing command...\n");const i=resolveNpxInvocation([s,...c]);await executeCommand(i.command,i.args),console.log(chalk.green(`\nā Project updated successfully to version ${t}!`)),console.log(chalk.blue("Updated configuration saved to caspian.config.json"))}catch(e){e instanceof Error?e.message.includes("no such file or directory")?console.error(chalk.red("The configuration file 'caspian.config.json' was not found in the current directory.")):console.error(chalk.red(`Error during update: ${e.message}`)):console.error("Error in script execution:",e)}};main().catch(e=>{console.error("Unhandled error in main function:",e)});
|
|
2
|
+
import chalk from"chalk";import{spawn}from"child_process";import fs from"fs";import path from"path";import prompts from"prompts";const args=process.argv.slice(2),isNonInteractive=args.includes("-y"),resolveNpxInvocation=e=>"win32"===process.platform?{command:process.env.ComSpec||process.env.COMSPEC||"cmd.exe",args:["/d","/s","/c","npx.cmd",...e]}:{command:"npx",args:e},readJsonFile=e=>{const o=fs.readFileSync(e,"utf8");return JSON.parse(o)},executeCommand=(e,o=[],n={})=>new Promise((a,r)=>{const t=spawn(e,o,{stdio:"inherit",shell:!1,...n});t.on("error",e=>{console.error(`Execution error: ${e.message}`),r(e)}),t.on("close",e=>{0===e?a():r(new Error(`Process exited with code ${e}`))})});async function getAnswer(e){const o=[{type:"toggle",name:"shouldProceed",message:`This command will update the ${chalk.blue("create-caspian-app")} package and overwrite all default files. ${chalk.blue("Do you want to proceed")}?`,initial:!1,active:"Yes",inactive:"No"}];e||o.push({type:e=>e?"text":null,name:"versionTag",message:`Enter version tag (e.g., ${chalk.cyan("latest")}, ${chalk.cyan("v4-alpha")}, ${chalk.cyan("1.2.3")}), or press Enter for ${chalk.green("latest")}:`,initial:"latest",validate:e=>!(!e||""===e.trim())||"Version tag cannot be empty"});const n=await prompts(o,{onCancel:()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)}});if(0===Object.keys(n).length)return null;let a="latest";return e?a=e:n.versionTag&&(a=n.versionTag),{shouldProceed:n.shouldProceed,versionTag:a}}const commandsToExecute={update:"npx casp update project"},normalizeVersionTag=e=>{const o=e.startsWith("@")?e.slice(1):e;if(!o.trim())throw new Error("Version tag cannot be empty.");return o},addVersionTag=(e,o)=>{if(e&&e!==o)throw new Error(`Conflicting version tags provided: ${e} and ${o}`);return o},parseCliArgs=e=>{const o=[];let n;for(let a=0;a<e.length;a+=1){const r=e[a];if("-y"!==r){if("--tag"===r||"--version"===r){const o=e[a+1];if(!o||o.startsWith("-"))throw new Error(`Missing value for ${r}.`);n=addVersionTag(n,normalizeVersionTag(o)),a+=1;continue}r.startsWith("--tag=")?n=addVersionTag(n,normalizeVersionTag(r.slice(6))):r.startsWith("--version=")?n=addVersionTag(n,normalizeVersionTag(r.slice(10))):o.push(r)}}if("update"===o[0]&&"project"===o[1]){const e=o.slice(2);if(e.length>1)throw new Error(`Too many arguments for update project: ${e.join(" ")}`);return 1===e.length&&(n=addVersionTag(n,normalizeVersionTag(e[0]))),{formattedCommand:commandsToExecute.update,versionTag:n}}return{formattedCommand:`npx casp ${o.join(" ")}`,versionTag:n}},main=async()=>{if(0===args.length)return console.log("No command provided."),console.log("\nUsage:"),console.log(` ${chalk.cyan("npx casp update project")} - Update to latest version`),console.log(` ${chalk.cyan("npx casp update project beta")} - Update to specific tag`),console.log(` ${chalk.cyan("npx casp update project --tag beta")} - Update to a specific tag via named option`),console.log(` ${chalk.cyan("npx casp update project 1.2.3")} - Update to specific version`),void console.log(` ${chalk.cyan("npx casp update project -y")} - Update without prompts (non-interactive)`);let e;try{e=parseCliArgs(args)}catch(e){return void(e instanceof Error?console.error(chalk.red(e.message)):console.error(chalk.red("Failed to parse command arguments.")))}const{formattedCommand:o,versionTag:n}=e;if(!Object.values(commandsToExecute).includes(o))return console.log("Command not recognized or not allowed."),console.log("\nAvailable commands:"),console.log(` ${chalk.cyan("update project")} - Update project files`),console.log(` ${chalk.cyan("update project beta")} - Update to a specific tag`),console.log(` ${chalk.cyan("update project --tag beta")} - PowerShell-safe tagged update`),void console.log(` ${chalk.cyan("-y")} - Non-interactive mode (skip prompts)`);if(o===commandsToExecute.update)try{let e;if(isNonInteractive)e={shouldProceed:!0,versionTag:n||"latest"},console.log(chalk.blue("Running in non-interactive mode..."));else if(e=await getAnswer(n),!e?.shouldProceed)return void console.log(chalk.red("Operation cancelled by the user."));const o=process.cwd(),a=path.join(o,"caspian.config.json");if(!fs.existsSync(a))return void console.error(chalk.red("The configuration file 'caspian.config.json' was not found in the current directory."));const r=readJsonFile(a),t=e.versionTag||"latest",s=`create-caspian-app@${t}`;console.log(chalk.blue(`\nUpdating to: ${chalk.green(s)}\n`));const c=[r.projectName];r.backendOnly&&c.push("--backend-only"),r.tailwindcss&&c.push("--tailwindcss"),r.prisma&&c.push("--prisma"),r.mcp&&c.push("--mcp"),r.websocket&&c.push("--websocket"),r.typescript&&c.push("--typescript"),isNonInteractive&&c.push("-y"),console.log("Executing command...\n");const i=resolveNpxInvocation([s,...c]);await executeCommand(i.command,i.args),console.log(chalk.green(`\nā Project updated successfully to version ${t}!`)),console.log(chalk.blue("Updated configuration saved to caspian.config.json"))}catch(e){e instanceof Error?e.message.includes("no such file or directory")?console.error(chalk.red("The configuration file 'caspian.config.json' was not found in the current directory.")):console.error(chalk.red(`Error during update: ${e.message}`)):console.error("Error in script execution:",e)}};main().catch(e=>{console.error("Unhandled error in main function:",e)});
|
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);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},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},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},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},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,"/"),a=s.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${a.startsWith("/")?a.substring(1):a}/`}}}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"};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 -p browserSync ${i.join(" ")}`;let a=["projectName"];n.tailwindcss&&a.unshift("tailwind:build"),n.typescript&&!n.backendOnly&&a.unshift("ts:build"),c.build=`npm-run-all ${a.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 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.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.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})=>{copyRecursiveSync(path.join(__dirname,n),path.join(e,s),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})}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:"/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=path.join(__dirname,n),i=path.join(e,t);if(checkExcludeFiles(i))return;if("/pyproject.toml"===n&&updateAnswer?.isUpdate&&fs.existsSync(i))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const c=fs.readFileSync(s,"utf8");fs.writeFileSync(i,c,{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);const i=`# -----------------------------------------------------------------------------\n# Application Runtime\n# -----------------------------------------------------------------------------\n\n# Application environment.\n# Use "development" for local work and "production" for deployed environments.\nAPP_ENV="development"\n\n# Application timezone used by server-side date/time helpers.\nAPP_TIMEZONE="UTC"\n\n# Show detailed errors in the browser.\n# Keep true for development and false in production.\nSHOW_ERRORS="true"\n\n\n# -----------------------------------------------------------------------------\n# Public URL, CORS, and Origin Validation\n# -----------------------------------------------------------------------------\n\n# Canonical public origin for this deployment.\n# Leave empty for local development when the browser URL and app runtime URL match.\n# Set in production when the browser-facing URL differs from the internal URL seen\n# by the application process, such as when running behind an ingress, reverse\n# proxy, load balancer, gateway, edge network, or TLS terminator.\n#\n# Example:\n# APP_BASE_URL="https://yourdomain.com"\nAPP_BASE_URL=""\n\n# Additional browser origins allowed to call protected endpoints such as\n# PulsePoint/Caspian RPC. Use when the same deployed app is reachable from more\n# than one public origin, for example a primary domain plus an alternate domain.\n#\n# Multiple values must be comma-separated with no spaces.\n# Example:\n# CORS_ALLOWED_ORIGINS="https://primary.example.com,https://alternate.example.com"\nCORS_ALLOWED_ORIGINS=""\n\n# Only trust Forwarded/X-Forwarded-* headers when every request reaches the app\n# through trusted infrastructure that removes client-supplied forwarded headers\n# before adding its own. Keep false by default.\n#\n# When false, RPC origin checks use APP_BASE_URL, CORS_ALLOWED_ORIGINS, and the\n# direct request URL instead of trusting spoofable forwarded headers.\nTRUST_FORWARDED_HEADERS="false"\n\n# Allow cookies/Authorization headers on cross-origin requests from allowed\n# origins. Keep true only when credentialed cross-origin requests are required.\nCORS_ALLOW_CREDENTIALS="true"\n\n# Allowed HTTP methods for CORS preflight responses.\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\n\n# Allowed request headers for CORS preflight responses.\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\n\n# Response headers exposed to browser JavaScript.\nCORS_EXPOSE_HEADERS=""\n\n# Browser preflight cache duration in seconds.\nCORS_MAX_AGE="86400"\n\n\n# -----------------------------------------------------------------------------\n# Authentication and Sessions\n# -----------------------------------------------------------------------------\n\n# Authentication secret key for JWT/session signing or encryption.\n# Generate a unique strong value per app and per environment.\nAUTH_SECRET="${generateAuthSecret()}"\n\n# Authentication cookie name.\n# Use a unique value when multiple apps may share the same parent domain.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Session lifetime in hours.\nSESSION_LIFETIME_HOURS="7"\n\n\n# -----------------------------------------------------------------------------\n# RPC and Request Security\n# -----------------------------------------------------------------------------\n\n# Secret key for encrypting or signing function calls.\n# Generate a unique strong value per app and per environment.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Maximum accepted request body size in megabytes.\nMAX_CONTENT_LENGTH_MB="16"\n\n\n# -----------------------------------------------------------------------------\n# Cache\n# -----------------------------------------------------------------------------\n\n# Enable or disable application cache.\nCACHE_ENABLED="false"\n\n# Cache time-to-live in seconds.\nCACHE_TTL="600"\n\n\n# -----------------------------------------------------------------------------\n# Rate Limiting\n# -----------------------------------------------------------------------------\n\n# Default request rate limit.\nRATE_LIMIT_DEFAULT="200 per minute"\n\n# RPC request rate limit.\nRATE_LIMIT_RPC="60 per minute"\n\n# Authentication-related request rate limit.\nRATE_LIMIT_AUTH="60 per minute"\n\n\n# -----------------------------------------------------------------------------\n# Server Process\n# -----------------------------------------------------------------------------\n\n# 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 this at 1 unless the app is designed for multi-process\n# coordination and testing shows a real concurrency bottleneck.\nUVICORN_WORKERS="1"`;if(n.prisma){const n=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,n)}else await createOrUpdateEnvFile(e,i)}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,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,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("--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,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("--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.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"}));const a=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:a.tailwindcss??e.tailwindcss??!1,typescript:a.typescript??e.typescript??!1,mcp:a.mcp??e.mcp??!1,prisma:a.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 a=t[4]??null,r=s[4]??null;return a&&!r?-1:!a&&r?1:a&&r?a.localeCompare(r):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.0","@types/browser-sync":"2.29.1","@types/node":"25.9.1","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"8.0.1","http-proxy-middleware":"4.0.0","npm-run-all":"4.1.5",postcss:"8.5.15","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.3.0",tsx:"4.22.3",typescript:"6.0.3",vite:"8.0.10","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.18","caspian-utils":"0.1.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 a=await fetchPackageVersion("create-caspian-app");t.version=t.version||a,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.136.1","uvicorn==0.47.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.14.3","slowapi==0.1.9","python-multipart==0.0.29","starsessions==2.2.1","httpx==0.28.1","werkzeug==3.1.8","cuid2==2.0.1","nanoid==2.0.0","python-ulid==3.1.0","cuid==0.4","caspian-utils~=0.3"];return e.mcp&&n.push("fastmcp==3.2.4"),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 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 a=buildPythonDependencies(n),r=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 o=r.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...o,...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=")),a=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,o=!1;if(t){const s=process.cwd(),c=path.join(s,"caspian.config.json");if(i&&a){o=!0;const s={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(s,n)}else if(fs.existsSync(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const n=path.join(s,e);fs.existsSync(n)&&a.push(n.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:s};const o={projectName:t,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp};r=await getAnswer(o,n),null!==r&&(updateAnswer={projectName:t,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:s})}else{const s={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(s,n)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},n);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const 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(o){const n=path.join(d,t);fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,await setupStarterKit(u,r),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("--prisma")&&(n.prisma=!0),r={...r,backendOnly:n.backendOnly,tailwindcss:n.tailwindcss,typescript:n.typescript,mcp:n.mcp,prisma:n.prisma};let t=[];n.excludeFiles?.map(e=>{const n=path.join(u,e);fs.existsSync(n)&&t.push(n.replace(/\\/g,"/"))}),updateAnswer={...r,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(r.projectName,{recursive:!0}),u=path.join(d,r.projectName),process.chdir(r.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("http-proxy-middleware"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python"),npmPkg("caspian-utils")];r.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),r.prisma&&execSync(buildManagedNpmCommand(["install","-g","prisma-client-python@latest"]),{stdio:"inherit"}),r.typescript&&!r.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),r.starterKit&&!o&&await setupStarterKit(u,r),await installNpmDependencies(u,m,!0);let y=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.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(r.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.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),a=getPyProjectDependencyNames(u);y=c.filter(e=>a.has(e.toLowerCase())),y.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${y.join(", ")}`))}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),n=bsConfigUrls(e),t={projectName:r.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.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,r,y),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);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 s=e.substring(0,n+8).replace(/\\/g,"\\\\"),t=e.replace(new RegExp(`^${s}`),"").replace(/\\/g,"/");let i=`http://localhost/${t}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),a=t.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${a.startsWith("/")?a.substring(1):a}/`}}}async function updatePackageJson(e,n){const s=path.join(e,"package.json");if(checkExcludeFiles(s))return;const t=JSON.parse(fs.readFileSync(s,"utf8"));t.scripts={...t.scripts,projectName:"tsx settings/project-name.ts"};let i=[];n.tailwindcss&&(t.scripts={...t.scripts,tailwind:"tsx settings/run-postcss.ts watch","tailwind:build":"tsx settings/run-postcss.ts build"},i.push("tailwind")),n.typescript&&!n.backendOnly&&(t.scripts={...t.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&&(t.scripts={...t.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...t.scripts};c.browserSync="tsx settings/bs-config.ts",c.dev=`npm-run-all projectName -p browserSync ${i.join(" ")}`;let a=["projectName"];n.tailwindcss&&a.unshift("tailwind:build"),n.typescript&&!n.backendOnly&&a.unshift("ts:build"),c.build=`npm-run-all ${a.join(" ")}`,t.scripts=c,t.type="module",fs.writeFileSync(s,JSON.stringify(t,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,n,s){const t=fs.existsSync(e),i=t&&fs.statSync(e);if(t&&i&&i.isDirectory()){const t=n.toLowerCase();if(!s.mcp&&t.includes("src\\lib\\mcp"))return;if(!s.websocket&&t.includes("src\\lib\\websocket"))return;if((!s.typescript||s.backendOnly)&&(t.endsWith("\\ts")||t.includes("\\ts\\")))return;if((!s.typescript||s.backendOnly)&&(t.endsWith("\\vite-plugins")||t.includes("\\vite-plugins\\")||t.includes("\\vite-plugins")))return;if(s.backendOnly&&t.includes("public\\js")||s.backendOnly&&t.includes("public\\css")||s.backendOnly&&t.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(t=>{copyRecursiveSync(path.join(e,t),path.join(n,t),s)})}else{if(checkExcludeFiles(n))return;const t=n.replace(/\\/g,"/").toLowerCase();if(t.endsWith("/settings/run-vite-watch.ts")&&(!s.typescript||s.backendOnly))return;if(t.endsWith("/ts/tailwind-merge.ts")&&(!s.typescript||s.backendOnly||!s.tailwindcss))return;if(!s.tailwindcss&&(n.includes("globals.css")||n.includes("styles.css")))return;if(!s.mcp&&n.includes("restart-mcp.ts"))return;if(!s.websocket&&n.includes("src\\lib\\websocket"))return;if(s.backendOnly&&nonBackendFiles.some(e=>n.includes(e)))return;if(s.backendOnly&&n.includes("layout.html"))return;if(s.tailwindcss&&n.includes("index.css"))return;if(!s.prisma&&n.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,n,0)}}async function executeCopy(e,n,s){n.forEach(({src:n,dest:t})=>{copyRecursiveSync(path.join(__dirname,n),path.join(e,t),s)})}function modifyLayoutPHP(e,n){const s=path.join(e,"src","app","layout.html");if(!checkExcludeFiles(s))try{let e=fs.readFileSync(s,"utf8"),t="";n.backendOnly||(n.tailwindcss||(t='\n <link href="/css/index.css" rel="stylesheet" />'),t+='\n <script type="module" src="/js/main.js"><\/script>');let i="";n.backendOnly||(i=n.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${t}`:t),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(s,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.html:"),e)}}async function createOrUpdateEnvFile(e,n){const s=path.join(e,".env");checkExcludeFiles(s)||fs.writeFileSync(s,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"),s=path.join(e,"public","js","tailwind-merge.mjs"),t=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs.map"),i=path.join(e,"public","js","bundle-mjs.mjs.map");if(!checkExcludeFiles(s)){if(!fs.existsSync(n))throw new Error(`tailwind-merge bundle not found at ${n}`);fs.mkdirSync(path.dirname(s),{recursive:!0}),fs.copyFileSync(n,s),!checkExcludeFiles(i)&&fs.existsSync(t)&&fs.copyFileSync(t,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 s=e.replace(/\\/g,"/");return n.endsWith("/"+s)||n===s})}async function createDirectoryStructure(e,n){const s=[{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&&s.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),n.typescript&&!n.backendOnly&&s.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const t=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"},{src:"/.vscode",dest:"/.vscode"}];n.typescript&&!n.backendOnly&&t.push({src:"/ts",dest:"/ts"}),s.forEach(({src:n,dest:s})=>{const t=path.join(__dirname,n),i=path.join(e,s);if(checkExcludeFiles(i))return;if("/pyproject.toml"===n&&updateAnswer?.isUpdate&&fs.existsSync(i))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const c=fs.readFileSync(t,"utf8");fs.writeFileSync(i,c,{flag:"w"})}),await executeCopy(e,t,n),n.tailwindcss&&!n.backendOnly&&(n.typescript?writeTailwindTypeScriptMain(e):(copyTailwindMergeBundle(e),writeTailwindMainJs(e))),await updatePackageJson(e,n),!n.tailwindcss&&n.backendOnly||modifyLayoutPHP(e,n);const i=`# -----------------------------------------------------------------------------\n# Application Runtime\n# -----------------------------------------------------------------------------\n\n# Application environment.\n# Use "development" for local work and "production" for deployed environments.\nAPP_ENV="development"\n\n# Application timezone used by server-side date/time helpers.\nAPP_TIMEZONE="UTC"\n\n# Show detailed errors in the browser.\n# Keep true for development and false in production.\nSHOW_ERRORS="true"\n\n# -----------------------------------------------------------------------------\n# Public URL, CORS, and Origin Validation\n# -----------------------------------------------------------------------------\n\n# Canonical public origin for this deployment.\n# Leave empty for local development when the browser URL and app runtime URL match.\n# Set in production when the browser-facing URL differs from the internal URL seen\n# by the application process, such as when running behind an ingress, reverse\n# proxy, load balancer, gateway, edge network, or TLS terminator.\n#\n# Example:\n# APP_BASE_URL="https://yourdomain.com"\nAPP_BASE_URL=""\n\n# Additional browser origins allowed to call protected endpoints such as\n# PulsePoint/Caspian RPC. Use when the same deployed app is reachable from more\n# than one public origin, for example a primary domain plus an alternate domain.\n#\n# Multiple values must be comma-separated with no spaces.\n# Example:\n# CORS_ALLOWED_ORIGINS="https://primary.example.com,https://alternate.example.com"\nCORS_ALLOWED_ORIGINS=""\n\n# Only trust Forwarded/X-Forwarded-* headers when every request reaches the app\n# through trusted infrastructure that removes client-supplied forwarded headers\n# before adding its own. Keep false by default.\n#\n# When false, RPC origin checks use APP_BASE_URL, CORS_ALLOWED_ORIGINS, and the\n# direct request URL instead of trusting spoofable forwarded headers.\nTRUST_FORWARDED_HEADERS="false"\n\n# Allow cookies/Authorization headers on cross-origin requests from allowed\n# origins. Keep true only when credentialed cross-origin requests are required.\nCORS_ALLOW_CREDENTIALS="true"\n\n# Allowed HTTP methods for CORS preflight responses.\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\n\n# Allowed request headers for CORS preflight responses.\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\n\n# Response headers exposed to browser JavaScript.\nCORS_EXPOSE_HEADERS=""\n\n# Browser preflight cache duration in seconds.\nCORS_MAX_AGE="86400"\n\n# -----------------------------------------------------------------------------\n# Authentication and Sessions\n# -----------------------------------------------------------------------------\n\n# Authentication secret key for JWT/session signing or encryption.\n# Generate a unique strong value per app and per environment.\nAUTH_SECRET="${generateAuthSecret()}"\n\n# Authentication cookie name.\n# Use a unique value when multiple apps may share the same parent domain.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Session lifetime in hours.\nSESSION_LIFETIME_HOURS="7"\n\n# -----------------------------------------------------------------------------\n# RPC and Request Security\n# -----------------------------------------------------------------------------\n\n# Secret key for encrypting or signing function calls.\n# Generate a unique strong value per app and per environment.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# 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. Defaults to 16 if unset.\nMAX_CONTENT_LENGTH_MB="16"\n\n# -----------------------------------------------------------------------------\n# Cache\n# -----------------------------------------------------------------------------\n\n# Enable or disable application cache.\nCACHE_ENABLED="false"\n\n# Cache time-to-live in seconds.\nCACHE_TTL="600"\n\n# -----------------------------------------------------------------------------\n# Rate Limiting\n# -----------------------------------------------------------------------------\n\n# Default request rate limit.\nRATE_LIMIT_DEFAULT="200 per minute"\n\n# RPC request rate limit.\nRATE_LIMIT_RPC="60 per minute"\n\n# Authentication-related request rate limit.\nRATE_LIMIT_AUTH="60 per minute"\n\n# -----------------------------------------------------------------------------\n# Server Process\n# -----------------------------------------------------------------------------\n\n# 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 this at 1 unless the app is designed for multi-process\n# coordination and testing shows a real concurrency bottleneck.\nUVICORN_WORKERS="1"`;if(n.prisma){const n=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,n)}else await createOrUpdateEnvFile(e,i)}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 s=null;if(STARTER_KITS[n]&&(s=STARTER_KITS[n]),s){const t={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:s.features.backendOnly??!1,tailwindcss:s.features.tailwindcss??!1,prisma:s.features.prisma??!1,mcp:s.features.mcp??!1,websocket:s.features.websocket??!1,typescript:s.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(t.backendOnly=!0),i.includes("--tailwindcss")&&(t.tailwindcss=!0),i.includes("--mcp")&&(t.mcp=!0),i.includes("--websocket")&&(t.websocket=!0),i.includes("--prisma")&&(t.prisma=!0),i.includes("--typescript")&&(t.typescript=!0),t}if(e.starterKitSource){const s={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,websocket:!1,typescript:!1},t=process.argv.slice(2);return t.includes("--backend-only")&&(s.backendOnly=!0),t.includes("--tailwindcss")&&(s.tailwindcss=!0),t.includes("--mcp")&&(s.mcp=!0),t.includes("--websocket")&&(s.websocket=!0),t.includes("--prisma")&&(s.prisma=!0),t.includes("--typescript")&&(s.typescript=!0),s}}const s=[];e.projectName||s.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||s.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(s,{onCancel:t}),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 a=await prompts(c,{onCancel:t});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:a.tailwindcss??e.tailwindcss??!1,typescript:a.typescript??e.typescript??!1,mcp:a.mcp??e.mcp??!1,websocket:a.websocket??e.websocket??!1,prisma:a.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,n,s=!1){console.log("Uninstalling Node dependencies:"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=buildManagedNpmCommand(["uninstall",s?"--save-dev":"--save",...n]);execSync(t,{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,s)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>{try{const e=JSON.parse(t);n(e["dist-tags"].latest)}catch(e){s(new Error("Failed to parse JSON response"))}})}).on("error",e=>s(e))})}const readJsonFile=e=>{const n=fs.readFileSync(e,"utf8");return JSON.parse(n)};function compareVersions(e,n){const s=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),t=n.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!s||!t)return e.localeCompare(n);const i=s.slice(1,4).map(Number),c=t.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 a=s[4]??null,r=t[4]??null;return a&&!r?-1:!a&&r?1:a&&r?a.localeCompare(r):0}function getInstalledPackageInfo(e){try{const n=execSync(buildManagedNpmCommand(["list","-g",e,"--depth=0"])).toString(),s=n.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return s?{version:s[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(),s=`${path.sep}_npx${path.sep}`.toLowerCase();return n.includes(s)}async function installNpmDependencies(e,n,s=!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((s?"Installing development dependencies":"Installing dependencies")+":"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=buildManagedNpmCommand(["install",...s?["--save-dev"]:[],...n]);execSync(t,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.3.1","@types/browser-sync":"2.29.1","@types/node":"26.0.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"8.0.2","npm-run-all":"4.1.5",postcss:"8.5.15","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.3.1",tsx:"4.22.4",typescript:"6.0.3",vite:"8.0.10","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.19","caspian-utils":"0.1.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 s=n;if("win32"===globalThis.process?.platform&&("EPERM"===s.code||"EACCES"===s.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 s=null;if(STARTER_KITS[n.starterKit]?s=STARTER_KITS[n.starterKit]:n.starterKitSource&&(s={id:n.starterKit,name:`Custom Starter Kit (${n.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:n.starterKitSource}}),s){if(console.log(chalk.green(`Setting up ${s.name}...`)),s.source)try{const t=s.source.branch?`git clone -b ${s.source.branch} --depth 1 ${s.source.url} "${e}"`:`git clone --depth 1 ${s.source.url} "${e}"`;execSync(t,{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 s=JSON.parse(fs.readFileSync(i,"utf8")),t=e,c=bsConfigUrls(t);s.projectName=n.projectName,s.projectRootPath=t,s.bsTarget=c.bsTarget,s.bsPathRewrite=c.bsPathRewrite;const a=await fetchPackageVersion("create-caspian-app");s.version=s.version||a,fs.writeFileSync(i,JSON.stringify(s,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}s.customSetup&&await s.customSetup(e,n),console.log(chalk.green(`ā ${s.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 s=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");s&&console.log(chalk.magenta(` Features: ${s}`)),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,s){const t=spawnSync(e,n,{cwd:s,stdio:"inherit",shell:!1,encoding:"utf8"});if(t.error)throw t.error;if(0!==t.status)throw new Error(`Command failed (${e} ${n.join(" ")}), exit=${t.status}`)}function tryRunCmd(e,n,s){const t=spawnSync(e,n,{cwd:s,stdio:"ignore",shell:!1,encoding:"utf8"});return!t.error&&0===t.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 s of n)if(tryRunCmd(s.cmd,s.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 s of n)if(tryRunCmd(s.cmd,[...s.argsPrefix,"--version"],e))return s;if(tryInstallUv(e))for(const s of n)if(tryRunCmd(s.cmd,[...s.argsPrefix,"--version"],e))return s;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.138.0","uvicorn==0.49.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","httpx==0.28.1","werkzeug==3.1.8","cuid2==2.0.1","nanoid==2.0.0","python-ulid==3.1.0","cuid==0.4","caspian-utils~=0.3"];return e.mcp&&n.push("fastmcp==3.2.4"),e.websocket&&n.push("websockets==16.0.0"),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 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 s=fs.readFileSync(n,"utf8").replace(/\r\n/g,"\n").match(/^[ \t]*dependencies[ \t]*=[ \t]*\[([\s\S]*?)\]/m);if(!s)return new Set;const t=new Set,i=/"([^"]+)"/g;let c;for(;null!==(c=i.exec(s[1]));){const e=c[1].trim().match(/^([A-Za-z0-9._-]+)/)?.[1];e&&t.add(e.toLowerCase())}return t}function ensurePyProjectExists(e){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))throw new Error(`pyproject.toml not found at: ${n}`);let s=fs.readFileSync(n,"utf8");s=s.replace(/\r\n/g,"\n"),s.includes("package = false")||(s=s.includes("[tool.uv]")?s.replace("[tool.uv]","[tool.uv]\npackage = false"):`${s.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(n,s,"utf8")}async function ensurePythonVenvAndDeps(e,n,s=[]){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 t=path.join(e,"requirements.txt");fs.existsSync(t)&&(fs.unlinkSync(t),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 a=buildPythonDependencies(n),r=a.map(e=>getPythonRequirementName(e)).filter(e=>null!==e);s.length>0&&(console.log(chalk.blue("Removing obsolete Python dependencies via uv remove...")),runCmd(i.cmd,[...i.argsPrefix,"remove",...s],e));const o=r.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...o,...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 s=e[0];const t=e.find(e=>e.startsWith("--starter-kit=")),i=t?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),a=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,o=!1;if(s){const t=process.cwd(),c=path.join(t,"caspian.config.json");if(i&&a){o=!0;const t={projectName:s,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};r=await getAnswer(t,n)}else if(fs.existsSync(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const n=path.join(t,e);fs.existsSync(n)&&a.push(n.replace(/\\/g,"/"))}),updateAnswer={projectName:s,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:a??[],filePath:t};const o={projectName:s,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp,websocket:e.includes("--websocket")||(i.websocket??!1)};r=await getAnswer(o,n),null!==r&&(updateAnswer={projectName:s,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,websocket:r.websocket,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:t})}else{const t={projectName:s,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};r=await getAnswer(t,n)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},n);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const 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(s)if(o){const n=path.join(d,s);fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,await setupStarterKit(u,r),process.chdir(u);const t=path.join(u,"caspian.config.json");if(fs.existsSync(t)){const n=JSON.parse(fs.readFileSync(t,"utf8"));e.includes("--backend-only")&&(n.backendOnly=!0),e.includes("--tailwindcss")&&(n.tailwindcss=!0),e.includes("--typescript")&&(n.typescript=!0),e.includes("--mcp")&&(n.mcp=!0),e.includes("--websocket")&&(n.websocket=!0),e.includes("--prisma")&&(n.prisma=!0),r={...r,backendOnly:n.backendOnly,tailwindcss:n.tailwindcss,typescript:n.typescript,mcp:n.mcp,websocket:n.websocket??!1,prisma:n.prisma};let s=[];n.excludeFiles?.map(e=>{const n=path.join(u,e);fs.existsSync(n)&&s.push(n.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:s??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),n=path.join(d,s),t=path.join(n,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(n)&&fs.existsSync(t)?(u=n,process.chdir(n)):(fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,process.chdir(n))}else fs.mkdirSync(r.projectName,{recursive:!0}),u=path.join(d,r.projectName),process.chdir(r.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")];r.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),r.prisma&&execSync(buildManagedNpmCommand(["install","-g","prisma-client-python@latest"]),{stdio:"inherit"}),r.typescript&&!r.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),r.starterKit&&!o&&await setupStarterKit(u,r),await installNpmDependencies(u,m,!0);let y=[];if(s||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],n=[],s=e=>{try{const n=path.join(u,"package.json");if(fs.existsSync(n)){const s=JSON.parse(fs.readFileSync(n,"utf8"));return!!(s.dependencies&&s.dependencies[e]||s.devDependencies&&s.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 t=path.join(u,"public","js","tailwind-merge.mjs");fs.existsSync(t)&&(fs.unlinkSync(t),console.log(`${t} 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=>{s(n)&&e.push(n)}),n.push("tailwind-merge")}if(r.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=>{s(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 t=path.join(u,"settings","vite-plugins");fs.existsSync(t)&&(fs.rmSync(t,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(n=>{s(n)&&e.push(n)})}const t=e=>Array.from(new Set(e)),i=t(e);i.length>0&&(console.log(`Uninstalling npm packages: ${i.join(", ")}`),await uninstallNpmDependencies(u,i,!0));const c=t(n),a=getPyProjectDependencyNames(u);y=c.filter(e=>a.has(e.toLowerCase())),y.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${y.join(", ")}`))}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),n=bsConfigUrls(e),s={projectName:r.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,websocket:r.websocket,prisma:r.prisma,typescript:r.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(s,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,r,y),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();
|