create-prisma-php-app 5.1.0-alpha.2 → 5.1.0-alpha.21

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.
@@ -7,13 +7,14 @@
7
7
  - Treat `node_modules/prisma-php/dist/docs` as framework reference docs that teach AI how Prisma PHP works. The presence of a page in that docs folder does not mean the current workspace has that feature enabled.
8
8
  - Read the matching doc in `node_modules/prisma-php/dist/docs` before generating or editing framework-specific Prisma PHP code.
9
9
  - Expect `AGENTS.md` in the project root and keep it aligned with the installed Prisma PHP docs contract.
10
- - In the Prisma PHP package source repo, keep `AGENTS.md`, `.github/copilot-instructions.md`, any `.github/instructions/**/*.instructions.md`, and `dist/docs` aligned so the published docs remain correct after install.
10
+ - In the Prisma PHP package source repo, keep the source-repo `AGENTS.md`, `.github/copilot-instructions.md`, any `.github/instructions/**/*.instructions.md`, and source-repo `dist/docs` aligned so the published docs remain correct after install. In consumer apps, the installed docs path is `node_modules/prisma-php/dist/docs`.
11
11
  - Do not assume installed consumer apps also ship a root `.github/copilot-instructions.md` unless the generator explicitly creates one.
12
12
  - If `.github/instructions/**/*.instructions.md` exists, treat those files as workspace-local task instructions for third-party libraries, component systems, icon packs, and other implementation-specific rules.
13
13
  - Before generating or editing code, inspect `.github/instructions/` and read any `*.instructions.md` files that match the current task, named library, target files, or implementation surface.
14
- - Keep every `dist/docs/*.md` page AI-discoverable on its own: the frontmatter description and opening section should clearly say when agents should read that file and which adjacent docs to consult next.
14
+ - In the Prisma PHP package source repo, keep every `dist/docs/*.md` page AI-discoverable on its own. In consumer apps, those installed docs live at `node_modules/prisma-php/dist/docs/*.md`. The frontmatter description and opening section should clearly say when agents should read that file and which adjacent docs to consult next.
15
15
  - When a task maps to an optional feature such as `backendOnly`, `swaggerDocs`, `typescript`, `websocket`, or `mcp`, inspect `./prisma-php.json` first, then read the matching docs page to learn the implementation contract.
16
- - When docs and project files still leave a runtime gap, inspect the narrow core file that owns the behavior: `TemplateCompiler.php` for HTML fragment compilation and route root scoping, `ImportComponent.php` for imported partials, `MainLayout.php` for metadata and head/footer scripts, `PrismaPHPSettings.php` plus generated settings JSON for component and route maps, `Request.php` for request handling, `Validator.php`/`Rule.php` for validation, and feature-specific files such as `UploadFile.php`, `Mailer.php`, or `Streaming/SSE.php`.
16
+ - When docs and project files still leave a runtime gap, inspect the narrow core file that owns the behavior: `TemplateCompiler.php` for HTML fragment compilation and route root scoping, `PHPX.php` plus `TwMerge.php` for PHPX class composition and frontend `twMerge(...)` emission, `ImportComponent.php` for imported partials, `MainLayout.php` for metadata and head/footer scripts, `PrismaPHPSettings.php` plus generated settings JSON for component and route maps, `Request.php` for request handling, `Validator.php`/`Rule.php` for validation, and feature-specific files such as `UploadFile.php`, `Mailer.php`, or `Streaming/SSE.php`.
17
+ - When validation or rule-builder syntax is involved, read `node_modules/prisma-php/dist/docs/validator.md` first. If method shape is still unclear after that, inspect `vendor/tsnc/prisma-php/src/Rule.php` to confirm which `Rule` methods are static entry points and which methods must be chained on a builder instance.
17
18
 
18
19
  ## Workspace Task Instructions
19
20
 
@@ -28,9 +29,11 @@
28
29
  - Keep `src/app` focused on route files, layouts, handlers, and route-scoped partials.
29
30
  - Prefer `src/Components` for reusable application UI components shared across pages or layouts.
30
31
  - Keep reusable non-UI code such as services, auth, middleware, Prisma classes, and helpers in `src/Lib`.
32
+ - Treat route-private folders such as `src/app/<route>/_components` as an implementation detail for files that stay owned by that route only.
31
33
  - Treat `./public/uploads` as the default local public upload directory for file uploads.
32
34
  - Treat generated component libraries such as `src/Lib/PHPXUI` and `src/Lib/PPIcons` as library-specific surfaces governed by their manifests and `.github/instructions/*.instructions.md` files.
33
35
  - If a partial starts as route-local but becomes shared across the app, move it from `src/app` to `src/Components`.
36
+ - Do not default to creating `src/app/<route>/_components` for app-owned section components such as `HeroSection`, `FormSection`, or `SidebarSection`; prefer `src/Components` unless the user explicitly wants route-local colocation and the files are truly private to that route.
34
37
  - Suggest this structure by default when helping users organize growing Prisma PHP apps.
35
38
 
36
39
  ## Component Tag Contract
@@ -43,6 +46,14 @@
43
46
  - Use mustache values for reactive props, such as `selected-date="{selectedDate}"` and `on-date-select="{setSelectedDate}"`.
44
47
  - Write component examples as HTML-first Prisma PHP markup using the current `x-` tag contract.
45
48
 
49
+ ## Tailwind Merge Contract
50
+
51
+ - In Tailwind-enabled Prisma PHP apps, Tailwind utility conflict resolution belongs to the frontend `twMerge(...)` runtime helper.
52
+ - `getMergeClasses(...)` and `PP\PHPX\TwMerge::merge(...)` emit frontend `twMerge(...)` expressions for the browser runtime to resolve.
53
+ - `twMerge(...)` is an app-level browser helper, not a PulsePoint built-in.
54
+ - In TypeScript-enabled apps, Prisma PHP registers that helper from `ts/main.ts`; use `typescript.md` for route usage and `components.md` for PHPX usage.
55
+ - Keep Tailwind merge decisions on the frontend runtime instead of trying to finalize conflicting utility classes in PHP.
56
+
46
57
  ## Framework-Managed Package Scripts
47
58
 
48
59
  - Prisma PHP can generate `package.json` scripts for BrowserSync, Tailwind, TypeScript, WebSocket, MCP, Swagger docs, and related helpers.
@@ -50,7 +61,7 @@
50
61
  - Do not default to telling users to run `npm run tailwind`, `npm run tailwind:build`, `npm run ts:watch`, or `npm run ts:build` after routine file changes, because those are usually orchestrated through the generated top-level scripts.
51
62
  - Use `npm run websocket` or `npm run mcp` only when isolating local runtime startup, debugging, or when the project's scripts show those services are not already covered by the normal development flow.
52
63
  - Use `npm run create-swagger-docs` only when Swagger or OpenAPI output must be intentionally generated or refreshed.
53
- - When package-script behavior matters, read `dist/docs/commands.md` first and inspect the actual `package.json` in the target project before assuming which scripts exist.
64
+ - When package-script behavior matters, read `node_modules/prisma-php/dist/docs/commands.md` first and inspect the actual `package.json` in the target project before assuming which scripts exist.
54
65
 
55
66
  ## BrowserSync URL Source Of Truth
56
67
 
@@ -94,16 +105,20 @@
94
105
  - For page-local interactivity, prefer `index.php` or nested `layout.php` with a plain inline `<script>` that contains PulsePoint state and functions directly, and use `pp.fetchFunction(...)` for backend calls.
95
106
  - Do not wrap inline PulsePoint code in `DOMContentLoaded`, IIFEs, manual `pp.mount()` calls, or custom scoping/bootstrap helpers. Prisma PHP scopes the component boundary and runs the script for you.
96
107
  - Reserve plain browser JavaScript or TypeScript modules for reusable helpers in `ts/`, third-party libraries, low-level browser APIs, or behavior that does not belong inside a PulsePoint component boundary.
97
- - Use `pp-style` for template-driven inline CSS, `pp-spread="{...attrs}"` for dynamic attribute objects, `pp-for` only on `<template>`, and plain `key` for keyed diffing.
108
+ - Do not treat app-registered helpers such as `twMerge(...)` as PulsePoint built-ins; only use them after the relevant Prisma PHP feature flag and entry-file docs confirm they exist.
109
+ - Use `pp-style` whenever inline CSS contains `{...}` interpolation or any other template-driven/reactive value, reserve plain `style` for fully static inline CSS, use `pp-spread="{...attrs}"` for dynamic attribute objects, keep `pp-for` only on `<template>`, and use plain `key` for keyed diffing.
110
+ - Do not generate reactive inline CSS inside a plain `style` attribute such as `style="width: {progress}%";` use `pp-style="width: {progress}%";` instead so editor CSS validation does not flag the source markup as invalid.
98
111
  - Use `pp.ref(...)`, `pp-ref`, `pp.portal(...)`, `pp.createContext(...)`, `Context.Provider`, and `pp.context(...)` according to `pulsepoint.md`.
99
112
  - Use `value`, `defaultvalue`, and `defaultchecked` form bindings according to `pulsepoint.md`; do not author internal `data-pp-*` runtime attributes.
100
113
 
101
114
  ## Route File Conventions
102
115
 
103
- - For PulsePoint-aware `index.php` and nested `layout.php`, keep file order as PHP first, then one parent HTML element; keep the PulsePoint `<script>` as the last child inside that same root element.
104
- - `index.php` and nested `layout.php` must render a single parent HTML element. Treat that root like a React-style component boundary rather than loose sibling markup.
116
+ - For PulsePoint-aware `index.php` and nested `layout.php`, keep file order as PHP first, then one parent HTML element as the route boundary, then the visible route content inside that boundary, and keep the PulsePoint `<script>` as the last child of that boundary root.
117
+ - `index.php` and nested `layout.php` must render a single parent HTML element. Treat that root like a component boundary rather than loose sibling markup.
118
+ - If the visible page or layout content should stay inside a semantic element such as `<main>`, `<section>`, or `<article>`, wrap it in a neutral parent such as `<div>` so the route boundary can still own the `<script>`.
105
119
  - For pages and nested layouts, author a plain single root element and let Prisma PHP inject the PulsePoint `pp-component` scope automatically.
