create-caspian-app 0.2.0-beta.45 → 0.2.0-beta.47

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.
@@ -29,7 +29,9 @@
29
29
  - Decide route privacy in `src/lib/auth/auth_config.py` at app setup time: use `is_all_routes_private=True` when only a few routes should stay public, otherwise keep `is_all_routes_private=False` and list the protected routes in `private_routes`.
30
30
  - In all-private mode, keep public exceptions in `public_routes`; the runtime defaults keep `/` public and keep `auth_routes=["/signin", "/signup"]` public.
31
31
  - Do not treat `token_auto_refresh` as the switch that makes routes private. In the current app it only affects sliding-session refresh if `auth.refresh_session()` is called.
32
- - Use PulsePoint and `pp.rpc(...)` as the default frontend and client-to-server contract unless the user requests another stack.
32
+ - Use PulsePoint as the default reactive frontend layer unless the user requests another stack.
33
+ - For CRUD operations and any browser-initiated reads from the backend, use route or backend `@rpc()` actions on the server and `pp.rpc(...)` from PulsePoint code on the client unless the user explicitly asks for another integration pattern.
34
+ - For route creation, keep page markup in `src/app/**/index.html`. If a route is UI-only, `index.html` alone is sufficient. Add `src/app/**/index.py` only as a companion when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side behavior. Do not place route HTML in `index.py`; use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
33
35
  - For file uploads and file-manager flows, keep browser interaction in route templates, keep upload and delete `@rpc()` actions in the owning `src/app/**/index.py`, keep shared storage and persistence helpers in `src/lib/**`, store metadata in Prisma, and store browser-accessible blobs under `public/storage/**`.
34
36
  - When runtime uploads write into `public/storage/**`, keep `public/storage` in `settings/bs-config.ts` `PUBLIC_IGNORE_DIRS` so `npm run dev` does not reload on each upload.
35
37
  - For logout flows, prefer `pp.rpc("signout")` backed by `@rpc(require_auth=True)` from page-level or component-level UI. Use a dedicated signout route only for plain form POST, no-JavaScript fallback, or other full-navigation edge cases.
@@ -87,6 +89,8 @@
87
89
  ### `src/app/**/*.html`
88
90
 
89
91
  - Keep route templates and layouts server-rendered first, with PulsePoint enhancement as the default interactive layer.
92
+ - When a route renders UI, author that markup in the route's `index.html` even if the route also has an `index.py` companion.
93
+ - For route-level reactivity, prefer PulsePoint state, effects, refs, and template directives together with `pp.rpc(...)` instead of manual DOM mutation or ad hoc browser fetch code.
90
94
  - Preserve Caspian template syntax such as `[[...]]` in layouts and `pp-*` runtime attributes in rendered HTML.
91
95
  - Do not author `pp-component="..."` manually in route or layout templates; the Python render pipeline injects it onto the single root element.
92
96
  - Do not author `type="text/pp"` manually in route or layout templates either. Use plain `<script>` in source and let the render path rewrite it.
package/dist/AGENTS.md CHANGED
@@ -64,6 +64,8 @@ Important rules:
64
64
  - In all-private mode, treat `public_routes` as the exception list. The runtime defaults keep `/` public and keep `auth_routes=["/signin", "/signup"]` public.
65
65
  - `token_auto_refresh` does not make routes private in the current app; it only affects sliding-session refresh if `auth.refresh_session()` is called.
66
66
  - Prefer logout via auth-protected RPC from page-level or component-level UI: `pp.rpc("signout")` backed by `@rpc(require_auth=True)`. Use a dedicated signout route only for plain form POST or no-JavaScript edge cases.
67
+ - Use PulsePoint as the default reactive frontend layer for app UI.
68
+ - For CRUD operations and any browser-initiated reads from the backend, use server `@rpc()` actions and client `pp.rpc(...)` calls unless the user explicitly asks for another integration pattern.
67
69
  - Protect customized `src/lib/auth/auth_config.py` from framework updates by adding `./src/lib/auth/auth_config.py` to `excludeFiles` in `caspian.config.json`.
68
70
  - This workspace already has an app-owned Python database layer in `src/lib/prisma/`.
69
71
  - Do not assume `src/lib/mcp/**`, `settings/restart-mcp.ts`, or MCP-related scripts exist unless `caspian.config.json` confirms MCP is enabled and the update workflow has run.
@@ -88,7 +90,8 @@ Important rules:
88
90
  - `StateManager` reads and writes `request.state.session`, but the current middleware stack in `main.py` does not mirror `request.session` into `request.state.session`.
89
91
  - Do not assume `StateManager` persistence survives across requests until that bridge exists.
90
92
  - Route HTML caching uses `caches/` and `caches/cache_manifest.json` through `casp.cache_handler`.
91
- - The current app tree has root templates in `src/app/` and already includes route-specific `index.py` files where routes need server-side logic, including upload and file-manager flows.
93
+ - The current app tree has root templates in `src/app/`, and route-specific `index.py` files belong only where routes need server-side logic, metadata, or non-visual handling such as uploads or redirects.
94
+ - For route creation, keep page markup in `src/app/**/index.html`. If a route is UI-only, stop there. Add `src/app/**/index.py` only as a companion when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side behavior. Do not place route HTML in `index.py`; use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
92
95
 
93
96
  ## Task Routing
94
97
 
@@ -114,6 +117,7 @@ Use this map before making changes.
114
117
  - Keep app-owned shared code in `src/lib/**`.
115
118
  - Keep reusable application UI components in `src/components/**`.
116
119
  - Keep route-specific logic in `src/app/**`.
120
+ - For route creation, keep page markup in `src/app/**/index.html`. If a route is UI-only, `index.html` alone is sufficient. Add `src/app/**/index.py` only as a companion when the same route needs metadata, `page()`, `@rpc()` actions, auth checks, caching, redirects, or other server-side behavior. Do not place route HTML in `index.py`; use a lone `index.py` only for non-visual routes such as redirect-only or action-only handlers.
117
121
  - For file-manager work, keep route-owned upload and delete `@rpc()` actions in `src/app/**/index.py`, keep shared storage and Prisma helper logic in `src/lib/**`, and keep uploaded public blobs under `public/storage/**`.
118
122
  - When deciding between `src/components/**` and `src/lib/**`, put reusable rendered UI in `src/components/**` and put services, validators, adapters, database helpers, and other non-UI support code in `src/lib/**`.
