create-caspian-app 0.0.1
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/README.md +418 -0
- package/dist/.prettierrc +10 -0
- package/dist/app-gitignore +28 -0
- package/dist/caspian.js +2 -0
- package/dist/index.js +2 -0
- package/dist/main.py +525 -0
- package/dist/postcss.config.js +6 -0
- package/dist/public/css/styles.css +1 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/js/main.js +1 -0
- package/dist/public/js/pp-reactive-v1.js +1 -0
- package/dist/pyproject.toml +9 -0
- package/dist/settings/bs-config.json +7 -0
- package/dist/settings/bs-config.ts +291 -0
- package/dist/settings/build.ts +19 -0
- package/dist/settings/component-map.json +1361 -0
- package/dist/settings/component-map.ts +381 -0
- package/dist/settings/files-list.json +36 -0
- package/dist/settings/files-list.ts +49 -0
- package/dist/settings/project-name.ts +119 -0
- package/dist/settings/python-server.ts +173 -0
- package/dist/settings/utils.ts +239 -0
- package/dist/settings/vite-plugins/generate-global-types.ts +246 -0
- package/dist/src/app/error.html +130 -0
- package/dist/src/app/globals.css +24 -0
- package/dist/src/app/index.html +159 -0
- package/dist/src/app/layout.html +22 -0
- package/dist/src/app/not-found.html +18 -0
- package/dist/ts/global-functions.ts +35 -0
- package/dist/ts/main.ts +5 -0
- package/dist/tsconfig.json +111 -0
- package/dist/vite.config.ts +61 -0
- package/package.json +42 -0
- package/tsconfig.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# Caspian — The Native Python Web Framework for the Reactive Web
|
|
2
|
+
|
|
3
|
+
Caspian is a high‑performance, **FastAPI-powered** full‑stack framework that brings **reactive UI** to Python without forcing a JavaScript backend. It combines:
|
|
4
|
+
|
|
5
|
+
- **FastAPI Engine** for async-native performance and the broader FastAPI ecosystem
|
|
6
|
+
- A **Hybrid Frontend Engine**: start with zero-build HTML, then upgrade to **Vite + NPM + TypeScript** when needed
|
|
7
|
+
- **Direct async RPC** (“Zero‑API”): call Python functions from the browser via `pp.rpc()`
|
|
8
|
+
- **File‑system routing** with nested layouts and dynamic routes
|
|
9
|
+
- **Prisma ORM integration** with an auto-generated, type-safe Python client
|
|
10
|
+
- A secure, session-based **auth system** and built-in security defaults
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1) Requirements
|
|
17
|
+
|
|
18
|
+
- **Node.js**: v22.13.0+
|
|
19
|
+
- **Python**: v3.14.0+
|
|
20
|
+
|
|
21
|
+
### 2) Create an app (interactive wizard)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx create-caspian-app@latest
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Example prompts you’ll see in the wizard:
|
|
28
|
+
|
|
29
|
+
- Project name
|
|
30
|
+
- Tailwind CSS
|
|
31
|
+
- Prisma ORM
|
|
32
|
+
- Swagger / OpenAPI docs
|
|
33
|
+
|
|
34
|
+
### 3) Run dev server
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd my-app
|
|
38
|
+
npm run dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## What “Reactive Python” looks like
|
|
44
|
+
|
|
45
|
+
A Caspian page can be plain HTML with reactive directives, plus a small `<script>` block for state.
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<!-- src/app/todos/index.html -->
|
|
49
|
+
|
|
50
|
+
<!-- Import Python Components -->
|
|
51
|
+
<!-- @import { Badge } from ../components/ui -->
|
|
52
|
+
|
|
53
|
+
<div class="flex gap-2 mb-4">
|
|
54
|
+
<Badge variant="default">Tasks: {todos.length}</Badge>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Reactive Loop -->
|
|
58
|
+
<ul>
|
|
59
|
+
<template pp-for="todo in todos">
|
|
60
|
+
<li key="{todo.id}" class="p-2 border-b">{todo.title}</li>
|
|
61
|
+
</template>
|
|
62
|
+
</ul>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
// State initialized by Python backend automatically
|
|
66
|
+
const [todos, setTodos] = pp.state([[todos]]);
|
|
67
|
+
</script>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Why developers choose Caspian
|
|
73
|
+
|
|
74
|
+
### FastAPI engine, async-native
|
|
75
|
+
|
|
76
|
+
Your logic runs in native async Python and can leverage FastAPI/Starlette features (DI, middleware, validation, etc.) without a separate JS backend.
|
|
77
|
+
|
|
78
|
+
### Hybrid frontend engine (zero-build → Vite)
|
|
79
|
+
|
|
80
|
+
Start with simple HTML-first development for speed and clarity, then adopt Vite + NPM + TypeScript when you need richer libraries or complex bundles.
|
|
81
|
+
|
|
82
|
+
### “Zero‑API” server actions (RPC)
|
|
83
|
+
|
|
84
|
+
Define `async def` actions and call them directly from the browser; Caspian handles serialization, security, and async execution.
|
|
85
|
+
|
|
86
|
+
### File‑system routing with nested layouts
|
|
87
|
+
|
|
88
|
+
Routes are determined by files in `src/app`, supporting dynamic segments (`[id]`), catch-alls (`[...slug]`), route groups (`(auth)`), and layout nesting via `layout.html`.
|
|
89
|
+
|
|
90
|
+
### Prisma ORM integration (type-safe Python client)
|
|
91
|
+
|
|
92
|
+
Define a single Prisma schema and generate a typed Python client—autocomplete-first database access without boilerplate.
|
|
93
|
+
|
|
94
|
+
### Security defaults and authentication
|
|
95
|
+
|
|
96
|
+
Built-in CSRF protection, strict Origin validation, HttpOnly cookies, and a session-based auth model with RBAC support.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Installation & DX setup (VS Code)
|
|
101
|
+
|
|
102
|
+
For the best developer experience, Caspian’s docs recommend:
|
|
103
|
+
|
|
104
|
+
- **Caspian Official Framework Support** (component autocompletion + snippets)
|
|
105
|
+
- **Prisma** schema formatting/highlighting
|
|
106
|
+
- **Tailwind CSS** IntelliSense & class sorting
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Core concepts
|
|
111
|
+
|
|
112
|
+
### Routing
|
|
113
|
+
|
|
114
|
+
Caspian uses **file-system routing** under `src/app`:
|
|
115
|
+
|
|
116
|
+
| File Path | URL Path |
|
|
117
|
+
| ------------------------------- | ------------- |
|
|
118
|
+
| `src/app/index.html` | `/` |
|
|
119
|
+
| `src/app/about/index.py` | `/about` |
|
|
120
|
+
| `src/app/blog/posts/index.html` | `/blog/posts` |
|
|
121
|
+
|
|
122
|
+
#### Dynamic segments
|
|
123
|
+
|
|
124
|
+
```txt
|
|
125
|
+
src/app/users/[id]/index.py -> /users/123
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Catch-all segments
|
|
129
|
+
|
|
130
|
+
```txt
|
|
131
|
+
src/app/docs/[...slug]/index.py -> /docs/getting-started/setup
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### Route groups (organize without changing URLs)
|
|
135
|
+
|
|
136
|
+
```txt
|
|
137
|
+
src/app/(auth)/login/index.py -> /login
|
|
138
|
+
src/app/(auth)/register/index.py -> /register
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Nested layouts
|
|
142
|
+
|
|
143
|
+
Layouts wrap pages and preserve state during navigation:
|
|
144
|
+
|
|
145
|
+
- Root: `src/app/layout.html`
|
|
146
|
+
- Nested: e.g., `/dashboard/settings` inherits root + dashboard layout automatically
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### Components (Python-first, HTML when you want it)
|
|
151
|
+
|
|
152
|
+
Components are Python functions decorated with `@component`.
|
|
153
|
+
|
|
154
|
+
#### Atomic component (best for buttons, badges, icons, etc.)
|
|
155
|
+
|
|
156
|
+
```py
|
|
157
|
+
from casp.html_attrs import get_attributes, merge_classes
|
|
158
|
+
from casp.component_decorator import component
|
|
159
|
+
|
|
160
|
+
@component
|
|
161
|
+
def Container(**props):
|
|
162
|
+
incoming_class = props.pop("class", "")
|
|
163
|
+
final_class = merge_classes("mx-auto max-w-7xl px-4", incoming_class)
|
|
164
|
+
|
|
165
|
+
children = props.pop("children", "")
|
|
166
|
+
|
|
167
|
+
attributes = get_attributes({"class": final_class}, props)
|
|
168
|
+
return f'<div {attributes}>{children}</div>'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**DX speed tip:** the VS Code extension can generate boilerplate via a snippet like `caspcom`.
|
|
172
|
+
|
|
173
|
+
#### Type-safe props (TypeScript-like autocomplete)
|
|
174
|
+
|
|
175
|
+
```py
|
|
176
|
+
from typing import Literal, Any
|
|
177
|
+
from casp.component_decorator import component
|
|
178
|
+
|
|
179
|
+
ButtonVariant = Literal["default", "destructive", "outline"]
|
|
180
|
+
ButtonSize = Literal["default", "sm", "lg"]
|
|
181
|
+
|
|
182
|
+
@component
|
|
183
|
+
def Button(
|
|
184
|
+
children: Any = "",
|
|
185
|
+
variant: ButtonVariant = "default",
|
|
186
|
+
size: ButtonSize = "default",
|
|
187
|
+
**props,
|
|
188
|
+
) -> str:
|
|
189
|
+
# merge classes + attrs here
|
|
190
|
+
return f"<button>...</button>"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**HTML templates for complex UI**
|
|
194
|
+
For larger layouts or reactive UIs, bridge to an HTML file:
|
|
195
|
+
|
|
196
|
+
```py
|
|
197
|
+
from casp.component_decorator import component, render_html
|
|
198
|
+
|
|
199
|
+
@component
|
|
200
|
+
def Counter(**props):
|
|
201
|
+
return render_html("Counter.html", **props)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Reactivity (PulsePoint)
|
|
207
|
+
|
|
208
|
+
Caspian is built on **PulsePoint**, a lightweight reactive DOM engine, plus Caspian-specific helpers for full-stack workflows.
|
|
209
|
+
|
|
210
|
+
Core directives/primitives include:
|
|
211
|
+
|
|
212
|
+
- `pp-component`, `pp-spread`, `pp-ref`, `pp-for`, `pp.state`, `pp.effect`, `pp.ref`, `pp-ignore`
|
|
213
|
+
|
|
214
|
+
#### `pp.rpc(functionName, data?)`
|
|
215
|
+
|
|
216
|
+
The bridge to your Python backend. Caspian handles:
|
|
217
|
+
|
|
218
|
+
- smart serialization (JSON ↔ FormData when `File` is present)
|
|
219
|
+
- auto-redirects when server returns redirect headers
|
|
220
|
+
- `X-CSRF-Token` injection for security
|
|
221
|
+
|
|
222
|
+
#### `searchParams`
|
|
223
|
+
|
|
224
|
+
A reactive wrapper around `URLSearchParams` that updates the URL without full reloads.
|
|
225
|
+
|
|
226
|
+
#### Navigation
|
|
227
|
+
|
|
228
|
+
Caspian intercepts internal `<a>` links for client-side navigation; for programmatic navigation use:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
pp.redirect("/dashboard");
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Backend: Async Server Actions (RPC)
|
|
237
|
+
|
|
238
|
+
Decorate `async def` functions with `@rpc`, then call them from the client using `pp.rpc()`.
|
|
239
|
+
|
|
240
|
+
**Backend (`src/app/todos/index.py`)**
|
|
241
|
+
|
|
242
|
+
```py
|
|
243
|
+
from lib.rpc import rpc
|
|
244
|
+
from lib.validate import Validate
|
|
245
|
+
from lib.prisma.db import prisma
|
|
246
|
+
|
|
247
|
+
@rpc()
|
|
248
|
+
async def create_todo(title):
|
|
249
|
+
if Validate.with_rules(title, "required|min:3") is not True:
|
|
250
|
+
raise ValueError("Title must be at least 3 chars")
|
|
251
|
+
|
|
252
|
+
new_todo = await prisma.todo.create(data={
|
|
253
|
+
"title": title,
|
|
254
|
+
"completed": False
|
|
255
|
+
})
|
|
256
|
+
return new_todo.to_dict()
|
|
257
|
+
|
|
258
|
+
@rpc(require_auth=True)
|
|
259
|
+
async def delete_todo(id, _current_user_id=None):
|
|
260
|
+
await prisma.todo.delete(where={"id": id})
|
|
261
|
+
return {"success": True}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Frontend (`src/app/todos/index.html`)**
|
|
265
|
+
|
|
266
|
+
```html
|
|
267
|
+
<form onsubmit="add(event)">
|
|
268
|
+
<input name="title" required />
|
|
269
|
+
<button>Add</button>
|
|
270
|
+
</form>
|
|
271
|
+
|
|
272
|
+
<script>
|
|
273
|
+
const [todos, setTodos] = pp.state([]);
|
|
274
|
+
|
|
275
|
+
async function add(e) {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
const data = Object.fromEntries(new FormData(e.target));
|
|
278
|
+
|
|
279
|
+
const newTodo = await pp.rpc("create_todo", data);
|
|
280
|
+
setTodos([newTodo, ...todos]);
|
|
281
|
+
e.target.reset();
|
|
282
|
+
}
|
|
283
|
+
</script>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Database: Prisma ORM
|
|
289
|
+
|
|
290
|
+
Caspian uses a Prisma schema as the single source of truth and generates a typed Python client. It is designed to translate Prisma syntax into optimized SQL without requiring the heavy Prisma Engine binary.
|
|
291
|
+
|
|
292
|
+
**Schema (`prisma/schema.prisma`)**
|
|
293
|
+
|
|
294
|
+
```prisma
|
|
295
|
+
model User {
|
|
296
|
+
id String @id @default(cuid())
|
|
297
|
+
email String @unique
|
|
298
|
+
name String?
|
|
299
|
+
posts Post[]
|
|
300
|
+
createdAt DateTime @default(now())
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
model Post {
|
|
304
|
+
id String @id @default(cuid())
|
|
305
|
+
title String
|
|
306
|
+
published Boolean @default(false)
|
|
307
|
+
author User @relation(fields: [authorId], references: [id])
|
|
308
|
+
authorId String
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Client usage
|
|
313
|
+
|
|
314
|
+
```py
|
|
315
|
+
from lib.prisma.db import prisma
|
|
316
|
+
|
|
317
|
+
users = prisma.user.find_many()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The docs also describe connection pooling and common CRUD patterns (create, find, update, delete), plus aggregations and transactions.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Authentication (session-based, secure defaults)
|
|
325
|
+
|
|
326
|
+
Caspian includes session-based authentication with HttpOnly cookies and RBAC-friendly conventions.
|
|
327
|
+
|
|
328
|
+
**Configure auth in `utils/auth.py`**
|
|
329
|
+
|
|
330
|
+
- global protection toggle (`IS_ALL_ROUTES_PRIVATE`)
|
|
331
|
+
- public routes whitelist
|
|
332
|
+
- auth routes (signin/signup)
|
|
333
|
+
- default redirects
|
|
334
|
+
|
|
335
|
+
The global `auth` object provides:
|
|
336
|
+
|
|
337
|
+
- `auth.sign_in(data, redirect_to?)`
|
|
338
|
+
- `auth.sign_out(redirect_to?)`
|
|
339
|
+
- `auth.is_authenticated()`
|
|
340
|
+
- `auth.get_payload()`
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## CLI reference
|
|
345
|
+
|
|
346
|
+
### Create projects
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
npx create-caspian-app
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Useful flags
|
|
353
|
+
|
|
354
|
+
- `--tailwindcss` Tailwind CSS v4 + PostCSS + `globals.css`
|
|
355
|
+
- `--typescript` TypeScript support with Vite + `tsconfig.json`
|
|
356
|
+
- `--websocket` WebSocket server scaffolding
|
|
357
|
+
- `--mcp` Model Context Protocol server scaffolding (AI Agents)
|
|
358
|
+
- `--backend-only` skip frontend assets
|
|
359
|
+
|
|
360
|
+
### Code generation
|
|
361
|
+
|
|
362
|
+
Generate strict Python data classes (Pydantic) from your Prisma schema:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
npx ppy generate
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Updating the project
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
npx casp update project
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Tip: use `excludeFiles` in `caspian.config.json` to prevent overwrites during updates.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Built-in icon workflow (ppicons)
|
|
379
|
+
|
|
380
|
+
Caspian integrates **ppicons** (Lucide-based), offering 1,500+ icons and an instant add command:
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
npx ppicons add Rocket
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Then use in HTML:
|
|
387
|
+
|
|
388
|
+
```html
|
|
389
|
+
<!-- @import { Rocket } from ../lib/ppicons -->
|
|
390
|
+
<Rocket class="w-6 h-6 text-primary" />
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Project structure (generated by the CLI)
|
|
396
|
+
|
|
397
|
+
High-level layout:
|
|
398
|
+
|
|
399
|
+
- `main.py` — FastAPI app & ASGI entry
|
|
400
|
+
- `caspian.config.json` — project config
|
|
401
|
+
- `prisma/` — schema + seed scripts
|
|
402
|
+
- `src/` — app routes, pages, styles, shared libs
|
|
403
|
+
- `public/` — static assets served directly
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## License
|
|
408
|
+
|
|
409
|
+
MIT
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Learn more
|
|
414
|
+
|
|
415
|
+
- Documentation: [Caspian docs](https://caspian.tsnc.tech/docs)
|
|
416
|
+
- Site: [caspian.tsnc.tech](https://caspian.tsnc.tech)
|
|
417
|
+
- PulsePoint docs: [pulsepoint.tsnc.tech](https://pulsepoint.tsnc.tech)
|
|
418
|
+
- ppicons library: [ppicons.tsnc.tech](https://ppicons.tsnc.tech)
|
package/dist/.prettierrc
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Node modules
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# virtual environment
|
|
5
|
+
.venv/
|
|
6
|
+
|
|
7
|
+
# Environment files
|
|
8
|
+
.env
|
|
9
|
+
.env.*
|
|
10
|
+
|
|
11
|
+
# IDE-specific files
|
|
12
|
+
.vscode/
|
|
13
|
+
.idea/
|
|
14
|
+
|
|
15
|
+
# OS-specific files
|
|
16
|
+
.DS_Store # macOS
|
|
17
|
+
Thumbs.db # Windows
|
|
18
|
+
|
|
19
|
+
# Log files
|
|
20
|
+
*.log
|
|
21
|
+
|
|
22
|
+
# Backup and temporary files
|
|
23
|
+
*.swp
|
|
24
|
+
|
|
25
|
+
# Caspian settings files
|
|
26
|
+
.casp/
|
|
27
|
+
caches/
|
|
28
|
+
settings/bs-config.json
|
package/dist/caspian.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
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,t)=>{const s=spawn(e,o,{stdio:"inherit",shell:!0,...n});s.on("error",e=>{console.error(`Execution error: ${e.message}`),t(e)}),s.on("close",e=>{0===e?a():t(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"},parseVersionFromArgs=e=>{const o=e.find(e=>e.startsWith("@")||e.match(/^\d+\.\d+\.\d+/));if(o)return o.startsWith("@")?o.slice(1):o},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 @v4-alpha")} - Update to specific tag`),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)`);const e=parseVersionFromArgs(args),o=`npx casp ${args.filter(e=>!e.startsWith("@")&&!e.match(/^\d+\.\d+\.\d+/)&&"-y"!==e).join(" ")}`;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`),void console.log(` ${chalk.cyan("-y")} - Non-interactive mode (skip prompts)`);if(o===commandsToExecute.update)try{let o;if(isNonInteractive)o={shouldProceed:!0,versionTag:e||"latest"},console.log(chalk.blue("Running in non-interactive mode..."));else if(o=await getAnswer(e),!o?.shouldProceed)return void console.log(chalk.red("Operation cancelled by the user."));const n=process.cwd(),a=path.join(n,"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 t=readJsonFile(a),s=o.versionTag||"latest",c=`create-caspian-app@${s}`;console.log(chalk.blue(`\nUpdating to: ${chalk.green(c)}\n`));const r=[t.projectName];t.backendOnly&&r.push("--backend-only"),t.tailwindcss&&r.push("--tailwindcss"),t.prisma&&r.push("--prisma"),t.mcp&&r.push("--mcp"),t.typescript&&r.push("--typescript"),isNonInteractive&&r.push("-y"),console.log("Executing command...\n"),await executeCommand("npx",[c,...r]),t.version=s,fs.writeFileSync(a,JSON.stringify(t,null,2)),console.log(chalk.green(`\n✓ Project updated successfully to version ${s}!`)),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
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);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 i=`http://localhost/${n}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),a=n.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,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 i=[];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"},i.push("tailwind")),s.typescript&&!s.backendOnly&&(n.scripts={...n.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},i.push("ts:watch")),s.mcp&&(n.scripts={...n.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...n.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(" ")}`,n.scripts=c,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),i=n&&fs.statSync(e);if(n&&i&&i.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 i=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))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 i="";s.backendOnly||(i=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" /> ${n}`:n),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,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){return!!updateAnswer?.isUpdate&&(updateAnswer?.excludeFilePath?.includes(e.replace(/\\/g,"/"))??!1)}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),i=path.join(e,t);if(checkExcludeFiles(i))return;const c=fs.readFileSync(n,"utf8");fs.writeFileSync(i,c,{flag:"w"})}),await executeCopy(e,n,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 (CSV or JSON array)\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="10 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 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},i=process.argv.slice(2);return i.includes("--backend-only")&&(n.backendOnly=!0),i.includes("--tailwindcss")&&(n.tailwindcss=!0),i.includes("--mcp")&&(n.mcp=!0),i.includes("--prisma")&&(n.prisma=!0),i.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)},i=await prompts(t,{onCancel:n}),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("Caspian 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("Caspian ORM")}?`,initial:!1,active:"Yes",inactive:"No"}));const a=await prompts(c,{onCancel:n});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,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)};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.1.18","@types/browser-sync":"2.29.1","@types/node":"25.0.3","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.2","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.6","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.1.18",tsx:"4.21.0",typescript:"5.9.3",vite:"7.3.0","fast-glob":"3.3.3","tree-sitter":"^0.25.0","tree-sitter-python":"^0.25.0"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}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"});const i=path.join(e,".git");fs.existsSync(i)&&fs.rmSync(i,{recursive:!0,force:!0}),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.replace(/\\/g,"\\"),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 getVenvPythonPath(e){const s=path.join(e,".venv","Scripts","python.exe");if(fs.existsSync(s))return s;return path.join(e,".venv","bin","python")}async function uninstallPythonDependencies(e,s){const t=getVenvPythonPath(e);if(!fs.existsSync(t))throw new Error(`Venv python not found at: ${t}`);console.log("Uninstalling Python dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`)),runCmd(t,["-m","pip","uninstall","-y",...s],e)}async function ensurePythonVenvAndDeps(e,s){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: creating venv + installing dependencies")),console.log(chalk.green("=========================\n"));const t=path.join(e,".venv");if(fs.existsSync(t)&&(fs.existsSync(path.join(e,".venv","Scripts","python.exe"))||fs.existsSync(path.join(e,".venv","bin","python"))))console.log(chalk.gray("Venv already exists: .venv"));else if(console.log(chalk.blue("Creating virtual environment: .venv")),tryRunCmd("py",["-m","venv",".venv"],e));else if(tryRunCmd("python",["-m","venv",".venv"],e));else if(!tryRunCmd("python3",["-m","venv",".venv"],e))throw new Error("Could not create venv. Install Python and ensure `py` or `python` is in PATH.");const n=getVenvPythonPath(e);if(!fs.existsSync(n))throw new Error(`Venv python not found at: ${n}`);console.log(chalk.blue("Upgrading pip...")),runCmd(n,["-m","pip","install","--upgrade","pip"],e);let i=["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","caspian-utils"];s.tailwindcss&&i.push("tailwind-merge>=0.3.3,<0.4.0"),s.mcp&&i.push("mcp"),console.log(chalk.blue("Generating requirements.txt..."));const c=path.join(e,"requirements.txt"),a=i.join("\n")+"\n";fs.writeFileSync(c,a,"utf8"),console.log(chalk.gray(`Created requirements.txt with ${i.length} packages.`)),console.log(chalk.blue("Installing dependencies from requirements.txt...")),i.forEach(e=>console.log(`- ${chalk.gray(e)}`)),runCmd(n,["-m","pip","install","-r","requirements.txt"],e),console.log(chalk.green("\n✓ Python venv ready and FastAPI dependencies installed.\n"))}function setupVsCodeSettings(e){const s=path.join(e,".vscode");fs.existsSync(s)||fs.mkdirSync(s);const t={"python.defaultInterpreterPath":"win32"===process.platform?"${workspaceFolder}/.venv/Scripts/python.exe":"${workspaceFolder}/.venv/bin/python","python.terminal.activateEnvironment":!0,"python.analysis.typeCheckingMode":"basic","python.analysis.autoImportCompletions":!0};fs.writeFileSync(path.join(s,"settings.json"),JSON.stringify(t,null,2)),console.log(chalk.gray("Created .vscode/settings.json for auto-activation."))}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=")),i=n?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),a=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,o=!1;if(t){const n=process.cwd(),c=path.join(n,"caspian.config.json");if(i&&a){o=!0;const n={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),swaggerDocs:e.includes("--swagger-docs"),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(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const s=path.join(n,e);fs.existsSync(s)&&a.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:n};const o={projectName:t,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp};r=await getAnswer(o,s),null!==r&&(updateAnswer={projectName:t,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:n})}else{const n={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),swaggerDocs:e.includes("--swagger-docs"),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=process.cwd();let p;if(t)if(o){const s=path.join(l,t);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),p=s,await setupStarterKit(p,r),process.chdir(p);const n=path.join(p,"caspian.config.json");if(fs.existsSync(n)){const s=JSON.parse(fs.readFileSync(n,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--swagger-docs")&&(s.swaggerDocs=!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(p,e);fs.existsSync(s)&&t.push(s.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:t??[],filePath:p}}}else{const e=path.join(l,"caspian.config.json"),s=path.join(l,t),n=path.join(s,"caspian.config.json");fs.existsSync(e)?p=l:fs.existsSync(s)&&fs.existsSync(n)?(p=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),p=s,process.chdir(s))}else fs.mkdirSync(r.projectName,{recursive:!0}),p=path.join(l,r.projectName),process.chdir(r.projectName);let d=[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("tree-sitter"),npmPkg("tree-sitter-python")];if(r.prisma&&d.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&d.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano")),console.log("Answer:",r),r.prisma,r.typescript&&!r.backendOnly&&d.push(npmPkg("vite"),npmPkg("fast-glob")),r.starterKit&&!o&&await setupStarterKit(p,r),await installNpmDependencies(p,d,!0),t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(p,r),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],s=[],t=e=>{try{const s=path.join(p,"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(p,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(p,"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(p,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(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(p,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(p,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),s.push("mcp")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals"].forEach(s=>{t(s)&&e.push(s)})}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(p,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(p,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts 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(p,n,!0)),s.length>0&&await uninstallPythonDependencies(p,s)}if(!o||!fs.existsSync(path.join(p,"caspian.config.json"))){const e=p.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:"0.0.1",componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(p,"caspian.config.json"),JSON.stringify(t,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(p,r),setupVsCodeSettings(p),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(p.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
|