106
- - Author plain `<script>` tags inside that root when PulsePoint is needed. Put the PulsePoint code at the top level of that script. Do not manually add `type="text/pp"`, `DOMContentLoaded` wrappers, IIFEs, or manual bootstrap code; Prisma PHP normalizes the script contract for the runtime.
120
+ - Author plain `<script>` tags inside that boundary root when PulsePoint is needed, usually as a sibling of the visible content container instead of nesting the script inside the semantic content element by default. Put the PulsePoint code at the top level of that script. Do not manually add `type="text/pp"`, `DOMContentLoaded` wrappers, IIFEs, or manual bootstrap code; Prisma PHP normalizes the script contract for the runtime.
121
+ - Do not leave the route `<script>` outside the route boundary.
107
122
  - Only the root `layout.php` should define `<html>`, `<head>`, and `<body>`. When PulsePoint is present, keep `MainLayout::$children;` and any `<script>` inside one clear wrapper.
108
123
 
109
124
  ## Component Boundary Rules
@@ -113,30 +128,43 @@
113
128
  - Do not manually add `pp-component` inside `ImportComponent` partial source; Prisma PHP injects it there.
114
129
  - When imported partials need PulsePoint logic, keep the `<script>` inside that same root element and author it as a plain `<script>` tag without `type="text/pp"`, DOM-ready wrappers, or manual bootstrap code.
115
130
 
131
+ ## Validation Rules
132
+
133
+ - Use `PP\Validator` as the backend validation and normalization layer.
134
+ - Prefer the `Rule` builder for rule-based validation.
135
+ - Start `Rule` builders with `Rule::required()`, `Rule::optional()`, or `Rule::make()`.
136
+ - Chain rule methods such as `->min(...)`, `->max(...)`, `->email()`, and `->regex(...)` on that builder instance.
137
+ - Do not generate static calls such as `Rule::max(80)` or `Rule::email()`.
138
+ - For optional constrained fields, use `Rule::optional()->max(80)` or `Rule::make()->max(80)`.
139
+ - Validate in PHP even when the frontend already performs local checks.
140
+ - Return structured validation results for expected failures instead of treating routine invalid input as an uncaught exception.
141
+ - When internals matter, inspect `vendor/tsnc/prisma-php/src/Validator.php` and `vendor/tsnc/prisma-php/src/Rule.php`.
142
+
116
143
  ## Relevant Docs
117
144
 
118
- - Project structure and feature placement: `dist/docs/project-structure.md`
119
- - CLI project creation and update commands: `dist/docs/commands.md`
120
- - First-time project installation and local setup: `dist/docs/installation.md`
121
- - Existing-project upgrades and feature refreshes: `dist/docs/upgrading.md`
122
- - TypeScript frontend tooling, the `typescript` flag, and `ts/main.ts` registration: `dist/docs/typescript.md`
123
- - Backend-only API usage and `backendOnly`: `dist/docs/backend-only.md`
124
- - Route and layout structure: `dist/docs/layouts-and-pages.md`
125
- - AI integration, provider-backed chat, streaming, and MCP boundary: `dist/docs/get-started-ia.md`
126
- - Data loading, `#[Exposed]`, and SSE streaming: `dist/docs/fetching-data.md`
127
- - Bootstrap flow, runtime init order, request initialization, and function-call protection: `dist/docs/bootstrap-runtime.md`
128
- - PulsePoint runtime rules: `dist/docs/pulsepoint.md`
129
- - Component and `ImportComponent` rules: `dist/docs/components.md`
130
- - Cache behavior and `CacheHandler`: `dist/docs/caching.md`
131
- - Validation rules: `dist/docs/validator.md`
132
- - Prisma ORM schema, migrations, and generated PHP classes: `dist/docs/prisma-php-orm.md`
133
- - Environment variables and `PP\Env` usage: `dist/docs/env.md`
134
- - File uploads and file manager behavior: `dist/docs/file-manager.md`
135
- - Email and SMTP workflows: `dist/docs/email.md`
136
- - WebSocket and realtime behavior: `dist/docs/websocket.md`
137
- - MCP server and tool rules: `dist/docs/mcp.md`
138
- - Authentication: `dist/docs/authentication.md`
139
- - Error handling, expected failures, and route error files: `dist/docs/error-handling.md`
140
- - Metadata and icons: `dist/docs/metadata-and-og-images.md`
141
- - API-style handlers and webhooks: `dist/docs/route-handlers.md`
142
- - Swagger/OpenAPI generation and `swaggerDocs`: `dist/docs/swagger-docs.md`
145
+ - Project structure and feature placement: `node_modules/prisma-php/dist/docs/project-structure.md`
146
+ - CLI project creation and update commands: `node_modules/prisma-php/dist/docs/commands.md`
147
+ - First-time project installation and local setup: `node_modules/prisma-php/dist/docs/installation.md`
148
+ - Existing-project upgrades and feature refreshes: `node_modules/prisma-php/dist/docs/upgrading.md`
149
+ - TypeScript frontend tooling, the `typescript` flag, and `ts/main.ts` registration: `node_modules/prisma-php/dist/docs/typescript.md`
150
+ - Backend-only API usage and `backendOnly`: `node_modules/prisma-php/dist/docs/backend-only.md`
151
+ - Route and layout structure: `node_modules/prisma-php/dist/docs/layouts-and-pages.md`
152
+ - AI integration, provider-backed chat, streaming, and MCP boundary: `node_modules/prisma-php/dist/docs/get-started-ia.md`
153
+ - Data loading, `#[Exposed]`, and SSE streaming: `node_modules/prisma-php/dist/docs/fetching-data.md`
154
+ - Bootstrap flow, runtime init order, request initialization, and function-call protection: `node_modules/prisma-php/dist/docs/bootstrap-runtime.md`
155
+ - PulsePoint runtime rules: `node_modules/prisma-php/dist/docs/pulsepoint.md`
156
+ - Component and `ImportComponent` rules: `node_modules/prisma-php/dist/docs/components.md`
157
+ - Frontend Tailwind class composition and `twMerge(...)`: `node_modules/prisma-php/dist/docs/components.md`, `node_modules/prisma-php/dist/docs/typescript.md`, and `node_modules/prisma-php/dist/docs/layouts-and-pages.md`
158
+ - Cache behavior and `CacheHandler`: `node_modules/prisma-php/dist/docs/caching.md`
159
+ - Validation rules: `node_modules/prisma-php/dist/docs/validator.md`
160
+ - Prisma ORM schema, migrations, and generated PHP classes: `node_modules/prisma-php/dist/docs/prisma-php-orm.md`
161
+ - Environment variables and `PP\Env` usage: `node_modules/prisma-php/dist/docs/env.md`
162
+ - File uploads and file manager behavior: `node_modules/prisma-php/dist/docs/file-manager.md`
163
+ - Email and SMTP workflows: `node_modules/prisma-php/dist/docs/email.md`
164
+ - WebSocket and realtime behavior: `node_modules/prisma-php/dist/docs/websocket.md`
165
+ - MCP server and tool rules: `node_modules/prisma-php/dist/docs/mcp.md`
166
+ - Authentication: `node_modules/prisma-php/dist/docs/authentication.md`
167
+ - Error handling, expected failures, and route error files: `node_modules/prisma-php/dist/docs/error-handling.md`
168
+ - Metadata and icons: `node_modules/prisma-php/dist/docs/metadata-and-og-images.md`
169
+ - API-style handlers and webhooks: `node_modules/prisma-php/dist/docs/route-handlers.md`
170
+ - Swagger/OpenAPI generation and `swaggerDocs`: `node_modules/prisma-php/dist/docs/swagger-docs.md`
package/dist/AGENTS.md CHANGED
@@ -35,7 +35,8 @@ Important rules:
35
35
  - expect `./AGENTS.md` at the project root
36
36
  - when the installed docs and a habit from another framework conflict, follow Prisma PHP
37
37
  - when a workspace instruction file and the general Prisma PHP docs both apply, follow both; keep `./prisma-php.json` as the source of truth for feature enablement and prefer the most specific matching instruction for library- or file-scoped implementation details
38
- - when updating Prisma PHP package/docs sources, keep `AGENTS.md` and `dist/docs` aligned for consumer apps; if the Prisma PHP package source repo also maintains `.github/copilot-instructions.md` or `.github/instructions/**/*.instructions.md`, keep those source-repo files aligned there too
38
+ - in consumer apps, use `./node_modules/prisma-php/dist/docs` as the installed docs location; do not assume a root-level `./dist/docs` directory exists
39
+ - when updating Prisma PHP package/docs sources in the Prisma PHP package source repo, keep the source-repo `AGENTS.md` and source-repo `dist/docs` aligned; if that repo also maintains `.github/copilot-instructions.md` or `.github/instructions/**/*.instructions.md`, keep those source-repo files aligned there too
39
40
 
40
41
  ## Runtime lookup for AI
41
42
 
@@ -44,6 +45,7 @@ Use this map only after `prisma-php.json`, matching workspace instructions, inst
44
45
  | Need to verify | Runtime/source file |
45
46
  | --- | --- |
46
47
  | route root scoping, HTML fragment parsing, component tag compilation, prop casing | `vendor/tsnc/prisma-php/src/PHPX/TemplateCompiler.php` |
48
+ | PHPX class composition, `getMergeClasses(...)`, and frontend `twMerge(...)` expression generation | `vendor/tsnc/prisma-php/src/PHPX/PHPX.php`, `vendor/tsnc/prisma-php/src/PHPX/TwMerge.php` |
47
49
  | imported PHP partial execution, one-root enforcement, prop serialization, imported `#[Exposed]` functions | `vendor/tsnc/prisma-php/src/ImportComponent.php` |
48
50
  | metadata, custom head tags, dynamic head/footer scripts | `vendor/tsnc/prisma-php/src/MainLayout.php` |
49
51
  | route and component maps | `vendor/tsnc/prisma-php/src/PrismaPHPSettings.php`, `settings/files-list.json`, `settings/component-map.json` |
