create-caspian-app 0.2.0-beta.71 → 0.2.0-beta.73

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.
@@ -3,6 +3,12 @@
3
3
  - Read `AGENTS.md` before working in `main.py`, `src/lib/**`, `.venv/Lib/site-packages/casp/**`, `public/js/**`, `prisma/**`, or `node_modules/caspian-utils/dist/docs/**`.
4
4
  - Keep repo-wide always-on Copilot guidance in this file. Use `.github/instructions/**/*.instructions.md` for narrower task-, file-, library-, or implementation-specific guidance when that extra context should not load on every request.
5
5
 
6
+ ## Document Ownership
7
+
8
+ - This file owns repo-wide always-on rules for the workspace.
9
+ - `AGENTS.md` should focus on task routing, runtime cross-checking, and packaged-doc maintenance rather than repeating full rule blocks from this file.
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
+
6
12
  ## Global Rules
7
13
 
8
14
  - 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.
@@ -33,7 +39,8 @@
33
39
  - 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.
34
40
  - 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 />`.
35
41
  - 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.
36
- - 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. Do not place route HTML in `index.py`; use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
42
+ - 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 synchronous 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.
43
+ - When the user asks for a dashboard, admin area, account area, or any grouped child-route section, follow the same mental model as the Next.js App Router: create a parent folder with `layout.html`, add `layout.py` only when that section needs shared synchronous props or metadata, and place the child routes beneath it. Use a normal folder such as `dashboard/` when the segment should appear in the URL, and use `(group)/` only when it should not.
37
44
  - When a single route needs to affect a wrapping layout, have `page()` return `(render_page(__file__, page_context), {"dashboard_body_class": ...})` and consume that value as `[[ layout.dashboard_body_class ]]` in `layout.html`. Use `layout.py` when the same prop should apply across a whole subtree.
38
45
  - For file uploads and file-manager flows, keep browser interaction in route templates, keep upload and delete `@rpc()` actions in the owning `src/app/**/index.py`, keep shared storage and persistence helpers in `src/lib/**`, store metadata in Prisma, and store browser-accessible blobs under `public/uploads/**` when the files should be served directly.
39
46
  - Local upload helpers should create `public/uploads` on demand when it does not exist yet; do not assume the folder is committed ahead of time.
@@ -45,7 +52,7 @@
45
52
  - Dynamic route params currently reach `page()` as a single positional `dict`, with query params injected by name and `request` injected by keyword when declared.
46
53
  - In `layout.py`, return a dict for standard `[[ layout.* ]]` props. Use `render_layout(__file__, {...})` only when that layout should consume direct local variables such as `[[ my_class ]]` instead of `[[ layout.my_class ]]`.
47
54
  - Do not assume `StateManager` survives across requests unless `request.state.session` is explicitly bridged from `request.session`.
48
- - Route, layout, and component HTML templates must keep a single top-level lowercase HTML element so Caspian can inject `pp-component`. Think React-style single parent wrapper: good one root containing the markup and any owned PulsePoint script, bad sibling top-level tags.
55
+ - Route, layout, and component HTML templates must keep exactly one authored top-level parent node so Caspian can inject `pp-component` after component expansion. In source, that parent may be a native HTML element or a single imported `x-*` component tag, but it must resolve to one final HTML root. Keep any owned PulsePoint script inside that same parent, and keep top-of-file `<!-- @import ... -->` directives above it.
49
56
 
50
57
  ## BrowserSync URL Source Of Truth
51
58
 
@@ -94,13 +101,15 @@
94
101
  ### `src/app/**/*.html`
95
102
 
96
103
  - Keep route templates and layouts server-rendered first, with PulsePoint enhancement as the default interactive layer.
104
+ - 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.
97
105
  - When a route renders UI, author that markup in the route's `index.html` even if the route also has an `index.py` companion.
98
106
  - When route templates import reusable Python components, render them as kebab-cased `x-*` tags such as `<x-button />` after a top-level `<!-- @import Button from "..." -->` directive.
99
107
  - For route-level reactivity, prefer PulsePoint state, effects, refs, and template directives together with `pp.rpc(...)` instead of manual DOM mutation or ad hoc browser fetch code.
100
108
  - Preserve Caspian template syntax such as `[[...]]` in layouts and `pp-*` runtime attributes in rendered HTML.
101
109
  - Do not author `pp-component="..."` manually in route or layout templates; the Python render pipeline injects it onto the single root element.
102
110
  - Do not author `type="text/pp"` manually in route or layout templates either. Use plain `<script>` in source and let the render path rewrite it.
103
- - Keep authored route and layout templates to one top-level lowercase HTML root element, the same constraint used for component templates. If a script is needed, keep it inside that root instead of as a sibling top-level node.
111
+ - Keep authored route and layout templates to exactly one top-level parent node, the same constraint used for component templates. In source, that parent may be a native HTML element or a single imported `x-*` component tag. If a script is needed, keep it inside that parent instead of as a sibling top-level node.
112
+ - 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.
104
113
  - For upload managers and similar interactive lists, prefer `pp.state(...)` plus `pp-for` over manual DOM painting so rerenders keep the list stable.
105
114
  - Do not assume React, Vue, JSX-first component syntax, HTMX, or another frontend runtime unless the user explicitly requests one.
106
115
 
package/dist/AGENTS.md CHANGED
@@ -8,6 +8,12 @@ When you work here, use `caspian.config.json` and the code that actually runs as
8
8
 
9
9
  Do not treat the existence of a packaged doc as proof that the feature is enabled in this project.
10
10
 
11
+ ## Document Ownership
12
+
13
+ - Keep repo-wide always-on rules in `.github/copilot-instructions.md`.
14
+ - Keep this file focused on decision order, task routing, workspace-specific clarifications, and packaged-doc maintenance.
15
+ - Keep packaged docs under `node_modules/caspian-utils/dist/docs/` framework-oriented and use `core-runtime-map.md` when those docs need to point AI back to `main.py` or the installed `casp` runtime.
16
+
11
17
  ## Decision Order
12
18
 
13
19
  Use this order depending on the question being answered:
@@ -50,53 +56,14 @@ Important rules:
50
56
  - confirm the current `local`, `external`, `ui`, and `uiExternal` values in `./settings/bs-config.json` before suggesting a browser URL, route test URL, or BrowserSync UI URL
51
57
  - when frontend console logs, network errors, or terminal output suggest the app is being tested through the wrong URL or proxy port, re-check `./settings/bs-config.json` before changing app code
52
58
 
53
- ## Workspace Rules
59
+ ## Workspace Clarifications
60
+
61
+ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. This file keeps only the workspace-specific retrieval and maintenance notes that help AI decide where to look next.
54
62
 
55
63
  - Local Caspian docs live under `node_modules/caspian-utils/dist/docs/`.
56
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.
57
- - `main.py` is the application entry point and owns auth bootstrap, FastAPI setup, static asset routes, route registration, error handlers, cache defaults, and middleware wiring.
58
- - Middleware is added in this source order: `RPCMiddleware`, `AuthMiddleware`, `CSRFMiddleware`, `SessionMiddleware`.
59
- - Effective request order is therefore: `SessionMiddleware -> CSRFMiddleware -> AuthMiddleware -> RPCMiddleware`.
60
- - App-level auth policy lives in `src/lib/auth/auth_config.py`.
61
- - `main.py` applies auth settings with `configure_auth(build_auth_settings())` and registers `GithubProvider()` plus `GoogleProvider()`.
62
- - In the app-owned starter config used in this workspace, routes start public because `src/lib/auth/auth_config.py` sets `is_all_routes_private=False` by default.
63
- - Choose route privacy mode in `src/lib/auth/auth_config.py` at app setup time: use `is_all_routes_private=True` when most routes should require auth, otherwise keep `is_all_routes_private=False` and list only the protected routes in `private_routes`.
64
- - In all-private mode, treat `public_routes` as the exception list. The runtime defaults keep `/` public and keep `auth_routes=["/signin", "/signup"]` public.
65
- - `token_auto_refresh` does not make routes private in the current app; it only affects sliding-session refresh if `auth.refresh_session()` is called.
66
- - Prefer logout via auth-protected RPC from page-level or component-level UI: `pp.rpc("signout")` backed by `@rpc(require_auth=True)`. Use a dedicated signout route only for plain form POST or no-JavaScript edge cases.
67
- - Use PulsePoint as the default reactive frontend layer for app UI.
68
- - 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 expressions and scripts may call global `twMerge(...)` directly.
69
- - For CRUD operations and any browser-initiated reads from the backend, use server `@rpc()` actions and client `pp.rpc(...)` calls unless the user explicitly asks for another integration pattern.
70
- - When a single route needs to affect a wrapping layout, have `page()` return `(render_page(__file__, page_context), {"dashboard_body_class": ...})` and consume that value as `[[ layout.dashboard_body_class ]]` in `layout.html`. Use `layout.py` when the same prop should apply across an entire subtree.
71
- - Protect customized `src/lib/auth/auth_config.py` from framework updates by adding `./src/lib/auth/auth_config.py` to `excludeFiles` in `caspian.config.json`.
72
- - This workspace already has an app-owned Python database layer in `src/lib/prisma/`.
73
- - Do not assume `src/lib/mcp/**`, `settings/restart-mcp.ts`, or MCP-related scripts exist unless `caspian.config.json` confirms MCP is enabled and the update workflow has run.
74
- - Treat `package.json` scripts as opt-in operational commands, not default validation steps for ordinary source edits.
75
- - Reuse `src/lib/prisma/prisma`, `PrismaClient`, generated models, and helper types instead of creating a second Python database abstraction.
76
- - Prisma schema source of truth is `prisma/schema.prisma`.
77
- - The schema-change workflow is: `npx prisma migrate dev`; if seed flow or `prisma/seed.ts` is involved, run `npx prisma generate` and then `npx prisma db seed`; then run `npx ppy generate`.
78
- - `npx ppy generate` owns `src/lib/prisma/__init__.py`, `src/lib/prisma/db.py`, `src/lib/prisma/models.py`, and `settings/prisma-schema.json`; do not hand-edit those generated files.
79
- - `caspian.config.json` is the first config file to check for enabled workspace features. Read the actual values instead of inferring them from the docs.
80
- - As the app grows, prefer `src/components/` for reusable rendered UI and `src/lib/` for reusable non-UI support code.
81
- - PulsePoint runtime code is shipped in `public/js/pp-reactive-v2.js` and loaded from `public/js/main.js`.
82
- - Treat imported Python components as HTML-first `x-*` tags in authored templates, such as `<x-button />` or `<x-command-dialog />`.
83
- - `pp-component` is injected by the Python render pipeline onto page, layout, and component roots; authored route and component templates should not add it manually.
84
- - `main.py` runs `transform_scripts(...)`, so authored body `<script>` tags are rewritten to `<script type="text/pp">` in rendered HTML; route, layout, and component templates should write plain `<script>` in source.
85
- - Route and component HTML templates must keep exactly one top-level lowercase HTML element so Caspian can inject `pp-component`. Think React-style single parent wrapper: good `<div>...</div>` with any owned script inside that same root, bad sibling top-level tags such as `<div>...</div><script ...></script>`.
86
- - File-manager flows in this repo should keep upload and delete `@rpc()` actions in the owning `src/app/**/index.py`, keep shared filesystem and Prisma helpers in `src/lib/**`, persist metadata in Prisma, and store browser-accessible blobs under `public/uploads/**` when they should be served directly.
87
- - Local upload helpers should create `public/uploads` on demand if the directory does not exist yet.
88
- - When `npm run dev` is intentionally running, let that long-running stack own generated outputs such as `public/css/styles.css`, `settings/component-map.json`, `settings/files-list.json`, `__pycache__/`, and `.pyc` files. Treat those as generated artifacts, not authored source.
89
- - `settings/component-map.json` and `settings/files-list.json` are generated by `settings/component-map.ts` and `settings/files-list.ts` through the dev and build pipelines. Analyze them when needed, but do not hand-edit them.
90
- - `settings/bs-config.ts` should keep the public-root-relative upload directory name `uploads` in `PUBLIC_IGNORE_DIRS` so runtime uploads under `public/uploads/**` do not trigger BrowserSync reloads during the local stack.
91
- - In the current router inside `main.py`, path params are passed to `page()` as the first positional `dict` argument.
92
- - Matching query params can still be injected by name, and `request` is injected by keyword when declared.
93
- - The installed `casp.layout` runtime calls `layout()` synchronously. Keep async I/O in `page()` or `@rpc()`.
94
- - In `layout.py`, return a dict for standard `[[ layout.* ]]` props. Use `render_layout(__file__, {...})` only when that layout should consume direct local variables such as `[[ my_class ]]` instead of `[[ layout.my_class ]]`.
95
- - `StateManager` reads and writes `request.state.session`, but the current middleware stack in `main.py` does not mirror `request.session` into `request.state.session`.
96
- - Do not assume `StateManager` persistence survives across requests until that bridge exists.
97
- - Route HTML caching uses `caches/` and `caches/cache_manifest.json` through `casp.cache_handler`.
98
- - The current app tree has root templates in `src/app/`, and route-specific `index.py` files belong only where routes need server-side logic, metadata, or non-visual handling such as uploads or redirects.
99
- - For route creation, keep page markup in `src/app/**/index.html`. If a route is UI-only, stop there. 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. Do not place route HTML in `index.py`; use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
65
+ - Use `node_modules/caspian-utils/dist/docs/core-runtime-map.md` when a behavior is controlled by `main.py` or `.venv/Lib/site-packages/casp/**` and the owning file is not obvious yet.
66
+ - Before updating docs, verify runtime-specific claims such as middleware order, route param injection, `layout()` sync behavior, and `StateManager` persistence against the current `main.py` and installed `casp` package rather than copying older notes.
100
67
 
101
68
  ## Task Routing
102
69
 
@@ -106,6 +73,7 @@ Use this map before making changes.
106
73
  | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
107
74
  | Project layout and file placement | `node_modules/caspian-utils/dist/docs/index.md`, `node_modules/caspian-utils/dist/docs/project-structure.md` | current workspace tree |
108
75
  | Feature availability and tooling switches | `caspian.config.json` | current workspace tree, `main.py`, `prisma/**`, `public/js/**` |
76
+ | Framework internals and core-file lookup | `node_modules/caspian-utils/dist/docs/core-runtime-map.md` | `main.py`, `.venv/Lib/site-packages/casp/**`, matching feature docs |
109
77
  | Library-specific and task-specific rules | matching `.github/instructions/**/*.instructions.md` files | `caspian.config.json`, current workspace tree, owning app and lib files |
110
78
  | MCP server layout and launch flow | `node_modules/caspian-utils/dist/docs/mcp.md` | `settings/restart-mcp.ts`, `package.json`, `src/lib/mcp/**` |
111
79
  | Routing, layouts, metadata | `node_modules/caspian-utils/dist/docs/routing.md` | `main.py`, `.venv/Lib/site-packages/casp/layout.py` |
@@ -117,45 +85,6 @@ Use this map before making changes.
117
85
  | Validation | `node_modules/caspian-utils/dist/docs/validation.md` | `.venv/Lib/site-packages/casp/validate.py` |
118
86
  | Database and seed flow | `node_modules/caspian-utils/dist/docs/database.md` | `prisma/schema.prisma`, `prisma/seed.ts`, `src/lib/prisma/**` |
119
87
 
120
- ## Editing Rules
121
-
122
- - Keep app-owned shared code in `src/lib/**`.
123
- - Keep reusable application UI components in `src/components/**`.
124
- - Keep route-specific logic in `src/app/**`.
125
- - 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. Do not place route HTML in `index.py`; use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
126
- - If a single route only needs to tweak a parent layout, return `(render_page(__file__, ...), {"dashboard_body_class": ...})` from `page()` instead of introducing one-off global state or moving route HTML into `index.py`.
127
- - For file-manager work, keep route-owned upload and delete `@rpc()` actions in `src/app/**/index.py`, keep shared storage and Prisma helper logic in `src/lib/**`, keep uploaded public blobs under `public/uploads/**`, and create that directory on demand in the helper when it is missing.
128
- - When deciding between `src/components/**` and `src/lib/**`, put reusable rendered UI in `src/components/**` and put services, validators, adapters, database helpers, and other non-UI support code in `src/lib/**`.
129
- - Read `caspian.config.json` before deciding whether a Caspian feature should be used, documented, scaffolded, or avoided in the current workspace.
130
- - Treat `caspian.config.json` as the single source of truth for optional features. Do not use feature-specific files, commands, or docs until the corresponding flag is enabled.
131
- - If a feature flag is false and the user wants that feature, ask for confirmation first, then update `caspian.config.json` and run `npx casp update project` so framework-managed files align with the new feature set.
132
- - Do not run `package.json` scripts unless the user explicitly asks for them, the task genuinely requires that exact script, or deployment preparation needs `npm run build`.
133
- - Use `npm run build` for deployment prep or an explicit build request, not as the default validation step for routine route, feature, or documentation edits.
134
- - Never treat `__pycache__/` directories or `.pyc` files as authored source. Do not edit them manually and do not leave them in the final diff if a tool run creates them.
135
- - Never hand-edit `settings/component-map.json` or `settings/files-list.json`. Inspect them if needed, but let the framework regeneration flow update them.
136
- - Treat `.venv/Lib/site-packages/casp/**` as framework internals unless the task is explicitly about Caspian core behavior or installed-runtime documentation.
137
- - When a task involves Python-side database access, reuse `src/lib/prisma/**` instead of introducing a parallel helper, but do not hand-edit generated files in that directory.
138
- - When `prisma/schema.prisma` changes, run `npx prisma migrate dev` first. If seed flow or `prisma/seed.ts` is involved, run `npx prisma generate` and then `npx prisma db seed`. After that, run `npx ppy generate` so the Python ORM layer and `settings/prisma-schema.json` stay aligned.
139
- - Keep auth policy in `src/lib/auth/auth_config.py`.
140
- - Keep auth bootstrap, middleware ordering, provider wiring, and router behavior in `main.py`.
141
- - Treat the app-owned `src/lib/auth/auth_config.py` as the effective default when generating routes for this repo: it starts public-first with `is_all_routes_private=False` unless the app explicitly switches to all-private mode.
142
- - Decide all-private versus mixed public/private routing in `src/lib/auth/auth_config.py` before creating many routes; use all-private mode only when public routes are the minority.
143
- - Keep public exceptions in `public_routes`, keep explicit protected routes in `private_routes` when not using all-private mode, and leave default `auth_routes` alone unless the app explicitly needs custom auth endpoints.
144
- - Do not treat `token_auto_refresh` as the switch for private routes.
145
- - For logout flows, prefer `pp.rpc("signout")` plus `@rpc(require_auth=True)` in page or component UI. Only scaffold a signout route for no-JavaScript, form-post, or full-navigation edge cases.
146
- - When MCP is enabled, keep the app-owned FastMCP server in `src/lib/mcp/mcp_server.py` and the default config in `src/lib/mcp/fastmcp.json`. If those paths move, update `settings/restart-mcp.ts` and the MCP docs together.
147
- - Use PulsePoint as the default reactive frontend and browser-side UI layer unless the user explicitly wants another stack.
148
- - When HTML templates import reusable Python components, render them as kebab-cased `x-*` tags after a top-level `<!-- @import ... -->` directive.
149
- - For CRUD operations and any browser-initiated reads from the backend, use `@rpc()` plus `pp.rpc(...)` as the default contract instead of ad hoc fetch or parallel REST endpoints unless the user explicitly asks for them.
150
- - Treat `pp-component` as a framework-owned attribute on authored templates. Document it, but do not manually add it in normal route or component HTML.
151
- - Treat `type="text/pp"` on PulsePoint scripts as a render-time attribute too. In authored route, layout, and component HTML, write plain `<script>` and let Caspian rewrite it.
152
- - Keep route and component HTML templates to a single top-level lowercase HTML element so the Python side can inject `pp-component` safely. Keep any owned PulsePoint script inside that same root instead of as a sibling top-level node.
153
- - Keep repo-wide always-on Copilot guidance consolidated in `.github/copilot-instructions.md`.
154
- - Use `.github/instructions/**/*.instructions.md` for narrower file-, task-, library-, or implementation-specific instructions, and keep those files tightly scoped instead of duplicating general repo rules.
155
- - When writing docs about route behavior, describe the param passing and layout behavior implemented in the current runtime, not generic upstream assumptions.
156
- - When a runtime change affects documentation, update the matching page in `node_modules/caspian-utils/dist/docs/`.
157
- - When a repo-level rule changes, update this file too.
158
-
159
88
  ## Docs Maintenance Rules
160
89
 
161
90
  - Treat `node_modules/caspian-utils/dist/docs/**` as packaged Caspian feature docs and AI routing docs, not as a snapshot of the current project.
@@ -165,6 +94,7 @@ Use this map before making changes.
165
94
  - Do not record this project's current feature flags, script inventory, or temporary file tree status inside the packaged docs.
166
95
  - Gate optional docs with `caspian.config.json`. Use phrasing such as `when caspian.config.json enables MCP` instead of `this workspace has mcp: false`.
167
96
  - Use the packaged docs to make AI aware of what Caspian can do, when a doc applies, and which project files should be inspected next.
97
+ - Use `core-runtime-map.md` to map packaged docs back to `main.py` and installed `casp` modules instead of restating the full runtime file list in every page.
168
98
  - 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.
169
99
  - 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.
170
100
  - Keep `index.md` and cross-links aligned so AI can discover the right task doc quickly.
@@ -176,4 +106,5 @@ Before merging doc or runtime changes:
176
106
 
177
107
  1. Compare the claim or behavior against `main.py`, `src/lib/**`, and `.venv/Lib/site-packages/casp/**`.
178
108
  2. Update the matching packaged doc in `node_modules/caspian-utils/dist/docs/` if the running behavior changed.
179
- 3. Update this file if the repo-level decision rules, workspace rules, or packaged-doc maintenance rules changed.
109
+ 3. Update `.github/copilot-instructions.md` if the repo-wide implementation rules changed.
110
+ 4. Update this file if the decision order, task routing, workspace clarifications, or packaged-doc maintenance rules changed.
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,"/"),r=s.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${r.startsWith("/")?r.substring(1):r}/`}}}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 r=["projectName"];n.tailwindcss&&r.unshift("tailwind:build"),n.typescript&&!n.backendOnly&&r.unshift("ts:build"),c.build=`npm-run-all ${r.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");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)}}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"}];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=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins allowed for CORS (comma-separated)\nCORS_ALLOWED_ORIGINS=""\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Session & Security\nSESSION_LIFETIME_HOURS="7"\nMAX_CONTENT_LENGTH_MB="16"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;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 r=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:r.tailwindcss??e.tailwindcss??!1,typescript:r.typescript??e.typescript??!1,mcp:r.mcp??e.mcp??!1,prisma:r.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=`npm uninstall ${t?"--save-dev":"--save"} ${n.join(" ")}`;execSync(s,{stdio:"inherit",cwd:e})}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 r=t[4]??null,a=s[4]??null;return r&&!a?-1:!r&&a?1:r&&a?r.localeCompare(a):0}function getInstalledPackageInfo(e){try{const n=execSync(`npm 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("npm init -y",{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=`npm install ${t?"--save-dev":""} ${n.join(" ")}`;execSync(s,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.2.4","@types/browser-sync":"2.29.1","@types/node":"25.6.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.7","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.12","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.2.4",tsx:"4.21.0",typescript:"6.0.3",vite:"8.0.8","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.18","caspian-utils":"0.0.x","tailwind-merge":"3.5.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 r=await fetchPackageVersion("create-caspian-app");t.version=t.version||r,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.46.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.14.3","slowapi==0.1.9","python-multipart==0.0.27","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.2"];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 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 r=buildPythonDependencies(n);t.length>0&&(console.log(chalk.blue("Removing obsolete Python dependencies via uv remove...")),runCmd(i.cmd,[...i.argsPrefix,"remove",...t],e)),console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...r],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=")),r=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let a=null,o=!1;if(t){const s=process.cwd(),c=path.join(s,"caspian.config.json");if(i&&r){o=!0;const s={projectName:t,starterKit:i,starterKitSource:r,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};a=await getAnswer(s,n)}else if(fs.existsSync(c)){const i=readJsonFile(c);let r=[];i.excludeFiles?.map(e=>{const n=path.join(s,e);fs.existsSync(n)&&r.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:r??[],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};a=await getAnswer(o,n),null!==a&&(updateAnswer={projectName:t,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,prisma:a.prisma,typescript:a.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:r??[],filePath:s})}else{const s={projectName:t,starterKit:i,starterKitSource:r,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};a=await getAnswer(s,n)}if(null===a)return void console.log(chalk.red("Installation cancelled."))}else a=await getAnswer({},n);if(null===a)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync("npm uninstall -g create-caspian-app",{stdio:"inherit"}),execSync("npm install -g create-caspian-app",{stdio:"inherit"})):execSync("npm 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,a),process.chdir(u);const s=path.join(u,"caspian.config.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));e.includes("--backend-only")&&(n.backendOnly=!0),e.includes("--tailwindcss")&&(n.tailwindcss=!0),e.includes("--typescript")&&(n.typescript=!0),e.includes("--mcp")&&(n.mcp=!0),e.includes("--prisma")&&(n.prisma=!0),a={...a,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={...a,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),n=path.join(d,t),s=path.join(n,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(n)&&fs.existsSync(s)?(u=n,process.chdir(n)):(fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,process.chdir(n))}else fs.mkdirSync(a.projectName,{recursive:!0}),u=path.join(d,a.projectName),process.chdir(a.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("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")];a.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),a.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),a.prisma&&execSync("npm install -g prisma-client-python@latest",{stdio:"inherit"}),a.typescript&&!a.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),a.starterKit&&!o&&await setupStarterKit(u,a),await installNpmDependencies(u,m,!0);let y=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,a),a.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],n=[],t=e=>{try{const n=path.join(u,"package.json");if(fs.existsSync(n)){const t=JSON.parse(fs.readFileSync(n,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const n=path.join(u,"src","app",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const n=path.join(u,"src","app",e);fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const n=path.join(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"public","js","tailwind-merge.mjs");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${s} was deleted successfully.`));const i=path.join(u,"ts","tailwind-merge.ts");fs.existsSync(i)&&(fs.unlinkSync(i),console.log(`${i} was deleted successfully.`));["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano","tailwind-merge"].forEach(n=>{t(n)&&e.push(n)}),n.push("tailwind-merge")}if(a.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(n){console.warn(chalk.yellow(`Failed to delete ${e}: ${n}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const n=path.join(u,"settings",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),n.push("fastmcp")}if(!updateAnswer.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),r=getPyProjectDependencyNames(u);y=c.filter(e=>r.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:a.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,prisma:a.prisma,typescript:a.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(t,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,a,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},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,"/"),r=s.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${r.startsWith("/")?r.substring(1):r}/`}}}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 r=["projectName"];n.tailwindcss&&r.unshift("tailwind:build"),n.typescript&&!n.backendOnly&&r.unshift("ts:build"),c.build=`npm-run-all ${r.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");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)}}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"}];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=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins allowed for CORS (comma-separated)\nCORS_ALLOWED_ORIGINS=""\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Session & Security\nSESSION_LIFETIME_HOURS="7"\nMAX_CONTENT_LENGTH_MB="16"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;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 r=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:r.tailwindcss??e.tailwindcss??!1,typescript:r.typescript??e.typescript??!1,mcp:r.mcp??e.mcp??!1,prisma:r.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=`npm uninstall ${t?"--save-dev":"--save"} ${n.join(" ")}`;execSync(s,{stdio:"inherit",cwd:e})}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 r=t[4]??null,a=s[4]??null;return r&&!a?-1:!r&&a?1:r&&a?r.localeCompare(a):0}function getInstalledPackageInfo(e){try{const n=execSync(`npm 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("npm init -y",{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=`npm install ${t?"--save-dev":""} ${n.join(" ")}`;execSync(s,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.2.4","@types/browser-sync":"2.29.1","@types/node":"25.6.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.7","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.12","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.2.4",tsx:"4.21.0",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.0.x","tailwind-merge":"3.5.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 r=await fetchPackageVersion("create-caspian-app");t.version=t.version||r,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.46.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.14.3","slowapi==0.1.9","python-multipart==0.0.27","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.2"];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 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 r=buildPythonDependencies(n);t.length>0&&(console.log(chalk.blue("Removing obsolete Python dependencies via uv remove...")),runCmd(i.cmd,[...i.argsPrefix,"remove",...t],e)),console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...r],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=")),r=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let a=null,o=!1;if(t){const s=process.cwd(),c=path.join(s,"caspian.config.json");if(i&&r){o=!0;const s={projectName:t,starterKit:i,starterKitSource:r,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};a=await getAnswer(s,n)}else if(fs.existsSync(c)){const i=readJsonFile(c);let r=[];i.excludeFiles?.map(e=>{const n=path.join(s,e);fs.existsSync(n)&&r.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:r??[],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};a=await getAnswer(o,n),null!==a&&(updateAnswer={projectName:t,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,prisma:a.prisma,typescript:a.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:r??[],filePath:s})}else{const s={projectName:t,starterKit:i,starterKitSource:r,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};a=await getAnswer(s,n)}if(null===a)return void console.log(chalk.red("Installation cancelled."))}else a=await getAnswer({},n);if(null===a)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync("npm uninstall -g create-caspian-app",{stdio:"inherit"}),execSync("npm install -g create-caspian-app",{stdio:"inherit"})):execSync("npm 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,a),process.chdir(u);const s=path.join(u,"caspian.config.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));e.includes("--backend-only")&&(n.backendOnly=!0),e.includes("--tailwindcss")&&(n.tailwindcss=!0),e.includes("--typescript")&&(n.typescript=!0),e.includes("--mcp")&&(n.mcp=!0),e.includes("--prisma")&&(n.prisma=!0),a={...a,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={...a,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),n=path.join(d,t),s=path.join(n,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(n)&&fs.existsSync(s)?(u=n,process.chdir(n)):(fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,process.chdir(n))}else fs.mkdirSync(a.projectName,{recursive:!0}),u=path.join(d,a.projectName),process.chdir(a.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("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")];a.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),a.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),a.prisma&&execSync("npm install -g prisma-client-python@latest",{stdio:"inherit"}),a.typescript&&!a.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),a.starterKit&&!o&&await setupStarterKit(u,a),await installNpmDependencies(u,m,!0);let y=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,a),a.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],n=[],t=e=>{try{const n=path.join(u,"package.json");if(fs.existsSync(n)){const t=JSON.parse(fs.readFileSync(n,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const n=path.join(u,"src","app",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const n=path.join(u,"src","app",e);fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const n=path.join(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"public","js","tailwind-merge.mjs");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${s} was deleted successfully.`));const i=path.join(u,"ts","tailwind-merge.ts");fs.existsSync(i)&&(fs.unlinkSync(i),console.log(`${i} was deleted successfully.`));["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano","tailwind-merge"].forEach(n=>{t(n)&&e.push(n)}),n.push("tailwind-merge")}if(a.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(n){console.warn(chalk.yellow(`Failed to delete ${e}: ${n}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const n=path.join(u,"settings",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),n.push("fastmcp")}if(!updateAnswer.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),r=getPyProjectDependencyNames(u);y=c.filter(e=>r.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:a.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,prisma:a.prisma,typescript:a.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(t,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,a,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();