119
123
  - Read `caspian.config.json` before deciding whether a Caspian feature should be used, documented, scaffolded, or avoided in the current workspace.
@@ -134,7 +138,8 @@ Use this map before making changes.
134
138
  - Do not treat `token_auto_refresh` as the switch for private routes.
135
139
  - For logout flows, prefer `pp.rpc("signout")` plus `@rpc(require_auth=True)` in page or component UI. Only scaffold a signout route for no-JavaScript, form-post, or full-navigation edge cases.
136
140
  - When MCP is enabled, keep the app-owned FastMCP server in `src/lib/mcp/mcp_server.py` and the default config in `src/lib/mcp/fastmcp.json`. If those paths move, update `settings/restart-mcp.ts` and the MCP docs together.
137
- - Use PulsePoint and `pp.rpc(...)` as the default frontend and browser-to-server contract unless the user explicitly wants another stack.
141
+ - Use PulsePoint as the default reactive frontend and browser-side UI layer unless the user explicitly wants another stack.
142
+ - For CRUD operations and any browser-initiated reads from the backend, use `@rpc()` plus `pp.rpc(...)` as the default contract instead of ad hoc fetch or parallel REST endpoints unless the user explicitly asks for them.
138
143
  - Treat `pp-component` as a framework-owned attribute on authored templates. Document it, but do not manually add it in normal route or component HTML.
139
144
  - Treat `type="text/pp"` on PulsePoint scripts as a render-time attribute too. In authored route, layout, and component HTML, write plain `<script>` and let Caspian rewrite it.