@@ -116,6 +118,9 @@ Use the docs router to learn how Prisma PHP implements a task. Use `./prisma-php
116
118
  - **Creating, editing, composing, or reviewing PHPX components, props, children, fragments, icons, buttons, accordions, or component file placement**
117
119
  Read `components.md`
118
120
 
121
+ - **Tailwind utility class composition, `getMergeClasses(...)`, `PP\PHPX\TwMerge`, or frontend `twMerge(...)` usage**
122
+ Confirm `tailwindcss` in `prisma-php.json`, then read `components.md` for PHPX emission, `typescript.md` for app-level helper registration, and `layouts-and-pages.md` for route examples
123
+
119
124
  - **TypeScript frontend tooling, the `typescript` feature flag, the root `ts/` directory, `ts/main.ts`, npm packages, or registered browser helpers used from template expressions and PulsePoint scripts**
120
125
  Read `typescript.md`, then use `pulsepoint.md`, `layouts-and-pages.md`, or `components.md` for the affected component boundary
121
126
 
@@ -232,8 +237,10 @@ When organizing a growing Prisma PHP app, keep route code and reusable code sepa
232
237
  - keep `src/app` focused on the route tree, route-local layouts, pages, handlers, and route-scoped partials
233
238
  - prefer `src/Components` for reusable application UI components shared across multiple routes or layouts
234
239
  - keep reusable non-UI code such as services, auth, middleware, Prisma classes, and helper libraries in `src/Lib`
240
+ - treat route-private folders such as `src/app/<route>/_components` as an implementation detail for files that stay owned by that route only
235
241
  - treat generated libraries such as `src/Lib/PHPXUI` and `src/Lib/PPIcons` as library-specific surfaces governed by their manifests and matching `.github/instructions/**/*.instructions.md` files
236
242
  - if a partial starts in `src/app` but becomes shared across the app, promote it into `src/Components`
243
+ - do **not** default to creating `src/app/<route>/_components` for app-owned section components such as `HeroSection`, `FormSection`, or `SidebarSection`; prefer `src/Components` unless the user explicitly wants route-local colocation and the files are truly private to that route
237
244
  - do **not** default to placing app-wide reusable components under `src/app` unless the user explicitly wants route-local colocation
238
245
 
239
246
  ## HTML-first component tag contract
@@ -354,15 +361,19 @@ There are two related structure rules, and AI must not mix their responsibilitie
354
361
  Use this pattern:
355
362
 
356
363
  1. PHP first
357
- 2. one parent HTML element for the route content
358
- 3. when PulsePoint is present, let Prisma PHP inject the route or layout `pp-component` scope on that root automatically
359
- 4. keep one `<script>` block as the last child inside that same root element
364
+ 2. one parent HTML element as the route boundary
365
+ 3. place the visible page or layout content inside that boundary
366
+ 4. when PulsePoint is present, let Prisma PHP inject the route or layout `pp-component` scope on that root automatically
367
+ 5. keep one `<script>` block as the last child of that boundary root
360
368
 
361
369
  Also follow these route-file rules:
362
370
 
363
371
  - `index.php` and nested `layout.php` must render a single parent HTML element
372
+ - use that single parent element as the route boundary; if the visible content should stay inside a semantic element such as `<main>`, `<section>`, or `<article>`, wrap it in a neutral parent such as `<div>`
364
373
  - for normal pages and nested layouts, do **not** manually author `pp-component` on that root; Prisma PHP adds it automatically
365
374
  - author a plain `<script>` tag inside that root when PulsePoint logic is needed and do **not** add `type="text/pp"` manually
375
+ - keep the `<script>` as the last child of the route boundary, usually as a sibling of the visible content container instead of nesting it inside the semantic content element by default
376
+ - do **not** leave the `<script>` outside the route boundary
366
377
  - write PulsePoint state, derived values, and functions directly at the top level of that script; do **not** wrap them in `DOMContentLoaded`, an IIFE, manual `pp.mount()` calls, or custom scoping helpers
367
378
  - only the root `layout.php` should define `<html>`, `<head>`, and `<body>`
368
379
  - when PulsePoint is present in a root `layout.php`, keep `MainLayout::$children` and any `<script>` inside one clear wrapper
@@ -378,13 +389,16 @@ MainLayout::$title = 'Todos';
378
389
  MainLayout::$description = 'Track tasks and view the current item count.';
379
390
  ?>
380
391
 
381
- <section>
382
- <h1>Todos</h1>
383
- <p>Count: {count}</p>
392
+ <div>
393
+ <section>
394
+ <h1>Todos</h1>
395
+ <p>Count: {count}</p>
396
+ </section>
397
+
384
398
  <script>
385
399
  const [count, setCount] = pp.state(0);
386
400
  </script>
387
- </section>
401
+ </div>
388
402
  ```
389
403
 
390
404
  ### Imported partials rendered with `ImportComponent::render(...)`
@@ -680,6 +694,9 @@ Default Prisma PHP validation rules:
680
694
 
681
695
  - use `PP\Validator` as the backend validation and normalization layer
682
696
  - prefer the `Rule` builder for rule-based validation
697
+ - start `Rule` builders with `Rule::required()`, `Rule::optional()`, or `Rule::make()`
698
+ - chain rule methods such as `->min(...)`, `->max(...)`, `->email()`, and `->regex(...)` on that builder instance
699
+ - do **not** generate static calls such as `Rule::max(80)`; for optional constrained fields use `Rule::optional()->max(80)` or `Rule::make()->max(80)`
683
700
  - validate in PHP even when the frontend already performs local checks
684
701
  - return structured validation results for expected failures
685
702
  - do not treat routine invalid input as an uncaught exception
@@ -689,6 +706,7 @@ When internals matter, the documented Prisma PHP core validator location is:
689
706
 
690
707
  ```txt
691
708
  vendor/tsnc/prisma-php/src/Validator.php
709
+ vendor/tsnc/prisma-php/src/Rule.php
692
710
  ```
693
711
 
694
712
  ## PulsePoint rules
@@ -705,7 +723,9 @@ Also follow these rules:
705
723
  - do not write React, Vue, Alpine, or Livewire syntax and call it PulsePoint
706
724
  - keep backend concerns separate from PulsePoint runtime concerns
707
725
  - prefer simple documented runtime primitives over abstractions copied from other ecosystems
708
- - use `pp-style` for template-driven inline CSS and plain `style` for fully static inline CSS
726
+ - do not treat app-registered helpers such as `twMerge(...)` as PulsePoint built-ins; route those tasks through the Tailwind-enabled Prisma PHP docs after checking feature flags
727
+ - use `pp-style` whenever inline CSS contains `{...}` interpolation or any other template-driven/reactive value, and reserve plain `style` for fully static inline CSS
728
+ - do **not** generate reactive inline CSS inside a plain `style` attribute such as `style="width: {progress}%";` use `pp-style="width: {progress}%";` instead so source markup stays editor-friendly
709
729
  - use `pp-spread="{...attrs}"` for dynamic attribute objects and omit nullish values from those objects
710
730
  - use `pp-for` only on `<template>` with `item in items` or `(item, index) in items`
711
731
  - use plain `key` for keyed diffing; do not invent `pp-key`
@@ -723,6 +743,8 @@ Also follow these rules:
723
743
  - use kebab-case component attributes and rely on the runtime to hydrate camelCase PHPX properties and PulsePoint props
724
744
  - keep component file names and class names aligned
725
745
  - preserve documented PHPX patterns for `$props`, `$children`, `$class`, and `getAttributes(...)`
746
+ - in Tailwind-enabled apps, use `getMergeClasses(...)` and `PP\PHPX\TwMerge::merge(...)` as emitters of frontend `twMerge(...)` expressions instead of finalizing Tailwind class conflicts in PHP
747
+ - when a route or imported partial needs direct Tailwind-aware class composition, treat `twMerge(...)` as an app-level browser helper available only when `tailwindcss` is enabled
726
748
  - follow documented component placement and grouping conventions before inspecting framework internals
727
749
 
728
750
  ## When to inspect framework internals
@@ -733,6 +755,8 @@ Useful app-mode core locations include:
733
755
 
