create-caspian-app 0.2.0-beta.3 ā 0.2.0-beta.31
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 +98 -0
- package/dist/AGENTS.md +113 -0
- package/dist/CLAUDE.md +1 -0
- package/dist/caspian.js +1 -1
- package/dist/index.js +1 -1
- package/dist/main.py +6 -0
- package/dist/public/js/pp-reactive-v2.js +1 -1
- package/dist/pyproject.toml +1 -1
- package/dist/settings/files-list.ts +8 -0
- package/dist/settings/restart-mcp.ts +148 -0
- package/dist/settings/utils.ts +8 -4
- package/dist/src/app/error.html +66 -85
- package/dist/src/lib/mcp/fastmcp.json +15 -0
- package/dist/src/lib/mcp/mcp_server.py +91 -0
- package/package.json +1 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copilot Instructions
|
|
2
|
+
|
|
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
|
+
- Keep all repo-level Copilot guidance in this file. Do not add `.github/instructions/` for this workspace.
|
|
5
|
+
|
|
6
|
+
## Global Rules
|
|
7
|
+
|
|
8
|
+
- Use this source-of-truth order: app runtime and app-owned code first, installed `casp` runtime second, packaged markdown docs third.
|
|
9
|
+
- 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`.
|
|
10
|
+
- For current repo behavior, trust `main.py`, `src/lib/**`, `public/js/**`, `prisma/**`, and `src/app/**` over generic Caspian docs.
|
|
11
|
+
- For framework internals, trust `.venv/Lib/site-packages/casp/**` over generic or older upstream guidance.
|
|
12
|
+
- When docs and runtime disagree, align the docs to the code that actually runs in this workspace.
|
|
13
|
+
- When `prisma/schema.prisma` changes, follow this order: run `npx prisma migrate dev`; if the change affects seed flow or `prisma/seed.ts`, run `npx prisma generate` and then `npx prisma db seed`; then run `npx ppy generate` so the Python ORM stays aligned with the schema.
|
|
14
|
+
- Reuse the existing Python database layer in `src/lib/prisma/**`; do not create a second app-owned database abstraction unless the user explicitly asks for one.
|
|
15
|
+
- Treat `src/lib/prisma/__init__.py`, `src/lib/prisma/db.py`, `src/lib/prisma/models.py`, and `settings/prisma-schema.json` as generated outputs owned by `npx ppy generate`; do not create or hand-edit them manually.
|
|
16
|
+
- When `caspian.config.json` has `mcp: true`, treat `src/lib/mcp/mcp_server.py` as the app-owned FastMCP server and `src/lib/mcp/fastmcp.json` as the default MCP config. Use `npm run mcp` or `fastmcp run src/lib/mcp/fastmcp.json`; do not assume root `fastmcp.json` auto-discovery.
|
|
17
|
+
- Keep auth policy in `src/lib/auth/auth_config.py` and keep auth bootstrap, middleware wiring, and provider registration in `main.py`.
|
|
18
|
+
- Use PulsePoint and `pp.rpc(...)` as the default frontend and client-to-server contract unless the user requests another stack.
|
|
19
|
+
- Treat `pp-component` on routes, layouts, and components, and `type="text/pp"` on owned PulsePoint scripts, as compiler-injected by the Python side; do not add them manually in authored templates unless the task is explicitly about runtime internals.
|
|
20
|
+
- `layout()` is synchronous in the installed runtime. Put async I/O in `page()` or `@rpc()`.
|
|
21
|
+
- Dynamic route params currently reach `page()` as a single positional `dict`, with query params injected by name and `request` injected by keyword when declared.
|
|
22
|
+
- Do not assume `StateManager` survives across requests unless `request.state.session` is explicitly bridged from `request.session`.
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
25
|
+
## Path-Specific Rules
|
|
26
|
+
|
|
27
|
+
### `main.py`
|
|
28
|
+
|
|
29
|
+
- Treat `main.py` as the repo source of truth for FastAPI setup, static asset routes, auth bootstrap, middleware order, route registration, cache defaults, and error handlers.
|
|
30
|
+
- Preserve the effective middleware execution order unless the task explicitly changes request semantics: `SessionMiddleware -> CSRFMiddleware -> AuthMiddleware -> RPCMiddleware`.
|
|
31
|
+
- Document route param behavior exactly as implemented here.
|
|
32
|
+
- Do not use `main.py` alone to infer whether optional features are enabled; confirm that in `caspian.config.json` first.
|
|
33
|
+
|
|
34
|
+
### `src/lib/**/*.py`
|
|
35
|
+
|
|
36
|
+
- Keep `src/lib/` for app-owned shared code, service wrappers, and reusable helpers.
|
|
37
|
+
- Reuse the generated `src/lib/prisma/` package for Python database access, but do not hand-edit files under `src/lib/prisma/`; regenerate them with `npx ppy generate` after schema changes.
|
|
38
|
+
- 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.
|
|
39
|
+
- Keep auth policy in `src/lib/auth/auth_config.py`. Keep auth bootstrap and middleware order changes in `main.py`.
|
|
40
|
+
|
|
41
|
+
### `public/js/main.js`
|
|
42
|
+
|
|
43
|
+
- Treat `public/js/main.js` as the thin browser bootstrap entry point.
|
|
44
|
+
- Keep it minimal and point it at the runtime shipped in `public/js/pp-reactive-v2.js`.
|
|
45
|
+
- Do not duplicate PulsePoint runtime logic here.
|
|
46
|
+
|
|
47
|
+
### `public/js/pp-reactive-v2.js`
|
|
48
|
+
|
|
49
|
+
- Treat `public/js/pp-reactive-v2.js` as the browser-side PulsePoint runtime source of truth for component execution, refs, directives, SPA navigation, and `pp.rpc(...)` behavior.
|
|
50
|
+
- Preserve the current public runtime contract unless the task explicitly changes Caspian frontend behavior.
|
|
51
|
+
- At runtime, component logic is discovered from `script[type="text/pp"]` inside `pp-component` roots. In authored route, layout, and component templates, write plain `<script>` and let `main.py` plus `casp.scripts_type.transform_scripts(...)` add the type.
|
|
52
|
+
|
|
53
|
+
### `src/app/**/*.html`
|
|
54
|
+
|
|
55
|
+
- Keep route templates and layouts server-rendered first, with PulsePoint enhancement as the default interactive layer.
|
|
56
|
+
- Preserve Caspian template syntax such as `[[...]]` in layouts and `pp-*` runtime attributes in rendered HTML.
|
|
57
|
+
- Do not author `pp-component="..."` manually in route or layout templates; the Python render pipeline injects it onto the single root element.
|
|
58
|
+
- 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.
|
|
59
|
+
- 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.
|
|
60
|
+
- Do not assume React, Vue, JSX, HTMX, or another frontend runtime unless the user explicitly requests one.
|
|
61
|
+
|
|
62
|
+
### `prisma/**`
|
|
63
|
+
|
|
64
|
+
- Treat `prisma/schema.prisma` as the data-model source of truth.
|
|
65
|
+
- Treat `prisma.config.ts` as the datasource and migration or seed configuration source of truth.
|
|
66
|
+
- After changing `prisma/schema.prisma`, run `npx prisma migrate dev` first so migrations and the development database stay aligned.
|
|
67
|
+
- If the schema change affects seed data or `prisma/seed.ts`, run `npx prisma generate` and then `npx prisma db seed`.
|
|
68
|
+
- Run `npx ppy generate` after every schema change so the Python ORM files and `settings/prisma-schema.json` stay aligned with Prisma.
|
|
69
|
+
- Keep Node-side generation and seeding aligned with `npx prisma generate` and `prisma/seed.ts`.
|
|
70
|
+
- Keep Python-side database access aligned with `src/lib/prisma/**`, and treat that directory as generated output rather than a manual editing surface.
|
|
71
|
+
|
|
72
|
+
### `.venv/Lib/site-packages/casp/**/*.py`
|
|
73
|
+
|
|
74
|
+
- Treat these files as framework internals.
|
|
75
|
+
- Only change them when the task is explicitly about Caspian core behavior, installed-runtime debugging, or documentation that must match the installed implementation.
|
|
76
|
+
- If behavior changes here, update the matching docs under `node_modules/caspian-utils/dist/docs/`.
|
|
77
|
+
|
|
78
|
+
### `node_modules/caspian-utils/dist/docs/**/*.md`
|
|
79
|
+
|
|
80
|
+
- These files are the local documentation layer, not the runtime. Verify every behavior claim against the actual code that runs.
|
|
81
|
+
- Use this verification order:
|
|
82
|
+
1. `caspian.config.json`, then `main.py`, `src/lib/**`, `public/js/**`, `prisma/**`, `src/app/**`
|
|
83
|
+
2. `.venv/Lib/site-packages/casp/**`
|
|
84
|
+
3. the markdown file being edited
|
|
85
|
+
- Keep repo-specific facts accurate when they matter:
|
|
86
|
+
- `caspian.config.json` is the first config file to read for enabled workspace features and scan directories
|
|
87
|
+
- this workspace already has `src/lib/prisma/**`
|
|
88
|
+
- this workspace's app-owned FastMCP server lives in `src/lib/mcp/mcp_server.py`
|
|
89
|
+
- the default FastMCP config lives in `src/lib/mcp/fastmcp.json`
|
|
90
|
+
- `package.json` starts MCP through `npm run mcp`, which runs `settings/restart-mcp.ts`; manual FastMCP runs should pass the explicit nested config path because root auto-discovery does not find it
|
|
91
|
+
- auth policy lives in `src/lib/auth/auth_config.py`
|
|
92
|
+
- PulsePoint runtime lives in `public/js/pp-reactive-v2.js`
|
|
93
|
+
- `pp-component` is injected by the Python render pipeline, and `main.py` rewrites authored body scripts to `type="text/pp"`; authored route, layout, and component templates should not add those attributes manually
|
|
94
|
+
- route, layout, and component templates must keep a single top-level lowercase HTML root for `pp-component` injection, with any owned plain `<script>` kept inside that same root
|
|
95
|
+
- dynamic route params are passed to `page()` as a single positional `dict`
|
|
96
|
+
- `layout()` is sync-only in the installed runtime
|
|
97
|
+
- `StateManager` persistence depends on `request.state.session`, which is not bridged from `request.session` in the current `main.py`
|
|
98
|
+
- Keep `index.md` and cross-links aligned when adding or changing pages.
|
package/dist/AGENTS.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Caspian Agent Guide
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This workspace is a Caspian application plus a local copy of the packaged Caspian docs.
|
|
6
|
+
|
|
7
|
+
When you work here, use the code that actually runs as the source of truth and use the markdown docs as the routing and explanation layer.
|
|
8
|
+
|
|
9
|
+
## Source Of Truth Order
|
|
10
|
+
|
|
11
|
+
Use this precedence whenever behavior, docs, and generated code disagree:
|
|
12
|
+
|
|
13
|
+
1. App runtime and app-owned code
|
|
14
|
+
- `main.py`
|
|
15
|
+
- `src/app/**`
|
|
16
|
+
- `src/lib/**`
|
|
17
|
+
- `public/js/**`
|
|
18
|
+
- `prisma/**`
|
|
19
|
+
- `caspian.config.json`
|
|
20
|
+
2. Installed Caspian framework runtime
|
|
21
|
+
- `.venv/Lib/site-packages/casp/**`
|
|
22
|
+
3. Packaged Caspian docs in this workspace
|
|
23
|
+
- `node_modules/caspian-utils/dist/docs/**`
|
|
24
|
+
|
|
25
|
+
If the task is about current repo behavior, prefer the app runtime.
|
|
26
|
+
|
|
27
|
+
If the task is about framework internals, prefer the installed `casp` package.
|
|
28
|
+
|
|
29
|
+
If docs differ from either of those, update the docs to match the code that actually runs.
|
|
30
|
+
|
|
31
|
+
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`.
|
|
32
|
+
|
|
33
|
+
## Verified Workspace Facts
|
|
34
|
+
|
|
35
|
+
- Local Caspian docs live under `node_modules/caspian-utils/dist/docs/`.
|
|
36
|
+
- `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.
|
|
37
|
+
- Middleware is added in this source order: `RPCMiddleware`, `AuthMiddleware`, `CSRFMiddleware`, `SessionMiddleware`.
|
|
38
|
+
- Effective request order is therefore: `SessionMiddleware -> CSRFMiddleware -> AuthMiddleware -> RPCMiddleware`.
|
|
39
|
+
- App-level auth policy lives in `src/lib/auth/auth_config.py`.
|
|
40
|
+
- `main.py` applies auth settings with `configure_auth(build_auth_settings())` and registers `GithubProvider()` plus `GoogleProvider()`.
|
|
41
|
+
- This workspace already has an app-owned Python database layer in `src/lib/prisma/`.
|
|
42
|
+
- This workspace now has an app-owned FastMCP server at `src/lib/mcp/mcp_server.py`, with `src/lib/mcp/fastmcp.json` as the default MCP server spec.
|
|
43
|
+
- `package.json` starts MCP through `npm run mcp`, which runs `settings/restart-mcp.ts` and prefers `src/lib/mcp/fastmcp.json` before legacy root configs. Direct FastMCP runs should pass the explicit nested config path.
|
|
44
|
+
- Reuse `src/lib/prisma/prisma`, `PrismaClient`, generated models, and helper types instead of creating a second Python database abstraction.
|
|
45
|
+
- Prisma schema source of truth is `prisma/schema.prisma`.
|
|
46
|
+
- The schema-change workflow in this workspace 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`.
|
|
47
|
+
- `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.
|
|
48
|
+
- `caspian.config.json` is the first config file to check for enabled workspace features. In the current workspace it sets `backendOnly: false`, `tailwindcss: true`, `mcp: true`, `prisma: true`, `typescript: false`, and `componentScanDirs: ["src"]`.
|
|
49
|
+
- PulsePoint runtime code is shipped in `public/js/pp-reactive-v2.js` and loaded from `public/js/main.js`.
|
|
50
|
+
- `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.
|
|
51
|
+
- `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.
|
|
52
|
+
- 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>`.
|
|
53
|
+
- In the current router inside `main.py`, path params are passed to `page()` as the first positional `dict` argument.
|
|
54
|
+
- Matching query params can still be injected by name, and `request` is injected by keyword when declared.
|
|
55
|
+
- The installed `casp.layout` runtime calls `layout()` synchronously. Keep async I/O in `page()` or `@rpc()`.
|
|
56
|
+
- `StateManager` reads and writes `request.state.session`, but the current middleware stack in `main.py` does not mirror `request.session` into `request.state.session`.
|
|
57
|
+
- Do not assume `StateManager` persistence survives across requests until that bridge exists.
|
|
58
|
+
- Route HTML caching uses `caches/` and `caches/cache_manifest.json` through `casp.cache_handler`.
|
|
59
|
+
- The current app tree has root templates in `src/app/` and does not currently include route-specific `index.py` files.
|
|
60
|
+
|
|
61
|
+
## Task Routing
|
|
62
|
+
|
|
63
|
+
Use this map before making changes.
|
|
64
|
+
|
|
65
|
+
| Task area | Read first | Verify against |
|
|
66
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
67
|
+
| 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 |
|
|
68
|
+
| Feature availability and tooling switches | `caspian.config.json` | current workspace tree, `main.py`, `prisma/**`, `public/js/**` |
|
|
69
|
+
| MCP server layout and launch flow | `node_modules/caspian-utils/dist/docs/mcp.md` | `settings/restart-mcp.ts`, `package.json`, `src/lib/mcp/**` |
|
|
70
|
+
| Routing, layouts, metadata | `node_modules/caspian-utils/dist/docs/routing.md` | `main.py`, `.venv/Lib/site-packages/casp/layout.py` |
|
|
71
|
+
| Auth, sessions, RBAC, providers | `node_modules/caspian-utils/dist/docs/auth.md` | `src/lib/auth/auth_config.py`, `main.py`, `.venv/Lib/site-packages/casp/auth.py` |
|
|
72
|
+
| RPC, data loading, streaming, uploads | `node_modules/caspian-utils/dist/docs/fetch-data.md`, `node_modules/caspian-utils/dist/docs/pulsepoint.md` | `.venv/Lib/site-packages/casp/rpc.py`, `public/js/pp-reactive-v2.js`, `main.py` |
|
|
73
|
+
| Server state | `node_modules/caspian-utils/dist/docs/state.md` | `.venv/Lib/site-packages/casp/state_manager.py`, `main.py` |
|
|
74
|
+
| Page caching | `node_modules/caspian-utils/dist/docs/cache.md` | `.venv/Lib/site-packages/casp/cache_handler.py`, `main.py` |
|
|
75
|
+
| Validation | `node_modules/caspian-utils/dist/docs/validation.md` | `.venv/Lib/site-packages/casp/validate.py` |
|
|
76
|
+
| Database and seed flow | `node_modules/caspian-utils/dist/docs/database.md` | `prisma/schema.prisma`, `prisma/seed.ts`, `src/lib/prisma/**` |
|
|
77
|
+
|
|
78
|
+
## Editing Rules
|
|
79
|
+
|
|
80
|
+
- Keep app-owned shared code in `src/lib/**`.
|
|
81
|
+
- Keep route-specific logic in `src/app/**`.
|
|
82
|
+
- Read `caspian.config.json` before deciding whether a Caspian feature should be used, documented, scaffolded, or avoided in the current workspace.
|
|
83
|
+
- Treat `.venv/Lib/site-packages/casp/**` as framework internals unless the task is explicitly about Caspian core behavior or installed-runtime documentation.
|
|
84
|
+
- 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.
|
|
85
|
+
- 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.
|
|
86
|
+
- Keep auth policy in `src/lib/auth/auth_config.py`.
|
|
87
|
+
- Keep auth bootstrap, middleware ordering, provider wiring, and router behavior in `main.py`.
|
|
88
|
+
- 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.
|
|
89
|
+
- Use PulsePoint and `pp.rpc(...)` as the default frontend and browser-to-server contract unless the user explicitly wants another stack.
|
|
90
|
+
- 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.
|
|
91
|
+
- 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.
|
|
92
|
+
- 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.
|
|
93
|
+
- Keep Copilot guidance consolidated in `.github/copilot-instructions.md`; do not add `.github/instructions/` in this workspace.
|
|
94
|
+
- When writing docs about route behavior, describe the param passing and layout behavior implemented in the current runtime, not generic upstream assumptions.
|
|
95
|
+
- When a runtime change affects documentation, update the matching page in `node_modules/caspian-utils/dist/docs/`.
|
|
96
|
+
- When a repo-level rule changes, update this file too.
|
|
97
|
+
|
|
98
|
+
## Docs Alignment Notes
|
|
99
|
+
|
|
100
|
+
The packaged docs in this workspace are already mostly aligned with the installed runtime, but keep these repo-specific clarifications in mind:
|
|
101
|
+
|
|
102
|
+
- `database.md` is the source doc for the Prisma and Python ORM workflow in this repo: schema changes go through `npx prisma migrate dev`, optional `npx prisma generate` plus `npx prisma db seed`, then `npx ppy generate`, and generated ORM files under `src/lib/prisma/` plus `settings/prisma-schema.json` are not hand-edited.
|
|
103
|
+
- `mcp.md` is the source doc for the nested FastMCP config path, the app-owned MCP server location, direct FastMCP command usage, and the workspace runner discovery order.
|
|
104
|
+
- `state.md` is correct to warn that cross-request persistence depends on `request.state.session`, which is not bridged in the current `main.py`.
|
|
105
|
+
- `routing.md`, `components.md`, `auth.md`, `fetch-data.md`, `cache.md`, `pulsepoint.md`, and `validation.md` should continue to be validated against the installed `casp` package before any behavior claims are changed.
|
|
106
|
+
|
|
107
|
+
## Maintenance Checklist
|
|
108
|
+
|
|
109
|
+
Before merging doc or runtime changes:
|
|
110
|
+
|
|
111
|
+
1. Compare the claim or behavior against `main.py`, `src/lib/**`, and `.venv/Lib/site-packages/casp/**`.
|
|
112
|
+
2. Update the matching packaged doc in `node_modules/caspian-utils/dist/docs/` if the running behavior changed.
|
|
113
|
+
3. Update this file if the repo-level source-of-truth rules or workspace facts changed.
|
package/dist/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
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"),readJsonFile=e=>{const o=fs.readFileSync(e,"utf8");return JSON.parse(o)},executeCommand=(e,o=[],n={})=>new Promise((a,
|
|
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)});
|
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 s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+8).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let c=`http://localhost/${n}`;c=c.endsWith("/")?c.slice(0,-1):c;const i=c.replace(/(?<!:)(\/\/+)/g,"/"),a=n.replace(/\/\/+/g,"/");return{bsTarget:`${i}/`,bsPathRewrite:{"^/":`/${a.startsWith("/")?a.substring(1):a}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const n=JSON.parse(fs.readFileSync(t,"utf8"));n.scripts={...n.scripts,projectName:"tsx settings/project-name.ts"};let c=[];s.tailwindcss&&(n.scripts={...n.scripts,tailwind:"postcss src/app/globals.css -o public/css/styles.css --watch","tailwind:build":"postcss src/app/globals.css -o public/css/styles.css"},c.push("tailwind")),s.typescript&&!s.backendOnly&&(n.scripts={...n.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},c.push("ts:watch")),s.mcp&&(n.scripts={...n.scripts,mcp:"tsx settings/restart-mcp.ts"},c.push("mcp"));let i={...n.scripts};i.browserSync="tsx settings/bs-config.ts",i["browserSync:build"]="tsx settings/build.ts",i.dev=`npm-run-all projectName -p browserSync ${c.join(" ")}`;let a=["browserSync:build"];s.tailwindcss&&a.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&a.unshift("ts:build"),i.build=`npm-run-all ${a.join(" ")}`,n.scripts=i,n.type="module",fs.writeFileSync(t,JSON.stringify(n,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const n=fs.existsSync(e),c=n&&fs.statSync(e);if(n&&c&&c.isDirectory()){const n=s.toLowerCase();if(!t.mcp&&n.includes("src\\lib\\mcp"))return;if((!t.typescript||t.backendOnly)&&(n.endsWith("\\ts")||n.includes("\\ts\\")))return;if((!t.typescript||t.backendOnly)&&(n.endsWith("\\vite-plugins")||n.includes("\\vite-plugins\\")||n.includes("\\vite-plugins")))return;if(t.backendOnly&&n.includes("public\\js")||t.backendOnly&&n.includes("public\\css")||t.backendOnly&&n.includes("public\\assets"))return;const c=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(c))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),t)})}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")))return;if(!t.mcp&&s.includes("restart-mcp.ts"))return;if(t.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(t.backendOnly&&s.includes("layout.html"))return;if(t.tailwindcss&&s.includes("index.css"))return;if(!t.prisma&&s.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach(({src:s,dest:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),t)})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.html");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),n="";s.backendOnly||(s.tailwindcss||(n='\n <link href="/css/index.css" rel="stylesheet" />'),n+='\n <script type="module" src="/js/main.js"><\/script>');let c="";s.backendOnly||(c=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${n}`:n),e=e.replace("</head>",`${c}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.html:"),e)}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const t=e.replace(/\\/g,"/");return s.endsWith("/"+t)||s===t})}async function createDirectoryStructure(e,s){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"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),s.typescript&&!s.backendOnly&&t.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const n=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"}];s.typescript&&!s.backendOnly&&n.push({src:"/ts",dest:"/ts"}),t.forEach(({src:s,dest:t})=>{const n=path.join(__dirname,s),c=path.join(e,t);if(checkExcludeFiles(c))return;const i=fs.readFileSync(n,"utf8");fs.writeFileSync(c,i,{flag:"w"})}),await executeCopy(e,n,s),await updatePackageJson(e,s),s.tailwindcss&&modifyPostcssConfig(e),!s.tailwindcss&&s.backendOnly||modifyLayoutPHP(e,s);const c=`# 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(s.prisma){const s=`${'# 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${c}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,c)}async function getAnswer(e={},s=!1){if(s)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 s=e.starterKit;let t=null;if(STARTER_KITS[s]&&(t=STARTER_KITS[s]),t){const n={projectName:e.projectName??"my-app",starterKit:s,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},c=process.argv.slice(2);return c.includes("--backend-only")&&(n.backendOnly=!0),c.includes("--tailwindcss")&&(n.tailwindcss=!0),c.includes("--mcp")&&(n.mcp=!0),c.includes("--prisma")&&(n.prisma=!0),c.includes("--typescript")&&(n.typescript=!0),n}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,typescript:!1},n=process.argv.slice(2);return n.includes("--backend-only")&&(t.backendOnly=!0),n.includes("--tailwindcss")&&(t.tailwindcss=!0),n.includes("--mcp")&&(t.mcp=!0),n.includes("--prisma")&&(t.prisma=!0),n.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 n=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},c=await prompts(t,{onCancel:n}),i=[];c.backendOnly??e.backendOnly??!1?(e.mcp||i.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||i.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||i.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||i.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||i.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||i.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(i,{onCancel:n});return{projectName:c.projectName?String(c.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:c.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,s,t=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let n="";e.on("data",e=>n+=e),e.on("end",()=>{try{const e=JSON.parse(n);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}})}).on("error",e=>t(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.split(".").map(Number),n=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>n[e])return 1;if(t[e]<n[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:(console.error(`Package ${e} is not installed`),null)}catch(e){return console.error(e instanceof Error?e.message:String(e)),null}}async function installNpmDependencies(e,s,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")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const n=`npm install ${t?"--save-dev":""} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.2.2","@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.4","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.9","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.2.2",tsx:"4.21.0",typescript:"6.0.2",vite:"7.3.0","fast-glob":"3.3.3","@lezer/common":"^1.5.1","@lezer/python":"^1.1.18"};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(s){const t=s;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 s}}async function setupStarterKit(e,s){if(!s.starterKit)return;let t=null;if(STARTER_KITS[s.starterKit]?t=STARTER_KITS[s.starterKit]:s.starterKitSource&&(t={id:s.starterKit,name:`Custom Starter Kit (${s.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:s.starterKitSource}}),t){if(console.log(chalk.green(`Setting up ${t.name}...`)),t.source)try{const n=t.source.branch?`git clone -b ${t.source.branch} --depth 1 ${t.source.url} "${e}"`:`git clone --depth 1 ${t.source.url} "${e}"`;execSync(n,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const c=path.join(e,"caspian.config.json");if(fs.existsSync(c))try{const t=JSON.parse(fs.readFileSync(c,"utf8")),n=e,i=bsConfigUrls(n);t.projectName=s.projectName,t.projectRootPath=n,t.bsTarget=i.bsTarget,t.bsPathRewrite=i.bsPathRewrite;const a=await fetchPackageVersion("create-caspian-app");t.version=t.version||a,fs.writeFileSync(c,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,s),console.log(chalk.green(`ā ${t.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\nš Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),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,s,t){const n=spawnSync(e,s,{cwd:t,stdio:"inherit",shell:!1,encoding:"utf8"});if(n.error)throw n.error;if(0!==n.status)throw new Error(`Command failed (${e} ${s.join(" ")}), exit=${n.status}`)}function tryRunCmd(e,s,t){const n=spawnSync(e,s,{cwd:t,stdio:"ignore",shell:!1,encoding:"utf8"});return!n.error&&0===n.status}function tryInstallUv(e){console.log(chalk.blue("uv not found. Attempting to install uv..."));const s=[{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 s)if(tryRunCmd(t.cmd,t.args,e))return!0;return!1}function resolveUvCommand(e){const s=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const t of s)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;if(tryInstallUv(e))for(const t of s)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,s){const t=["fastapi>=0.110,<0.128","uvicorn>=0.27,<0.40","python-dotenv>=1.0,<2.0","jinja2>=3.1,<4.0","beautifulsoup4>=4.12,<5.0","slowapi>=0.1,<0.2","python-multipart>=0.0.9,<0.1","starsessions>=1.3,<2.2","httpx>=0.27,<0.29","werkzeug>=3.0,<4.0","cuid2>=2.0,<3.0","nanoid>=2.0,<3.0","python-ulid>=2.7,<3.1","cuid>=0.4.0,<0.5.0",s];return e.tailwindcss&&t.push("tailwind-merge>=0.3.3,<0.4.0"),e.mcp&&t.push("fastmcp<3"),e.prisma&&(t.push("psycopg2-binary>=2.9,<3.0"),t.push("asyncpg>=0.31.0,<1.0"),t.push("aiosqlite>=0.22.1,<1.0"),t.push("aiomysql>=0.3.2,<1.0")),t}function updatePyProjectDependencies(e,s,t){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))throw new Error(`pyproject.toml not found at: ${n}`);const c=buildPythonDependencies(s,t).map(e=>` "${e}",`).join("\n");let i=fs.readFileSync(n,"utf8");const a=/\[project\]([\s\S]*?)(?=\n\[[^\]]+\]|$)/m,r=i.match(a);if(!r){i=`${`[project]\nname = "${path.basename(e).replace(/[^A-Za-z0-9._-]/g,"-")}"\nversion = "0.1.0"\nrequires-python = ">=3.10"\ndependencies = [\n${c}\n]\n\n`}${i.trimStart()}`;const s=/\[tool\.uv\]([\s\S]*?)(?=\n\[[^\]]+\]|$)/m,t=i.match(s);if(t){const e=t[0],n=t[1],c=/^\s*package\s*=\s*.+$/m.test(n)?e.replace(/^\s*package\s*=\s*.+$/m,"package = false"):`${e}\npackage = false`;i=i.replace(s,c)}else i=`${i.trimEnd()}\n\n[tool.uv]\npackage = false\n`;return void fs.writeFileSync(n,i,"utf8")}const o=r[0],l=r[1];let p;p=/^\s*dependencies\s*=\s*\[[\s\S]*?\]/m.test(l)?o.replace(/^\s*dependencies\s*=\s*\[[\s\S]*?\]/m,`dependencies = [\n${c}\n]`):/^\s*requires-python\s*=\s*".*?"\s*$/m.test(l)?o.replace(/^(\s*requires-python\s*=\s*".*?"\s*$)/m,`$1\ndependencies = [\n${c}\n]`):`${o}\ndependencies = [\n${c}\n]`,i=i.replace(a,p);const d=/\[tool\.uv\]([\s\S]*?)(?=\n\[[^\]]+\]|$)/m,u=i.match(d);if(u){const e=u[0],s=u[1];let t=e;t=/^\s*package\s*=\s*.+$/m.test(s)?e.replace(/^\s*package\s*=\s*.+$/m,"package = false"):`${e}\npackage = false`,i=i.replace(d,t)}else i=`${i.trimEnd()}\n\n[tool.uv]\npackage = false\n`;fs.writeFileSync(n,i,"utf8")}function fetchPyPiPackageVersion(e){return new Promise((s,t)=>{https.get(`https://pypi.org/pypi/${e}/json`,e=>{let n="";e.on("data",e=>{n+=e}),e.on("end",()=>{if(200===e.statusCode)try{const e=JSON.parse(n);s(e.info.version)}catch{t(new Error("Failed to parse PyPI JSON response"))}else t(new Error(`PyPI request failed with status code ${e.statusCode}`))})}).on("error",e=>t(e))})}async function ensurePythonVenvAndDeps(e,s){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: syncing dependencies with uv")),console.log(chalk.green("=========================\n"));let t="caspian-utils";try{const e=await fetchPyPiPackageVersion("caspian-utils");t=`caspian-utils==${e}`,console.log(chalk.blue(`Pinned caspian-utils to version ${e}`))}catch(e){console.warn(chalk.yellow("Could not fetch latest caspian-utils version. Using unpinned dependency."))}console.log(chalk.blue("Updating pyproject.toml dependencies...")),updatePyProjectDependencies(e,s,t);const n=path.join(e,"requirements.txt");fs.existsSync(n)&&(fs.unlinkSync(n),console.log(chalk.gray("Removed legacy requirements.txt")));const c=resolveUvCommand(e);console.log(chalk.blue("Creating or updating the virtual environment with uv...")),runCmd(c.cmd,[...c.argsPrefix,"venv",".venv"],e),console.log(chalk.blue("Generating uv.lock...")),runCmd(c.cmd,[...c.argsPrefix,"lock"],e),console.log(chalk.blue("Installing dependencies from pyproject.toml...")),runCmd(c.cmd,[...c.argsPrefix,"sync","--exact"],e),console.log(chalk.green("\nā uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let t=e[0];const n=e.find(e=>e.startsWith("--starter-kit=")),c=n?.split("=")[1],i=e.find(e=>e.startsWith("--starter-kit-source=")),a=i?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,o=!1;if(t){const n=process.cwd(),i=path.join(n,"caspian.config.json");if(c&&a){o=!0;const n={projectName:t,starterKit:c,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(n,s)}else if(fs.existsSync(i)){const c=readJsonFile(i);let a=[];c.excludeFiles?.map(e=>{const s=path.join(n,e);fs.existsSync(s)&&a.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:c.backendOnly,tailwindcss:c.tailwindcss,mcp:c.mcp,prisma:c.prisma,typescript:c.typescript,isUpdate:!0,componentScanDirs:c.componentScanDirs??[],excludeFiles:c.excludeFiles??[],excludeFilePath:a??[],filePath:n};const o={projectName:t,backendOnly:e.includes("--backend-only")||c.backendOnly,tailwindcss:e.includes("--tailwindcss")||c.tailwindcss,typescript:e.includes("--typescript")||c.typescript,prisma:e.includes("--prisma")||c.prisma,mcp:e.includes("--mcp")||c.mcp};r=await getAnswer(o,s),null!==r&&(updateAnswer={projectName:t,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:c.componentScanDirs??[],excludeFiles:c.excludeFiles??[],excludeFilePath:a??[],filePath:n})}else{const n={projectName:t,starterKit:c,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(n,s)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},s);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageVersion("create-caspian-app");p?-1===compareVersions(p,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 s=path.join(d,t);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,r),process.chdir(u);const n=path.join(u,"caspian.config.json");if(fs.existsSync(n)){const s=JSON.parse(fs.readFileSync(n,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),r={...r,backendOnly:s.backendOnly,tailwindcss:s.tailwindcss,typescript:s.typescript,mcp:s.mcp,prisma:s.prisma};let t=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&t.push(s.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),s=path.join(d,t),n=path.join(s,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(n)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}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")];if(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")),r.prisma&&execSync("npm 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),t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],s=[],t=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const t=JSON.parse(fs.readFileSync(s,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano"].forEach(s=>{t(s)&&e.push(s)}),s.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(s){console.warn(chalk.yellow(`Failed to delete ${e}: ${s}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),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.")),s.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(s=>{t(s)&&e.push(s)}),s.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const n=path.join(u,"settings","vite-plugins");fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(s=>{t(s)&&e.push(s)})}const n=(e=>Array.from(new Set(e)))(e);n.length>0&&(console.log(`Uninstalling npm packages: ${n.join(", ")}`),await uninstallNpmDependencies(u,n,!0)),s.length>0&&console.log(chalk.gray(`Python dependencies will be reconciled by uv sync --exact: ${s.join(", ")}`))}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),t={projectName:r.projectName,projectRootPath:e,bsTarget:s.bsTarget,bsPathRewrite:s.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),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 s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const n=e.substring(0,s+8).replace(/\\/g,"\\\\"),t=e.replace(new RegExp(`^${n}`),"").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,s){const n=path.join(e,"package.json");if(checkExcludeFiles(n))return;const t=JSON.parse(fs.readFileSync(n,"utf8"));t.scripts={...t.scripts,projectName:"tsx settings/project-name.ts"};let i=[];s.tailwindcss&&(t.scripts={...t.scripts,tailwind:"postcss src/app/globals.css -o public/css/styles.css --watch","tailwind:build":"postcss src/app/globals.css -o public/css/styles.css"},i.push("tailwind")),s.typescript&&!s.backendOnly&&(t.scripts={...t.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},i.push("ts:watch")),s.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["browserSync:build"]="tsx settings/build.ts",c.dev=`npm-run-all projectName -p browserSync ${i.join(" ")}`;let a=["browserSync:build"];s.tailwindcss&&a.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&a.unshift("ts:build"),c.build=`npm-run-all ${a.join(" ")}`,t.scripts=c,t.type="module",fs.writeFileSync(n,JSON.stringify(t,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,n){const t=fs.existsSync(e),i=t&&fs.statSync(e);if(t&&i&&i.isDirectory()){const t=s.toLowerCase();if(!n.mcp&&t.includes("src\\lib\\mcp"))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\ts")||t.includes("\\ts\\")))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\vite-plugins")||t.includes("\\vite-plugins\\")||t.includes("\\vite-plugins")))return;if(n.backendOnly&&t.includes("public\\js")||n.backendOnly&&t.includes("public\\css")||n.backendOnly&&t.includes("public\\assets"))return;const i=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(t=>{copyRecursiveSync(path.join(e,t),path.join(s,t),n)})}else{if(checkExcludeFiles(s))return;if(!n.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")))return;if(!n.mcp&&s.includes("restart-mcp.ts"))return;if(n.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(n.backendOnly&&s.includes("layout.html"))return;if(n.tailwindcss&&s.includes("index.css"))return;if(!n.prisma&&s.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,n){s.forEach(({src:s,dest:t})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,t),n)})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const n=path.join(e,"src","app","layout.html");if(!checkExcludeFiles(n))try{let e=fs.readFileSync(n,"utf8"),t="";s.backendOnly||(s.tailwindcss||(t='\n <link href="/css/index.css" rel="stylesheet" />'),t+='\n <script type="module" src="/js/main.js"><\/script>');let i="";s.backendOnly||(i=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${t}`:t),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(n,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.html:"),e)}}async function createOrUpdateEnvFile(e,s){const n=path.join(e,".env");checkExcludeFiles(n)||fs.writeFileSync(n,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const n=e.replace(/\\/g,"/");return s.endsWith("/"+n)||s===n})}async function createDirectoryStructure(e,s){const n=[{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"}];s.tailwindcss&&n.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),s.typescript&&!s.backendOnly&&n.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"}];s.typescript&&!s.backendOnly&&t.push({src:"/ts",dest:"/ts"}),n.forEach(({src:s,dest:n})=>{const t=path.join(__dirname,s),i=path.join(e,n);if(checkExcludeFiles(i))return;const c=fs.readFileSync(t,"utf8");fs.writeFileSync(i,c,{flag:"w"})}),await executeCopy(e,t,s),await updatePackageJson(e,s),s.tailwindcss&&modifyPostcssConfig(e),!s.tailwindcss&&s.backendOnly||modifyLayoutPHP(e,s);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(s.prisma){const s=`${'# 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,s)}else await createOrUpdateEnvFile(e,i)}async function getAnswer(e={},s=!1){if(s)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 s=e.starterKit;let n=null;if(STARTER_KITS[s]&&(n=STARTER_KITS[s]),n){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:n.features.backendOnly??!1,tailwindcss:n.features.tailwindcss??!1,prisma:n.features.prisma??!1,mcp:n.features.mcp??!1,typescript:n.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("--prisma")&&(t.prisma=!0),i.includes("--typescript")&&(t.typescript=!0),t}if(e.starterKitSource){const n={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,typescript:!1},t=process.argv.slice(2);return t.includes("--backend-only")&&(n.backendOnly=!0),t.includes("--tailwindcss")&&(n.tailwindcss=!0),t.includes("--mcp")&&(n.mcp=!0),t.includes("--prisma")&&(n.prisma=!0),t.includes("--typescript")&&(n.typescript=!0),n}}const n=[];e.projectName||n.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||n.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(n,{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.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: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,prisma:a.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,s,n=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm uninstall ${n?"--save-dev":"--save"} ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,n)=>{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);s(e["dist-tags"].latest)}catch(e){n(new Error("Failed to parse JSON response"))}})}).on("error",e=>n(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const n=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),t=s.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!n||!t)return e.localeCompare(s);const i=n.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=n[4]??null,r=t[4]??null;return a&&!r?-1:!a&&r?1:a&&r?a.localeCompare(r):0}function getInstalledPackageInfo(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString(),n=s.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return n?{version:n[1],isLinked:s.includes(`${e}@`)&&s.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 s=path.resolve(e).toLowerCase(),n=`${path.sep}_npx${path.sep}`.toLowerCase();return s.includes(n)}async function installNpmDependencies(e,s,n=!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((n?"Installing development dependencies":"Installing dependencies")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm install ${n?"--save-dev":""} ${s.join(" ")}`;execSync(t,{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.10","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"};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(s){const n=s;if("win32"===globalThis.process?.platform&&("EPERM"===n.code||"EACCES"===n.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 s}}async function setupStarterKit(e,s){if(!s.starterKit)return;let n=null;if(STARTER_KITS[s.starterKit]?n=STARTER_KITS[s.starterKit]:s.starterKitSource&&(n={id:s.starterKit,name:`Custom Starter Kit (${s.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:s.starterKitSource}}),n){if(console.log(chalk.green(`Setting up ${n.name}...`)),n.source)try{const t=n.source.branch?`git clone -b ${n.source.branch} --depth 1 ${n.source.url} "${e}"`:`git clone --depth 1 ${n.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 n=JSON.parse(fs.readFileSync(i,"utf8")),t=e,c=bsConfigUrls(t);n.projectName=s.projectName,n.projectRootPath=t,n.bsTarget=c.bsTarget,n.bsPathRewrite=c.bsPathRewrite;const a=await fetchPackageVersion("create-caspian-app");n.version=n.version||a,fs.writeFileSync(i,JSON.stringify(n,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}n.customSetup&&await n.customSetup(e,s),console.log(chalk.green(`ā ${n.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\nš Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const n=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");n&&console.log(chalk.magenta(` Features: ${n}`)),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,s,n){const t=spawnSync(e,s,{cwd:n,stdio:"inherit",shell:!1,encoding:"utf8"});if(t.error)throw t.error;if(0!==t.status)throw new Error(`Command failed (${e} ${s.join(" ")}), exit=${t.status}`)}function tryRunCmd(e,s,n){const t=spawnSync(e,s,{cwd:n,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 s=[{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 n of s)if(tryRunCmd(n.cmd,n.args,e))return!0;return!1}function resolveUvCommand(e){const s=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const n of s)if(tryRunCmd(n.cmd,[...n.argsPrefix,"--version"],e))return n;if(tryInstallUv(e))for(const n of s)if(tryRunCmd(n.cmd,[...n.argsPrefix,"--version"],e))return n;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 s=["fastapi==0.136.0","uvicorn==0.45.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.14.3","slowapi==0.1.9","python-multipart==0.0.26","starsessions==1.3.0","httpx==0.28.1","werkzeug==3.1.8","cuid2==2.0.1","nanoid==2.0.0","python-ulid==2.7.0","cuid==0.4","caspian-utils>=0.2,<0.3"];return e.tailwindcss&&s.push("tailwind-merge==0.3.3"),e.mcp&&s.push("fastmcp==3.2.4"),e.prisma&&(s.push("psycopg2-binary==2.9.12"),s.push("asyncpg==0.31.0"),s.push("aiosqlite==0.22.1"),s.push("aiomysql==0.3.2")),s}function ensurePyProjectExists(e){const s=path.join(e,"pyproject.toml");if(!fs.existsSync(s))throw new Error(`pyproject.toml not found at: ${s}`);let n=fs.readFileSync(s,"utf8");n=n.replace(/\r\n/g,"\n"),n.includes("package = false")||(n=n.includes("[tool.uv]")?n.replace("[tool.uv]","[tool.uv]\npackage = false"):`${n.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(s,n,"utf8")}async function ensurePythonVenvAndDeps(e,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 n=path.join(e,"requirements.txt");fs.existsSync(n)&&(fs.unlinkSync(n),console.log(chalk.gray("Removed legacy requirements.txt")));const t=resolveUvCommand(e),i=path.join(e,".venv");fs.existsSync(i)?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(t.cmd,[...t.argsPrefix,"venv",".venv"],e));const c=buildPythonDependencies(s);console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(t.cmd,[...t.argsPrefix,"add",...c],e),console.log(chalk.blue("Syncing dependencies...")),runCmd(t.cmd,[...t.argsPrefix,"sync","--exact"],e),console.log(chalk.green("\nā uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let n=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(n){const t=process.cwd(),c=path.join(t,"caspian.config.json");if(i&&a){o=!0;const t={projectName:n,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(t,s)}else if(fs.existsSync(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const s=path.join(t,e);fs.existsSync(s)&&a.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:n,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:t};const o={projectName:n,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,s),null!==r&&(updateAnswer={projectName:n,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:t})}else{const t={projectName:n,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(t,s)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},s);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("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(n)if(o){const s=path.join(d,n);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,r),process.chdir(u);const t=path.join(u,"caspian.config.json");if(fs.existsSync(t)){const s=JSON.parse(fs.readFileSync(t,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),r={...r,backendOnly:s.backendOnly,tailwindcss:s.tailwindcss,typescript:s.typescript,mcp:s.mcp,prisma:s.prisma};let n=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&n.push(s.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:n??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),s=path.join(d,n),t=path.join(s,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(t)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}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")];if(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")),r.prisma&&execSync("npm 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),n||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],s=[],n=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));return!!(n.dependencies&&n.dependencies[e]||n.devDependencies&&n.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano"].forEach(s=>{n(s)&&e.push(s)}),s.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(s){console.warn(chalk.yellow(`Failed to delete ${e}: ${s}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),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.")),s.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(s=>{n(s)&&e.push(s)}),s.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{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(s=>{n(s)&&e.push(s)})}const t=(e=>Array.from(new Set(e)))(e);t.length>0&&(console.log(`Uninstalling npm packages: ${t.join(", ")}`),await uninstallNpmDependencies(u,t,!0)),s.length>0&&console.log(chalk.gray(`Python dependencies will be reconciled by uv sync --exact: ${s.join(", ")}`))}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),n={projectName:r.projectName,projectRootPath:e,bsTarget:s.bsTarget,bsPathRewrite:s.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(n,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,r),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
|
package/dist/main.py
CHANGED
|
@@ -428,6 +428,8 @@ def register_single_route(url_pattern: str, file_path: str):
|
|
|
428
428
|
page_metadata=page_metadata,
|
|
429
429
|
page_layout_props=page_layout_props,
|
|
430
430
|
context_data=full_context,
|
|
431
|
+
page_component_source=file_path,
|
|
432
|
+
control_mode=True,
|
|
431
433
|
component_compiler=transform_components
|
|
432
434
|
)
|
|
433
435
|
|
|
@@ -481,6 +483,8 @@ async def custom_404_handler(request: Request, exc: StarletteHTTPException):
|
|
|
481
483
|
},
|
|
482
484
|
page_layout_props=None,
|
|
483
485
|
context_data={'request': request},
|
|
486
|
+
page_component_source=not_found_path,
|
|
487
|
+
control_mode=True,
|
|
484
488
|
transform_fn=transform_scripts
|
|
485
489
|
)
|
|
486
490
|
resp = HTMLResponse(content=html_output, status_code=404)
|
|
@@ -512,6 +516,8 @@ async def custom_general_exception_handler(request: Request, exc: Exception):
|
|
|
512
516
|
},
|
|
513
517
|
page_layout_props=None,
|
|
514
518
|
context_data=context_data,
|
|
519
|
+
page_component_source=error_page_path,
|
|
520
|
+
control_mode=True,
|
|
515
521
|
transform_fn=transform_scripts
|
|
516
522
|
)
|
|
517
523
|
resp = HTMLResponse(content=html_output, status_code=500)
|