140
145
  - Keep route and component HTML templates to a single top-level lowercase HTML element so the Python side can inject `pp-component` safely. Keep any owned PulsePoint script inside that same root instead of as a sibling top-level node.
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
2
- import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.html","not-found.html","error.html"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.html","src/app/index.html"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.html","src/app/index.html","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const n=e.substring(0,s+8).replace(/\\/g,"\\\\"),t=e.replace(new RegExp(`^${n}`),"").replace(/\\/g,"/");let i=`http://localhost/${t}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),r=t.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${r.startsWith("/")?r.substring(1):r}/`}}}async function updatePackageJson(e,s){const n=path.join(e,"package.json");if(checkExcludeFiles(n))return;const t=JSON.parse(fs.readFileSync(n,"utf8"));t.scripts={...t.scripts,projectName:"tsx settings/project-name.ts"};let i=[];s.tailwindcss&&(t.scripts={...t.scripts,tailwind:"postcss src/app/globals.css -o public/css/styles.css --watch","tailwind:build":"postcss src/app/globals.css -o public/css/styles.css"},i.push("tailwind")),s.typescript&&!s.backendOnly&&(t.scripts={...t.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},i.push("ts:watch")),s.mcp&&(t.scripts={...t.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...t.scripts};c.browserSync="tsx settings/bs-config.ts",c["browserSync:build"]="tsx settings/build.ts",c.dev=`npm-run-all projectName -p browserSync ${i.join(" ")}`;let r=["browserSync:build"];s.tailwindcss&&r.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&r.unshift("ts:build"),c.build=`npm-run-all ${r.join(" ")}`,t.scripts=c,t.type="module",fs.writeFileSync(n,JSON.stringify(t,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,n){const t=fs.existsSync(e),i=t&&fs.statSync(e);if(t&&i&&i.isDirectory()){const t=s.toLowerCase();if(!n.mcp&&t.includes("src\\lib\\mcp"))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\ts")||t.includes("\\ts\\")))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\vite-plugins")||t.includes("\\vite-plugins\\")||t.includes("\\vite-plugins")))return;if(n.backendOnly&&t.includes("public\\js")||n.backendOnly&&t.includes("public\\css")||n.backendOnly&&t.includes("public\\assets"))return;const i=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(t=>{copyRecursiveSync(path.join(e,t),path.join(s,t),n)})}else{if(checkExcludeFiles(s))return;if(!n.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")))return;if(!n.mcp&&s.includes("restart-mcp.ts"))return;if(n.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(n.backendOnly&&s.includes("layout.html"))return;if(n.tailwindcss&&s.includes("index.css"))return;if(!n.prisma&&s.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,n){s.forEach(({src:s,dest:t})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,t),n)})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const n=path.join(e,"src","app","layout.html");if(!checkExcludeFiles(n))try{let e=fs.readFileSync(n,"utf8"),t="";s.backendOnly||(s.tailwindcss||(t='\n <link href="/css/index.css" rel="stylesheet" />'),t+='\n <script type="module" src="/js/main.js"><\/script>');let i="";s.backendOnly||(i=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${t}`:t),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(n,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.html:"),e)}}async function createOrUpdateEnvFile(e,s){const n=path.join(e,".env");checkExcludeFiles(n)||fs.writeFileSync(n,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const n=e.replace(/\\/g,"/");return s.endsWith("/"+n)||s===n})}async function createDirectoryStructure(e,s){const n=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/AGENTS.md",dest:"/AGENTS.md"},{src:"/CLAUDE.md",dest:"/CLAUDE.md"}];s.tailwindcss&&n.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),s.typescript&&!s.backendOnly&&n.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const t=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"}];s.typescript&&!s.backendOnly&&t.push({src:"/ts",dest:"/ts"}),n.forEach(({src:s,dest:n})=>{const t=path.join(__dirname,s),i=path.join(e,n);if(checkExcludeFiles(i))return;if("/pyproject.toml"===s&&updateAnswer?.isUpdate&&fs.existsSync(i))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const c=fs.readFileSync(t,"utf8");fs.writeFileSync(i,c,{flag:"w"})}),await executeCopy(e,t,s),await updatePackageJson(e,s),s.tailwindcss&&modifyPostcssConfig(e),!s.tailwindcss&&s.backendOnly||modifyLayoutPHP(e,s);const i=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins allowed for CORS (comma-separated)\nCORS_ALLOWED_ORIGINS=""\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Session & Security\nSESSION_LIFETIME_HOURS="7"\nMAX_CONTENT_LENGTH_MB="16"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i)}async function getAnswer(e={},s=!1){if(s)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,prisma:e.prisma??!1};if(e.starterKit){const s=e.starterKit;let n=null;if(STARTER_KITS[s]&&(n=STARTER_KITS[s]),n){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:n.features.backendOnly??!1,tailwindcss:n.features.tailwindcss??!1,prisma:n.features.prisma??!1,mcp:n.features.mcp??!1,typescript:n.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(t.backendOnly=!0),i.includes("--tailwindcss")&&(t.tailwindcss=!0),i.includes("--mcp")&&(t.mcp=!0),i.includes("--prisma")&&(t.prisma=!0),i.includes("--typescript")&&(t.typescript=!0),t}if(e.starterKitSource){const n={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,typescript:!1},t=process.argv.slice(2);return t.includes("--backend-only")&&(n.backendOnly=!0),t.includes("--tailwindcss")&&(n.tailwindcss=!0),t.includes("--mcp")&&(n.mcp=!0),t.includes("--prisma")&&(n.prisma=!0),t.includes("--typescript")&&(n.typescript=!0),n}}const n=[];e.projectName||n.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||n.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(n,{onCancel:t}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}));const r=await prompts(c,{onCancel:t});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:r.tailwindcss??e.tailwindcss??!1,typescript:r.typescript??e.typescript??!1,mcp:r.mcp??e.mcp??!1,prisma:r.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,s,n=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm uninstall ${n?"--save-dev":"--save"} ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,n)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>{try{const e=JSON.parse(t);s(e["dist-tags"].latest)}catch(e){n(new Error("Failed to parse JSON response"))}})}).on("error",e=>n(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const n=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),t=s.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!n||!t)return e.localeCompare(s);const i=n.slice(1,4).map(Number),c=t.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>c[e])return 1;if(i[e]<c[e])return-1}const r=n[4]??null,a=t[4]??null;return r&&!a?-1:!r&&a?1:r&&a?r.localeCompare(a):0}function getInstalledPackageInfo(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString(),n=s.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return n?{version:n[1],isLinked:s.includes(`${e}@`)&&s.includes("->")}:(console.error(`Package ${e} is not installed`),{version:null,isLinked:!1})}catch(e){return console.error(e instanceof Error?e.message:String(e)),{version:null,isLinked:!1}}}function isRunningFromNpxCache(e){const s=path.resolve(e).toLowerCase(),n=`${path.sep}_npx${path.sep}`.toLowerCase();return s.includes(n)}async function installNpmDependencies(e,s,n=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),console.log((n?"Installing development dependencies":"Installing dependencies")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm install ${n?"--save-dev":""} ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.2.4","@types/browser-sync":"2.29.1","@types/node":"25.6.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.7","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.10","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.2.4",tsx:"4.21.0",typescript:"6.0.3",vite:"8.0.8","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.18","caspian-utils":"0.0.x"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(s){const n=s;if("win32"===globalThis.process?.platform&&("EPERM"===n.code||"EACCES"===n.code)){try{spawnSync("cmd",["/c","attrib","-R","-H","-S","/S","/D",`${e}\\*`],{stdio:"ignore"})}catch{}return void spawnSync("cmd",["/c","rd","/s","/q",e],{stdio:"ignore"})}throw s}}async function setupStarterKit(e,s){if(!s.starterKit)return;let n=null;if(STARTER_KITS[s.starterKit]?n=STARTER_KITS[s.starterKit]:s.starterKitSource&&(n={id:s.starterKit,name:`Custom Starter Kit (${s.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:s.starterKitSource}}),n){if(console.log(chalk.green(`Setting up ${n.name}...`)),n.source)try{const t=n.source.branch?`git clone -b ${n.source.branch} --depth 1 ${n.source.url} "${e}"`:`git clone --depth 1 ${n.source.url} "${e}"`;execSync(t,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const i=path.join(e,"caspian.config.json");if(fs.existsSync(i))try{const n=JSON.parse(fs.readFileSync(i,"utf8")),t=e,c=bsConfigUrls(t);n.projectName=s.projectName,n.projectRootPath=t,n.bsTarget=c.bsTarget,n.bsPathRewrite=c.bsPathRewrite;const r=await fetchPackageVersion("create-caspian-app");n.version=n.version||r,fs.writeFileSync(i,JSON.stringify(n,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}n.customSetup&&await n.customSetup(e,s),console.log(chalk.green(`✓ ${n.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\n🚀 Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const n=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");n&&console.log(chalk.magenta(` Features: ${n}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,s,n){const t=spawnSync(e,s,{cwd:n,stdio:"inherit",shell:!1,encoding:"utf8"});if(t.error)throw t.error;if(0!==t.status)throw new Error(`Command failed (${e} ${s.join(" ")}), exit=${t.status}`)}function tryRunCmd(e,s,n){const t=spawnSync(e,s,{cwd:n,stdio:"ignore",shell:!1,encoding:"utf8"});return!t.error&&0===t.status}function tryInstallUv(e){console.log(chalk.blue("uv not found. Attempting to install uv..."));const s=[{cmd:"py",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python3",args:["-m","pip","install","--upgrade","uv"]}];for(const n of s)if(tryRunCmd(n.cmd,n.args,e))return!0;return!1}function resolveUvCommand(e){const s=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const n of s)if(tryRunCmd(n.cmd,[...n.argsPrefix,"--version"],e))return n;if(tryInstallUv(e))for(const n of s)if(tryRunCmd(n.cmd,[...n.argsPrefix,"--version"],e))return n;throw new Error("Could not find or install uv. Install uv and ensure `uv`, `py`, or `python` is available in PATH.")}function buildPythonDependencies(e){const s=["fastapi==0.136.0","uvicorn==0.45.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.14.3","slowapi==0.1.9","python-multipart==0.0.26","starsessions==2.2.1","httpx==0.28.1","werkzeug==3.1.8","cuid2==2.0.1","nanoid==2.0.0","python-ulid==3.1.0","cuid==0.4","caspian-utils~=0.2"];return e.tailwindcss&&s.push("tailwind-merge==0.3.3"),e.mcp&&s.push("fastmcp==3.2.4"),e.prisma&&(s.push("psycopg2-binary==2.9.12"),s.push("asyncpg==0.31.0"),s.push("aiosqlite==0.22.1"),s.push("aiomysql==0.3.2")),s}function getPyProjectDependencyNames(e){const s=path.join(e,"pyproject.toml");if(!fs.existsSync(s))return new Set;const n=fs.readFileSync(s,"utf8").replace(/\r\n/g,"\n").match(/^[ \t]*dependencies[ \t]*=[ \t]*\[([\s\S]*?)\]/m);if(!n)return new Set;const t=new Set,i=/"([^"]+)"/g;let c;for(;null!==(c=i.exec(n[1]));){const e=c[1].trim().match(/^([A-Za-z0-9._-]+)/)?.[1];e&&t.add(e.toLowerCase())}return t}function ensurePyProjectExists(e){const s=path.join(e,"pyproject.toml");if(!fs.existsSync(s))throw new Error(`pyproject.toml not found at: ${s}`);let n=fs.readFileSync(s,"utf8");n=n.replace(/\r\n/g,"\n"),n.includes("package = false")||(n=n.includes("[tool.uv]")?n.replace("[tool.uv]","[tool.uv]\npackage = false"):`${n.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(s,n,"utf8")}async function ensurePythonVenvAndDeps(e,s,n=[]){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: syncing dependencies with uv")),console.log(chalk.green("=========================\n")),console.log(chalk.blue("Preparing pyproject.toml...")),ensurePyProjectExists(e);const t=path.join(e,"requirements.txt");fs.existsSync(t)&&(fs.unlinkSync(t),console.log(chalk.gray("Removed legacy requirements.txt")));const i=resolveUvCommand(e),c=path.join(e,".venv");fs.existsSync(c)?console.log(chalk.blue("Existing .venv detected. Reusing it so uv sync can update dependencies without replacing the environment.")):(console.log(chalk.blue("Creating the virtual environment with uv...")),runCmd(i.cmd,[...i.argsPrefix,"venv",".venv"],e));const r=buildPythonDependencies(s);n.length>0&&(console.log(chalk.blue("Removing obsolete Python dependencies via uv remove...")),runCmd(i.cmd,[...i.argsPrefix,"remove",...n],e)),console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...r],e),console.log(chalk.blue("Syncing dependencies...")),runCmd(i.cmd,[...i.argsPrefix,"sync"],e),console.log(chalk.green("\n✓ uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let n=e[0];const t=e.find(e=>e.startsWith("--starter-kit=")),i=t?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),r=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let a=null,o=!1;if(n){const t=process.cwd(),c=path.join(t,"caspian.config.json");if(i&&r){o=!0;const t={projectName:n,starterKit:i,starterKitSource:r,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};a=await getAnswer(t,s)}else if(fs.existsSync(c)){const i=readJsonFile(c);let r=[];i.excludeFiles?.map(e=>{const s=path.join(t,e);fs.existsSync(s)&&r.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:n,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:r??[],filePath:t};const o={projectName:n,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp};a=await getAnswer(o,s),null!==a&&(updateAnswer={projectName:n,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,prisma:a.prisma,typescript:a.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:r??[],filePath:t})}else{const t={projectName:n,starterKit:i,starterKitSource:r,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};a=await getAnswer(t,s)}if(null===a)return void console.log(chalk.red("Installation cancelled."))}else a=await getAnswer({},s);if(null===a)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync("npm uninstall -g create-caspian-app",{stdio:"inherit"}),execSync("npm install -g create-caspian-app",{stdio:"inherit"})):execSync("npm install -g create-caspian-app",{stdio:"inherit"});const d=process.cwd();let u;if(n)if(o){const s=path.join(d,n);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,a),process.chdir(u);const t=path.join(u,"caspian.config.json");if(fs.existsSync(t)){const s=JSON.parse(fs.readFileSync(t,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),a={...a,backendOnly:s.backendOnly,tailwindcss:s.tailwindcss,typescript:s.typescript,mcp:s.mcp,prisma:s.prisma};let n=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&n.push(s.replace(/\\/g,"/"))}),updateAnswer={...a,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:n??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),s=path.join(d,n),t=path.join(s,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(t)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}else fs.mkdirSync(a.projectName,{recursive:!0}),u=path.join(d,a.projectName),process.chdir(a.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("http-proxy-middleware"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python"),npmPkg("caspian-utils")];a.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),a.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano")),a.prisma&&execSync("npm install -g prisma-client-python@latest",{stdio:"inherit"}),a.typescript&&!a.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),a.starterKit&&!o&&await setupStarterKit(u,a),await installNpmDependencies(u,m,!0);let y=[];if(n||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,a),a.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],s=[],n=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));return!!(n.dependencies&&n.dependencies[e]||n.devDependencies&&n.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano"].forEach(s=>{n(s)&&e.push(s)}),s.push("tailwind-merge")}if(a.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(s){console.warn(chalk.yellow(`Failed to delete ${e}: ${s}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),s.push("fastmcp")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(s=>{n(s)&&e.push(s)}),s.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const t=path.join(u,"settings","vite-plugins");fs.existsSync(t)&&(fs.rmSync(t,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(s=>{n(s)&&e.push(s)})}const t=e=>Array.from(new Set(e)),i=t(e);i.length>0&&(console.log(`Uninstalling npm packages: ${i.join(", ")}`),await uninstallNpmDependencies(u,i,!0));const c=t(s),r=getPyProjectDependencyNames(u);y=c.filter(e=>r.has(e.toLowerCase())),y.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${y.join(", ")}`))}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),n={projectName:a.projectName,projectRootPath:e,bsTarget:s.bsTarget,bsPathRewrite:s.bsPathRewrite,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,prisma:a.prisma,typescript:a.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(n,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,a,y),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
1
+ #!/usr/bin/env node
2
+ import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.html","not-found.html","error.html"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.html","src/app/index.html"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.html","src/app/index.html","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+8).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let i=`http://localhost/${n}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),r=n.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${r.startsWith("/")?r.substring(1):r}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const n=JSON.parse(fs.readFileSync(t,"utf8"));n.scripts={...n.scripts,projectName:"tsx settings/project-name.ts"};let i=[];s.tailwindcss&&(n.scripts={...n.scripts,tailwind:"tsx settings/run-postcss.ts watch","tailwind:build":"tsx settings/run-postcss.ts build"},i.push("tailwind")),s.typescript&&!s.backendOnly&&(n.scripts={...n.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},i.push("ts:watch")),s.mcp&&(n.scripts={...n.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...n.scripts};c.browserSync="tsx settings/bs-config.ts",c["browserSync:build"]="tsx settings/build.ts",c.dev=`npm-run-all projectName -p browserSync ${i.join(" ")}`;let r=["browserSync:build"];s.tailwindcss&&r.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&r.unshift("ts:build"),c.build=`npm-run-all ${r.join(" ")}`,n.scripts=c,n.type="module",fs.writeFileSync(t,JSON.stringify(n,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const n=fs.existsSync(e),i=n&&fs.statSync(e);if(n&&i&&i.isDirectory()){const n=s.toLowerCase();if(!t.mcp&&n.includes("src\\lib\\mcp"))return;if((!t.typescript||t.backendOnly)&&(n.endsWith("\\ts")||n.includes("\\ts\\")))return;if((!t.typescript||t.backendOnly)&&(n.endsWith("\\vite-plugins")||n.includes("\\vite-plugins\\")||n.includes("\\vite-plugins")))return;if(t.backendOnly&&n.includes("public\\js")||t.backendOnly&&n.includes("public\\css")||t.backendOnly&&n.includes("public\\assets"))return;const i=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),t)})}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")))return;if(!t.mcp&&s.includes("restart-mcp.ts"))return;if(t.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(t.backendOnly&&s.includes("layout.html"))return;if(t.tailwindcss&&s.includes("index.css"))return;if(!t.prisma&&s.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach(({src:s,dest:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),t)})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.html");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),n="";s.backendOnly||(s.tailwindcss||(n='\n <link href="/css/index.css" rel="stylesheet" />'),n+='\n <script type="module" src="/js/main.js"><\/script>');let i="";s.backendOnly||(i=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${n}`:n),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.html:"),e)}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const t=e.replace(/\\/g,"/");return s.endsWith("/"+t)||s===t})}async function createDirectoryStructure(e,s){const t=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/AGENTS.md",dest:"/AGENTS.md"},{src:"/CLAUDE.md",dest:"/CLAUDE.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),i=path.join(e,t);if(checkExcludeFiles(i))return;if("/pyproject.toml"===s&&updateAnswer?.isUpdate&&fs.existsSync(i))return void console.log(chalk.gray("Preserving existing pyproject.toml during update."));const c=fs.readFileSync(n,"utf8");fs.writeFileSync(i,c,{flag:"w"})}),await executeCopy(e,n,s),await updatePackageJson(e,s),!s.tailwindcss&&s.backendOnly||modifyLayoutPHP(e,s);const i=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins allowed for CORS (comma-separated)\nCORS_ALLOWED_ORIGINS=""\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Session & Security\nSESSION_LIFETIME_HOURS="7"\nMAX_CONTENT_LENGTH_MB="16"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i)}async function getAnswer(e={},s=!1){if(s)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,prisma:e.prisma??!1};if(e.starterKit){const s=e.starterKit;let t=null;if(STARTER_KITS[s]&&(t=STARTER_KITS[s]),t){const n={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,prisma:t.features.prisma??!1,mcp:t.features.mcp??!1,typescript:t.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(n.backendOnly=!0),i.includes("--tailwindcss")&&(n.tailwindcss=!0),i.includes("--mcp")&&(n.mcp=!0),i.includes("--prisma")&&(n.prisma=!0),i.includes("--typescript")&&(n.typescript=!0),n}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,typescript:!1},n=process.argv.slice(2);return n.includes("--backend-only")&&(t.backendOnly=!0),n.includes("--tailwindcss")&&(t.tailwindcss=!0),n.includes("--mcp")&&(t.mcp=!0),n.includes("--prisma")&&(t.prisma=!0),n.includes("--typescript")&&(t.typescript=!0),t}}const t=[];e.projectName||t.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||t.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const n=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(t,{onCancel:n}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}));const r=await prompts(c,{onCancel:n});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:r.tailwindcss??e.tailwindcss??!1,typescript:r.typescript??e.typescript??!1,mcp:r.mcp??e.mcp??!1,prisma:r.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,s,t=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let n="";e.on("data",e=>n+=e),e.on("end",()=>{try{const e=JSON.parse(n);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}})}).on("error",e=>t(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),n=s.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!t||!n)return e.localeCompare(s);const i=t.slice(1,4).map(Number),c=n.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>c[e])return 1;if(i[e]<c[e])return-1}const r=t[4]??null,a=n[4]??null;return r&&!a?-1:!r&&a?1:r&&a?r.localeCompare(a):0}function getInstalledPackageInfo(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString(),t=s.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return t?{version:t[1],isLinked:s.includes(`${e}@`)&&s.includes("->")}:(console.error(`Package ${e} is not installed`),{version:null,isLinked:!1})}catch(e){return console.error(e instanceof Error?e.message:String(e)),{version:null,isLinked:!1}}}function isRunningFromNpxCache(e){const s=path.resolve(e).toLowerCase(),t=`${path.sep}_npx${path.sep}`.toLowerCase();return s.includes(t)}async function installNpmDependencies(e,s,t=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),console.log((t?"Installing development dependencies":"Installing dependencies")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const n=`npm install ${t?"--save-dev":""} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.2.4","@types/browser-sync":"2.29.1","@types/node":"25.6.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.7","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.10","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.2.4",tsx:"4.21.0",typescript:"6.0.3",vite:"8.0.8","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.18","caspian-utils":"0.0.x"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(s){const 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 i=path.join(e,"caspian.config.json");if(fs.existsSync(i))try{const t=JSON.parse(fs.readFileSync(i,"utf8")),n=e,c=bsConfigUrls(n);t.projectName=s.projectName,t.projectRootPath=n,t.bsTarget=c.bsTarget,t.bsPathRewrite=c.bsPathRewrite;const r=await fetchPackageVersion("create-caspian-app");t.version=t.version||r,fs.writeFileSync(i,JSON.stringify(t,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}t.customSetup&&await t.customSetup(e,s),console.log(chalk.green(`✓ ${t.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\n🚀 Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const t=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");t&&console.log(chalk.magenta(` Features: ${t}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,s,t){const n=spawnSync(e,s,{cwd:t,stdio:"inherit",shell:!1,encoding:"utf8"});if(n.error)throw n.error;if(0!==n.status)throw new Error(`Command failed (${e} ${s.join(" ")}), exit=${n.status}`)}function tryRunCmd(e,s,t){const n=spawnSync(e,s,{cwd:t,stdio:"ignore",shell:!1,encoding:"utf8"});return!n.error&&0===n.status}function tryInstallUv(e){console.log(chalk.blue("uv not found. Attempting to install uv..."));const s=[{cmd:"py",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python3",args:["-m","pip","install","--upgrade","uv"]}];for(const t of s)if(tryRunCmd(t.cmd,t.args,e))return!0;return!1}function resolveUvCommand(e){const s=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const t of s)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;if(tryInstallUv(e))for(const t of s)if(tryRunCmd(t.cmd,[...t.argsPrefix,"--version"],e))return t;throw new Error("Could not find or install uv. Install uv and ensure `uv`, `py`, or `python` is available in PATH.")}function buildPythonDependencies(e){const s=["fastapi==0.136.0","uvicorn==0.45.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.14.3","slowapi==0.1.9","python-multipart==0.0.26","starsessions==2.2.1","httpx==0.28.1","werkzeug==3.1.8","cuid2==2.0.1","nanoid==2.0.0","python-ulid==3.1.0","cuid==0.4","caspian-utils~=0.2"];return e.tailwindcss&&s.push("tailwind-merge==0.3.3"),e.mcp&&s.push("fastmcp==3.2.4"),e.prisma&&(s.push("psycopg2-binary==2.9.12"),s.push("asyncpg==0.31.0"),s.push("aiosqlite==0.22.1"),s.push("aiomysql==0.3.2")),s}function getPyProjectDependencyNames(e){const s=path.join(e,"pyproject.toml");if(!fs.existsSync(s))return new Set;const t=fs.readFileSync(s,"utf8").replace(/\r\n/g,"\n").match(/^[ \t]*dependencies[ \t]*=[ \t]*\[([\s\S]*?)\]/m);if(!t)return new Set;const n=new Set,i=/"([^"]+)"/g;let c;for(;null!==(c=i.exec(t[1]));){const e=c[1].trim().match(/^([A-Za-z0-9._-]+)/)?.[1];e&&n.add(e.toLowerCase())}return n}function ensurePyProjectExists(e){const s=path.join(e,"pyproject.toml");if(!fs.existsSync(s))throw new Error(`pyproject.toml not found at: ${s}`);let t=fs.readFileSync(s,"utf8");t=t.replace(/\r\n/g,"\n"),t.includes("package = false")||(t=t.includes("[tool.uv]")?t.replace("[tool.uv]","[tool.uv]\npackage = false"):`${t.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(s,t,"utf8")}async function ensurePythonVenvAndDeps(e,s,t=[]){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: syncing dependencies with uv")),console.log(chalk.green("=========================\n")),console.log(chalk.blue("Preparing pyproject.toml...")),ensurePyProjectExists(e);const n=path.join(e,"requirements.txt");fs.existsSync(n)&&(fs.unlinkSync(n),console.log(chalk.gray("Removed legacy requirements.txt")));const i=resolveUvCommand(e),c=path.join(e,".venv");fs.existsSync(c)?console.log(chalk.blue("Existing .venv detected. Reusing it so uv sync can update dependencies without replacing the environment.")):(console.log(chalk.blue("Creating the virtual environment with uv...")),runCmd(i.cmd,[...i.argsPrefix,"venv",".venv"],e));const r=buildPythonDependencies(s);t.length>0&&(console.log(chalk.blue("Removing obsolete Python dependencies via uv remove...")),runCmd(i.cmd,[...i.argsPrefix,"remove",...t],e)),console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(i.cmd,[...i.argsPrefix,"add",...r],e),console.log(chalk.blue("Syncing dependencies...")),runCmd(i.cmd,[...i.argsPrefix,"sync"],e),console.log(chalk.green("\n✓ uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let t=e[0];const n=e.find(e=>e.startsWith("--starter-kit=")),i=n?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),r=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let a=null,o=!1;if(t){const n=process.cwd(),c=path.join(n,"caspian.config.json");if(i&&r){o=!0;const n={projectName:t,starterKit:i,starterKitSource:r,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};a=await getAnswer(n,s)}else if(fs.existsSync(c)){const i=readJsonFile(c);let r=[];i.excludeFiles?.map(e=>{const s=path.join(n,e);fs.existsSync(s)&&r.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:t,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:r??[],filePath:n};const o={projectName:t,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp};a=await getAnswer(o,s),null!==a&&(updateAnswer={projectName:t,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,prisma:a.prisma,typescript:a.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:r??[],filePath:n})}else{const n={projectName:t,starterKit:i,starterKitSource:r,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};a=await getAnswer(n,s)}if(null===a)return void console.log(chalk.red("Installation cancelled."))}else a=await getAnswer({},s);if(null===a)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync("npm uninstall -g create-caspian-app",{stdio:"inherit"}),execSync("npm install -g create-caspian-app",{stdio:"inherit"})):execSync("npm install -g create-caspian-app",{stdio:"inherit"});const d=process.cwd();let u;if(t)if(o){const s=path.join(d,t);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,a),process.chdir(u);const n=path.join(u,"caspian.config.json");if(fs.existsSync(n)){const s=JSON.parse(fs.readFileSync(n,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),a={...a,backendOnly:s.backendOnly,tailwindcss:s.tailwindcss,typescript:s.typescript,mcp:s.mcp,prisma:s.prisma};let t=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&t.push(s.replace(/\\/g,"/"))}),updateAnswer={...a,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:t??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),s=path.join(d,t),n=path.join(s,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(n)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}else fs.mkdirSync(a.projectName,{recursive:!0}),u=path.join(d,a.projectName),process.chdir(a.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("http-proxy-middleware"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python"),npmPkg("caspian-utils")];a.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),a.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano")),a.prisma&&execSync("npm install -g prisma-client-python@latest",{stdio:"inherit"}),a.typescript&&!a.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),a.starterKit&&!o&&await setupStarterKit(u,a),await installNpmDependencies(u,m,!0);let y=[];if(t||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,a),a.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],s=[],t=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const t=JSON.parse(fs.readFileSync(s,"utf8"));return!!(t.dependencies&&t.dependencies[e]||t.devDependencies&&t.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano"].forEach(s=>{t(s)&&e.push(s)}),s.push("tailwind-merge")}if(a.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(s){console.warn(chalk.yellow(`Failed to delete ${e}: ${s}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),s.push("fastmcp")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(s=>{t(s)&&e.push(s)}),s.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const n=path.join(u,"settings","vite-plugins");fs.existsSync(n)&&(fs.rmSync(n,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(s=>{t(s)&&e.push(s)})}const n=e=>Array.from(new Set(e)),i=n(e);i.length>0&&(console.log(`Uninstalling npm packages: ${i.join(", ")}`),await uninstallNpmDependencies(u,i,!0));const c=n(s),r=getPyProjectDependencyNames(u);y=c.filter(e=>r.has(e.toLowerCase())),y.length>0&&console.log(chalk.gray(`Python dependencies will be removed via uv remove: ${y.join(", ")}`))}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),t={projectName:a.projectName,projectRootPath:e,bsTarget:s.bsTarget,bsPathRewrite:s.bsPathRewrite,backendOnly:a.backendOnly,tailwindcss:a.tailwindcss,mcp:a.mcp,prisma:a.prisma,typescript:a.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(t,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,a,y),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
@@ -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
+ };
@@ -1,7 +1,7 @@
1
- {
2
- "local": "http://localhost:5090",
3
- "external": "http://172.18.64.1:5090",
4
- "ui": "http://localhost:3001",
5
- "uiExternal": "http://172.18.64.1:3001",
6
- "backend": "http://localhost:5091"
1
+ {
2
+ "local": "http://localhost:5090",
3
+ "external": "http://172.18.64.1:5090",
4
+ "ui": "http://localhost:3001",
5
+ "uiExternal": "http://172.18.64.1:3001",
6
+ "backend": "http://localhost:5091"
7
7
  }
@@ -21,7 +21,7 @@ const { __dirname } = getFileMeta();
21
21
  const bs: BrowserSyncInstance = browserSync.create();
22
22
 
23
23
  const WORKSPACE_ROOT = join(__dirname, "..");
24
- const PUBLIC_IGNORE_DIRS = [""];
24
+ const PUBLIC_IGNORE_DIRS = ["uploads"];
25
25
  let previousRouteFiles: string[] = [];
26
26
  let lastChangedFile: string | null = null;
27
27
 
@@ -93,11 +93,9 @@ function hasLoopbackListener(port: number): Promise<boolean> {
93
93
 
94
94
  async function hasWindowsTcpListener(port: number): Promise<boolean> {
95
95
  try {
96
- const { stdout } = await execFileAsync(
97
- "netstat",
98
- ["-ano", "-p", "TCP"],
99
- { windowsHide: true },
100
- );
96
+ const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "TCP"], {
97
+ windowsHide: true,
98
+ });
101
99
 
102
100
  return stdout.split(/\r?\n/).some((line) => {
103
101
  const parts = line.trim().split(/\s+/);
@@ -31,7 +31,7 @@ const getAllFiles = (dirPath: string): string[] => {
31
31
 
32
32
  const relativePath = `.${sep}${relative(
33
33
  join(__dirname, ".."),
34
- fullPath
34
+ fullPath,
35
35
  )}`;
36
36
  files.push(relativePath.replace(/\\/g, "/").replace(/^\.\.\//, ""));
37
37
  }
@@ -49,7 +49,7 @@ export const generateFileListJson = async (): Promise<void> => {
49
49
  if (allFiles.length > 0) {
50
50
  writeFileSync(jsonFilePath, JSON.stringify(allFiles, null, 2));
51
51
  console.log(
52
- `File list generated: ${appFiles.length} app files, ${publicFiles.length} public files`
52
+ `File list generated: ${appFiles.length} app files, ${publicFiles.length} public files`,
53
53
  );
54
54
  } else {
55
55
  console.error("No files found to save in the JSON file.");
@@ -12,12 +12,12 @@ const configFilePath = join(currentProjectRoot, "caspian.config.json");
12
12
  function updateProjectNameInConfig(
13
13
  filePath: string,
14
14
  newProjectName: string,
15
- currentRoot: string
15
+ currentRoot: string,
16
16
  ): void {
17
17
  const newWebPath = calculateDynamicWebPath(
18
18
  currentRoot,
19
19
  caspianConfigJson.projectRootPath,
20
- caspianConfigJson.bsPathRewrite?.["^/"]
20
+ caspianConfigJson.bsPathRewrite?.["^/"],
21
21
  );
22
22
 
23
23
  caspianConfigJson.projectName = newProjectName;
@@ -39,16 +39,16 @@ function updateProjectNameInConfig(
39
39
  return;
40
40
  }
41
41
  console.log(
42
- `Configuration updated.\nProject: ${newProjectName}\nURL: http://localhost${newWebPath}`
42
+ `Configuration updated.\nProject: ${newProjectName}\nURL: http://localhost${newWebPath}`,
43
43
  );
44
- }
44
+ },
45
45
  );
46
46
  }
47
47
 
48
48
  function calculateDynamicWebPath(
49
49
  currentPath: string,
50
50
  oldPath?: string,
51
- oldUrl?: string
51
+ oldUrl?: string,
52
52
  ): string {
53
53
  let webRoot: string | null = null;
54
54
 
@@ -76,7 +76,7 @@ function calculateDynamicWebPath(
76
76
  updateProjectNameInConfig(configFilePath, newProjectName, currentProjectRoot);
77
77
 
78
78
  export const deleteFilesIfExist = async (
79
- filePaths: string[]
79
+ filePaths: string[],
80
80
  ): Promise<void> => {
81
81
  for (const filePath of filePaths) {
82
82
  try {
@@ -90,7 +90,7 @@ export const deleteFilesIfExist = async (
90
90
  };
91
91
 
92
92
  export async function deleteDirectoriesIfExist(
93
- dirPaths: string[]
93
+ dirPaths: string[],
94
94
  ): Promise<void> {
95
95
  for (const dirPath of dirPaths) {
96
96
  try {
@@ -0,0 +1,208 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { mkdir, readFile, readFileSync, rm, rmSync, writeFile } from "node:fs";
3
+ import { promisify } from "node:util";
4
+ import { dirname, join } from "node:path";
5
+ import process from "node:process";
6
+
7
+ const mode: "watch" | "build" =
8
+ process.argv[2] === "watch" ? "watch" : "build";
9
+ const watcherPidFile = join(process.cwd(), ".pp", "postcss-watch.pid");
10
+ const mkdirAsync = promisify(mkdir);
11
+ const readFileAsync = promisify(readFile);
12
+ const rmAsync = promisify(rm);
13
+ const writeFileAsync = promisify(writeFile);
14
+
15
+ const args: string[] = [
16
+ "--max-old-space-size=6144",
17
+ "./node_modules/postcss-cli/index.js",
18
+ "src/app/globals.css",
19
+ "-o",
20
+ "public/css/styles.css",
21
+ ];
22
+
23
+ if (mode === "watch") {
24
+ args.push("--watch");
25
+ }
26
+
27
+ function isValidPid(value: string): boolean {
28
+ return /^\d+$/.test(value.trim());
29
+ }
30
+
31
+ function isProcessRunning(pid: number): boolean {
32
+ try {
33
+ process.kill(pid, 0);
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ function killWindowsProcessTree(pid: number): Promise<void> {
41
+ return new Promise((resolve) => {
42
+ const killer = execFile(
43
+ "taskkill",
44
+ ["/F", "/T", "/PID", String(pid)],
45
+ () => resolve(),
46
+ );
47
+
48
+ killer.on("error", () => resolve());
49
+ });
50
+ }
51
+
52
+ async function killProcessTree(pid: number): Promise<void> {
53
+ if (process.platform === "win32") {
54
+ await killWindowsProcessTree(pid);
55
+ return;
56
+ }
57
+
58
+ try {
59
+ process.kill(pid, "SIGTERM");
60
+ } catch {
61
+ return;
62
+ }
63
+ }
64
+
65
+ async function ensurePidDirectory(): Promise<void> {
66
+ await mkdirAsync(dirname(watcherPidFile), { recursive: true });
67
+ }
68
+
69
+ async function clearPidFile(): Promise<void> {
70
+ try {
71
+ const storedPid = (await readFileAsync(watcherPidFile, "utf8")).trim();
72
+ if (storedPid === String(process.pid)) {
73
+ await rmAsync(watcherPidFile, { force: true });
74
+ }
75
+ } catch {}
76
+ }
77
+
78
+ function clearPidFileSync(): void {
79
+ try {
80
+ const storedPid = readFileSync(watcherPidFile, "utf8").trim();
81
+ if (storedPid === String(process.pid)) {
82
+ rmSync(watcherPidFile, { force: true });
83
+ }
84
+ } catch {}
85
+ }
86
+
87
+ async function cleanupStaleWatcher(): Promise<void> {
88
+ if (mode !== "watch") {
89
+ return;
90
+ }
91
+
92
+ await ensurePidDirectory();
93
+
94
+ let storedPid = "";
95
+
96
+ try {
97
+ storedPid = (await readFileAsync(watcherPidFile, "utf8")).trim();
98
+ } catch {
99
+ return;
100
+ }
101
+
102
+ if (!isValidPid(storedPid)) {
103
+ await rmAsync(watcherPidFile, { force: true });
104
+ return;
105
+ }
106
+
107
+ const pid = Number(storedPid);
108
+
109
+ if (pid === process.pid) {
110
+ return;
111
+ }
112
+
113
+ if (!isProcessRunning(pid)) {
114
+ await rmAsync(watcherPidFile, { force: true });
115
+ return;
116
+ }
117
+
118
+ console.warn(
119
+ `[tailwind] Found stale PostCSS watcher (PID ${pid}), stopping it before restart.`,
120
+ );
121
+ await killProcessTree(pid);
122
+ await rmAsync(watcherPidFile, { force: true });
123
+ }
124
+
125
+ async function writePidFile(): Promise<void> {
126
+ if (mode !== "watch") {
127
+ return;
128
+ }
129
+
130
+ await ensurePidDirectory();
131
+ await writeFileAsync(watcherPidFile, `${process.pid}\n`, "utf8");
132
+ }
133
+
134
+ await cleanupStaleWatcher();
135
+ await writePidFile();
136
+
137
+ const child = spawn(process.execPath, args, {
138
+ stdio: "inherit",
139
+ windowsHide: true,
140
+ env: {
141
+ ...process.env,
142
+ PP_POSTCSS_MODE: mode,
143
+ },
144
+ });
145
+
146
+ let shuttingDown = false;
147
+
148
+ child.on("error", (error) => {
149
+ console.error(error);
150
+ void shutdown(1);
151
+ });
152
+
153
+ async function shutdown(exitCode: number): Promise<void> {
154
+ if (shuttingDown) {
155
+ return;
156
+ }
157
+
158
+ shuttingDown = true;
159
+
160
+ if (child.pid && child.exitCode === null && !child.killed) {
161
+ await killProcessTree(child.pid);
162
+ }
163
+
164
+ await clearPidFile();
165
+ process.exit(exitCode);
166
+ }
167
+
168
+ child.on("exit", async (code, signal) => {
169
+ await clearPidFile();
170
+
171
+ if (shuttingDown) {
172
+ process.exit(code ?? 0);
173
+ return;
174
+ }
175
+
176
+ if (signal) {
177
+ try {
178
+ process.kill(process.pid, signal);
179
+ } catch {
180
+ process.exit(1);
181
+ }
182
+ return;
183
+ }
184
+
185
+ process.exit(code ?? 0);
186
+ });
187
+
188
+ process.once("SIGINT", () => {
189
+ void shutdown(0);
190
+ });
191
+
192
+ process.once("SIGTERM", () => {
193
+ void shutdown(0);
194
+ });
195
+
196
+ process.once("uncaughtException", (error) => {
197
+ console.error(error);
198
+ void shutdown(1);
199
+ });
200
+
201
+ process.once("unhandledRejection", (reason) => {
202
+ console.error(reason);
203
+ void shutdown(1);
204
+ });
205
+
206
+ process.once("exit", () => {
207
+ clearPidFileSync();
208
+ });
@@ -1,4 +1,6 @@
1
- @import "tailwindcss";
1
+ @import "tailwindcss" source(none);
2
+ @source "../";
3
+ @source "../../ts";
2
4
 
3
5
  :root {
4
6
  --background: #ffffff;
package/dist/ts/main.ts CHANGED
@@ -2,4 +2,23 @@ import "/js/pp-reactive-v2.js";
2
2
 
3
3
  // The following global names have already been declared elsewhere in the project:
4
4
  // - pp: Used for the Reactive Core functionality.
5
- // - searchParams: Manages URL manipulation and query parameters.
5
+
6
+ // Imports goes here --Start
7
+
8
+ // Uncomment the following line if you need to use the createGlobalSingleton function in this file.
9
+ // import { createGlobalSingleton } from "./global-functions.js";
10
+ // import { myCustomFunction } from "./money.js";
11
+
12
+ // createGlobalSingleton("myCustomFunction", myCustomFunction);
13
+
14
+ // Imports goes here --End
15
+
16
+ const pp = (globalThis as any).pp;
17
+
18
+ if (document.readyState !== "loading") {
19
+ pp?.mount?.();
20
+ } else {
21
+ document.addEventListener("DOMContentLoaded", () => pp?.mount?.(), {
22
+ once: true,
23
+ });
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-caspian-app",
3
- "version": "0.2.0-beta.45",
3
+ "version": "0.2.0-beta.47",
4
4
  "description": "Scaffold a new Caspian project (FastAPI-powered reactive Python framework).",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- []
@@ -1 +0,0 @@
1
- []