734
756
  ```txt
735
757
  vendor/tsnc/prisma-php/src
758
+ vendor/tsnc/prisma-php/src/PHPX/PHPX.php
759
+ vendor/tsnc/prisma-php/src/PHPX/TwMerge.php
736
760
  vendor/tsnc/prisma-php/src/PHPX/TemplateCompiler.php
737
761
  vendor/tsnc/prisma-php/src/ImportComponent.php
738
762
  vendor/tsnc/prisma-php/src/MainLayout.php
@@ -1568,6 +1568,7 @@ try {
1568
1568
  CacheHandler::saveCache(Request::$decodedUri, MainLayout::$html);
1569
1569
  }
1570
1570
 
1571
+ header('Content-Type: text/html; charset=UTF-8');
1571
1572
  echo MainLayout::$html;
1572
1573
  } else {
1573
1574
  $layoutPath = Bootstrap::$isContentIncluded
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.php","metadata.php","not-found.php","error.php"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!1,swaggerDocs:!1,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","src/app/layout.php","src/app/index.php"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/app/layout.php","src/app/index.php","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,websocket:!1,prisma:!0,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,websocket:!0,prisma:!0,swaggerDocs:!0,mcp:!0},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/lib/websocket","src/lib/mcp"]},ecommerce:{id:"ecommerce",name:"E-commerce Starter",description:"Full e-commerce application with cart, payments, and admin",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,swaggerDocs:!0,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-ecommerce-starter",branch:"main"}},blog:{id:"blog",name:"Blog CMS",description:"Blog content management system",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,swaggerDocs:!1,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-blog-starter"}}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+8).replace(/\\/g,"\\\\"),c=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let n=`http://localhost/${c}`;n=n.endsWith("/")?n.slice(0,-1):n;const o=n.replace(/(?<!:)(\/\/+)/g,"/"),i=c.replace(/\/\/+/g,"/");return{bsTarget:`${o}/`,bsPathRewrite:{"^/":`/${i.startsWith("/")?i.substring(1):i}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const c=JSON.parse(fs.readFileSync(t,"utf8"));c.scripts={...c.scripts,projectName:"tsx settings/project-name.ts"};let n=[];if(s.tailwindcss&&(c.scripts={...c.scripts,tailwind:"postcss src/app/globals.css -o public/css/styles.css --watch","tailwind:build":"postcss src/app/globals.css -o public/css/styles.css"},n.push("tailwind")),s.typescript&&!s.backendOnly&&(c.scripts={...c.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},n.push("ts:watch")),s.websocket&&(c.scripts={...c.scripts,websocket:"tsx settings/restart-websocket.ts"},n.push("websocket")),s.mcp&&(c.scripts={...c.scripts,mcp:"tsx settings/restart-mcp.ts"},n.push("mcp")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";c.scripts={...c.scripts,"create-swagger-docs":e}}let o={...c.scripts};o.browserSync="tsx settings/bs-config.ts",o["browserSync:build"]="tsx settings/build.ts",o.dev=`npm-run-all projectName -p browserSync ${n.join(" ")}`;let i=["browserSync:build"];s.tailwindcss&&i.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&i.unshift("ts:build"),o.build=`npm-run-all ${i.join(" ")}`,c.scripts=o,c.type="module",fs.writeFileSync(t,JSON.stringify(c,null,2))}async function updateComposerJson(e){checkExcludeFiles(path.join(e,"composer.json"))}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"public","js","main.js");if(checkExcludeFiles(t))return;let c=fs.readFileSync(t,"utf8");c+='\nwindow.ws = new WebSocket("ws://localhost:9001");\n',fs.writeFileSync(t,c,"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const c=fs.existsSync(e),n=c&&fs.statSync(e);if(c&&n&&n.isDirectory()){const c=s.toLowerCase();if(!t.websocket&&c.includes("src\\lib\\websocket"))return;if(!t.mcp&&c.includes("src\\lib\\mcp"))return;if((!t.typescript||t.backendOnly)&&(c.endsWith("\\ts")||c.includes("\\ts\\")))return;if((!t.typescript||t.backendOnly)&&(c.endsWith("\\vite-plugins")||c.includes("\\vite-plugins\\")||c.includes("\\vite-plugins")))return;if(t.backendOnly&&c.includes("public\\js")||t.backendOnly&&c.includes("public\\css")||t.backendOnly&&c.includes("public\\assets"))return;if(!t.swaggerDocs&&c.includes("src\\app\\swagger-docs"))return;const n=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(n))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(c=>{copyRecursiveSync(path.join(e,c),path.join(s,c),t)})}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")))return;if(!t.websocket&&s.includes("restart-websocket.ts"))return;if(!t.mcp&&s.includes("restart-mcp.ts"))return;if(t.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(!t.backendOnly&&s.includes("route.php"))return;if(t.backendOnly&&!t.swaggerDocs&&s.includes("layout.php"))return;if(!t.swaggerDocs&&s.includes("swagger-config.ts"))return;if(t.tailwindcss&&s.includes("index.css"))return;if((!t.swaggerDocs||!t.prisma)&&(s.includes("auto-swagger-docs.ts")||s.includes("prisma-schema-config.json")))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach(({src:s,dest:c})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,c),t)})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.php");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),c="";s.backendOnly||(s.tailwindcss||(c='\n <link href="/css/index.css" rel="stylesheet" />'),c+='\n <script type="module" src="/js/main.js"><\/script>');let n="";s.backendOnly||(n=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" /> ${c}`:c),e=e.replace("</head>",`${n}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.php:"),e)}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");fs.existsSync(t)&&checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const t=e.replace(/\\/g,"/");return s.endsWith("/"+t)||s===t})}async function createDirectoryStructure(e,s){const t=[{src:"/bootstrap.php",dest:"/bootstrap.php"},{src:"/.htaccess",dest:"/.htaccess"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/CLAUDE.md",dest:"/CLAUDE.md"},{src:"/AGENTS.md",dest:"/AGENTS.md"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),s.typescript&&!s.backendOnly&&t.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const c=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"}];s.typescript&&!s.backendOnly&&c.push({src:"/ts",dest:"/ts"}),t.forEach(({src:s,dest:t})=>{const c=path.join(__dirname,s),n=path.join(e,t);if(checkExcludeFiles(n))return;const o=fs.readFileSync(c,"utf8");fs.writeFileSync(n,o,{flag:"w"})}),await executeCopy(e,c,s),await updatePackageJson(e,s),await updateComposerJson(e),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&modifyPostcssConfig(e),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const n=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins (CSV or JSON array)\nCORS_ALLOWED_ORIGINS=[]\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${n}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,n)}async function getAnswer(e={},s=!1){if(s)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,swaggerDocs:e.swaggerDocs??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,websocket:e.websocket??!1,mcp:e.mcp??!1,prisma:e.prisma??!1};if(e.starterKit){const s=e.starterKit;let t=null;if(STARTER_KITS[s]&&(t=STARTER_KITS[s]),t){const c={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,websocket:t.features.websocket??!1,prisma:t.features.prisma??!1,swaggerDocs:t.features.swaggerDocs??!1,mcp:t.features.mcp??!1,typescript:t.features.typescript??!1},n=process.argv.slice(2);return n.includes("--backend-only")&&(c.backendOnly=!0),n.includes("--swagger-docs")&&(c.swaggerDocs=!0),n.includes("--tailwindcss")&&(c.tailwindcss=!0),n.includes("--websocket")&&(c.websocket=!0),n.includes("--mcp")&&(c.mcp=!0),n.includes("--prisma")&&(c.prisma=!0),n.includes("--typescript")&&(c.typescript=!0),c}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,swaggerDocs:!0,mcp:!1,typescript:!1},c=process.argv.slice(2);return c.includes("--backend-only")&&(t.backendOnly=!0),c.includes("--swagger-docs")&&(t.swaggerDocs=!0),c.includes("--tailwindcss")&&(t.tailwindcss=!0),c.includes("--websocket")&&(t.websocket=!0),c.includes("--mcp")&&(t.mcp=!0),c.includes("--prisma")&&(t.prisma=!0),c.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 c=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},n=await prompts(t,{onCancel:c}),o=[];n.backendOnly??e.backendOnly??!1?(e.swaggerDocs||o.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||o.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||o.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||o.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||o.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||o.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||o.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||o.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||o.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||o.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}));const i=await prompts(o,{onCancel:c});return{projectName:n.projectName?String(n.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:n.backendOnly??e.backendOnly??!1,swaggerDocs:i.swaggerDocs??e.swaggerDocs??!1,tailwindcss:i.tailwindcss??e.tailwindcss??!1,typescript:i.typescript??e.typescript??!1,websocket:i.websocket??e.websocket??!1,mcp:i.mcp??e.mcp??!1,prisma:i.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,s,t=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const c=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(c,{stdio:"inherit",cwd:e})}async function uninstallComposerDependencies(e,s){console.log("Uninstalling Composer dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let c="";e.on("data",e=>c+=e),e.on("end",()=>{try{const e=JSON.parse(c);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}})}).on("error",e=>t(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.split(".").map(Number),c=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>c[e])return 1;if(t[e]<c[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:(console.error(`Package ${e} is not installed`),null)}catch(e){return console.error(e instanceof Error?e.message:String(e)),null}}async function installNpmDependencies(e,s,t=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const c=`npm install ${t?"--save-dev":""} ${s.join(" ")}`;execSync(c,{stdio:"inherit",cwd:e})}function getComposerCmd(){try{return execSync("composer --version",{stdio:"ignore"}),console.log("✓ Using global composer command"),{cmd:"composer",baseArgs:[]}}catch{const e="C:\\xampp\\php\\php.exe",s="C:\\ProgramData\\ComposerSetup\\bin\\composer.phar";if(!fs.existsSync(e))throw console.error(`✗ PHP not found at ${e}`),new Error(`PHP executable not found at ${e}`);if(!fs.existsSync(s))throw console.error(`✗ Composer not found at ${s}`),new Error(`Composer phar not found at ${s}`);return console.log("✓ Using XAMPP PHP with Composer phar"),{cmd:e,baseArgs:[s]}}}export async function installComposerDependencies(e,s){const{cmd:t,baseArgs:c}=getComposerCmd(),n=path.join(e,"composer.json"),o=fs.existsSync(n);if(console.log(chalk.green("Composer project initialization: "+(o?"Updating existing project…":"Setting up new project…"))),fs.existsSync(e)||(console.log(`Creating base directory: ${e}`),fs.mkdirSync(e,{recursive:!0})),!o){const s=[...c,"init","--no-interaction","--name","tsnc/prisma-php-app","--require","php:^8.2","--type","project","--version","1.0.0"];console.log("Attempting composer init...");const o=spawnSync(t,s,{cwd:e,stdio:["ignore","pipe","pipe"],encoding:"utf8"}),i=fs.existsSync(n);if(0===o.status&&i)console.log("✓ Composer init successful and composer.json created");else{0!==o.status?(console.log(`Composer init failed with status ${o.status}`),o.stderr&&console.log(`Stderr: ${o.stderr}`)):console.log("Composer init reported success but didn't create composer.json"),console.log("Creating composer.json manually...");const s={name:"tsnc/prisma-php-app",type:"project",version:"1.0.0",require:{php:"^8.2"},autoload:{"psr-4":{"":"src/"}}};try{const t=path.resolve(e,"composer.json");if(console.log(`Writing composer.json to: ${t}`),fs.writeFileSync(t,JSON.stringify(s,null,2),{encoding:"utf8"}),!fs.existsSync(t))throw new Error("File creation appeared to succeed but file doesn't exist");console.log("✓ Successfully created composer.json")}catch(s){if(console.error("✗ Failed to create composer.json:",s),console.error(`Base directory: ${e}`),console.error(`Absolute base directory: ${path.resolve(e)}`),console.error(`Target file path: ${n}`),console.error(`Absolute target file path: ${path.resolve(n)}`),console.error(`Current working directory: ${process.cwd()}`),console.error(`Base directory exists: ${fs.existsSync(e)}`),fs.existsSync(e))try{const s=fs.statSync(e);console.error(`Base directory is writable: ${s.isDirectory()}`)}catch(e){console.error(`Cannot stat base directory: ${e}`)}throw new Error(`Cannot create composer.json: ${s}`)}}}const i=path.resolve(e,"composer.json");if(!fs.existsSync(i))throw console.error(`✗ composer.json still not found at ${i}`),console.error("Directory contents:",fs.readdirSync(e)),new Error("Failed to create composer.json - file does not exist after all attempts");let r;try{const e=fs.readFileSync(i,"utf8");console.log("✓ Successfully read composer.json"),r=JSON.parse(e)}catch(e){throw console.error("✗ Failed to read/parse composer.json:",e),new Error(`Cannot read composer.json: ${e}`)}r.autoload??={},r.autoload["psr-4"]??={},r.autoload["psr-4"][""]??="src/";try{fs.writeFileSync(i,JSON.stringify(r,null,2)),console.log("✓ Updated composer.json with PSR-4 autoload")}catch(e){throw console.error("✗ Failed to update composer.json:",e),e}if(s.length){console.log("Installing Composer dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));try{const n=`${t} ${[...c,"require","--no-interaction","-W",...s].join(" ")}`;execSync(n,{stdio:"inherit",cwd:e,env:{...process.env}}),console.log("✓ Composer dependencies installed")}catch(e){throw console.error("✗ Failed to install composer dependencies:",e),e}}if(o)try{execSync(`${t} ${[...c,"update","--lock","--no-install","--no-interaction"].join(" ")}`,{stdio:"inherit",cwd:e}),console.log("✓ Composer lock updated")}catch(e){throw console.error("✗ Failed to update composer lock:",e),e}try{execSync(`${t} ${[...c,"dump-autoload","--quiet"].join(" ")}`,{stdio:"inherit",cwd:e}),console.log("✓ Composer autoloader regenerated")}catch(e){throw console.error("✗ Failed to regenerate autoloader:",e),e}}const npmPinnedVersions={"@tailwindcss/postcss":"4.2.3","@types/browser-sync":"2.29.1","@types/node":"25.6.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.7","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5","php-parser":"3.5.1",postcss:"8.5.10","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.2.3",tsx:"4.21.0",typescript:"6.0.3",vite:"8.0.8","fast-glob":"3.3.3","prisma-php":"0.0.x"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}const composerPinnedVersions={"vlucas/phpdotenv":"5.6.3","firebase/php-jwt":"7.0.5","phpmailer/phpmailer":"7.0.1","guzzlehttp/guzzle":"7.10.0","symfony/uid":"7.4.8","brick/math":"0.17.0","cboden/ratchet":"0.4.4","tsnc/prisma-php":"2.0.0","php-mcp/server":"3.3.0","gehrisandro/tailwind-merge-php":"1.2.0"};function composerPkg(e){return composerPinnedVersions[e]?`${e}:${composerPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(s){const t=s;if("win32"===globalThis.process?.platform&&("EPERM"===t.code||"EACCES"===t.code)){try{spawnSync("cmd",["/c","attrib","-R","-H","-S","/S","/D",`${e}\\*`],{stdio:"ignore"})}catch{}return void spawnSync("cmd",["/c","rd","/s","/q",e],{stdio:"ignore"})}throw s}}async function setupStarterKit(e,s){if(!s.starterKit)return;let t=null;if(STARTER_KITS[s.starterKit]?t=STARTER_KITS[s.starterKit]:s.starterKitSource&&(t={id:s.starterKit,name:`Custom Starter Kit (${s.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:s.starterKitSource}}),t){if(console.log(chalk.green(`Setting up ${t.name}...`)),t.source)try{const c=t.source.branch?`git clone -b ${t.source.branch} --depth 1 ${t.source.url} "${e}"`:`git clone --depth 1 ${t.source.url} "${e}"`;execSync(c,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const n=path.join(e,"prisma-php.json");if(fs.existsSync(n))try{const t=JSON.parse(fs.readFileSync(n,"utf8")),c=e,o=bsConfigUrls(c);t.projectName=s.projectName,t.projectRootPath=c,t.bsTarget=o.bsTarget,t.bsPathRewrite=o.bsPathRewrite;const i=await fetchPackageVersion("create-prisma-php-app");t.version=t.version||i,fs.writeFileSync(n,JSON.stringify(t,null,2)),console.log(chalk.green("Updated prisma-php.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update prisma-php.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}t.customSetup&&await t.customSetup(e,s),console.log(chalk.green(`✓ ${t.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\n🚀 Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const t=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");t&&console.log(chalk.magenta(` Features: ${t}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-prisma-php-app my-project --starter-kit=basic"),console.log(" npx create-prisma-php-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let t=e[0];const c=e.find(e=>e.startsWith("--starter-kit=")),n=c?.split("=")[1],o=e.find(e=>e.startsWith("--starter-kit-source=")),i=o?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,a=!1;if(t){const c=process.cwd(),o=path.join(c,"prisma-php.json");if(n&&i){a=!0;const c={projectName:t,starterKit:n,starterKitSource:i,backendOnly:e.includes("--backend-only"),swaggerDocs:e.includes("--swagger-docs"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),websocket:e.includes("--websocket"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(c,s)}else if(fs.existsSync(o)){const n=readJsonFile(o);let i=[];n.excludeFiles?.map(e=>{const s=path.join(c,e);fs.existsSync(s)&&i.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:n.backendOnly,swaggerDocs:n.swaggerDocs,tailwindcss:n.tailwindcss,websocket:n.websocket,mcp:n.mcp,prisma:n.prisma,typescript:n.typescript,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:i??[],filePath:c};const a={projectName:t,backendOnly:e.includes("--backend-only")||n.backendOnly,swaggerDocs:e.includes("--swagger-docs")||n.swaggerDocs,tailwindcss:e.includes("--tailwindcss")||n.tailwindcss,typescript:e.includes("--typescript")||n.typescript,websocket:e.includes("--websocket")||n.websocket,prisma:e.includes("--prisma")||n.prisma,mcp:e.includes("--mcp")||n.mcp};r=await getAnswer(a,s),null!==r&&(updateAnswer={projectName:t,backendOnly:r.backendOnly,swaggerDocs:r.swaggerDocs,tailwindcss:r.tailwindcss,websocket:r.websocket,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:n.componentScanDirs??[],excludeFiles:n.excludeFiles??[],excludeFilePath:i??[],filePath:c})}else{const c={projectName:t,starterKit:n,starterKitSource:i,backendOnly:e.includes("--backend-only"),swaggerDocs:e.includes("--swagger-docs"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),websocket:e.includes("--websocket"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(c,s)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},s);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const p=await fetchPackageVersion("create-prisma-php-app"),l=getInstalledPackageVersion("create-prisma-php-app");l?-1===compareVersions(l,p)&&(execSync("npm uninstall -g create-prisma-php-app",{stdio:"inherit"}),execSync("npm install -g create-prisma-php-app",{stdio:"inherit"})):execSync("npm install -g create-prisma-php-app",{stdio:"inherit"});const d=process.cwd();let u;if(t)if(a){const s=path.join(d,t);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,r),process.chdir(u);const c=path.join(u,"prisma-php.json");if(fs.existsSync(c)){const s=JSON.parse(fs.readFileSync(c,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--swagger-docs")&&(s.swaggerDocs=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--websocket")&&(s.websocket=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),r={...r,backendOnly:s.backendOnly,swaggerDocs:s.swaggerDocs,tailwindcss:s.tailwindcss,typescript:s.typescript,websocket:s.websocket,mcp:s.mcp,prisma:s.prisma};let t=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&t.push(s.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"prisma-php.json"),s=path.join(d,t),c=path.join(s,"prisma-php.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(c)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}else fs.mkdirSync(r.projectName,{recursive:!0}),u=path.join(d,r.projectName),process.chdir(r.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("http-proxy-middleware"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("php-parser"),npmPkg("prisma-php")],g=[composerPkg("vlucas/phpdotenv"),composerPkg("firebase/php-jwt"),composerPkg("phpmailer/phpmailer"),composerPkg("guzzlehttp/guzzle"),composerPkg("symfony/uid"),composerPkg("brick/math"),composerPkg("tsnc/prisma-php")];if(r.swaggerDocs&&m.push(npmPkg("swagger-jsdoc"),npmPkg("@types/swagger-jsdoc")),r.swaggerDocs&&r.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&(m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano")),g.push("gehrisandro/tailwind-merge-php")),r.websocket&&g.push("cboden/ratchet"),r.mcp&&g.push("php-mcp/server"),r.prisma&&execSync("npm install -g prisma-client-php@latest",{stdio:"inherit"}),r.typescript&&!r.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),r.starterKit&&!a&&await setupStarterKit(u,r),await installNpmDependencies(u,m,!0),await installComposerDependencies(u,g),t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.prisma&&execSync("npx ppo init --prisma-php",{stdio:"inherit"}),r.swaggerDocs){const e=path.join(u,"src","app","swagger-docs"),s=path.join(e,"apis"),t=path.join(u,"public","assets"),c=path.join(t,"dist");fs.existsSync(e)&&fs.readdirSync(e).length>0&&(console.log("Removing existing swagger-docs directory..."),fs.rmSync(e,{recursive:!0,force:!0})),console.log(chalk.blue("Cloning swagger-docs repository...")),execSync(`git clone https://github.com/TheSteelNinjaCode/prisma-php-swagger-docs.git ${e}`,{stdio:"inherit"});const n=path.join(e,".git");fs.existsSync(n)&&fs.rmSync(n,{recursive:!0,force:!0}),fs.existsSync(t)||(console.log(chalk.blue("Creating public/assets directory...")),fs.mkdirSync(t,{recursive:!0}));const o=path.join(e,"dist");fs.existsSync(o)?(console.log(chalk.blue("Moving dist folder to public/assets/dist...")),fs.existsSync(c)&&fs.rmSync(c,{recursive:!0,force:!0}),fs.renameSync(o,c),console.log(chalk.green("✓ Moved dist to public/assets/dist"))):console.warn(chalk.yellow("Warning: dist folder not found in cloned repository")),fs.existsSync(s)?console.log(chalk.green("✓ APIs folder preserved in src/app/swagger-docs/apis")):console.warn(chalk.yellow("Warning: apis folder not found in cloned repository")),console.log(chalk.green("✓ Swagger docs setup complete"))}if(updateAnswer?.isUpdate){const e=[],s=[],t=e=>{try{const s=path.join(u,"composer.json");if(fs.existsSync(s)){const t=JSON.parse(fs.readFileSync(s,"utf8"));return!(!t.require||!t.require[e])}return!1}catch{return!1}},c=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const t=JSON.parse(fs.readFileSync(s,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.swaggerDocs){const s=path.join(u,"src","app","swagger-docs");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("swagger-docs was deleted successfully."));["swagger-config.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))}),c("swagger-jsdoc")&&e.push("swagger-jsdoc"),c("@types/swagger-jsdoc")&&e.push("@types/swagger-jsdoc"),c("prompts")&&e.push("prompts"),c("@types/prompts")&&e.push("@types/prompts")}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano"].forEach(s=>{c(s)&&e.push(s)});const n="gehrisandro/tailwind-merge-php";t(n)&&s.push(n)}if(!updateAnswer.websocket){["restart-websocket.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","Lib","Websocket");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("Websocket folder was deleted successfully.")),t("cboden/ratchet")&&s.push("cboden/ratchet")}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","Lib","MCP");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),t("php-mcp/server")&&s.push("php-mcp/server")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(s=>{c(s)&&e.push(s)})}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const t=path.join(u,"settings","vite-plugins");fs.existsSync(t)&&(fs.rmSync(t,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(s=>{c(s)&&e.push(s)})}const n=e=>Array.from(new Set(e)),o=n(e),i=n(s);o.length>0&&(console.log(`Uninstalling npm packages: ${o.join(", ")}`),await uninstallNpmDependencies(u,o,!0)),i.length>0&&(console.log(`Uninstalling composer packages: ${i.join(", ")}`),await uninstallComposerDependencies(u,i))}if(!a||!fs.existsSync(path.join(u,"prisma-php.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),t={projectName:r.projectName,projectRootPath:e,phpEnvironment:"XAMPP",phpRootPathExe:"C:\\xampp\\php\\php.exe",bsTarget:s.bsTarget,bsPathRewrite:s.bsPathRewrite,backendOnly:r.backendOnly,swaggerDocs:r.swaggerDocs,tailwindcss:r.tailwindcss,websocket:r.websocket,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,version:p,componentScanDirs:updateAnswer?.componentScanDirs??["src","vendor/tsnc/prisma-php/src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"prisma-php.json"),JSON.stringify(t,null,2),{flag:"w"})}execSync(updateAnswer?.isUpdate?"C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar update":"C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar install",{stdio:"inherit"}),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Prisma PHP project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
2
+ import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.php","metadata.php","not-found.php","error.php"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!1,swaggerDocs:!1,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","src/app/layout.php","src/app/index.php"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/app/layout.php","src/app/index.php","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,websocket:!1,prisma:!0,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,websocket:!0,prisma:!0,swaggerDocs:!0,mcp:!0},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/lib/websocket","src/lib/mcp"]},ecommerce:{id:"ecommerce",name:"E-commerce Starter",description:"Full e-commerce application with cart, payments, and admin",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,swaggerDocs:!0,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-ecommerce-starter",branch:"main"}},blog:{id:"blog",name:"Blog CMS",description:"Blog content management system",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,swaggerDocs:!1,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-blog-starter"}}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+8).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let c=`http://localhost/${n}`;c=c.endsWith("/")?c.slice(0,-1):c;const o=c.replace(/(?<!:)(\/\/+)/g,"/"),i=n.replace(/\/\/+/g,"/");return{bsTarget:`${o}/`,bsPathRewrite:{"^/":`/${i.startsWith("/")?i.substring(1):i}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const n=JSON.parse(fs.readFileSync(t,"utf8"));["tailwind","tailwind:build","ts:watch","ts:build","websocket","mcp","create-swagger-docs"].forEach(e=>{delete n.scripts[e]}),n.scripts={...n.scripts,projectName:"tsx settings/project-name.ts"};let c=[];if(s.tailwindcss&&(n.scripts={...n.scripts,tailwind:"tsx settings/run-postcss.ts watch","tailwind:build":"tsx settings/run-postcss.ts build"},c.push("tailwind")),s.typescript&&!s.backendOnly&&(n.scripts={...n.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},c.push("ts:watch")),s.websocket&&(n.scripts={...n.scripts,websocket:"tsx settings/restart-websocket.ts"},c.push("websocket")),s.mcp&&(n.scripts={...n.scripts,mcp:"tsx settings/restart-mcp.ts"},c.push("mcp")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";n.scripts={...n.scripts,"create-swagger-docs":e}}let o={...n.scripts};o.browserSync="tsx settings/bs-config.ts",o["browserSync:build"]="tsx settings/build.ts",o.dev=`npm-run-all projectName -p browserSync ${c.join(" ")}`;let i=["browserSync:build"];s.tailwindcss&&i.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&i.unshift("ts:build"),o.build=`npm-run-all ${i.join(" ")}`,n.scripts=o,n.type="module",fs.writeFileSync(t,JSON.stringify(n,null,2))}async function updateComposerJson(e){checkExcludeFiles(path.join(e,"composer.json"))}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"public","js","main.js");if(checkExcludeFiles(t))return;let n=fs.readFileSync(t,"utf8");n+='\nwindow.ws = new WebSocket("ws://localhost:9001");\n',fs.writeFileSync(t,n,"utf8")}async function copyTailwindMergeBundle(e,s){if(!s.tailwindcss||s.backendOnly||s.typescript)return;const t=path.join(e,"node_modules","tailwind-merge","dist","bundle-mjs.mjs"),n=path.join(e,"public","js","tailwind-merge.mjs");checkExcludeFiles(n)||(fs.mkdirSync(path.dirname(n),{recursive:!0}),fs.copyFileSync(t,n))}async function updateIndexJsForTailwindCss(e,s){if(!s.tailwindcss||s.backendOnly||s.typescript)return;const t=path.join(e,"public","js","main.js");if(checkExcludeFiles(t))return;fs.writeFileSync(t,'import "/js/pp-reactive-v2.js";\nimport { twMerge } from "/js/tailwind-merge.mjs";\n\nconst pp = (globalThis).pp;\n\nglobalThis.twMerge = twMerge;\n\nif (document.readyState !== "loading") {\n\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',"utf8")}async function updateMainTsForTailwindCss(e,s){if(!s.tailwindcss||!s.typescript||s.backendOnly)return;const t=path.join(e,"ts","main.ts");if(checkExcludeFiles(t))return;fs.writeFileSync(t,'import "/js/pp-reactive-v2.js";\n\n// The following global names have already been declared elsewhere in the project:\n// - pp: Used for the Reactive Core functionality.\n\n// Imports goes here --Start\nimport { createGlobalSingleton } from "./global-functions.js";\nimport { mergeTailwindClasses } from "./tailwind-merge.js";\n\ncreateGlobalSingleton("twMerge", mergeTailwindClasses);\n\n\n// Imports goes here --End\n\nconst pp = (globalThis as any).pp;\n\nif (document.readyState !== "loading") {\n\tpp?.mount?.();\n} else {\n\tdocument.addEventListener(\n\t\t"DOMContentLoaded",\n\t\t() => pp?.mount?.(),\n\t\t{ once: true },\n\t);\n}\n',"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const n=fs.existsSync(e),c=n&&fs.statSync(e);if(n&&c&&c.isDirectory()){const n=s.toLowerCase();if(!t.websocket&&n.includes("src\\lib\\websocket"))return;if(!t.mcp&&n.includes("src\\lib\\mcp"))return;if((!t.typescript||t.backendOnly)&&(n.endsWith("\\ts")||n.includes("\\ts\\")))return;if((!t.typescript||t.backendOnly)&&(n.endsWith("\\vite-plugins")||n.includes("\\vite-plugins\\")||n.includes("\\vite-plugins")))return;if(t.backendOnly&&n.includes("public\\js")||t.backendOnly&&n.includes("public\\css")||t.backendOnly&&n.includes("public\\assets"))return;if(!t.swaggerDocs&&n.includes("src\\app\\swagger-docs"))return;const c=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(c))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),t)})}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")||s.includes("run-postcss.ts")))return;if(!t.websocket&&s.includes("restart-websocket.ts"))return;if(!t.mcp&&s.includes("restart-mcp.ts"))return;if(t.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(!t.backendOnly&&s.includes("route.php"))return;if(t.backendOnly&&!t.swaggerDocs&&s.includes("layout.php"))return;if(!t.swaggerDocs&&s.includes("swagger-config.ts"))return;if(t.tailwindcss&&s.includes("index.css"))return;if((!t.swaggerDocs||!t.prisma)&&(s.includes("auto-swagger-docs.ts")||s.includes("prisma-schema-config.json")))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach(({src:s,dest:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),t)})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.php");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),n="";s.backendOnly||(s.tailwindcss||(n='\n <link href="/css/index.css" rel="stylesheet" />'),n+='\n <script type="module" src="/js/main.js"><\/script>');let c="";s.backendOnly||(c=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" /> ${n}`:n),e=e.replace("</head>",`${c}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.php:"),e)}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");fs.existsSync(t)&&checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const t=e.replace(/\\/g,"/");return s.endsWith("/"+t)||s===t})}async function createDirectoryStructure(e,s){const t=[{src:"/bootstrap.php",dest:"/bootstrap.php"},{src:"/.htaccess",dest:"/.htaccess"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/CLAUDE.md",dest:"/CLAUDE.md"},{src:"/AGENTS.md",dest:"/AGENTS.md"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),s.typescript&&!s.backendOnly&&t.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const n=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"}];s.typescript&&!s.backendOnly&&n.push({src:"/ts",dest:"/ts"}),t.forEach(({src:s,dest:t})=>{const n=path.join(__dirname,s),c=path.join(e,t);if(checkExcludeFiles(c))return;const o=fs.readFileSync(n,"utf8");fs.writeFileSync(c,o,{flag:"w"})}),await executeCopy(e,n,s),await updatePackageJson(e,s),await updateComposerJson(e),s.backendOnly||(await copyTailwindMergeBundle(e,s),await updateIndexJsForTailwindCss(e,s),await updateMainTsForTailwindCss(e,s),await updateIndexJsForWebSocket(e,s)),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const c=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins (CSV or JSON array)\nCORS_ALLOWED_ORIGINS=[]\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${c}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,c)}async function getAnswer(e={},s=!1){if(s)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,swaggerDocs:e.swaggerDocs??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,websocket:e.websocket??!1,mcp:e.mcp??!1,prisma:e.prisma??!1};if(e.starterKit){const s=e.starterKit;let t=null;if(STARTER_KITS[s]&&(t=STARTER_KITS[s]),t){const n={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,websocket:t.features.websocket??!1,prisma:t.features.prisma??!1,swaggerDocs:t.features.swaggerDocs??!1,mcp:t.features.mcp??!1,typescript:t.features.typescript??!1},c=process.argv.slice(2);return c.includes("--backend-only")&&(n.backendOnly=!0),c.includes("--swagger-docs")&&(n.swaggerDocs=!0),c.includes("--tailwindcss")&&(n.tailwindcss=!0),c.includes("--websocket")&&(n.websocket=!0),c.includes("--mcp")&&(n.mcp=!0),c.includes("--prisma")&&(n.prisma=!0),c.includes("--typescript")&&(n.typescript=!0),n}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,swaggerDocs:!0,mcp:!1,typescript:!1},n=process.argv.slice(2);return n.includes("--backend-only")&&(t.backendOnly=!0),n.includes("--swagger-docs")&&(t.swaggerDocs=!0),n.includes("--tailwindcss")&&(t.tailwindcss=!0),n.includes("--websocket")&&(t.websocket=!0),n.includes("--mcp")&&(t.mcp=!0),n.includes("--prisma")&&(t.prisma=!0),n.includes("--typescript")&&(t.typescript=!0),t}}const t=[];e.projectName||t.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||t.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const n=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},c=await prompts(t,{onCancel:n}),o=[];c.backendOnly??e.backendOnly??!1?(e.swaggerDocs||o.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||o.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||o.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||o.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||o.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||o.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||o.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||o.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||o.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||o.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}));const i=await prompts(o,{onCancel:n});return{projectName:c.projectName?String(c.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:c.backendOnly??e.backendOnly??!1,swaggerDocs:i.swaggerDocs??e.swaggerDocs??!1,tailwindcss:i.tailwindcss??e.tailwindcss??!1,typescript:i.typescript??e.typescript??!1,websocket:i.websocket??e.websocket??!1,mcp:i.mcp??e.mcp??!1,prisma:i.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,s,t=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}async function uninstallComposerDependencies(e,s){console.log("Uninstalling Composer dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let n="";e.on("data",e=>n+=e),e.on("end",()=>{try{const e=JSON.parse(n);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}})}).on("error",e=>t(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.split(".").map(Number),n=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>n[e])return 1;if(t[e]<n[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:(console.error(`Package ${e} is not installed`),null)}catch(e){return console.error(e instanceof Error?e.message:String(e)),null}}async function installNpmDependencies(e,s,t=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const n=`npm install ${t?"--save-dev":""} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}function getComposerCmd(){try{return execSync("composer --version",{stdio:"ignore"}),console.log("✓ Using global composer command"),{cmd:"composer",baseArgs:[]}}catch{const e="C:\\xampp\\php\\php.exe",s="C:\\ProgramData\\ComposerSetup\\bin\\composer.phar";if(!fs.existsSync(e))throw console.error(`✗ PHP not found at ${e}`),new Error(`PHP executable not found at ${e}`);if(!fs.existsSync(s))throw console.error(`✗ Composer not found at ${s}`),new Error(`Composer phar not found at ${s}`);return console.log("✓ Using XAMPP PHP with Composer phar"),{cmd:e,baseArgs:[s]}}}export async function installComposerDependencies(e,s){const{cmd:t,baseArgs:n}=getComposerCmd(),c=path.join(e,"composer.json"),o=fs.existsSync(c);if(console.log(chalk.green("Composer project initialization: "+(o?"Updating existing project…":"Setting up new project…"))),fs.existsSync(e)||(console.log(`Creating base directory: ${e}`),fs.mkdirSync(e,{recursive:!0})),!o){const s=[...n,"init","--no-interaction","--name","tsnc/prisma-php-app","--require","php:^8.2","--type","project","--version","1.0.0"];console.log("Attempting composer init...");const o=spawnSync(t,s,{cwd:e,stdio:["ignore","pipe","pipe"],encoding:"utf8"}),i=fs.existsSync(c);if(0===o.status&&i)console.log("✓ Composer init successful and composer.json created");else{0!==o.status?(console.log(`Composer init failed with status ${o.status}`),o.stderr&&console.log(`Stderr: ${o.stderr}`)):console.log("Composer init reported success but didn't create composer.json"),console.log("Creating composer.json manually...");const s={name:"tsnc/prisma-php-app",type:"project",version:"1.0.0",require:{php:"^8.2"},autoload:{"psr-4":{"":"src/"}}};try{const t=path.resolve(e,"composer.json");if(console.log(`Writing composer.json to: ${t}`),fs.writeFileSync(t,JSON.stringify(s,null,2),{encoding:"utf8"}),!fs.existsSync(t))throw new Error("File creation appeared to succeed but file doesn't exist");console.log("✓ Successfully created composer.json")}catch(s){if(console.error("✗ Failed to create composer.json:",s),console.error(`Base directory: ${e}`),console.error(`Absolute base directory: ${path.resolve(e)}`),console.error(`Target file path: ${c}`),console.error(`Absolute target file path: ${path.resolve(c)}`),console.error(`Current working directory: ${process.cwd()}`),console.error(`Base directory exists: ${fs.existsSync(e)}`),fs.existsSync(e))try{const s=fs.statSync(e);console.error(`Base directory is writable: ${s.isDirectory()}`)}catch(e){console.error(`Cannot stat base directory: ${e}`)}throw new Error(`Cannot create composer.json: ${s}`)}}}const i=path.resolve(e,"composer.json");if(!fs.existsSync(i))throw console.error(`✗ composer.json still not found at ${i}`),console.error("Directory contents:",fs.readdirSync(e)),new Error("Failed to create composer.json - file does not exist after all attempts");let r;try{const e=fs.readFileSync(i,"utf8");console.log("✓ Successfully read composer.json"),r=JSON.parse(e)}catch(e){throw console.error("✗ Failed to read/parse composer.json:",e),new Error(`Cannot read composer.json: ${e}`)}r.autoload??={},r.autoload["psr-4"]??={},r.autoload["psr-4"][""]??="src/";try{fs.writeFileSync(i,JSON.stringify(r,null,2)),console.log("✓ Updated composer.json with PSR-4 autoload")}catch(e){throw console.error("✗ Failed to update composer.json:",e),e}if(s.length){console.log("Installing Composer dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));try{const c=`${t} ${[...n,"require","--no-interaction","-W",...s].join(" ")}`;execSync(c,{stdio:"inherit",cwd:e,env:{...process.env}}),console.log("✓ Composer dependencies installed")}catch(e){throw console.error("✗ Failed to install composer dependencies:",e),e}}if(o)try{execSync(`${t} ${[...n,"update","--lock","--no-install","--no-interaction"].join(" ")}`,{stdio:"inherit",cwd:e}),console.log("✓ Composer lock updated")}catch(e){throw console.error("✗ Failed to update composer lock:",e),e}try{execSync(`${t} ${[...n,"dump-autoload","--quiet"].join(" ")}`,{stdio:"inherit",cwd:e}),console.log("✓ Composer autoloader regenerated")}catch(e){throw console.error("✗ Failed to regenerate autoloader:",e),e}}const npmPinnedVersions={"@tailwindcss/postcss":"4.3.3","@types/browser-sync":"2.29.1","@types/node":"26.4.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"6.0.0","chokidar-cli":"3.0.0",cssnano:"9.0.1","http-proxy-middleware":"4.2.0","npm-run-all":"4.1.5","php-parser":"3.7.0",postcss:"8.5.26","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.3.3","tailwind-merge":"3.6.0",tsx:"4.23.13",typescript:"7.0.2",vite:"8.2.2","fast-glob":"3.3.3","prisma-php":"0.0.26"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}const composerPinnedVersions={"vlucas/phpdotenv":"5.7.0","firebase/php-jwt":"7.1.0","phpmailer/phpmailer":"7.1.1","guzzlehttp/guzzle":"7.15.5","symfony/uid":"7.4.17","brick/math":"0.20.0","cboden/ratchet":"0.4.4","tsnc/prisma-php":"2.0.0","php-mcp/server":"3.3.0"};function composerPkg(e){return composerPinnedVersions[e]?`${e}:${composerPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(s){const t=s;if("win32"===globalThis.process?.platform&&("EPERM"===t.code||"EACCES"===t.code)){try{spawnSync("cmd",["/c","attrib","-R","-H","-S","/S","/D",`${e}\\*`],{stdio:"ignore"})}catch{}return void spawnSync("cmd",["/c","rd","/s","/q",e],{stdio:"ignore"})}throw s}}async function setupStarterKit(e,s){if(!s.starterKit)return;let t=null;if(STARTER_KITS[s.starterKit]?t=STARTER_KITS[s.starterKit]:s.starterKitSource&&(t={id:s.starterKit,name:`Custom Starter Kit (${s.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:s.starterKitSource}}),t){if(console.log(chalk.green(`Setting up ${t.name}...`)),t.source)try{const n=t.source.branch?`git clone -b ${t.source.branch} --depth 1 ${t.source.url} "${e}"`:`git clone --depth 1 ${t.source.url} "${e}"`;execSync(n,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const c=path.join(e,"prisma-php.json");if(fs.existsSync(c))try{const t=JSON.parse(fs.readFileSync(c,"utf8")),n=e,o=bsConfigUrls(n);t.projectName=s.projectName,t.projectRootPath=n,t.bsTarget=o.bsTarget,t.bsPathRewrite=o.bsPathRewrite;const i=await fetchPackageVersion("create-prisma-php-app");t.version=t.version||i,fs.writeFileSync(c,JSON.stringify(t,null,2)),console.log(chalk.green("Updated prisma-php.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update prisma-php.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}t.customSetup&&await t.customSetup(e,s),console.log(chalk.green(`✓ ${t.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\n🚀 Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const t=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");t&&console.log(chalk.magenta(` Features: ${t}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-prisma-php-app my-project --starter-kit=basic"),console.log(" npx create-prisma-php-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let t=e[0];const n=e.find(e=>e.startsWith("--starter-kit=")),c=n?.split("=")[1],o=e.find(e=>e.startsWith("--starter-kit-source=")),i=o?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,a=!1;if(t){const n=process.cwd(),o=path.join(n,"prisma-php.json");if(c&&i){a=!0;const n={projectName:t,starterKit:c,starterKitSource:i,backendOnly:e.includes("--backend-only"),swaggerDocs:e.includes("--swagger-docs"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),websocket:e.includes("--websocket"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(n,s)}else if(fs.existsSync(o)){const c=readJsonFile(o);let i=[];c.excludeFiles?.map(e=>{const s=path.join(n,e);fs.existsSync(s)&&i.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:c.backendOnly,swaggerDocs:c.swaggerDocs,tailwindcss:c.tailwindcss,websocket:c.websocket,mcp:c.mcp,prisma:c.prisma,typescript:c.typescript,isUpdate:!0,componentScanDirs:c.componentScanDirs??[],excludeFiles:c.excludeFiles??[],excludeFilePath:i??[],filePath:n};const a={projectName:t,backendOnly:e.includes("--backend-only")||c.backendOnly,swaggerDocs:e.includes("--swagger-docs")||c.swaggerDocs,tailwindcss:e.includes("--tailwindcss")||c.tailwindcss,typescript:e.includes("--typescript")||c.typescript,websocket:e.includes("--websocket")||c.websocket,prisma:e.includes("--prisma")||c.prisma,mcp:e.includes("--mcp")||c.mcp};r=await getAnswer(a,s),null!==r&&(updateAnswer={projectName:t,backendOnly:r.backendOnly,swaggerDocs:r.swaggerDocs,tailwindcss:r.tailwindcss,websocket:r.websocket,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:c.componentScanDirs??[],excludeFiles:c.excludeFiles??[],excludeFilePath:i??[],filePath:n})}else{const n={projectName:t,starterKit:c,starterKitSource:i,backendOnly:e.includes("--backend-only"),swaggerDocs:e.includes("--swagger-docs"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),websocket:e.includes("--websocket"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(n,s)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},s);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const p=await fetchPackageVersion("create-prisma-php-app"),l=getInstalledPackageVersion("create-prisma-php-app");l?-1===compareVersions(l,p)&&(execSync("npm uninstall -g create-prisma-php-app",{stdio:"inherit"}),execSync("npm install -g create-prisma-php-app",{stdio:"inherit"})):execSync("npm install -g create-prisma-php-app",{stdio:"inherit"});const d=process.cwd();let u;if(t)if(a){const s=path.join(d,t);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,r),process.chdir(u);const n=path.join(u,"prisma-php.json");if(fs.existsSync(n)){const s=JSON.parse(fs.readFileSync(n,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--swagger-docs")&&(s.swaggerDocs=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--websocket")&&(s.websocket=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),r={...r,backendOnly:s.backendOnly,swaggerDocs:s.swaggerDocs,tailwindcss:s.tailwindcss,typescript:s.typescript,websocket:s.websocket,mcp:s.mcp,prisma:s.prisma};let t=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&t.push(s.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"prisma-php.json"),s=path.join(d,t),n=path.join(s,"prisma-php.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(n)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}else fs.mkdirSync(r.projectName,{recursive:!0}),u=path.join(d,r.projectName),process.chdir(r.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("http-proxy-middleware"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("php-parser"),npmPkg("prisma-php")],g=[composerPkg("vlucas/phpdotenv"),composerPkg("firebase/php-jwt"),composerPkg("phpmailer/phpmailer"),composerPkg("guzzlehttp/guzzle"),composerPkg("symfony/uid"),composerPkg("brick/math"),composerPkg("tsnc/prisma-php")];if(r.swaggerDocs&&m.push(npmPkg("swagger-jsdoc"),npmPkg("@types/swagger-jsdoc")),r.swaggerDocs&&r.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano"),npmPkg("tailwind-merge")),r.websocket&&g.push("cboden/ratchet"),r.mcp&&g.push("php-mcp/server"),r.prisma&&execSync("npm install -g prisma-client-php@latest",{stdio:"inherit"}),r.typescript&&!r.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),r.starterKit&&!a&&await setupStarterKit(u,r),await installNpmDependencies(u,m,!0),await installComposerDependencies(u,g),t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.prisma&&execSync("npx ppo init --prisma-php",{stdio:"inherit"}),r.swaggerDocs){const e=path.join(u,"src","app","swagger-docs"),s=path.join(e,"apis"),t=path.join(u,"public","assets"),n=path.join(t,"dist");fs.existsSync(e)&&fs.readdirSync(e).length>0&&(console.log("Removing existing swagger-docs directory..."),fs.rmSync(e,{recursive:!0,force:!0})),console.log(chalk.blue("Cloning swagger-docs repository...")),execSync(`git clone https://github.com/TheSteelNinjaCode/prisma-php-swagger-docs.git ${e}`,{stdio:"inherit"});const c=path.join(e,".git");fs.existsSync(c)&&fs.rmSync(c,{recursive:!0,force:!0}),fs.existsSync(t)||(console.log(chalk.blue("Creating public/assets directory...")),fs.mkdirSync(t,{recursive:!0}));const o=path.join(e,"dist");fs.existsSync(o)?(console.log(chalk.blue("Moving dist folder to public/assets/dist...")),fs.existsSync(n)&&fs.rmSync(n,{recursive:!0,force:!0}),fs.renameSync(o,n),console.log(chalk.green("✓ Moved dist to public/assets/dist"))):console.warn(chalk.yellow("Warning: dist folder not found in cloned repository")),fs.existsSync(s)?console.log(chalk.green("✓ APIs folder preserved in src/app/swagger-docs/apis")):console.warn(chalk.yellow("Warning: apis folder not found in cloned repository")),console.log(chalk.green("✓ Swagger docs setup complete"))}if(updateAnswer?.isUpdate){const e=[],s=[],t=e=>{try{const s=path.join(u,"composer.json");if(fs.existsSync(s)){const t=JSON.parse(fs.readFileSync(s,"utf8"));return!(!t.require||!t.require[e])}return!1}catch{return!1}},n=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const t=JSON.parse(fs.readFileSync(s,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.swaggerDocs){const s=path.join(u,"src","app","swagger-docs");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("swagger-docs was deleted successfully."));["swagger-config.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))}),n("swagger-jsdoc")&&e.push("swagger-jsdoc"),n("@types/swagger-jsdoc")&&e.push("@types/swagger-jsdoc"),n("prompts")&&e.push("prompts"),n("@types/prompts")&&e.push("@types/prompts")}if(!updateAnswer.tailwindcss){[path.join(u,"postcss.config.js"),path.join(u,"settings","run-postcss.ts"),path.join(u,"public","js","tailwind-merge.mjs")].forEach(e=>{fs.existsSync(e)&&(fs.unlinkSync(e),console.log(`${path.relative(u,e)} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano","tailwind-merge"].forEach(s=>{n(s)&&e.push(s)})}if(!updateAnswer.websocket){["restart-websocket.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","Lib","Websocket");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("Websocket folder was deleted successfully.")),t("cboden/ratchet")&&s.push("cboden/ratchet")}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","Lib","MCP");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),t("php-mcp/server")&&s.push("php-mcp/server")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(s=>{n(s)&&e.push(s)})}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const t=path.join(u,"settings","vite-plugins");fs.existsSync(t)&&(fs.rmSync(t,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(s=>{n(s)&&e.push(s)})}const c=e=>Array.from(new Set(e)),o=c(e),i=c(s);o.length>0&&(console.log(`Uninstalling npm packages: ${o.join(", ")}`),await uninstallNpmDependencies(u,o,!0)),i.length>0&&(console.log(`Uninstalling composer packages: ${i.join(", ")}`),await uninstallComposerDependencies(u,i))}if(!a||!fs.existsSync(path.join(u,"prisma-php.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),t={projectName:r.projectName,projectRootPath:e,phpEnvironment:"XAMPP",phpRootPathExe:"C:\\xampp\\php\\php.exe",bsTarget:s.bsTarget,bsPathRewrite:s.bsPathRewrite,backendOnly:r.backendOnly,swaggerDocs:r.swaggerDocs,tailwindcss:r.tailwindcss,websocket:r.websocket,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,version:p,componentScanDirs:updateAnswer?.componentScanDirs??["src","vendor/tsnc/prisma-php/src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"prisma-php.json"),JSON.stringify(t,null,2),{flag:"w"})}execSync(updateAnswer?.isUpdate?"C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar update":"C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar install",{stdio:"inherit"}),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Prisma PHP 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();
@@ -1,6 +1,8 @@
1
+ const isWatchMode = process.env.PP_POSTCSS_MODE === "watch";
2
+
1
3
  export default {
2
4
  plugins: {
3
5
  "@tailwindcss/postcss": {},
4
- cssnano: {},
6
+ ...(isWatchMode ? {} : { cssnano: {} }),
5
7
  },
6
- };
8
+ };
@@ -40,7 +40,7 @@ RewriteEngine On
40
40
  Header set Content-Type "text/css; charset=UTF-8"
41
41
  </FilesMatch>
42
42
  # JS
43
- <FilesMatch "\.(js)$">
43
+ <FilesMatch "\.(js|mjs)$">
44
44
  Header set Content-Type "application/javascript; charset=UTF-8"
45
45
  </FilesMatch>
46
46
  </IfModule>