create-caspian-app 1.3.16 → 1.3.18
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Copilot Instructions
|
|
2
2
|
|
|
3
|
-
- Read `AGENTS.md` before
|
|
3
|
+
- Read `AGENTS.md` before any analysis or implementation in this workspace — starting with its "Caspian Core Contracts" section, the first-party digest of how Caspian actually works (feature gates, Jinja/PulsePoint brace dialects, authoring model, props passing, the closed PulsePoint template surface, data flows). It is the required first read for every task; do not skip it and implement from framework intuition.
|
|
4
4
|
- Keep repo-wide always-on Copilot guidance in this file. Use `.github/instructions/**/*.instructions.md` for narrower task-, file-, library-, or implementation-specific guidance when that extra context should not load on every request.
|
|
5
5
|
|
|
6
6
|
## Document Ownership
|
|
@@ -90,7 +90,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
|
|
|
90
90
|
- Treat `pp-component` on routes, layouts, and components as compiler-injected by the Python side; do not add it manually in authored templates unless the task is explicitly about runtime internals. Author owned PulsePoint logic as a plain `<script>` inside the component root.
|
|
91
91
|
- `layout()` can be synchronous or async in the installed runtime. Keep async layout work focused on shared layout props or metadata; use `page()` or `@rpc()` when the work belongs to a specific route or user action.
|
|
92
92
|
- Dynamic route params currently reach `page()` as a single positional `dict`, with query params injected by name and `request` injected by keyword when declared.
|
|
93
|
-
- In `layout.py`, `layout()` returns `html(r"""...""", **context)` — the same entrypoint pages and components use. It is
|
|
93
|
+
- In `layout.py`, `layout()` returns `html(r"""...""", **context)` — the same entrypoint pages and components use. It is _deferred_: `children` is the page beneath the layout and does not exist yet, so `html(...)` hands back an unrendered `LayoutTemplate` and the engine renders it later with `children`/`layout`/`metadata` merged in (those three always win over author context). Also accepted: `(html(...), props_dict)`, a props dict alone (passthrough `<slot />` shell), `None`, or a bare template string (the legacy form, still supported). A layout that never places its children — no `<slot />` and no `{{ children }}` — raises `LayoutChildrenError` rather than serving an empty shell.
|
|
94
94
|
- Never author markup as a Python f-string. `{{ x }}` is server interpolation and `{ x }` is a PulsePoint binding inside `html(...)`; an f-string inverts both, skips autoescaping while still marking the result trusted, and skips the `<x-*>` scope stash. The `templates` gate freezes the existing f-string components in `settings/fstring-components.json` and fails on new ones.
|
|
95
95
|
- Do not assume `StateManager` survives across requests unless `request.state.session` is explicitly bridged from `request.session`.
|
|
96
96
|
- Route, layout, and component templates must keep exactly one authored top-level parent node so Caspian can inject `pp-component` after component expansion. In source, that parent may be a native HTML element or a single imported `x-*` component tag, but it must resolve to one final HTML root. Keep any owned PulsePoint script inside that same parent.
|
|
@@ -142,7 +142,7 @@ This is the top architectural requirement for this workspace. Treat it as a hard
|
|
|
142
142
|
- `npm run check` only reports. Auto-fix with `npm run check:fix`, which runs `settings/fix.py` (**format first**, then safe ruff fixes, then the gate). pyright and pytest failures are never auto-fixed.
|
|
143
143
|
- **`html(r"""` stays on one line, with the markup starting on the next line.** `ruff format` will not produce that shape — it explodes any call whose first argument is a multiline string when the call has other arguments, in default and preview style alike — so `settings/format.py` rejoins the opening after ruff runs, and iterates the pair to a fixed point (rejoining the opening lets ruff also pull the closing `)` up on a sole-argument call). Consequence to know: running bare `ruff format`, or an IDE format-on-save, re-splits every `html(` opening; `npm run format` puts them back. Do not "fix" this by hand-editing call sites or by adding `# fmt: skip`.
|
|
144
144
|
- **Formatting is `npm run format` (`settings/format.py`), and it runs markup before Python.** It formats two surfaces: authored markup inside every `html(r"""...""")` via **djLint**, then app Python via **`ruff format`**. That order matters — reformatting a template changes how many lines its string literal spans, which changes how ruff wraps the enclosing `html(...)` call, so running ruff last is what makes a single pass converge. Prettier is not an option here: it has no Jinja awareness and de-indents `{% for %}` blocks to column 0. Use `npm run format:check` to report without writing (exit 1 if work remains). Do not add a separate markup-formatting script; `--markup` / `--python` already narrow the run.
|
|
145
|
-
- **The formatter never trusts djLint — it proves each block first.** djLint is a general HTML formatter, so it will insert a newline between a block tag and an adjacent inline or `<x-*>` tag, which renders as a visible space (a custom element's `display` comes from CSS the formatter cannot see). So every block is formatted, then checked against `settings/_markup_equivalence.py`, a tokenizer that decides whether the result is
|
|
145
|
+
- **The formatter never trusts djLint — it proves each block first.** djLint is a general HTML formatter, so it will insert a newline between a block tag and an adjacent inline or `<x-*>` tag, which renders as a visible space (a custom element's `display` comes from CSS the formatter cannot see). So every block is formatted, then checked against `settings/_markup_equivalence.py`, a tokenizer that decides whether the result is _guaranteed_ to render identically; only proven blocks are written, and the rest are skipped with a printed reason. `<script>`/`<style>`/`<pre>`/`<textarea>` bodies are masked out before djLint runs, so code and preformatted text are preserved byte-for-byte by construction rather than by proof — djLint otherwise reads `/>` inside a JS regex as a tag delimiter. A skipped block is not a failure and must not be "fixed" by loosening the oracle: it means the reformat would have changed rendering. Coverage is in `tests/test_format.py`; a false _positive_ from the oracle (calling a real change safe) is the only dangerous failure mode, so both directions are pinned there.
|
|
146
146
|
- Unused-import (`F401`) removal is handled carefully because component imports look unused to ruff. Single-file components import children used only as `<x-*>` tags in `html(...)` templates (`from .Dialog import DialogContent` → `<x-dialog-content>`); ruff cannot see that, and Caspian resolves the tag from module globals at render time, so deleting the import breaks rendering. Two layers keep it safe: `F401` is `unfixable` in `[tool.ruff.lint]` so a raw `ruff check --fix` never deletes any import; and `settings/fix.py` removes dead imports only from files that contain no `<x-*>`-tag import (component-guarded files are skipped whole and left for the gate). `settings/check.py` likewise suppresses the `F401` reports whose symbol is used as an `x-{camel_to_kebab(name)}` tag, so the gate fails only on genuinely dead imports. The tag detection is shared in `settings/_component_imports.py`. Do not blanket-ignore `F401` or re-enable its autofix globally. See `node_modules/caspian-utils/dist/docs/testing.md`.
|
|
147
147
|
- Keep tests in `tests/` app-focused: `main.py` helpers and route behavior (via `starlette.testclient.TestClient` against `main.app`), and `src/lib/**` policy such as `auth_config.py`. Do not test framework internals under `.venv/Lib/site-packages/casp/**`.
|
|
148
148
|
- `tests/conftest.py` puts the project root on `sys.path` and sets safe dev env defaults (`APP_ENV`, `AUTH_SECRET`) so importing `main` never fails during tests; extend it rather than duplicating that setup per test file.
|
package/dist/AGENTS.md
CHANGED
|
@@ -6,14 +6,205 @@
|
|
|
6
6
|
|
|
7
7
|
This workspace is a Caspian application plus a packaged copy of the Caspian docs.
|
|
8
8
|
|
|
9
|
+
**This file is the first-party Caspian reference and the required first read for every task — do not skip it and do not start implementing before it.** The "Caspian Core Contracts" section below is the condensed, always-read digest of how Caspian actually works — feature gates, the Jinja/PulsePoint brace dialects, the authoring model, the props-passing contract, the closed PulsePoint template/API surface, and the data flows. It exists because tasks fail when an agent implements from generic framework intuition instead of these shipped contracts; reading it first is what lets a feature land correctly in one pass. The packaged docs under `node_modules/caspian-utils/dist/docs/` remain the deep per-feature layer to open when a task touches that surface.
|
|
10
|
+
|
|
9
11
|
When you work here, use `caspian.config.json` and the code that actually runs as the source of truth for this project. Use workspace file instructions under `.github/instructions/**/*.instructions.md` as the task-specific instruction layer when they match the work, and use the packaged markdown docs under `node_modules/caspian-utils/dist/docs/` as the AI-facing Caspian feature and task-reference layer.
|
|
10
12
|
|
|
11
13
|
Do not treat the existence of a packaged doc as proof that the feature is enabled in this project.
|
|
12
14
|
|
|
15
|
+
## Caspian Core Contracts (Read Before Any Analysis)
|
|
16
|
+
|
|
17
|
+
Every rule in this section describes shipped behavior of the Caspian runtime this app runs on. Implement against these contracts, not framework intuition. When any claim here disagrees with `caspian.config.json`, the app code, or the installed runtime, the code wins — and this section should then be fixed together with the matching packaged doc.
|
|
18
|
+
|
|
19
|
+
**This is the digest, not the full documentation.** It exists to stop the highest-frequency implementation failures; it does not replace the packaged docs under `node_modules/caspian-utils/dist/docs/`, which remain the canonical deep layer per feature. Each subsection below ends with a "Deep dive" pointer — open that doc before implementing anything nontrivial on that surface, and use the "Task Routing" section further down to pick the right doc for the task as a whole. Never conclude from this digest alone that a detail, option, or edge case does not exist.
|
|
20
|
+
|
|
21
|
+
### Feature gates (`caspian.config.json`)
|
|
22
|
+
|
|
23
|
+
This workspace currently enables: `tailwindcss`, `mcp`, `prisma`, `typescript`, `websocket`; `backendOnly: false`; components are scanned under `src/`. Re-read the file when in doubt — it is the single source of truth for optional features. A packaged doc existing never proves a feature is enabled. If a disabled feature is requested, ask first, then enable the flag and follow the Caspian update workflow.
|
|
24
|
+
|
|
25
|
+
Deep dive: `node_modules/caspian-utils/dist/docs/index.md` (the docs manifest and retrieval order) and `commands.md` (scaffold and update workflows).
|
|
26
|
+
|
|
27
|
+
### The three brace dialects — the #1 source of broken implementations
|
|
28
|
+
|
|
29
|
+
Every template in this app is authored inside `html(r"""...""")` and rendered through Jinja **before** the PulsePoint compiler ever sees it. Three brace forms coexist and must never be confused:
|
|
30
|
+
|
|
31
|
+
| Syntax | Layer | Meaning |
|
|
32
|
+
| --------------------- | -------------------- | ------------------------------------------------------------------ |
|
|
33
|
+
| `{{ value }}` | Server (Jinja) | Python-to-HTML interpolation at render time. Autoescaped. |
|
|
34
|
+
| `{{ value \| json }}` | Server (Jinja) | Safe serialization of a server value into a `<script>`. |
|
|
35
|
+
| `{# comment #}` | Server (Jinja) | Stripped from output. |
|
|
36
|
+
| `{ expression }` | Browser (PulsePoint) | Left untouched by the server; evaluated reactively in the browser. |
|
|
37
|
+
|
|
38
|
+
Consequences that are always true:
|
|
39
|
+
|
|
40
|
+
- **Never author markup as a Python f-string.** It inverts both dialects (`{x}` becomes server interpolation, a PulsePoint binding must become `{{x}}`), skips autoescaping while still marking the output trusted, and skips the `<x-*>` scope stash. The `html-form` gate rule fails new f-strings. The one accepted markup form is `html(r"""...""")` — raw, triple-quoted, nothing else.
|
|
41
|
+
- **Autoescaping is ON.** `{{ value }}` is safe for user text. Trusted HTML needs `Markup(...)` or `| safe`. `children` is auto-safe.
|
|
42
|
+
- **Braces are escaped by the server too** (`{`/`}` → `{`/`}` on every non-`Markup` value), because PulsePoint compiles the rendered DOM and a stored `{fetch(...)}` would otherwise execute. `Markup` is the trust boundary: `get_attributes(...)`, `merge_classes(...)`, `| safe`, the `json` filter, and layout children keep their braces live. Therefore **you cannot build a PulsePoint expression by interpolating a plain server string** — `class="{{ some_expr }}"` renders inert. Author the expression in the template, or return `Markup` from the helper.
|
|
43
|
+
- Server-rendered `{{ }}` values are static after first paint; `{ }` values are reactive. Passing first-render data into reactive scripts goes through `{{ value | json }}` into a `pp.state(...)` initializer, or through props (see the props contract below).
|
|
44
|
+
|
|
45
|
+
Deep dive: `node_modules/caspian-utils/dist/docs/components.md` "Single-File Components With `html(...)`" (the dialects, autoescaping, and the `Markup` trust boundary in full).
|
|
46
|
+
|
|
47
|
+
### Authoring model — pages, layouts, components
|
|
48
|
+
|
|
49
|
+
Authoring is **Python-only and single-file**. There are no `.html` sidecars in this app; markup lives inline in the owning `.py` file, returned from `html(r"""...""", **context)` (import `html` — and `component` — from `casp.component_decorator`).
|
|
50
|
+
|
|
51
|
+
**Routes (`src/app/**/index.py`):\*\*
|
|
52
|
+
|
|
53
|
+
- Folders are URL segments (Next.js App Router model): `[id]` dynamic, `[...slug]` catch-all, `(group)` organizes without a URL segment.
|
|
54
|
+
- `page()` returns `html(r"""...""")` for UI routes, a `Response` for non-visual routes, or the tuple `(html(...), {"layout_prop": value})` to push a value up into wrapping layouts as `{{ layout.layout_prop }}`.
|
|
55
|
+
- Path params arrive as **one positional dict**: `async def page(params: dict)`. Query params inject by name; `request` injects by keyword when declared.
|
|
56
|
+
- The same `index.py` owns the route's metadata, `@rpc()` actions, auth checks, caching, redirects, and validation. Extract to `src/lib/**` only what is genuinely shared.
|
|
57
|
+
- **Component-first composition is the top authoring rule**: the page template is a short assembly of `x-*` chunk components (topbar, sidebar, sections, cards, forms, footer). Long markup moves into focused components in `src/components/` _before_ the route is written, not as cleanup.
|
|
58
|
+
|
|
59
|
+
**Layouts (`src/app/**/layout.py`):\*\*
|
|
60
|
+
|
|
61
|
+
- `layout()` returns `html(r"""...""", **context)` — but **deferred**: `children` (the page below) does not exist yet, so `html(...)` hands back an unrendered `LayoutTemplate` and the engine renders it later with `children` / `layout` / `metadata` merged in. Those three names are engine-owned and always win over author context.
|
|
62
|
+
- The layout must place its children — `<slot />` or `{{ children }}` — or it raises `LayoutChildrenError`.
|
|
63
|
+
- Also accepted returns: `(html(...), props_dict)` (props become `{{ layout.* }}` for the subtree), a bare props `dict`, `None`, or a legacy raw string.
|
|
64
|
+
- Deferral is keyed on the `layout()` frame only; a component or helper called from a layout still renders eagerly.
|
|
65
|
+
- Grouped sections (dashboard/admin/account) = parent folder + `layout.py` + child routes, exactly like the App Router. Put `pp-reset-scroll="true"` on the content pane that should reset on child navigation; leave shell scrollers unmarked.
|
|
66
|
+
|
|
67
|
+
**Components (`src/components/**/\*.py`):\*\*
|
|
68
|
+
|
|
69
|
+
- One `@component` function per responsibility, markup inline via `html(r"""...""")`, PulsePoint `<script>` inside the root. Split by responsibility exactly as you would split React components — **that analogy covers decomposition and hook API only, never markup syntax** (see the PulsePoint contract below).
|
|
70
|
+
- **Composition is Python-import-driven.** An `<x-*>` tag resolves from the `Component` objects imported into the module that authors the tag: `Container` → `<x-container>`, `CommandDialog` → `<x-command-dialog>`. Import every tag you write, including in pages and layouts. Same-file multi-exports are imported from that exact file. Directories that are not valid identifiers (hyphens, `(group)`) bind via `Name = importlib.import_module("src.app.some-dir.Name").Name`.
|
|
71
|
+
- Resolution precedence inside a component's output: inherited ancestor components, then the module's own imports (imports win). Slot content resolves in the scope where it was **authored**, so the module writing the tag must import it.
|
|
72
|
+
- A component may also be called directly as a function and interpolated with `{{ }}`; its nested tags still resolve from its own module's imports.
|
|
73
|
+
- **Root shape:** default to one authored top-level element with the `<script>` inside it.
|
|
74
|
+
- A **component** with sibling top-level nodes is a _fragment_ (the `<>…</>` equivalent) — framed by a compiler comment pair, materialized as `<pp-fragment style="display: contents">`, adds no element, and is the only shape that survives inside `<tbody>`/`<tr>`/`<select>`/`<optgroup>`. **A fragment cannot receive props** — any attribute on its `<x-*>` tag (including `pp-ref`) raises `FragmentPropsError`. Never hand-write `<pp-fragment>` or `<!--pp:…-->`.
|
|
75
|
+
- A **page or layout** with sibling top-level nodes gets a layout-neutral `<div pp-component style="display: contents">` boundary host instead — legal and expected.
|
|
76
|
+
- A component whose authored root is another `x-*` tag (composition component) gets the same host, carrying the parent's forwarded props and `pp-ref-forward`. An extra `display: contents` div in rendered DOM is expected output, not a bug.
|
|
77
|
+
- Never author `pp-component` or any runtime-managed attribute; the pipeline injects them.
|
|
78
|
+
- A template whose root is an `x-*` tag keeps its `<script>` inside that root: it travels as slot content owned by the authoring template (`pp-owner`, alias `app` for pages/layouts) and executes in the **author's** scope.
|
|
79
|
+
- Async components (`async def`) are allowed only when the component itself needs awaited I/O.
|
|
80
|
+
|
|
81
|
+
Deep dive: `node_modules/caspian-utils/dist/docs/routing.md` (routes, dynamic segments, groups, layouts, layout props), `components.md` (component authoring, imports, slots, direct calls, granularity), `file-conventions.md` (`index.py`/`layout.py`/`loading.py`/`not_found.py`/`error.py`), and `project-structure.md` (placement).
|
|
82
|
+
|
|
83
|
+
### Props passing — the contract that silently fails when skipped
|
|
84
|
+
|
|
85
|
+
There are **two separate handoffs**, and the Python component is the deliberate bridge between them. Skipping the bridge produces no error anywhere — just `undefined` props in the browser.
|
|
86
|
+
|
|
87
|
+
1. **Parent tag → Python.** Attributes on the `<x-*>` tag arrive as **raw string kwargs**, kebab-case converted to camelCase (`on-apply` → `onApply`). PulsePoint expressions are **not** evaluated: `open="{permOpen}"` arrives in Python as the literal string `"{permOpen}"`.
|
|
88
|
+
2. **Python → root → `pp.props`.** The browser computes `pp.props` from the **rendered root element's attributes**, never from the Python signature. So every prop the template's `{...}` expressions read must be re-emitted on the single native root:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
attributes = get_attributes({
|
|
92
|
+
"class": merge_classes("base-classes", props.pop("class", "")),
|
|
93
|
+
"open": open, "value": value, "onApply": onApply, # every prop the template reads
|
|
94
|
+
}, props) # **props = passthrough for the rest
|
|
95
|
+
return html(r"""
|
|
96
|
+
<section {{ attributes }} hidden="{!open}">
|
|
97
|
+
...
|
|
98
|
+
<script>const { open, value, onApply } = pp.props;</script>
|
|
99
|
+
</section>
|
|
100
|
+
""", attributes=attributes)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`{{ attributes }}` on the root **and** `attributes=attributes` into `html(...)` are both required. A named Python parameter is consumed out of `**props`, so it must be listed explicitly in the defaults dict or it never reaches the root.
|
|
104
|
+
|
|
105
|
+
Value-type contract (forwarding fixes presence, not type):
|
|
106
|
+
|
|
107
|
+
| Attribute on the rendered root | `pp.props.x` |
|
|
108
|
+
| ---------------------------------------------- | ------------------------------------------------------------------- |
|
|
109
|
+
| `volume="{vol}"` — brace expression | real type, evaluated in the **parent's** scope |
|
|
110
|
+
| `volume="0"` — literal from a server value | the **string** `"0"` (`volume === 0` is false) |
|
|
111
|
+
| valueless attribute | boolean `true` |
|
|
112
|
+
| `class`, `for` — JS reserved words | dropped; `pp.props.class` never exists |
|
|
113
|
+
| value was `None`/`False`/`""`/empty collection | attribute omitted by `get_attributes` → `undefined` (never `false`) |
|
|
114
|
+
|
|
115
|
+
Design rules: read booleans defensively (`!!pp.props.playing`); coerce server literals before strict comparison; avoid native-attribute collisions (`title` makes a tooltip — prefer `user-name`); camelCase round-trips through kebab-case (`isFullscreen` ↔ `is-fullscreen`). `get_attributes` aliases: `className`/`class_name` → `class`, `htmlFor`/`html_for` → `for`, `defaultValue` → `defaultvalue`, `defaultChecked` → `defaultchecked`. When Tailwind is enabled, `merge_classes(...)` emits a live `{twMerge(...)}` expression — pass it straight through, never wrap or re-merge it, and pop the incoming `class` from `props` first.
|
|
116
|
+
|
|
117
|
+
`pp-ref` on an `x-*` tag is parent-owned and binds the component's concrete DOM root (forwarded through composition hosts). A component can opt out by declaring an explicit `ppRef` parameter. For a child-defined imperative API, pass a parent ref as an ordinary prop and publish with `pp.imperativeHandle(pp.props.controlRef, () => ({...}), [])` — never author `pp-ref-forward`.
|
|
118
|
+
|
|
119
|
+
Deep dive: `node_modules/caspian-utils/dist/docs/components.md` "Receiving Props In A Python Component", "Every Prop A Template Reads Must Be Forwarded To The Root", and "HTML Attribute Helper Contract" (the full `get_attributes`/`merge_classes` behavior and end-to-end examples).
|
|
120
|
+
|
|
121
|
+
### PulsePoint templates — plain HTML, never JSX
|
|
122
|
+
|
|
123
|
+
The React comparison covers exactly two things: the `pp.*` hook API inside `<script>` and how components are split by responsibility. **The markup is plain HTML parsed by an HTML parser.** The one-line test before finishing any template: _would it still be valid HTML with every `{}` deleted?_
|
|
124
|
+
|
|
125
|
+
Fatal JSX constructs and their PulsePoint forms:
|
|
126
|
+
|
|
127
|
+
| Never write | Write instead |
|
|
128
|
+
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
129
|
+
| `{cond && (<div/>)}` / `{cond ? <A/> : <B/>}` | `<div hidden="{!cond}">…</div>` (element stays, guard inner expressions with `?.`) |
|
|
130
|
+
| `{list.map(item => (<li/>))}` | `<template pp-for="item in list"><li key="{item.id}">…</li></template>` |
|
|
131
|
+
| `class={expr}` — unquoted brace attribute | `class="{expr}"` — **always quote**. Unquoted is invalid HTML: the parser shreds the element and the route serves a **blank page with no console error**. |
|
|
132
|
+
| `className`, `htmlFor`, `onClick`, `defaultValue` | `class`, `for`, `onclick`, `defaultvalue` (lowercase HTML) |
|
|
133
|
+
| `style={{color:'red'}}` | `pp-style="{styleText}"` — a CSS **string** |
|
|
134
|
+
| `<>…</>` | one real root; or plain siblings (fragment rules above) |
|
|
135
|
+
| `dangerouslySetInnerHTML` | server-render trusted HTML |
|
|
136
|
+
|
|
137
|
+
**The directive list is closed.** All of it: `{expr}` in text/quoted attributes; native `on*` event attributes; `pp-for` (on `<template>` only, forms `item in items` / `(item, index) in items`, plain `key` on the repeated element); `pp-ref`; `defaultvalue`/`defaultchecked` (lowercase, uncontrolled seed); `pp-style`; `pp-spread="{...obj}"`; `<token.provider value="{v}">` (lowercase context provider); `pp-spa="false"`; `pp-reset-scroll`; `pp-scroll-key`; `pp-loading-content`; `pp-loading-url`; `pp-loading-transition`. There is **no** `pp-if`, `pp-show`, `pp-else`, `pp-model`, `pp-bind`, `pp-class`, `pp-key`, or `pp-context`. If it is not in `public/js/pp-reactive-v2.min.js`, it does not exist.
|
|
138
|
+
|
|
139
|
+
Runtime-managed, never authored: `pp-component`, `pp-owner`, `pp-event-owner`, `pp-ref-forward`, `<pp-context-provider>`, `data-pp-*`, `pp-keep`, `pp-keep-run`, `pp-keep-content`.
|
|
140
|
+
|
|
141
|
+
Rendering semantics worth knowing before debugging a "blank binding":
|
|
142
|
+
|
|
143
|
+
- Interpolations produce **text, never elements**, HTML-escaped, serialized with JSX-child rules: `true`/`false`/`null`/`undefined`/`""` render nothing; `0` and `NaN` print; arrays concatenate with no separator; objects/functions warn (`[PP-WARN] Invalid template child`) and render nothing. So `{items.length && 'x'}` leaks a `0` — write a ternary; bind display expressions (`{admin ? 'yes' : 'no'}`) when a value can be boolean/nullish.
|
|
144
|
+
- On non-boolean attributes, booleans serialize as `"true"`/`"false"` (correct for `aria-*`/`data-*`), and **nullish leaves the attribute present-but-empty** — guard URL attributes: `src="{avatar ? avatar : placeholder}"` (an empty `src` refetches the page).
|
|
145
|
+
- Form controls are controlled (`value="{state}"` + `oninput`) **or** uncontrolled (`defaultvalue="{expr}"`) for their lifetime. Binding `value` to state that starts `undefined` flips the mode and logs `[PP-WARN] … changed from uncontrolled to controlled` — fix the initial state, never add both attributes.
|
|
146
|
+
- `{...}` is safe in **any** attribute or position (SVG `d`/`viewBox`, `src`/`href`, date/number inputs, text in `<table>`/`<select>`) because the server defers each component root inside an inert `<template>`. Never add per-tag workarounds (hidden-gated `<img src>`, `data-*` URL holders, SSR-resolved initial values) to dodge first-paint validation.
|
|
147
|
+
- Event handlers get injected identifiers: `event`, `e`, `$event`, `target`, `currentTarget`, `el`. Lowercase `on*` = native DOM events; kebab-case attributes (`on-open-change`) = component props (`pp.props.onOpenChange`) — the `on-` prefix is convention, not magic.
|
|
148
|
+
- **A handler in slot content runs in the scope of the template that authored the markup**, not the component it renders inside. `ReferenceError: fn is not defined` from a handler that fired means the function lives in the wrong template's script — move the function to the authoring template (its script can stay inside the `x-*` root as slot content) or move the markup into the child. Wrapping in another component or blaming portals does not fix it.
|
|
149
|
+
|
|
150
|
+
Deep dive: `node_modules/caspian-utils/dist/docs/pulsepoint.md` "PulsePoint Is Not JSX", "Complete Directive And API Surface", "Conditional rendering", "Value serialization is the JSX child contract", and "A slot-authored `<script>` belongs to the template that authored it".
|
|
151
|
+
|
|
152
|
+
### Component scripts — hooks and runtime API
|
|
153
|
+
|
|
154
|
+
The script is a plain, untyped `<script>` inside the root: captured by the runtime before materialization, evaluated in component scope via `new Function(...)`. No `import`/`export`/top-level `await`. Only **top-level** declarations reach the template (functions, `const`s, every destructuring shape). Props are read via `pp.props` — there is no injected `props` variable.
|
|
155
|
+
|
|
156
|
+
Hooks (closed list): `pp.state`, `pp.effect`, `pp.layoutEffect`, `pp.ref`, `pp.memo`, `pp.callback`, `pp.reducer`, `pp.context`, `pp.portal`, `pp.id`, `pp.errorBoundary`, `pp.syncExternalStore`, `pp.imperativeHandle`, `pp.transition`, `pp.deferredValue`, `pp.optimistic`, plus `pp.props`. Utilities: `pp.createContext`, `pp.mount`, `pp.redirect`, `pp.rpc`, `pp.socket`, `pp.enablePerf`/`disablePerf`/`getPerfStats`/`resetPerfStats`. No `forwardRef`, `Suspense`, `lazy`, `useActionState`, or `pp.provideContext` — do not invent hooks.
|
|
157
|
+
|
|
158
|
+
Contracts:
|
|
159
|
+
|
|
160
|
+
- Effects return synchronous cleanups only (promises are ignored with a warning). Always pass a dependency array; deps compare by identity, so memoize object/function deps first.
|
|
161
|
+
- `pp.id()` for generated `id`/`for`/`aria-*` — never index- or counter-derived ids.
|
|
162
|
+
- `pp.syncExternalStore` needs a `pp.callback(..., [])`-stable subscribe.
|
|
163
|
+
- `pp.transition()` gives an accurate `isPending` but does **not** time-slice; PulsePoint renders synchronously.
|
|
164
|
+
- `pp.errorBoundary()` catches render/effect/cleanup throws (including its own), latches until `reset()`, gives up after five unreset captures; event-handler errors need `try`/`catch`.
|
|
165
|
+
- Context: `pp.createContext(default)` → lowercase `<themecontext.provider value="{theme}">` in markup → `pp.context(token)` in descendants. Share the token via props when scopes differ. Resolution walks component ancestry (portals included), not the DOM.
|
|
166
|
+
|
|
167
|
+
Performance ownership (the render contract):
|
|
168
|
+
|
|
169
|
+
- `pp.state` = "render required". Timers, request generations, cursors, and RPC-only query text go in `pp.ref` — a ref mutation never renders. Debouncing a setter limits frequency, not render cost: for server search, keep the query in a ref, debounce the RPC, discard stale responses with a generation check, and put only accepted rows in state.
|
|
170
|
+
- Keep high-frequency state in the smallest owning component; `pp.deferredValue` for consumers that may lag one commit.
|
|
171
|
+
- **Prop identity decides child re-renders** (shallow, by identity). Inline `rows="{list.filter(...)}"` or `on-select="{(r) => ...}"` re-renders the child every parent render — memoize arrays/objects with `pp.memo`, handlers with `pp.callback`, and pass those names. Primitives are free. Provider `value` objects must be memoized too, or every consumer re-renders each provider render.
|
|
172
|
+
- Key every `pp-for` row, keep the row body single-rooted, and the runtime reuses unchanged rows; a mounted child boundary is reconciled by its attributes, not its markup.
|
|
173
|
+
- Never "fix" performance with `querySelector`/`addEventListener`/`innerHTML` — diagnose ownership first, then `pp.enablePerf()` if byte-identical output still costs.
|
|
174
|
+
|
|
175
|
+
Interaction rules: bind first-party events with `on*` in the markup; ordinary forms use `onsubmit="{handler(event)}"` + `Object.fromEntries(new FormData(event.currentTarget).entries())` (input `name`s define the payload; Python validates) — never per-input `pp-ref` collection, never id/`data-*`-driven DOM wiring, never manual `innerHTML` list painting. Imperative DOM access (focus, measurement, media, third-party widgets) stays behind `pp.ref` + `pp.effect` inside the owning component.
|
|
176
|
+
|
|
177
|
+
Deep dive: `node_modules/caspian-utils/dist/docs/pulsepoint.md` "Hooks and runtime API", "High-performance authoring", "Context", "Error boundaries", and "SPA, loading, and navigation helpers"; `pulsepoint-runtime-map.md` for the fastest feature-to-owner lookup.
|
|
178
|
+
|
|
179
|
+
### Data — first render, RPC, streaming, uploads
|
|
180
|
+
|
|
181
|
+
- **First render:** load in `page()` (async when I/O-bound), pass into `html(...)` as context, render with `{{ }}`. Shared subtree data goes in `layout()` props.
|
|
182
|
+
- **Everything browser-triggered after that is RPC:** Python `@rpc()` (route-owned in the route's `index.py`; component RPC names are global) called via `pp.rpc(name, data?, options?)`. Never raw `fetch` to hand-made JSON endpoints.
|
|
183
|
+
- `@rpc(require_auth=True, allowed_roles=[...], limits="20/minute")` for protection. **Payload keys are filtered against the signature** — a parameter is client-settable only when declared; identity/ownership/privilege must be derived server-side (`auth.get_payload()`), never accepted as an argument. `**kwargs` opts into the whole payload — only deliberately.
|
|
184
|
+
- Options: `abortPrevious` (cancelled promise resolves `{ cancelled: true }`), `url`, `csrfUrl`, `credentials`, `onStream`, `onStreamError`, `onStreamComplete`, `onUploadProgress` (`{ loaded, total, percent }` — no `percentage`), `onUploadComplete`.
|
|
185
|
+
- **Streaming (the default for AI/LLM/chat tokens):** a generator `@rpc()` that `yield`s chunks (bridge an SDK stream with `async for ... yield`); consume with `pp.rpc(..., { onStream })` appending to state. Never `EventSource`, raw `ReadableStream`, or a WebSocket for one-way streams.
|
|
186
|
+
- **Uploads:** a payload containing `File`/`FileList` becomes multipart (non-file fields sent first; objects JSON-stringified; nullish omitted). Upload/delete actions live in the owning route's `index.py`; blobs under `public/uploads/**` (attachment-mode protected); metadata in Prisma; list UI via `pp.state` + `pp-for`.
|
|
187
|
+
- Server-push-only? RPC streaming. Genuinely bidirectional? Named sockets (see the workspace clarifications below).
|
|
188
|
+
|
|
189
|
+
Deep dive: `node_modules/caspian-utils/dist/docs/fetch-data.md` (first-render data, RPC, "Search, Filters, And Request Races", "Streaming Responses", serialization), `file-uploads.md` (the complete file-manager pattern), and `websockets.md` "Named Sockets".
|
|
190
|
+
|
|
191
|
+
### Server utilities
|
|
192
|
+
|
|
193
|
+
- **Validation** (`casp.validate`): `Validate.email/url/string/boolean/decimal/date/...` for single-value coercion (`Validate.string` trims + HTML-escapes by default); `Validate.with_rules(value, [Rule...], confirmation_value=None)` for multi-constraint form and RPC payloads. Validate every mutation payload in Python.
|
|
194
|
+
- **Metadata** (`casp.layout.Metadata`): static `metadata = Metadata(title=..., description=..., extra={"og:title": ...})` at module scope; dynamic `Metadata(...)` inside `page()` overrides it. Inheritance: root layout → nested layouts → route (route wins per field). Layouts read resolved values as `{{ metadata.* }}`.
|
|
195
|
+
- **Cache** (`casp.cache_handler`): `cache_settings = Cache(ttl=3600, enabled=True)` at module scope in a route's `index.py` (explicit assignment preferred). Public shareable HTML only — `CacheHandler` keys on URI alone and `main.py` refuses to cache authenticated renders. Invalidate after writes with `CacheHandler.invalidate_by_uri(...)`.
|
|
196
|
+
- **StateManager** (`casp.state_manager`): transient request-scoped server state (`get_state`/`set_state`/`reset_state`/`subscribe`) — flash-style messages, not a session store, not browser state. Do not assume cross-request persistence unless `request.state.session` is bridged.
|
|
197
|
+
- **Time** (`casp.app_time`): never bare `datetime.now()` in `src/**`/`main.py` — use `app_time.now()`/`today()`, `to_app_time(...)` for display, `day_bounds_utc(...)` with `gte`/`lt` for calendar-day queries. Session expiry and cache TTLs stay UTC.
|
|
198
|
+
|
|
199
|
+
Deep dive: `node_modules/caspian-utils/dist/docs/validation.md`, `metadata.md`, `cache.md`, `state.md`, `auth.md`, `database.md`, and `core-runtime-map.md` (which `casp` module owns which behavior, including `casp.app_time`).
|
|
200
|
+
|
|
201
|
+
The workspace-specific layers — the quality gate (`npm run check`), browser log (`npm run logs`), formatter, named sockets, auth, Prisma workflow, security invariants, and static export — are covered in the "Workspace Clarifications" and "Task Routing" sections below, and remain part of the required contract.
|
|
202
|
+
|
|
13
203
|
## Document Ownership
|
|
14
204
|
|
|
15
205
|
- Keep repo-wide always-on rules in `.github/copilot-instructions.md`.
|
|
16
|
-
- Keep
|
|
206
|
+
- Keep the "Caspian Core Contracts" section above as the first-party implementation-contract digest: the condensed, always-read version of what the packaged docs explain in depth (brace dialects, authoring model, props passing, PulsePoint surface, data flows). It is version-controlled and survives `node_modules` reinstalls, so when runtime behavior changes, update it together with the matching packaged doc.
|
|
207
|
+
- Keep the rest of this file focused on decision order, task routing, workspace-specific clarifications, and packaged-doc maintenance.
|
|
17
208
|
- Keep packaged docs under `node_modules/caspian-utils/dist/docs/` framework-oriented and use `core-runtime-map.md` when those docs need to point AI back to `main.py` or the installed `casp` runtime.
|
|
18
209
|
- **Only the built runtime under `public/js/**`exists in a generated Caspian app.** Whatever this workspace uses to produce it is a local build detail, not part of the product: never document it, reference it, or route AI to it from the packaged docs. Describe the runtime by its *behavior contract* — what the shipped runtime does — and treat `public/js/pp-reactive-v2.min.js` — the single minified PulsePoint bundle the app serves — as the artifact under discussion. Per-subsystem build output and any authoring source tree are development-only: never cite them as the runtime. The same rule applies to this workspace's own quality tooling and any performance measurement setup: they are development-only and never appear in packaged docs.
|
|
19
210
|
- `node_modules/` is not version-controlled, so every packaged-doc edit must also be ported into the `caspian-utils` package source or the next reinstall wipes it.
|
|
@@ -22,6 +213,8 @@ Do not treat the existence of a packaged doc as proof that the feature is enable
|
|
|
22
213
|
|
|
23
214
|
Use this order depending on the question being answered:
|
|
24
215
|
|
|
216
|
+
0. First-party Caspian implementation contracts, read before any analysis
|
|
217
|
+
- the "Caspian Core Contracts" section at the top of this file
|
|
25
218
|
1. Optional feature enablement and generated surface area
|
|
26
219
|
- `caspian.config.json`
|
|
27
220
|
2. App runtime and app-owned code for current project behavior
|
|
@@ -90,7 +283,7 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
|
|
|
90
283
|
- **A child whose props did not change is not re-walked.** `refreshPropsFromParent` only re-runs `bootstrapNestedComponents()` when the child produced nested runtime structure in its own last render (`hadNestedRuntimeStructures`, the same condition `render()` uses). For a leaf component — every card in a shell — the pass traversed nothing, rebuilt an empty provider set and collected an always-empty descendant list, once per child per parent render.
|
|
91
284
|
- **Every per-render capture store mints ids from a sequence that restarts at zero each render** (the `ppref_`, `ppinput_`, `ppselect_`, `ppchecked_`, `ppcontext*_`, `ppdefault*_` and `ppv_` families), so unchanged markup re-renders to an identical string. Do not give any of them a globally increasing counter: the loop capture store used to, which made every row carrying a per-row handler byte-different on every render and defeated both the byte-identical render skip and per-row reuse. These ids are also lifted out of event-handler source so all rows of one loop share a single compiled handler function — if an id format changes, the matching extraction must change with it, or every row compiles and caches its own handler.
|
|
92
285
|
- When `caspian.config.json` has `websocket: true`, socket behavior is app-owned: the single named-socket endpoint is wired in `main.py` and the layer lives in `src/lib/websocket/**`. Routes do not pass `websocket_path`/`websocket_url` into templates — `pp.socket(...)` already knows the shared endpoint; a route only names its `@socket()` function.
|
|
93
|
-
- **Named sockets are this workspace's preferred live-channel layer.** `src/lib/websocket/sockets.py` is the server half of `pp.socket(...)`: `@socket()` registers an async function by its own name (application-wide, duplicate names refused at registration), every connection lands on the single `SOCKET_PATH` endpoint (`/__pulsepoint/ws` — named for the PulsePoint runtime so every backend serving `pp.socket` uses the same path; wired in `main.py`, gated on `websocket: true`), the arguments arrive as the first frame (one JSON object, filtered against the handler signature like rpc payloads), and failure travels as an `{"error": "..."}` frame followed by a close. The handler declares a `socket` parameter (`Socket`: `recv`/`recv_text`/`send`/`sender`/`close`); `socket.sender()` + `SocketPool` is the broadcast pattern (see `src/app/chat/`). `@socket(require_auth=True, allowed_roles=[...])` delegates to `Auth`; the endpoint keeps the origin check, connection cap, message-size limit, per-connection rate, and idle timeout (outbound traffic counts as liveness). A socket in a route's `index.py` registers when the route first renders; shared sockets live in `src/lib/**`. The browser
|
|
286
|
+
- **Named sockets are this workspace's preferred live-channel layer.** `src/lib/websocket/sockets.py` is the server half of `pp.socket(...)`: `@socket()` registers an async function by its own name (application-wide, duplicate names refused at registration), every connection lands on the single `SOCKET_PATH` endpoint (`/__pulsepoint/ws` — named for the PulsePoint runtime so every backend serving `pp.socket` uses the same path; wired in `main.py`, gated on `websocket: true`), the arguments arrive as the first frame (one JSON object, filtered against the handler signature like rpc payloads), and failure travels as an `{"error": "..."}` frame followed by a close. The handler declares a `socket` parameter (`Socket`: `recv`/`recv_text`/`send`/`sender`/`close`); `socket.sender()` + `SocketPool` is the broadcast pattern (see `src/app/chat/`). `@socket(require_auth=True, allowed_roles=[...])` delegates to `Auth`; the endpoint keeps the origin check, connection cap, message-size limit, per-connection rate, and idle timeout (outbound traffic counts as liveness). A socket in a route's `index.py` registers when the route first renders; shared sockets live in `src/lib/**`. The shipped browser runtime (`public/js/pp-reactive-v2.min.js`) connects `pp.socket(...)` to that same default path, so treat `SOCKET_PATH` as fixed unless the served runtime's default changes with it. This is the only socket layer: hand-written `@app.websocket(...)` + native `WebSocket` is reserved for wires the JSON-frame contract cannot carry (binary, non-JSON protocols) and must run the same origin check and `Auth` delegation itself. Read `node_modules/caspian-utils/dist/docs/websockets.md` "Named Sockets"; tests in `tests/test_socket.py`.
|
|
94
287
|
- Socket auth policy is per socket, not per endpoint: `@socket()` is public, `@socket(require_auth=True)` needs a session, `@socket(allowed_roles=[...])` adds RBAC — all delegating to Caspian's `Auth` (`Auth.set_request(websocket)` plus `is_authenticated`/`get_payload`/`check_role`) inside `sockets.py`. **The old public/private channel layer is gone**: there are no `/ws/live` / `/ws/public` endpoints, no `authorize_websocket(...)` guard, and no `WebSocketConnectionManager` pools — do not reintroduce them or write per-endpoint session parsing. Keep authenticated and guest traffic in separate `SocketPool`s, and treat the socket session as read-only (mutations are not persisted to the cookie over a WebSocket).
|
|
95
288
|
- For socket clients, use `pp.socket(name, args, handlers)` inside the owning component script: open it in `pp.effect(..., [])`, keep the handle in `pp.ref(...)`, close it in the effect cleanup. Reach for a native `new WebSocket(...)` only for a wire the named-socket contract cannot carry (binary frames, non-JSON protocols).
|
|
96
289
|
- Before changing socket security, verify the running code in `src/lib/websocket/sockets.py` — it owns the whole surface: origin allow-list, `MAX_WEBSOCKET_CONNECTIONS`, auth delegation, idle timeout with outbound-liveness, message-size limit, per-connection message rate, and the error-frame-then-close behavior. HTTP route privacy and `AuthMiddleware` do not by themselves protect WebSocket scopes: the HTTP middleware stack early-returns on `scope["type"] == "websocket"`, so only `SessionMiddleware` runs and the socket endpoint authorizes each connection itself.
|
|
@@ -186,6 +379,7 @@ Before merging doc or runtime changes:
|
|
|
186
379
|
|
|
187
380
|
1. Compare the claim or behavior against `main.py`, `src/lib/**`, and `.venv/Lib/site-packages/casp/**`.
|
|
188
381
|
2. Update the matching packaged doc in `node_modules/caspian-utils/dist/docs/` if the running behavior changed.
|
|
189
|
-
3. Update
|
|
190
|
-
4. Update
|
|
382
|
+
3. Update the "Caspian Core Contracts" section in this file if a contract it states changed (brace dialects, authoring model, props passing, PulsePoint surface, data flows, server utilities).
|
|
383
|
+
4. Update `.github/copilot-instructions.md` if the repo-wide implementation rules changed.
|
|
384
|
+
5. Update this file if the decision order, task routing, workspace clarifications, or packaged-doc maintenance rules changed.
|
|
191
385
|
<!-- caspian:end -->
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),PACKAGE_ROOT=path.resolve(__dirname,".."),OPTIONAL_TEMPLATE_FILES=new Set([".python-version",".prettierrc"]),OPTIONAL_TEMPLATE_DIRECTORIES=new Set([".github",".vscode"]),CASPIAN_SECTION_START="\x3c!-- caspian:start --\x3e",CASPIAN_SECTION_END="\x3c!-- caspian:end --\x3e";let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.py","not-found.py","error.py"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1,websocket:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.py","src/app/index.py"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,websocket:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.py","src/app/index.py","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1,websocket:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0,websocket:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const n=e.indexOf("\\htdocs\\");if(-1===n)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,n+8).replace(/\\/g,"\\\\"),s=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let i=`http://localhost/${s}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),a=s.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${a.startsWith("/")?a.substring(1):a}/`}}}async function updatePackageJson(e,n){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const s=JSON.parse(fs.readFileSync(t,"utf8"));s.scripts={...s.scripts,projectName:"tsx settings/project-name.ts",format:"uv run python settings/format.py","format:check":"uv run python settings/format.py --check",check:"uv run python settings/check.py","check:fix":"uv run python settings/fix.py",logs:"uv run python settings/browser_log.py",static:"npm run build && uv run python settings/build-static.py","static:serve":"uv run python settings/serve-static.py"};let i=[];n.tailwindcss&&(s.scripts={...s.scripts,tailwind:"tsx settings/run-postcss.ts watch","tailwind:build":"tsx settings/run-postcss.ts build"},i.push("tailwind")),n.typescript&&!n.backendOnly&&(s.scripts={...s.scripts,"ts:watch":"vite build --watch","ts:watch:dev":"tsx settings/run-vite-watch.ts","ts:build":"vite build"},i.push("ts:watch:dev")),n.mcp&&(s.scripts={...s.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...s.scripts};c.browserSync="tsx settings/bs-config.ts",c.dev=`npm-run-all projectName -l -p browserSync ${i.join(" ")}`;let a=["projectName"];n.tailwindcss&&a.unshift("tailwind:build"),n.typescript&&!n.backendOnly&&a.unshift("ts:build"),c.build=`npm-run-all ${a.join(" ")}`,s.scripts=c,s.type="module",fs.writeFileSync(t,JSON.stringify(s,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function buildEnvSection(e,n){return`# =============================================================================\n${e.split("\n").map(e=>e.startsWith("#")?e:`# ${e}`).join("\n")}\n# =============================================================================\n\n${n.trimEnd()}`}function buildCaspianEnvContent(e){const n=generateAuthSecret(),t=generateHexEncodedKey(8),s=generateHexEncodedKey(32),i=[];return e.prisma&&i.push(buildEnvSection("1. DATABASE\n# Enforced by: prisma/schema.prisma, src/lib/prisma/db.py",'# Connection string. Prisma reads this directly from .env.\n# Format reference: https://pris.ly/d/connection-strings\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"\n\n# Connection-pool limit. Defaults: SQLite 5; MySQL and PostgreSQL 20.\n# Use 5 for local development; production does not need this unless you want to\n# limit the pool.\nDB_POOL_SIZE=5\n\n# Seconds idle before the client re-probes its connection. Default 30.\nPRISMA_CONN_PROBE_IDLE_SECONDS=30\n\n# Warn on queries that cause a full table scan. 0/false silences it. Default 1.\nPRISMA_WARN_FULL_SCAN=1')),i.push(buildEnvSection("2. APPLICATION RUNTIME\n# Enforced by: casp/runtime_security.py is_production_environment()",'# Environment selector, resolved FAIL-CLOSED: only an explicit development\n# value (dev, development, local, staging, test, testing) enables the\n# development relaxations. Unset or misspelled counts as production.\n#\n# Production turns on: HTTPS-only session cookie, Secure CSRF cookie, HSTS,\n# generic error messages, mandatory AUTH_SECRET, mandatory MCP_AUTH_TOKEN, and\n# it removes the localhost origin bypass and the WebSocket same-origin fallback.\n#\n# This single value gates most of the security posture. Set it deliberately.\nAPP_ENV="development"\n\n# Calendar timezone for the application, as an IANA name (e.g. "UTC",\n# "America/New_York", "America/Santo_Domingo"). Read by casp/app_time.py and\n# resolved once at boot in main.py.\n#\n# This sets which wall-clock DAY an instant belongs to: what casp.app_time.now()\n# and today() answer, how a stored timestamp reads back to a user, and the\n# boundaries a "today\'s totals" query uses. Timestamps are still STORED in UTC.\n#\n# It deliberately does NOT affect absolute time -- session expiry (casp/auth.py)\n# and cache TTLs (casp/cache_handler.py) stay on UTC, so changing this can never\n# extend a session or a cache entry.\n#\n# An unrecognized name raises InvalidAppTimezoneError at startup rather than\n# silently falling back to UTC. Empty or unset means UTC.\nAPP_TIMEZONE="UTC"'),buildEnvSection("3. PUBLIC URL, CORS, AND ORIGIN VALIDATION\n# Enforced by: casp/rpc.py origin checks, main.py CORS layer",'# Canonical public origin. Leave empty when the browser URL and the app runtime\n# URL match. Set it when they differ, i.e. behind an ingress, reverse proxy,\n# load balancer, gateway, edge network, or TLS terminator.\nAPP_BASE_URL=""\n\n# Extra browser origins allowed to call protected endpoints such as RPC. Use\n# when one deployment is reachable from more than one public origin.\n# Comma-separated, no spaces.\nCORS_ALLOWED_ORIGINS=""\n\n# Trust Forwarded/X-Forwarded-* headers. Enable ONLY when every request passes\n# through infrastructure that strips client-supplied forwarded headers before\n# setting its own, because a direct client can otherwise forge them.\n#\n# Affects two things: which origin RPC accepts, and which address the rate\n# limiter buckets on. Left false, both use the direct request instead.\nTRUST_FORWARDED_HEADERS="false"\n\n# Allow cookies/Authorization on cross-origin requests. Keep true only when\n# credentialed cross-origin requests are actually required.\nCORS_ALLOW_CREDENTIALS="true"\n\n# CORS preflight response fields.\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\n\n# Preflight cache duration in seconds.\nCORS_MAX_AGE="86400"'),buildEnvSection("4. AUTHENTICATION AND SESSIONS\n# Enforced by: casp/auth.py, main.py SessionMiddleware\n# Route privacy and RBAC live in src/lib/auth/auth_config.py, not here.",`# Session signing secret. Unique and strong per app and per environment.\n# In production the app refuses to start when this is missing or left on a\n# placeholder ("change-me"/"changeme"); in development it falls back.\nAUTH_SECRET="${n}"\n\n# Session cookie name. Use a unique value when several apps share a parent\n# domain, or their sessions overwrite each other.\nAUTH_COOKIE_NAME="${t}"\n\n# Session lifetime in hours (SessionMiddleware max_age).\nSESSION_LIFETIME_HOURS="7"`),buildEnvSection("5. OAUTH PROVIDERS\n# Enforced by: casp/auth.py; routes served by main.py AuthMiddleware","# Google and GitHub sign-in are already wired: AuthMiddleware serves\n# /api/auth/signin/{google,github} and /api/auth/callback/{google,github}.\n# Link a button at those paths, do not hand-roll OAuth.\n#\n# A provider with no client id is skipped SILENTLY: the redirect returns None\n# and the button appears dead, with no error and no log. Empty means disabled.\n\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\n\n# Must match the redirect URI registered in Google Cloud Console exactly.\n# Google is skipped unless BOTH the client id and this value are set.\nGOOGLE_REDIRECT_URI=\n\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET="),buildEnvSection("6. REQUEST SECURITY\n# Enforced by: main.py BodySizeLimitMiddleware, RequestDiagnosticsMiddleware",'# Max size of the whole HTTP request body in MB. Caps the entire body (file +\n# form fields + encoding overhead), so usable file size is a bit below this.\n# Middleware rejects oversized requests before the route runs. Raise if valid\n# uploads are blocked. Default 16.\nMAX_CONTENT_LENGTH_MB="16"\n\n# Seconds before a stalled route returns 504. Streaming paths (/mcp) are exempt\n# so long-lived transports are not cut mid-response. Default 20.\nCASPIAN_REQUEST_TIMEOUT_SECONDS=20'),buildEnvSection("7. SECURITY HEADERS\n# Enforced by: casp/runtime_security.py, main.py SecurityHeadersMiddleware","# Replaces the built-in Content-Security-Policy wholesale. Empty keeps the\n# default, which already permits the app's own assets.\n#\n# Any replacement MUST keep 'unsafe-eval' and 'unsafe-inline' in script-src:\n# the PulsePoint runtime compiles component templates with new Function(), so\n# removing them stops every page from rendering. Set this only to widen the\n# policy, e.g. for a CDN, analytics host, or an external frame embedder.\n#\n# Outside production, connect-src also allows http(s)/ws on localhost and\n# 127.0.0.1 on any port, because BrowserSync serves the proxied page on one port\n# while its injected live-reload client polls the BrowserSync server on another.\n# That is a separate origin, so 'self' does not cover it. Those entries are\n# omitted from a production policy. Setting an override here replaces BOTH, so\n# an override used in development must include the loopback sources itself or\n# live reload stops working.\n#\n# img-src and media-src admit remote content by scheme, so posters, avatars, CDN\n# thumbnails, and video load without per-project configuration. Plain http: is\n# development-only. Set an override here to pin them to named origins instead.\n#\n# Default: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';\n# style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:;\n# media-src 'self' data: blob: https:; font-src 'self' data:;\n# connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';\n# form-action 'self'; frame-ancestors 'self'\nCONTENT_SECURITY_POLICY="),buildEnvSection("8. RATE LIMITING\n# Enforced by: main.py RateLimitMiddleware, casp/rpc.py RPCRateLimiter\n# Buckets are per client address; see TRUST_FORWARDED_HEADERS for which one.",'# Per-IP cap on page requests, applied before session decryption and rendering.\n# Static assets (/css, /js, /assets, /favicon.ico) and /health are exempt, so a\n# page load does not spend its own budget on its assets. Empty disables it.\n# Default 200/minute.\nRATE_LIMIT_PAGES=200/minute\n\n# Fallback limit for @rpc() actions that declare no limits= of their own.\nRATE_LIMIT_RPC="60 per minute"\n\n# Limit applied to @rpc(require_auth=True) actions that declare no limits=.\n# Tighten this and the per-action limits on sign-in and other credential paths.\nRATE_LIMIT_AUTH="60 per minute"\n\n# Configured for slowapi\'s Limiter. Note that slowapi\'s middleware is not in\n# the stack, so this value is inert today; page limiting is RATE_LIMIT_PAGES.\nRATE_LIMIT_DEFAULT="200 per minute"\n\n# In-memory bucket ceiling and sweep interval for the limiter store.\n# Defaults 10000 buckets, swept every 60 seconds.\nRATE_LIMIT_MAX_BUCKETS=10000\nRATE_LIMIT_CLEANUP_INTERVAL=60')),e.websocket&&i.push(buildEnvSection("9. WEBSOCKETS\n# Enforced by: src/lib/websocket/websocket_security.py, main.py channel loop\n# Only active when caspian.config.json has websocket: true.","# Browser origins allowed to open a socket (anti-CSWSH). Falls back to\n# CORS_ALLOWED_ORIGINS then APP_BASE_URL when empty.\n#\n# REQUIRED IN PRODUCTION. The convenience same-origin fallback is derived from\n# the client-supplied Host header, so it is development-only: without an\n# explicit list a spoofed Host plus matching Origin would validate itself.\n#\n# The HTTP middleware stack skips websocket scopes, so this and the socket\n# guard are the only checks a handshake passes. Comma-separated, no spaces.\nWEBSOCKET_ALLOWED_ORIGINS=\n\n# Seconds a socket may stay silent before the server closes it. Default 120.\nWEBSOCKET_IDLE_TIMEOUT_SECONDS=120\n\n# Max size of one inbound socket message in bytes. Oversized closes with 1009.\n# Default 4096.\nMAX_WEBSOCKET_MESSAGE_BYTES=4096\n\n# Simultaneous connections per pool; authenticated and guest pools are counted\n# separately. Refused connections close with 1013 during the handshake. Every\n# open socket is a live task and a broadcast target. Default 200.\nMAX_WEBSOCKET_CONNECTIONS=200\n\n# Per-connection send budget: messages allowed per rolling window. Each\n# accepted message fans out to the whole pool, so this bounds how much\n# broadcast one connection can generate. Defaults 20 per 10 seconds.\nMAX_WEBSOCKET_MESSAGES_PER_WINDOW=20\nWEBSOCKET_RATE_WINDOW_SECONDS=10")),e.mcp&&i.push(buildEnvSection("10. MCP ENDPOINT\n# Enforced by: main.py MCPAuthMiddleware; tools in src/lib/mcp/mcp_server.py\n# Only active when caspian.config.json has mcp: true.",`# Bearer token required to call /mcp. The MCP app is mounted outside the page\n# routing tree, so AuthMiddleware does NOT protect it, and its tools enumerate\n# the workspace file inventory and component map.\n#\n# generated -> every request needs "Authorization: Bearer <token>"\n#\n# REQUIRED IN PRODUCTION for the endpoint to work at all.\nMCP_AUTH_TOKEN="${s}"`)),i.push(buildEnvSection("11. CACHE\n# Enforced by: casp/cache_handler.py, main.py is_request_cacheable()",'# Master switch for serving pages from the disk cache.\n#\n# Entries are keyed on the URI alone, with no session component, so an\n# authenticated render is never cached: is_request_cacheable() gates both the\n# read and the write, and a route\'s Cache(...) cannot override it.\nCACHE_ENABLED="false"\n\n# Default cache lifetime in seconds, used when a route sets no ttl.\nCACHE_TTL="600"'),buildEnvSection("12. SERVER PROCESS\n# Enforced by: main.py __main__, settings/serve-static.py",'# Uvicorn workers are separate OS processes for the same FastAPI app. More\n# workers can increase throughput under concurrent load, but they do not make a\n# single request faster and they duplicate memory, connection pools, and any\n# in-process state. Keep at 1 unless the app is designed for multi-process\n# coordination and testing shows a real concurrency bottleneck.\nUVICORN_WORKERS="1"')),i.join("\n\n")}function copyRecursiveSync(e,n,t){const s=fs.existsSync(e),i=s&&fs.statSync(e);if(s&&i&&i.isDirectory()){const s=n.toLowerCase();if(!t.mcp&&s.includes("src\\lib\\mcp"))return;if(!t.websocket&&s.includes("src\\lib\\websocket"))return;if((!t.typescript||t.backendOnly)&&(s.endsWith("\\ts")||s.includes("\\ts\\")))return;if((!t.typescript||t.backendOnly)&&(s.endsWith("\\vite-plugins")||s.includes("\\vite-plugins\\")||s.includes("\\vite-plugins")))return;if(t.backendOnly&&s.includes("public\\js")||t.backendOnly&&s.includes("public\\css")||t.backendOnly&&s.includes("public\\assets"))return;const i=n.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),fs.readdirSync(e).forEach(s=>{copyRecursiveSync(path.join(e,s),path.join(n,s),t)})}else{if(checkExcludeFiles(n))return;const s=n.replace(/\\/g,"/").toLowerCase();if(s.endsWith("/settings/run-vite-watch.ts")&&(!t.typescript||t.backendOnly))return;if(s.endsWith("/ts/tailwind-merge.ts")&&(!t.typescript||t.backendOnly||!t.tailwindcss))return;if(!t.tailwindcss&&(n.includes("globals.css")||n.includes("styles.css")))return;if(!t.mcp&&n.includes("restart-mcp.ts"))return;if(!t.websocket&&n.includes("src\\lib\\websocket"))return;if(t.backendOnly&&nonBackendFiles.some(e=>n.includes(e)))return;if(t.backendOnly&&n.includes("layout.py"))return;if(t.tailwindcss&&n.includes("index.css"))return;if(!t.prisma&&n.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,n,0)}}async function executeCopy(e,n,t){n.forEach(({src:n,dest:s})=>{const i=normalizeTemplatePath(n),c=resolveTemplateSourcePath(n,"directory"),a=path.join(e,s);if(!c){if(OPTIONAL_TEMPLATE_DIRECTORIES.has(i))return void console.log(chalk.gray(`Optional template directory not found, skipping: ${i}`));throw new Error(`Template directory not found: ${i}. The package may be incomplete.`)}copyRecursiveSync(c,a,t)})}function modifyLayoutPHP(e,n){const t=path.join(e,"src","app","layout.py");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),s="";n.backendOnly||(n.tailwindcss||(s='\n <link href="/css/index.css" rel="stylesheet" />'),s+='\n <script type="module" src="/js/main.js"><\/script>');let i="";n.backendOnly||(i=n.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${s}`:s),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.py:"),e)}}async function createOrUpdateEnvFile(e,n){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,n,{flag:"w"})}function ensureClaudeMd(e){const n=path.join(e,"CLAUDE.md");if(checkExcludeFiles(n))return;const t="@AGENTS.md";if(!fs.existsSync(n))return void fs.writeFileSync(n,`${t}\n`,{flag:"w"});const s=fs.readFileSync(n,"utf8").replace(/^\uFEFF/,"");if(s.trimStart().startsWith(t))return;const i=`${t}\n\n${s.trimStart()}`;fs.writeFileSync(n,i,{flag:"w"})}function writeTailwindMainJs(e){const n=path.join(e,"public","js","main.js");checkExcludeFiles(n)||(fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,'import "/js/pp-reactive-v2.min.js";\nimport { twMerge } from "/js/tailwind-merge.mjs";\n\nconst pp = (globalThis).pp;\n\nglobalThis.twMerge = twMerge;\n\nif (document.readyState !== "loading") {\n pp?.mount?.();\n} else {\n document.addEventListener(\n "DOMContentLoaded",\n () => pp?.mount?.(),\n { once: true },\n );\n}\n',{flag:"w"}))}function copyTailwindMergeBundle(e){const n=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs"),t=path.join(e,"public","js","tailwind-merge.mjs"),s=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs.map"),i=path.join(e,"public","js","bundle-mjs.mjs.map");if(!checkExcludeFiles(t)){if(!fs.existsSync(n))throw new Error(`tailwind-merge bundle not found at ${n}`);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.copyFileSync(n,t),!checkExcludeFiles(i)&&fs.existsSync(s)&&fs.copyFileSync(s,i)}}function writeTailwindTypeScriptMain(e){const n=path.join(e,"ts","main.ts");checkExcludeFiles(n)||(fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,'import "/js/pp-reactive-v2.min.js";\n\n// The following global names have already been declared elsewhere in the project:\n// - pp: Used for the Reactive Core functionality.\n\n// Imports goes here --Start\nimport { createGlobalSingleton } from "./global-functions.js";\nimport { mergeTailwindClasses } from "./tailwind-merge.js";\n\ncreateGlobalSingleton("twMerge", mergeTailwindClasses);\n\n\n// Imports goes here --End\n\nconst pp = (globalThis as any).pp;\n\nif (document.readyState !== "loading") {\n\tpp?.mount?.();\n} else {\n\tdocument.addEventListener(\n\t\t"DOMContentLoaded",\n\t\t() => pp?.mount?.(),\n\t\t{ once: true },\n\t);\n}\n',{flag:"w"}))}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const n=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(n)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const t=e.replace(/\\/g,"/");return n.endsWith("/"+t)||n===t})}function normalizeTemplatePath(e){return e.replace(/^[\\/]+/,"")}function resolveTemplateSourcePath(e,n){const t=normalizeTemplatePath(e),s=[path.join(__dirname,t),path.join(PACKAGE_ROOT,t)];for(const e of s){if(!fs.existsSync(e))continue;const t=fs.statSync(e);if("file"===n&&t.isFile())return e;if("directory"===n&&t.isDirectory())return e}return null}function extractCaspianSection(e){const n=e.indexOf(CASPIAN_SECTION_START);if(-1===n)return null;const t=e.indexOf(CASPIAN_SECTION_END,n);return-1===t?null:e.slice(t>n?n:0,t+20)}function mergeAgentsCaspianSection(e,n){const t=extractCaspianSection(n);if(!t)return e;const s=e.indexOf(CASPIAN_SECTION_START),i=e.indexOf(CASPIAN_SECTION_END,s);if(-1!==s&&-1!==i){return`${e.slice(0,s)}${t}${e.slice(i+20)}`}const c=e.endsWith("\n");return`${e}${c?"\n":"\n\n"}${t}\n`}async function createDirectoryStructure(e,n){const t=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/AGENTS.md",dest:"/AGENTS.md"},{src:"/.python-version",dest:"/.python-version"}];n.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),n.typescript&&!n.backendOnly&&t.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const s=[{src:"/settings",dest:"/settings"},{src:"/tests",dest:"/tests"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"},{src:"/.vscode",dest:"/.vscode"}];n.typescript&&!n.backendOnly&&s.push({src:"/ts",dest:"/ts"}),t.forEach(({src:n,dest:t})=>{const s=normalizeTemplatePath(n),i=resolveTemplateSourcePath(n,"file"),c=path.join(e,t);if(checkExcludeFiles(c))return;if(!i){if(OPTIONAL_TEMPLATE_FILES.has(s))return void console.log(chalk.gray(`Optional template file not found, skipping: ${s}`));throw new Error(`Template file not found: ${s}. The package may be incomplete.`)}if("/pyproject.toml"===n&&updateAnswer?.isUpdate&&fs.existsSync(c))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const a=fs.readFileSync(i,"utf8");if("/AGENTS.md"===n&&updateAnswer?.isUpdate&&fs.existsSync(c)){const e=mergeAgentsCaspianSection(fs.readFileSync(c,"utf8"),a);return void fs.writeFileSync(c,e,{flag:"w"})}fs.writeFileSync(c,a,{flag:"w"})}),await executeCopy(e,s,n),ensureClaudeMd(e),n.tailwindcss&&!n.backendOnly&&(n.typescript?writeTailwindTypeScriptMain(e):(copyTailwindMergeBundle(e),writeTailwindMainJs(e))),await updatePackageJson(e,n),!n.tailwindcss&&n.backendOnly||modifyLayoutPHP(e,n),await createOrUpdateEnvFile(e,buildCaspianEnvContent(n))}async function getAnswer(e={},n=!1){if(n)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,websocket:e.websocket??!1,prisma:e.prisma??!1};if(e.starterKit){const n=e.starterKit;let t=null;if(STARTER_KITS[n]&&(t=STARTER_KITS[n]),t){const s={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,prisma:t.features.prisma??!1,mcp:t.features.mcp??!1,websocket:t.features.websocket??!1,typescript:t.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(s.backendOnly=!0),i.includes("--tailwindcss")&&(s.tailwindcss=!0),i.includes("--mcp")&&(s.mcp=!0),i.includes("--websocket")&&(s.websocket=!0),i.includes("--prisma")&&(s.prisma=!0),i.includes("--typescript")&&(s.typescript=!0),s}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,websocket:!1,typescript:!1},s=process.argv.slice(2);return s.includes("--backend-only")&&(t.backendOnly=!0),s.includes("--tailwindcss")&&(t.tailwindcss=!0),s.includes("--mcp")&&(t.mcp=!0),s.includes("--websocket")&&(t.websocket=!0),s.includes("--prisma")&&(t.prisma=!0),s.includes("--typescript")&&(t.typescript=!0),t}}const t=[];e.projectName||t.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||t.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const s=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(t,{onCancel:s}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"}));const a=await prompts(c,{onCancel:s});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:a.tailwindcss??e.tailwindcss??!1,typescript:a.typescript??e.typescript??!1,mcp:a.mcp??e.mcp??!1,websocket:a.websocket??e.websocket??!1,prisma:a.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,n,t=!1){console.log("Uninstalling Node dependencies:"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=buildManagedNpmCommand(["uninstall",t?"--save-dev":"--save",...n]);execSync(s,{stdio:"inherit",cwd:e})}function buildManagedNpmCommand(e){return`npm ${e.join(" ")} --ignore-scripts=false --min-release-age=0 --audit=false`}function fetchPackageVersion(e){return new Promise((n,t)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let s="";e.on("data",e=>s+=e),e.on("end",()=>{try{const e=JSON.parse(s);n(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}})}).on("error",e=>t(e))})}const readJsonFile=e=>{const n=fs.readFileSync(e,"utf8");return JSON.parse(n)};function compareVersions(e,n){const t=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),s=n.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!t||!s)return e.localeCompare(n);const i=t.slice(1,4).map(Number),c=s.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>c[e])return 1;if(i[e]<c[e])return-1}const a=t[4]??null,o=s[4]??null;return a&&!o?-1:!a&&o?1:a&&o?a.localeCompare(o):0}function getInstalledPackageInfo(e){try{const n=execSync(buildManagedNpmCommand(["list","-g",e,"--depth=0"])).toString(),t=n.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return t?{version:t[1],isLinked:n.includes(`${e}@`)&&n.includes("->")}:(console.error(`Package ${e} is not installed`),{version:null,isLinked:!1})}catch(e){return console.error(e instanceof Error?e.message:String(e)),{version:null,isLinked:!1}}}function isRunningFromNpxCache(e){const n=path.resolve(e).toLowerCase(),t=`${path.sep}_npx${path.sep}`.toLowerCase();return n.includes(t)}async function installNpmDependencies(e,n,t=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync(buildManagedNpmCommand(["init","-y"]),{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=buildManagedNpmCommand(["install",...t?["--save-dev"]:[],...n]);execSync(s,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.3.3","@types/browser-sync":"2.29.1","@types/node":"26.2.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"6.0.0","chokidar-cli":"3.0.0",cssnano:"8.0.5","npm-run-all":"4.1.5",postcss:"8.5.26","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.3.3",tsx:"4.23.12",typescript:"7.0.2",vite:"8.2.0",vitest:"4.1.10","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.19","caspian-utils":"0.2.x","tailwind-merge":"3.6.0"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(n){const t=n;if("win32"===globalThis.process?.platform&&("EPERM"===t.code||"EACCES"===t.code)){try{spawnSync("cmd",["/c","attrib","-R","-H","-S","/S","/D",`${e}\\*`],{stdio:"ignore"})}catch{}return void spawnSync("cmd",["/c","rd","/s","/q",e],{stdio:"ignore"})}throw n}}async function setupStarterKit(e,n){if(!n.starterKit)return;let t=null;if(STARTER_KITS[n.starterKit]?t=STARTER_KITS[n.starterKit]:n.starterKitSource&&(t={id:n.starterKit,name:`Custom Starter Kit (${n.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:n.starterKitSource}}),t){if(console.log(chalk.green(`Setting up ${t.name}...`)),t.source)try{const s=t.source.branch?`git clone -b ${t.source.branch} --depth 1 ${t.source.url} "${e}"`:`git clone --depth 1 ${t.source.url} "${e}"`;execSync(s,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const i=path.join(e,"caspian.config.json");if(fs.existsSync(i))try{const t=JSON.parse(fs.readFileSync(i,"utf8")),s=e,c=bsConfigUrls(s);t.projectName=n.projectName,t.projectRootPath=s,t.bsTarget=c.bsTarget,t.bsPathRewrite=c.bsPathRewrite;const a=await fetchPackageVersion("create-caspian-app");t.version=t.version||a,fs.writeFileSync(i,JSON.stringify(t,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}t.customSetup&&await t.customSetup(e,n),console.log(chalk.green(`✓ ${t.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${n.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\n🚀 Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const n=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(n)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const t=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");t&&console.log(chalk.magenta(` Features: ${t}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,n,t){const s=spawnSync(e,n,{cwd:t,stdio:"inherit",shell:!1,encoding:"utf8"});if(s.error)throw s.error;if(0!==s.status)throw new Error(`Command failed (${e} ${n.join(" ")}), exit=${s.status}`)}function tryRunCmd(e,n,t){const s=spawnSync(e,n,{cwd:t,stdio:"ignore",shell:!1,encoding:"utf8"});return!s.error&&0===s.status}function tryInstallUv(e){console.log(chalk.blue("uv not found. Attempting to install uv..."));const n=[{cmd:"py",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python3",args:["-m","pip","install","--upgrade","uv"]}];for(const t of n)if(tryRunCmd(t.cmd,t.args,e))return!0;return!1}function resolveUvCommand(e){const n=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const t of n)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;if(tryInstallUv(e))for(const t of n)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;throw new Error("Could not find or install uv. Install uv and ensure `uv`, `py`, or `python` is available in PATH.")}function buildPythonDependencies(e){const n=["fastapi==0.141.1","uvicorn==0.52.1","python-dotenv==1.2.2","tzdata==2026.3","jinja2==3.1.6","beautifulsoup4==4.15.0","slowapi==0.1.10","python-multipart==0.0.32","starsessions==2.2.1","httpx2==2.10.0","werkzeug==3.1.8","cuid2==2.0.1","nanoid==2.0.0","python-ulid==4.0.1","cuid==0.4","caspian-utils~=0.4"];return e.mcp&&n.push("fastmcp==3.4.7"),e.websocket&&n.push("websockets==17.0.1"),e.prisma&&(n.push("psycopg2-binary==2.9.12"),n.push("asyncpg==0.31.0"),n.push("aiosqlite==0.22.1"),n.push("aiomysql==0.3.2")),n}function buildPythonDevDependencies(){return["pyright==1.1.411","ruff==0.16.1","pytest==9.1.1","djlint==1.43.2"]}function getPythonRequirementName(e){const n=e.trim().match(/^([A-Za-z0-9._-]+)/);return n?.[1]??null}function getPyProjectDependencyNames(e){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))return new Set;const t=fs.readFileSync(n,"utf8").replace(/\r\n/g,"\n").match(/^[ \t]*dependencies[ \t]*=[ \t]*\[([\s\S]*?)\]/m);if(!t)return new Set;const s=new Set,i=/"([^"]+)"/g;let c;for(;null!==(c=i.exec(t[1]));){const e=c[1].trim().match(/^([A-Za-z0-9._-]+)/)?.[1];e&&s.add(e.toLowerCase())}return s}function ensurePyProjectExists(e){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))throw new Error(`pyproject.toml not found at: ${n}`);let t=fs.readFileSync(n,"utf8");t=t.replace(/\r\n/g,"\n"),t.includes("package = false")||(t=t.includes("[tool.uv]")?t.replace("[tool.uv]","[tool.uv]\npackage = false"):`${t.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(n,t,"utf8")}async function ensurePythonVenvAndDeps(e,n,t=[]){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: syncing dependencies with uv")),console.log(chalk.green("=========================\n")),console.log(chalk.blue("Preparing pyproject.toml...")),ensurePyProjectExists(e);const s=path.join(e,"requirements.txt");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(chalk.gray("Removed legacy requirements.txt")));const i=resolveUvCommand(e),c=path.join(e,".venv");fs.existsSync(c)?console.log(chalk.blue("Existing .venv detected. Reusing it so uv sync can update dependencies without replacing the environment.")):(console.log(chalk.blue("Creating the virtual environment with uv...")),runCmd(i.cmd,[...i.argsPrefix,"venv",".venv"],e));const a=buildPythonDependencies(n),o=buildPythonDevDependencies(),r=a.map(e=>getPythonRequirementName(e)).filter(e=>null!==e),l=o.map(e=>getPythonRequirementName(e)).filter(e=>null!==e);t.length>0&&(console.log(chalk.blue("Removing obsolete Python dependencies via uv remove...")),runCmd(i.cmd,[...i.argsPrefix,"remove",...t],e));const p=r.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...p,...a],e);const d=l.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dev dependencies via uv add --dev...")),runCmd(i.cmd,[...i.argsPrefix,"add","--dev",...d,...o],e),console.log(chalk.blue("Syncing dependencies...")),runCmd(i.cmd,[...i.argsPrefix,"sync"],e),console.log(chalk.green("\n✓ uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),n=e.includes("-y");let t=e[0];const s=e.find(e=>e.startsWith("--starter-kit=")),i=s?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),a=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let o=null,r=!1;if(t){const s=process.cwd(),c=path.join(s,"caspian.config.json");if(i&&a){r=!0;const s={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};o=await getAnswer(s,n)}else if(fs.existsSync(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const n=path.join(s,e);fs.existsSync(n)&&a.push(n.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,websocket:i.websocket??!1,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:s};const r={projectName:t,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp,websocket:e.includes("--websocket")||(i.websocket??!1)};o=await getAnswer(r,n),null!==o&&(updateAnswer={projectName:t,backendOnly:o.backendOnly,tailwindcss:o.tailwindcss,mcp:o.mcp,websocket:o.websocket,prisma:o.prisma,typescript:o.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:s})}else{const s={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};o=await getAnswer(s,n)}if(null===o)return void console.log(chalk.red("Installation cancelled."))}else o=await getAnswer({},n);if(null===o)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync(buildManagedNpmCommand(["uninstall","-g","create-caspian-app"]),{stdio:"inherit"}),execSync(buildManagedNpmCommand(["install","-g","create-caspian-app"]),{stdio:"inherit"})):execSync(buildManagedNpmCommand(["install","-g","create-caspian-app"]),{stdio:"inherit"});const d=process.cwd();let u;if(t)if(r){const n=path.join(d,t);fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,await setupStarterKit(u,o),process.chdir(u);const s=path.join(u,"caspian.config.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));e.includes("--backend-only")&&(n.backendOnly=!0),e.includes("--tailwindcss")&&(n.tailwindcss=!0),e.includes("--typescript")&&(n.typescript=!0),e.includes("--mcp")&&(n.mcp=!0),e.includes("--websocket")&&(n.websocket=!0),e.includes("--prisma")&&(n.prisma=!0),o={...o,backendOnly:n.backendOnly,tailwindcss:n.tailwindcss,typescript:n.typescript,mcp:n.mcp,websocket:n.websocket??!1,prisma:n.prisma};let t=[];n.excludeFiles?.map(e=>{const n=path.join(u,e);fs.existsSync(n)&&t.push(n.replace(/\\/g,"/"))}),updateAnswer={...o,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),n=path.join(d,t),s=path.join(n,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(n)&&fs.existsSync(s)?(u=n,process.chdir(n)):(fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,process.chdir(n))}else fs.mkdirSync(o.projectName,{recursive:!0}),u=path.join(d,o.projectName),process.chdir(o.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python"),npmPkg("caspian-utils")];o.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),o.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),o.prisma&&execSync(buildManagedNpmCommand(["install","-g","prisma-client-python@latest"]),{stdio:"inherit"}),o.typescript&&!o.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),o.typescript&&m.push(npmPkg("vitest")),o.starterKit&&!r&&await setupStarterKit(u,o),await installNpmDependencies(u,m,!0);let h=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,o),o.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],n=[],t=e=>{try{const n=path.join(u,"package.json");if(fs.existsSync(n)){const t=JSON.parse(fs.readFileSync(n,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const n=path.join(u,"src","app",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const n=path.join(u,"src","app",e);fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const n=path.join(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"public","js","tailwind-merge.mjs");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${s} was deleted successfully.`));const i=path.join(u,"public","js","bundle-mjs.mjs.map");fs.existsSync(i)&&(fs.unlinkSync(i),console.log(`${i} was deleted successfully.`));const c=path.join(u,"ts","tailwind-merge.ts");fs.existsSync(c)&&(fs.unlinkSync(c),console.log(`${c} was deleted successfully.`));["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano","tailwind-merge"].forEach(n=>{t(n)&&e.push(n)}),n.push("tailwind-merge")}if(o.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(n){console.warn(chalk.yellow(`Failed to delete ${e}: ${n}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const n=path.join(u,"settings",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),n.push("fastmcp")}if(!updateAnswer.websocket){const e=path.join(u,"src","lib","websocket");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("WebSocket folder was deleted successfully.")),n.push("websockets")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(n=>{t(n)&&e.push(n)}),n.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts",path.join("settings","run-vite-watch.ts")].forEach(e=>{const n=path.join(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const n=path.join(u,"ts");fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const s=path.join(u,"settings","vite-plugins");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(n=>{t(n)&&e.push(n)})}const s=e=>Array.from(new Set(e)),i=s(e);i.length>0&&(console.log(`Uninstalling npm packages: ${i.join(", ")}`),await uninstallNpmDependencies(u,i,!0));const c=s(n),a=getPyProjectDependencyNames(u);h=c.filter(e=>a.has(e.toLowerCase())),h.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${h.join(", ")}`))}if(!r||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),n=bsConfigUrls(e),t={projectName:o.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:o.backendOnly,tailwindcss:o.tailwindcss,mcp:o.mcp,websocket:o.websocket,prisma:o.prisma,typescript:o.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,o,h),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
|
|
2
|
+
import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),PACKAGE_ROOT=path.resolve(__dirname,".."),OPTIONAL_TEMPLATE_FILES=new Set([".python-version",".prettierrc"]),OPTIONAL_TEMPLATE_DIRECTORIES=new Set([".github",".vscode"]),CASPIAN_SECTION_START="\x3c!-- caspian:start --\x3e",CASPIAN_SECTION_END="\x3c!-- caspian:end --\x3e";let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.py","not-found.py","error.py"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1,websocket:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.py","src/app/index.py"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,websocket:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.py","src/app/index.py","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1,websocket:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0,websocket:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const n=e.indexOf("\\htdocs\\");if(-1===n)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,n+8).replace(/\\/g,"\\\\"),s=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let i=`http://localhost/${s}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),a=s.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${a.startsWith("/")?a.substring(1):a}/`}}}async function updatePackageJson(e,n){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const s=JSON.parse(fs.readFileSync(t,"utf8"));s.scripts={...s.scripts,projectName:"tsx settings/project-name.ts",format:"uv run python settings/format.py","format:check":"uv run python settings/format.py --check",check:"uv run python settings/check.py","check:fix":"uv run python settings/fix.py",logs:"uv run python settings/browser_log.py",static:"npm run build && uv run python settings/build-static.py","static:serve":"uv run python settings/serve-static.py"};let i=[];n.tailwindcss&&(s.scripts={...s.scripts,tailwind:"tsx settings/run-postcss.ts watch","tailwind:build":"tsx settings/run-postcss.ts build"},i.push("tailwind")),n.typescript&&!n.backendOnly&&(s.scripts={...s.scripts,"ts:watch":"vite build --watch","ts:watch:dev":"tsx settings/run-vite-watch.ts","ts:build":"vite build"},i.push("ts:watch:dev")),n.mcp&&(s.scripts={...s.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...s.scripts};c.browserSync="tsx settings/bs-config.ts",c.dev=`npm-run-all projectName -l -p browserSync ${i.join(" ")}`;let a=["projectName"];n.tailwindcss&&a.unshift("tailwind:build"),n.typescript&&!n.backendOnly&&a.unshift("ts:build"),c.build=`npm-run-all ${a.join(" ")}`,s.scripts=c,s.type="module",fs.writeFileSync(t,JSON.stringify(s,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function buildEnvSection(e,n){return`# =============================================================================\n${e.split("\n").map(e=>e.startsWith("#")?e:`# ${e}`).join("\n")}\n# =============================================================================\n\n${n.trimEnd()}`}function buildCaspianEnvContent(e){const n=generateAuthSecret(),t=generateHexEncodedKey(8),s=generateHexEncodedKey(32),i=[];return e.prisma&&i.push(buildEnvSection("1. DATABASE\n# Enforced by: prisma/schema.prisma, src/lib/prisma/db.py",'# Connection string. Prisma reads this directly from .env.\n# Format reference: https://pris.ly/d/connection-strings\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"\n\n# Connection-pool limit. Defaults: SQLite 5; MySQL and PostgreSQL 20.\n# Use 5 for local development; production does not need this unless you want to\n# limit the pool.\nDB_POOL_SIZE=5\n\n# Seconds idle before the client re-probes its connection. Default 30.\nPRISMA_CONN_PROBE_IDLE_SECONDS=30\n\n# Warn on queries that cause a full table scan. 0/false silences it. Default 1.\nPRISMA_WARN_FULL_SCAN=1')),i.push(buildEnvSection("2. APPLICATION RUNTIME\n# Enforced by: casp/runtime_security.py is_production_environment()",'# Environment selector, resolved FAIL-CLOSED: only an explicit development\n# value (dev, development, local, staging, test, testing) enables the\n# development relaxations. Unset or misspelled counts as production.\n#\n# Production turns on: HTTPS-only session cookie, Secure CSRF cookie, HSTS,\n# generic error messages, mandatory AUTH_SECRET, mandatory MCP_AUTH_TOKEN, and\n# it removes the localhost origin bypass and the WebSocket same-origin fallback.\n#\n# This single value gates most of the security posture. Set it deliberately.\nAPP_ENV="development"\n\n# Calendar timezone for the application, as an IANA name (e.g. "UTC",\n# "America/New_York", "America/Santo_Domingo"). Read by casp/app_time.py and\n# resolved once at boot in main.py.\n#\n# This sets which wall-clock DAY an instant belongs to: what casp.app_time.now()\n# and today() answer, how a stored timestamp reads back to a user, and the\n# boundaries a "today\'s totals" query uses. Timestamps are still STORED in UTC.\n#\n# It deliberately does NOT affect absolute time -- session expiry (casp/auth.py)\n# and cache TTLs (casp/cache_handler.py) stay on UTC, so changing this can never\n# extend a session or a cache entry.\n#\n# An unrecognized name raises InvalidAppTimezoneError at startup rather than\n# silently falling back to UTC. Empty or unset means UTC.\nAPP_TIMEZONE="UTC"'),buildEnvSection("3. PUBLIC URL, CORS, AND ORIGIN VALIDATION\n# Enforced by: casp/rpc.py origin checks, main.py CORS layer",'# Canonical public origin. Leave empty when the browser URL and the app runtime\n# URL match. Set it when they differ, i.e. behind an ingress, reverse proxy,\n# load balancer, gateway, edge network, or TLS terminator.\nAPP_BASE_URL=""\n\n# Extra browser origins allowed to call protected endpoints such as RPC. Use\n# when one deployment is reachable from more than one public origin.\n# Comma-separated, no spaces.\nCORS_ALLOWED_ORIGINS=""\n\n# Trust Forwarded/X-Forwarded-* headers. Enable ONLY when every request passes\n# through infrastructure that strips client-supplied forwarded headers before\n# setting its own, because a direct client can otherwise forge them.\n#\n# Affects two things: which origin RPC accepts, and which address the rate\n# limiter buckets on. Left false, both use the direct request instead.\nTRUST_FORWARDED_HEADERS="false"\n\n# Allow cookies/Authorization on cross-origin requests. Keep true only when\n# credentialed cross-origin requests are actually required.\nCORS_ALLOW_CREDENTIALS="true"\n\n# CORS preflight response fields.\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\n\n# Preflight cache duration in seconds.\nCORS_MAX_AGE="86400"'),buildEnvSection("4. AUTHENTICATION AND SESSIONS\n# Enforced by: casp/auth.py, main.py SessionMiddleware\n# Route privacy and RBAC live in src/lib/auth/auth_config.py, not here.",`# Session signing secret. Unique and strong per app and per environment.\n# In production the app refuses to start when this is missing or left on a\n# placeholder ("change-me"/"changeme"); in development it falls back.\nAUTH_SECRET="${n}"\n\n# Session cookie name. Use a unique value when several apps share a parent\n# domain, or their sessions overwrite each other.\nAUTH_COOKIE_NAME="${t}"\n\n# Session lifetime in hours (SessionMiddleware max_age).\nSESSION_LIFETIME_HOURS="7"`),buildEnvSection("5. OAUTH PROVIDERS\n# Enforced by: casp/auth.py; routes served by main.py AuthMiddleware","# Google and GitHub sign-in are already wired: AuthMiddleware serves\n# /api/auth/signin/{google,github} and /api/auth/callback/{google,github}.\n# Link a button at those paths, do not hand-roll OAuth.\n#\n# A provider with no client id is skipped SILENTLY: the redirect returns None\n# and the button appears dead, with no error and no log. Empty means disabled.\n\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\n\n# Must match the redirect URI registered in Google Cloud Console exactly.\n# Google is skipped unless BOTH the client id and this value are set.\nGOOGLE_REDIRECT_URI=\n\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET="),buildEnvSection("6. REQUEST SECURITY\n# Enforced by: main.py BodySizeLimitMiddleware, RequestDiagnosticsMiddleware",'# Max size of the whole HTTP request body in MB. Caps the entire body (file +\n# form fields + encoding overhead), so usable file size is a bit below this.\n# Middleware rejects oversized requests before the route runs. Raise if valid\n# uploads are blocked. Default 16.\nMAX_CONTENT_LENGTH_MB="16"\n\n# Seconds before a stalled route returns 504. Streaming paths (/mcp) are exempt\n# so long-lived transports are not cut mid-response. Default 20.\nCASPIAN_REQUEST_TIMEOUT_SECONDS=20'),buildEnvSection("7. SECURITY HEADERS\n# Enforced by: casp/runtime_security.py, main.py SecurityHeadersMiddleware","# Replaces the built-in Content-Security-Policy wholesale. Empty keeps the\n# default, which already permits the app's own assets.\n#\n# Any replacement MUST keep 'unsafe-eval' and 'unsafe-inline' in script-src:\n# the PulsePoint runtime compiles component templates with new Function(), so\n# removing them stops every page from rendering. Set this only to widen the\n# policy, e.g. for a CDN, analytics host, or an external frame embedder.\n#\n# Outside production, connect-src also allows http(s)/ws on localhost and\n# 127.0.0.1 on any port, because BrowserSync serves the proxied page on one port\n# while its injected live-reload client polls the BrowserSync server on another.\n# That is a separate origin, so 'self' does not cover it. Those entries are\n# omitted from a production policy. Setting an override here replaces BOTH, so\n# an override used in development must include the loopback sources itself or\n# live reload stops working.\n#\n# img-src and media-src admit remote content by scheme, so posters, avatars, CDN\n# thumbnails, and video load without per-project configuration. Plain http: is\n# development-only. Set an override here to pin them to named origins instead.\n#\n# Default: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';\n# style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:;\n# media-src 'self' data: blob: https:; font-src 'self' data:;\n# connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';\n# form-action 'self'; frame-ancestors 'self'\nCONTENT_SECURITY_POLICY="),buildEnvSection("8. RATE LIMITING\n# Enforced by: main.py RateLimitMiddleware, casp/rpc.py RPCRateLimiter\n# Buckets are per client address; see TRUST_FORWARDED_HEADERS for which one.",'# Per-IP cap on page requests, applied before session decryption and rendering.\n# Static assets (/css, /js, /assets, /favicon.ico) and /health are exempt, so a\n# page load does not spend its own budget on its assets. Empty disables it.\n# Default 200/minute.\nRATE_LIMIT_PAGES=200/minute\n\n# Fallback limit for @rpc() actions that declare no limits= of their own.\nRATE_LIMIT_RPC="60 per minute"\n\n# Limit applied to @rpc(require_auth=True) actions that declare no limits=.\n# Tighten this and the per-action limits on sign-in and other credential paths.\nRATE_LIMIT_AUTH="60 per minute"\n\n# Configured for slowapi\'s Limiter. Note that slowapi\'s middleware is not in\n# the stack, so this value is inert today; page limiting is RATE_LIMIT_PAGES.\nRATE_LIMIT_DEFAULT="200 per minute"\n\n# In-memory bucket ceiling and sweep interval for the limiter store.\n# Defaults 10000 buckets, swept every 60 seconds.\nRATE_LIMIT_MAX_BUCKETS=10000\nRATE_LIMIT_CLEANUP_INTERVAL=60')),e.websocket&&i.push(buildEnvSection("9. WEBSOCKETS\n# Enforced by: src/lib/websocket/websocket_security.py, main.py channel loop\n# Only active when caspian.config.json has websocket: true.","# Browser origins allowed to open a socket (anti-CSWSH). Falls back to\n# CORS_ALLOWED_ORIGINS then APP_BASE_URL when empty.\n#\n# REQUIRED IN PRODUCTION. The convenience same-origin fallback is derived from\n# the client-supplied Host header, so it is development-only: without an\n# explicit list a spoofed Host plus matching Origin would validate itself.\n#\n# The HTTP middleware stack skips websocket scopes, so this and the socket\n# guard are the only checks a handshake passes. Comma-separated, no spaces.\nWEBSOCKET_ALLOWED_ORIGINS=\n\n# Seconds a socket may stay silent before the server closes it. Default 120.\nWEBSOCKET_IDLE_TIMEOUT_SECONDS=120\n\n# Max size of one inbound socket message in bytes. Oversized closes with 1009.\n# Default 4096.\nMAX_WEBSOCKET_MESSAGE_BYTES=4096\n\n# Simultaneous connections per pool; authenticated and guest pools are counted\n# separately. Refused connections close with 1013 during the handshake. Every\n# open socket is a live task and a broadcast target. Default 200.\nMAX_WEBSOCKET_CONNECTIONS=200\n\n# Per-connection send budget: messages allowed per rolling window. Each\n# accepted message fans out to the whole pool, so this bounds how much\n# broadcast one connection can generate. Defaults 20 per 10 seconds.\nMAX_WEBSOCKET_MESSAGES_PER_WINDOW=20\nWEBSOCKET_RATE_WINDOW_SECONDS=10")),e.mcp&&i.push(buildEnvSection("10. MCP ENDPOINT\n# Enforced by: main.py MCPAuthMiddleware; tools in src/lib/mcp/mcp_server.py\n# Only active when caspian.config.json has mcp: true.",`# Bearer token required to call /mcp. The MCP app is mounted outside the page\n# routing tree, so AuthMiddleware does NOT protect it, and its tools enumerate\n# the workspace file inventory and component map.\n#\n# generated -> every request needs "Authorization: Bearer <token>"\n#\n# REQUIRED IN PRODUCTION for the endpoint to work at all.\nMCP_AUTH_TOKEN="${s}"`)),i.push(buildEnvSection("11. CACHE\n# Enforced by: casp/cache_handler.py, main.py is_request_cacheable()",'# Master switch for serving pages from the disk cache.\n#\n# Entries are keyed on the URI alone, with no session component, so an\n# authenticated render is never cached: is_request_cacheable() gates both the\n# read and the write, and a route\'s Cache(...) cannot override it.\nCACHE_ENABLED="false"\n\n# Default cache lifetime in seconds, used when a route sets no ttl.\nCACHE_TTL="600"'),buildEnvSection("12. SERVER PROCESS\n# Enforced by: main.py __main__, settings/serve-static.py",'# Uvicorn workers are separate OS processes for the same FastAPI app. More\n# workers can increase throughput under concurrent load, but they do not make a\n# single request faster and they duplicate memory, connection pools, and any\n# in-process state. Keep at 1 unless the app is designed for multi-process\n# coordination and testing shows a real concurrency bottleneck.\nUVICORN_WORKERS="1"')),i.join("\n\n")}function copyRecursiveSync(e,n,t){const s=fs.existsSync(e),i=s&&fs.statSync(e);if(s&&i&&i.isDirectory()){const s=n.toLowerCase();if(!t.mcp&&s.includes("src\\lib\\mcp"))return;if(!t.websocket&&s.includes("src\\lib\\websocket"))return;if((!t.typescript||t.backendOnly)&&(s.endsWith("\\ts")||s.includes("\\ts\\")))return;if((!t.typescript||t.backendOnly)&&(s.endsWith("\\vite-plugins")||s.includes("\\vite-plugins\\")||s.includes("\\vite-plugins")))return;if(t.backendOnly&&s.includes("public\\js")||t.backendOnly&&s.includes("public\\css")||t.backendOnly&&s.includes("public\\assets"))return;const i=n.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),fs.readdirSync(e).forEach(s=>{copyRecursiveSync(path.join(e,s),path.join(n,s),t)})}else{if(checkExcludeFiles(n))return;const s=n.replace(/\\/g,"/").toLowerCase();if(s.endsWith("/settings/run-vite-watch.ts")&&(!t.typescript||t.backendOnly))return;if(s.endsWith("/ts/tailwind-merge.ts")&&(!t.typescript||t.backendOnly||!t.tailwindcss))return;if(!t.tailwindcss&&(n.includes("globals.css")||n.includes("styles.css")))return;if(!t.mcp&&n.includes("restart-mcp.ts"))return;if(!t.websocket&&n.includes("src\\lib\\websocket"))return;if(t.backendOnly&&nonBackendFiles.some(e=>n.includes(e)))return;if(t.backendOnly&&n.includes("layout.py"))return;if(t.tailwindcss&&n.includes("index.css"))return;if(!t.prisma&&n.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,n,0)}}async function executeCopy(e,n,t){n.forEach(({src:n,dest:s})=>{const i=normalizeTemplatePath(n),c=resolveTemplateSourcePath(n,"directory"),a=path.join(e,s);if(!c){if(OPTIONAL_TEMPLATE_DIRECTORIES.has(i))return void console.log(chalk.gray(`Optional template directory not found, skipping: ${i}`));throw new Error(`Template directory not found: ${i}. The package may be incomplete.`)}copyRecursiveSync(c,a,t)})}function modifyLayoutPHP(e,n){const t=path.join(e,"src","app","layout.py");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),s="";n.backendOnly||(n.tailwindcss||(s='\n <link href="/css/index.css" rel="stylesheet" />'),s+='\n <script type="module" src="/js/main.js"><\/script>');let i="";n.backendOnly||(i=n.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${s}`:s),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.py:"),e)}}async function createOrUpdateEnvFile(e,n){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,n,{flag:"w"})}function ensureClaudeMd(e){const n=path.join(e,"CLAUDE.md");if(checkExcludeFiles(n))return;const t="@AGENTS.md";if(!fs.existsSync(n))return void fs.writeFileSync(n,`${t}\n`,{flag:"w"});const s=fs.readFileSync(n,"utf8").replace(/^\uFEFF/,"");if(s.trimStart().startsWith(t))return;const i=`${t}\n\n${s.trimStart()}`;fs.writeFileSync(n,i,{flag:"w"})}function writeTailwindMainJs(e){const n=path.join(e,"public","js","main.js");checkExcludeFiles(n)||(fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,'import "/js/pp-reactive-v2.min.js";\nimport { twMerge } from "/js/tailwind-merge.mjs";\n\nconst pp = (globalThis).pp;\n\nglobalThis.twMerge = twMerge;\n\nif (document.readyState !== "loading") {\n pp?.mount?.();\n} else {\n document.addEventListener(\n "DOMContentLoaded",\n () => pp?.mount?.(),\n { once: true },\n );\n}\n',{flag:"w"}))}function copyTailwindMergeBundle(e){const n=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs"),t=path.join(e,"public","js","tailwind-merge.mjs"),s=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs.map"),i=path.join(e,"public","js","bundle-mjs.mjs.map");if(!checkExcludeFiles(t)){if(!fs.existsSync(n))throw new Error(`tailwind-merge bundle not found at ${n}`);fs.mkdirSync(path.dirname(t),{recursive:!0}),fs.copyFileSync(n,t),!checkExcludeFiles(i)&&fs.existsSync(s)&&fs.copyFileSync(s,i)}}function writeTailwindTypeScriptMain(e){const n=path.join(e,"ts","main.ts");checkExcludeFiles(n)||(fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.writeFileSync(n,'import "/js/pp-reactive-v2.min.js";\n\n// The following global names have already been declared elsewhere in the project:\n// - pp: Used for the Reactive Core functionality.\n\n// Imports goes here --Start\nimport { createGlobalSingleton } from "./global-functions.js";\nimport { mergeTailwindClasses } from "./tailwind-merge.js";\n\ncreateGlobalSingleton("twMerge", mergeTailwindClasses);\n\n\n// Imports goes here --End\n\nconst pp = (globalThis as any).pp;\n\nif (document.readyState !== "loading") {\n\tpp?.mount?.();\n} else {\n\tdocument.addEventListener(\n\t\t"DOMContentLoaded",\n\t\t() => pp?.mount?.(),\n\t\t{ once: true },\n\t);\n}\n',{flag:"w"}))}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const n=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(n)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const t=e.replace(/\\/g,"/");return n.endsWith("/"+t)||n===t})}function normalizeTemplatePath(e){return e.replace(/^[\\/]+/,"")}function resolveTemplateSourcePath(e,n){const t=normalizeTemplatePath(e),s=[path.join(__dirname,t),path.join(PACKAGE_ROOT,t)];for(const e of s){if(!fs.existsSync(e))continue;const t=fs.statSync(e);if("file"===n&&t.isFile())return e;if("directory"===n&&t.isDirectory())return e}return null}function extractCaspianSection(e){const n=e.indexOf(CASPIAN_SECTION_START);if(-1===n)return null;const t=e.indexOf(CASPIAN_SECTION_END,n);return-1===t?null:e.slice(t>n?n:0,t+20)}function mergeAgentsCaspianSection(e,n){const t=extractCaspianSection(n);if(!t)return e;const s=e.indexOf(CASPIAN_SECTION_START),i=e.indexOf(CASPIAN_SECTION_END,s);if(-1!==s&&-1!==i){return`${e.slice(0,s)}${t}${e.slice(i+20)}`}const c=e.endsWith("\n");return`${e}${c?"\n":"\n\n"}${t}\n`}async function createDirectoryStructure(e,n){const t=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/AGENTS.md",dest:"/AGENTS.md"},{src:"/.python-version",dest:"/.python-version"}];n.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),n.typescript&&!n.backendOnly&&t.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const s=[{src:"/settings",dest:"/settings"},{src:"/tests",dest:"/tests"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"},{src:"/.vscode",dest:"/.vscode"}];n.typescript&&!n.backendOnly&&s.push({src:"/ts",dest:"/ts"}),t.forEach(({src:n,dest:t})=>{const s=normalizeTemplatePath(n),i=resolveTemplateSourcePath(n,"file"),c=path.join(e,t);if(checkExcludeFiles(c))return;if(!i){if(OPTIONAL_TEMPLATE_FILES.has(s))return void console.log(chalk.gray(`Optional template file not found, skipping: ${s}`));throw new Error(`Template file not found: ${s}. The package may be incomplete.`)}if("/pyproject.toml"===n&&updateAnswer?.isUpdate&&fs.existsSync(c))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const a=fs.readFileSync(i,"utf8");if("/AGENTS.md"===n&&updateAnswer?.isUpdate&&fs.existsSync(c)){const e=mergeAgentsCaspianSection(fs.readFileSync(c,"utf8"),a);return void fs.writeFileSync(c,e,{flag:"w"})}fs.writeFileSync(c,a,{flag:"w"})}),await executeCopy(e,s,n),ensureClaudeMd(e),n.tailwindcss&&!n.backendOnly&&(n.typescript?writeTailwindTypeScriptMain(e):(copyTailwindMergeBundle(e),writeTailwindMainJs(e))),await updatePackageJson(e,n),!n.tailwindcss&&n.backendOnly||modifyLayoutPHP(e,n),await createOrUpdateEnvFile(e,buildCaspianEnvContent(n))}async function getAnswer(e={},n=!1){if(n)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,websocket:e.websocket??!1,prisma:e.prisma??!1};if(e.starterKit){const n=e.starterKit;let t=null;if(STARTER_KITS[n]&&(t=STARTER_KITS[n]),t){const s={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,prisma:t.features.prisma??!1,mcp:t.features.mcp??!1,websocket:t.features.websocket??!1,typescript:t.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(s.backendOnly=!0),i.includes("--tailwindcss")&&(s.tailwindcss=!0),i.includes("--mcp")&&(s.mcp=!0),i.includes("--websocket")&&(s.websocket=!0),i.includes("--prisma")&&(s.prisma=!0),i.includes("--typescript")&&(s.typescript=!0),s}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:n,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,websocket:!1,typescript:!1},s=process.argv.slice(2);return s.includes("--backend-only")&&(t.backendOnly=!0),s.includes("--tailwindcss")&&(t.tailwindcss=!0),s.includes("--mcp")&&(t.mcp=!0),s.includes("--websocket")&&(t.websocket=!0),s.includes("--prisma")&&(t.prisma=!0),s.includes("--typescript")&&(t.typescript=!0),t}}const t=[];e.projectName||t.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||t.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const s=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(t,{onCancel:s}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("WebSocket")}?`,initial:!1,active:"Yes",inactive:"No"}));const a=await prompts(c,{onCancel:s});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:a.tailwindcss??e.tailwindcss??!1,typescript:a.typescript??e.typescript??!1,mcp:a.mcp??e.mcp??!1,websocket:a.websocket??e.websocket??!1,prisma:a.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,n,t=!1){console.log("Uninstalling Node dependencies:"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=buildManagedNpmCommand(["uninstall",t?"--save-dev":"--save",...n]);execSync(s,{stdio:"inherit",cwd:e})}function buildManagedNpmCommand(e){return`npm ${e.join(" ")} --ignore-scripts=false --min-release-age=0 --audit=false`}function fetchPackageVersion(e){return new Promise((n,t)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let s="";e.on("data",e=>s+=e),e.on("end",()=>{try{const e=JSON.parse(s);n(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}})}).on("error",e=>t(e))})}const readJsonFile=e=>{const n=fs.readFileSync(e,"utf8");return JSON.parse(n)};function compareVersions(e,n){const t=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),s=n.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!t||!s)return e.localeCompare(n);const i=t.slice(1,4).map(Number),c=s.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>c[e])return 1;if(i[e]<c[e])return-1}const a=t[4]??null,o=s[4]??null;return a&&!o?-1:!a&&o?1:a&&o?a.localeCompare(o):0}function getInstalledPackageInfo(e){try{const n=execSync(buildManagedNpmCommand(["list","-g",e,"--depth=0"])).toString(),t=n.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return t?{version:t[1],isLinked:n.includes(`${e}@`)&&n.includes("->")}:(console.error(`Package ${e} is not installed`),{version:null,isLinked:!1})}catch(e){return console.error(e instanceof Error?e.message:String(e)),{version:null,isLinked:!1}}}function isRunningFromNpxCache(e){const n=path.resolve(e).toLowerCase(),t=`${path.sep}_npx${path.sep}`.toLowerCase();return n.includes(t)}async function installNpmDependencies(e,n,t=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync(buildManagedNpmCommand(["init","-y"]),{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),n.forEach(e=>console.log(`- ${chalk.blue(e)}`));const s=buildManagedNpmCommand(["install",...t?["--save-dev"]:[],...n]);execSync(s,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.3.3","@types/browser-sync":"2.29.1","@types/node":"26.2.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"6.0.0","chokidar-cli":"3.0.0",cssnano:"8.0.5","npm-run-all":"4.1.5",postcss:"8.5.26","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.3.3",tsx:"4.23.12",typescript:"7.0.2",vite:"8.2.0",vitest:"4.1.10","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.19","caspian-utils":"0.2.x","tailwind-merge":"3.6.0"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(n){const t=n;if("win32"===globalThis.process?.platform&&("EPERM"===t.code||"EACCES"===t.code)){try{spawnSync("cmd",["/c","attrib","-R","-H","-S","/S","/D",`${e}\\*`],{stdio:"ignore"})}catch{}return void spawnSync("cmd",["/c","rd","/s","/q",e],{stdio:"ignore"})}throw n}}async function setupStarterKit(e,n){if(!n.starterKit)return;let t=null;if(STARTER_KITS[n.starterKit]?t=STARTER_KITS[n.starterKit]:n.starterKitSource&&(t={id:n.starterKit,name:`Custom Starter Kit (${n.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:n.starterKitSource}}),t){if(console.log(chalk.green(`Setting up ${t.name}...`)),t.source)try{const s=t.source.branch?`git clone -b ${t.source.branch} --depth 1 ${t.source.url} "${e}"`:`git clone --depth 1 ${t.source.url} "${e}"`;execSync(s,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const i=path.join(e,"caspian.config.json");if(fs.existsSync(i))try{const t=JSON.parse(fs.readFileSync(i,"utf8")),s=e,c=bsConfigUrls(s);t.projectName=n.projectName,t.projectRootPath=s,t.bsTarget=c.bsTarget,t.bsPathRewrite=c.bsPathRewrite;const a=await fetchPackageVersion("create-caspian-app");t.version=t.version||a,fs.writeFileSync(i,JSON.stringify(t,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}t.customSetup&&await t.customSetup(e,n),console.log(chalk.green(`✓ ${t.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${n.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\n🚀 Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const n=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(n)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const t=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");t&&console.log(chalk.magenta(` Features: ${t}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,n,t){const s=spawnSync(e,n,{cwd:t,stdio:"inherit",shell:!1,encoding:"utf8"});if(s.error)throw s.error;if(0!==s.status)throw new Error(`Command failed (${e} ${n.join(" ")}), exit=${s.status}`)}function tryRunCmd(e,n,t){const s=spawnSync(e,n,{cwd:t,stdio:"ignore",shell:!1,encoding:"utf8"});return!s.error&&0===s.status}function tryInstallUv(e){console.log(chalk.blue("uv not found. Attempting to install uv..."));const n=[{cmd:"py",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python3",args:["-m","pip","install","--upgrade","uv"]}];for(const t of n)if(tryRunCmd(t.cmd,t.args,e))return!0;return!1}function resolveUvCommand(e){const n=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const t of n)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;if(tryInstallUv(e))for(const t of n)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;throw new Error("Could not find or install uv. Install uv and ensure `uv`, `py`, or `python` is available in PATH.")}function buildPythonDependencies(e){const n=["fastapi==0.141.1","uvicorn==0.52.1","python-dotenv==1.2.2","tzdata==2026.3","jinja2==3.1.6","beautifulsoup4==4.15.0","slowapi==0.1.10","python-multipart==0.0.32","starsessions==2.2.1","httpx2==2.10.0","werkzeug==3.1.8","cuid2==2.0.1","nanoid==2.0.0","python-ulid==4.0.1","cuid==0.4","caspian-utils~=0.4"];return e.mcp&&n.push("fastmcp==3.4.7"),e.websocket&&n.push("websockets==17.0.1"),e.prisma&&(n.push("psycopg2-binary==2.9.12"),n.push("asyncpg==0.31.0"),n.push("aiosqlite==0.22.1"),n.push("aiomysql==0.3.2")),n}function buildPythonDevDependencies(){return["pyright==1.1.411","ruff==0.16.2","pytest==9.1.1","djlint==1.44.2"]}function getPythonRequirementName(e){const n=e.trim().match(/^([A-Za-z0-9._-]+)/);return n?.[1]??null}function getPyProjectDependencyNames(e){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))return new Set;const t=fs.readFileSync(n,"utf8").replace(/\r\n/g,"\n").match(/^[ \t]*dependencies[ \t]*=[ \t]*\[([\s\S]*?)\]/m);if(!t)return new Set;const s=new Set,i=/"([^"]+)"/g;let c;for(;null!==(c=i.exec(t[1]));){const e=c[1].trim().match(/^([A-Za-z0-9._-]+)/)?.[1];e&&s.add(e.toLowerCase())}return s}function ensurePyProjectExists(e){const n=path.join(e,"pyproject.toml");if(!fs.existsSync(n))throw new Error(`pyproject.toml not found at: ${n}`);let t=fs.readFileSync(n,"utf8");t=t.replace(/\r\n/g,"\n"),t.includes("package = false")||(t=t.includes("[tool.uv]")?t.replace("[tool.uv]","[tool.uv]\npackage = false"):`${t.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(n,t,"utf8")}async function ensurePythonVenvAndDeps(e,n,t=[]){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: syncing dependencies with uv")),console.log(chalk.green("=========================\n")),console.log(chalk.blue("Preparing pyproject.toml...")),ensurePyProjectExists(e);const s=path.join(e,"requirements.txt");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(chalk.gray("Removed legacy requirements.txt")));const i=resolveUvCommand(e),c=path.join(e,".venv");fs.existsSync(c)?console.log(chalk.blue("Existing .venv detected. Reusing it so uv sync can update dependencies without replacing the environment.")):(console.log(chalk.blue("Creating the virtual environment with uv...")),runCmd(i.cmd,[...i.argsPrefix,"venv",".venv"],e));const a=buildPythonDependencies(n),o=buildPythonDevDependencies(),r=a.map(e=>getPythonRequirementName(e)).filter(e=>null!==e),l=o.map(e=>getPythonRequirementName(e)).filter(e=>null!==e);t.length>0&&(console.log(chalk.blue("Removing obsolete Python dependencies via uv remove...")),runCmd(i.cmd,[...i.argsPrefix,"remove",...t],e));const p=r.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...p,...a],e);const d=l.flatMap(e=>["--upgrade-package",e]);console.log(chalk.blue("Adding Python dev dependencies via uv add --dev...")),runCmd(i.cmd,[...i.argsPrefix,"add","--dev",...d,...o],e),console.log(chalk.blue("Syncing dependencies...")),runCmd(i.cmd,[...i.argsPrefix,"sync"],e),console.log(chalk.green("\n✓ uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),n=e.includes("-y");let t=e[0];const s=e.find(e=>e.startsWith("--starter-kit=")),i=s?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),a=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let o=null,r=!1;if(t){const s=process.cwd(),c=path.join(s,"caspian.config.json");if(i&&a){r=!0;const s={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};o=await getAnswer(s,n)}else if(fs.existsSync(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const n=path.join(s,e);fs.existsSync(n)&&a.push(n.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,websocket:i.websocket??!1,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:s};const r={projectName:t,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp,websocket:e.includes("--websocket")||(i.websocket??!1)};o=await getAnswer(r,n),null!==o&&(updateAnswer={projectName:t,backendOnly:o.backendOnly,tailwindcss:o.tailwindcss,mcp:o.mcp,websocket:o.websocket,prisma:o.prisma,typescript:o.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:s})}else{const s={projectName:t,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),websocket:e.includes("--websocket"),prisma:e.includes("--prisma")};o=await getAnswer(s,n)}if(null===o)return void console.log(chalk.red("Installation cancelled."))}else o=await getAnswer({},n);if(null===o)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync(buildManagedNpmCommand(["uninstall","-g","create-caspian-app"]),{stdio:"inherit"}),execSync(buildManagedNpmCommand(["install","-g","create-caspian-app"]),{stdio:"inherit"})):execSync(buildManagedNpmCommand(["install","-g","create-caspian-app"]),{stdio:"inherit"});const d=process.cwd();let u;if(t)if(r){const n=path.join(d,t);fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,await setupStarterKit(u,o),process.chdir(u);const s=path.join(u,"caspian.config.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));e.includes("--backend-only")&&(n.backendOnly=!0),e.includes("--tailwindcss")&&(n.tailwindcss=!0),e.includes("--typescript")&&(n.typescript=!0),e.includes("--mcp")&&(n.mcp=!0),e.includes("--websocket")&&(n.websocket=!0),e.includes("--prisma")&&(n.prisma=!0),o={...o,backendOnly:n.backendOnly,tailwindcss:n.tailwindcss,typescript:n.typescript,mcp:n.mcp,websocket:n.websocket??!1,prisma:n.prisma};let t=[];n.excludeFiles?.map(e=>{const n=path.join(u,e);fs.existsSync(n)&&t.push(n.replace(/\\/g,"/"))}),updateAnswer={...o,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),n=path.join(d,t),s=path.join(n,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(n)&&fs.existsSync(s)?(u=n,process.chdir(n)):(fs.existsSync(n)||fs.mkdirSync(n,{recursive:!0}),u=n,process.chdir(n))}else fs.mkdirSync(o.projectName,{recursive:!0}),u=path.join(d,o.projectName),process.chdir(o.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python"),npmPkg("caspian-utils")];o.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),o.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),o.prisma&&execSync(buildManagedNpmCommand(["install","-g","prisma-client-python@latest"]),{stdio:"inherit"}),o.typescript&&!o.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),o.typescript&&m.push(npmPkg("vitest")),o.starterKit&&!r&&await setupStarterKit(u,o),await installNpmDependencies(u,m,!0);let h=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,o),o.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],n=[],t=e=>{try{const n=path.join(u,"package.json");if(fs.existsSync(n)){const t=JSON.parse(fs.readFileSync(n,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const n=path.join(u,"src","app",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const n=path.join(u,"src","app",e);fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const n=path.join(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"public","js","tailwind-merge.mjs");fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${s} was deleted successfully.`));const i=path.join(u,"public","js","bundle-mjs.mjs.map");fs.existsSync(i)&&(fs.unlinkSync(i),console.log(`${i} was deleted successfully.`));const c=path.join(u,"ts","tailwind-merge.ts");fs.existsSync(c)&&(fs.unlinkSync(c),console.log(`${c} was deleted successfully.`));["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano","tailwind-merge"].forEach(n=>{t(n)&&e.push(n)}),n.push("tailwind-merge")}if(o.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(n){console.warn(chalk.yellow(`Failed to delete ${e}: ${n}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const n=path.join(u,"settings",e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),n.push("fastmcp")}if(!updateAnswer.websocket){const e=path.join(u,"src","lib","websocket");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("WebSocket folder was deleted successfully.")),n.push("websockets")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(n=>{t(n)&&e.push(n)}),n.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts",path.join("settings","run-vite-watch.ts")].forEach(e=>{const n=path.join(u,e);fs.existsSync(n)&&(fs.unlinkSync(n),console.log(`${e} was deleted successfully.`))});const n=path.join(u,"ts");fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const s=path.join(u,"settings","vite-plugins");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(n=>{t(n)&&e.push(n)})}const s=e=>Array.from(new Set(e)),i=s(e);i.length>0&&(console.log(`Uninstalling npm packages: ${i.join(", ")}`),await uninstallNpmDependencies(u,i,!0));const c=s(n),a=getPyProjectDependencyNames(u);h=c.filter(e=>a.has(e.toLowerCase())),h.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${h.join(", ")}`))}if(!r||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),n=bsConfigUrls(e),t={projectName:o.projectName,projectRootPath:e,bsTarget:n.bsTarget,bsPathRewrite:n.bsPathRewrite,backendOnly:o.backendOnly,tailwindcss:o.tailwindcss,mcp:o.mcp,websocket:o.websocket,prisma:o.prisma,typescript:o.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,o,h),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
|