create-caspian-app 0.2.0-beta.32 → 0.2.0-beta.34

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.
@@ -15,7 +15,13 @@
15
15
  - Treat `src/lib/prisma/__init__.py`, `src/lib/prisma/db.py`, `src/lib/prisma/models.py`, and `settings/prisma-schema.json` as generated outputs owned by `npx ppy generate`; do not create or hand-edit them manually.
16
16
  - When `caspian.config.json` has `mcp: true`, treat `src/lib/mcp/mcp_server.py` as the app-owned FastMCP server and `src/lib/mcp/fastmcp.json` as the default MCP config. Use `npm run mcp` or `fastmcp run src/lib/mcp/fastmcp.json`; do not assume root `fastmcp.json` auto-discovery.
17
17
  - Keep auth policy in `src/lib/auth/auth_config.py` and keep auth bootstrap, middleware wiring, and provider registration in `main.py`.
18
+ - In app-owned starter config like this workspace, routes start public because `src/lib/auth/auth_config.py` sets `is_all_routes_private=False` by default.
19
+ - 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`.
20
+ - In all-private mode, keep public exceptions in `public_routes`; the runtime defaults keep `/` public and keep `auth_routes=["/signin", "/signup"]` public.
21
+ - 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.
18
22
  - Use PulsePoint and `pp.rpc(...)` as the default frontend and client-to-server contract unless the user requests another stack.
23
+ - 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.
24
+ - Protect customized `src/lib/auth/auth_config.py` from updater overwrite by adding `./src/lib/auth/auth_config.py` to `excludeFiles` in `caspian.config.json`.
19
25
  - Treat `pp-component` on routes, layouts, and components, and `type="text/pp"` on owned PulsePoint scripts, as compiler-injected by the Python side; do not add them manually in authored templates unless the task is explicitly about runtime internals.
20
26
  - `layout()` is synchronous in the installed runtime. Put async I/O in `page()` or `@rpc()`.
21
27
  - Dynamic route params currently reach `page()` as a single positional `dict`, with query params injected by name and `request` injected by keyword when declared.
@@ -89,6 +95,12 @@
89
95
  - the default FastMCP config lives in `src/lib/mcp/fastmcp.json`
90
96
  - `package.json` starts MCP through `npm run mcp`, which runs `settings/restart-mcp.ts`; manual FastMCP runs should pass the explicit nested config path because root auto-discovery does not find it
91
97
  - auth policy lives in `src/lib/auth/auth_config.py`
98
+ - the app-owned starter config in this workspace begins public-first with `is_all_routes_private=False`, so treat routes as public by default unless the app explicitly switches to all-private mode
99
+ - choose all-private mode in `src/lib/auth/auth_config.py` only when public routes are the minority; otherwise keep mixed mode and maintain `private_routes`
100
+ - `public_routes` is the exception list for all-private apps, while `auth_routes=["/signin", "/signup"]` stays public by default unless the app explicitly needs different auth endpoints
101
+ - `token_auto_refresh` is not the route-privacy switch in the current app; it only matters when `auth.refresh_session()` is called
102
+ - protect customized `src/lib/auth/auth_config.py` by adding `./src/lib/auth/auth_config.py` to `excludeFiles` in `caspian.config.json`
103
+ - prefer logout via `pp.rpc("signout")` plus `@rpc(require_auth=True)` from page or component UI; use a dedicated signout route only for form-post or no-JavaScript fallbacks
92
104
  - PulsePoint runtime lives in `public/js/pp-reactive-v2.js`
93
105
  - `pp-component` is injected by the Python render pipeline, and `main.py` rewrites authored body scripts to `type="text/pp"`; authored route, layout, and component templates should not add those attributes manually
94
106
  - route, layout, and component templates must keep a single top-level lowercase HTML root for `pp-component` injection, with any owned plain `<script>` kept inside that same root
package/dist/AGENTS.md CHANGED
@@ -38,6 +38,12 @@ Before making feature, tooling, or scaffolding decisions, read `caspian.config.j
38
38
  - Effective request order is therefore: `SessionMiddleware -> CSRFMiddleware -> AuthMiddleware -> RPCMiddleware`.
39
39
  - App-level auth policy lives in `src/lib/auth/auth_config.py`.
40
40
  - `main.py` applies auth settings with `configure_auth(build_auth_settings())` and registers `GithubProvider()` plus `GoogleProvider()`.
41
+ - In the app-owned starter config used in this workspace, routes start public because `src/lib/auth/auth_config.py` sets `is_all_routes_private=False` by default.
42
+ - Choose route privacy mode in `src/lib/auth/auth_config.py` at app setup time: use `is_all_routes_private=True` when most routes should require auth, otherwise keep `is_all_routes_private=False` and list only the protected routes in `private_routes`.
43
+ - In all-private mode, treat `public_routes` as the exception list. The runtime defaults keep `/` public and keep `auth_routes=["/signin", "/signup"]` public.
44
+ - `token_auto_refresh` does not make routes private in the current app; it only affects sliding-session refresh if `auth.refresh_session()` is called.
45
+ - 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.
46
+ - 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`.
41
47
  - This workspace already has an app-owned Python database layer in `src/lib/prisma/`.
42
48
  - This workspace now has an app-owned FastMCP server at `src/lib/mcp/mcp_server.py`, with `src/lib/mcp/fastmcp.json` as the default MCP server spec.
43
49
  - `package.json` starts MCP through `npm run mcp`, which runs `settings/restart-mcp.ts` and prefers `src/lib/mcp/fastmcp.json` before legacy root configs. Direct FastMCP runs should pass the explicit nested config path.
@@ -85,6 +91,11 @@ Use this map before making changes.
85
91
  - When `prisma/schema.prisma` changes, run `npx prisma migrate dev` first. If seed flow or `prisma/seed.ts` is involved, run `npx prisma generate` and then `npx prisma db seed`. After that, run `npx ppy generate` so the Python ORM layer and `settings/prisma-schema.json` stay aligned.
86
92
  - Keep auth policy in `src/lib/auth/auth_config.py`.
87
93
  - Keep auth bootstrap, middleware ordering, provider wiring, and router behavior in `main.py`.
94
+ - Treat the app-owned `src/lib/auth/auth_config.py` as the effective default when generating routes for this repo: it starts public-first with `is_all_routes_private=False` unless the app explicitly switches to all-private mode.
95
+ - Decide all-private versus mixed public/private routing in `src/lib/auth/auth_config.py` before creating many routes; use all-private mode only when public routes are the minority.
96
+ - Keep public exceptions in `public_routes`, keep explicit protected routes in `private_routes` when not using all-private mode, and leave default `auth_routes` alone unless the app explicitly needs custom auth endpoints.
97
+ - Do not treat `token_auto_refresh` as the switch for private routes.
98
+ - 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.
88
99
  - When MCP is enabled, keep the app-owned FastMCP server in `src/lib/mcp/mcp_server.py` and the default config in `src/lib/mcp/fastmcp.json`. If those paths move, update `settings/restart-mcp.ts` and the MCP docs together.
89
100
  - Use PulsePoint and `pp.rpc(...)` as the default frontend and browser-to-server contract unless the user explicitly wants another stack.
90
101
  - 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.
@@ -101,6 +112,7 @@ The packaged docs in this workspace are already mostly aligned with the installe
101
112
 
102
113
  - `database.md` is the source doc for the Prisma and Python ORM workflow in this repo: schema changes go through `npx prisma migrate dev`, optional `npx prisma generate` plus `npx prisma db seed`, then `npx ppy generate`, and generated ORM files under `src/lib/prisma/` plus `settings/prisma-schema.json` are not hand-edited.
103
114
  - `mcp.md` is the source doc for the nested FastMCP config path, the app-owned MCP server location, direct FastMCP command usage, and the workspace runner discovery order.
115
+ - `auth.md` is the source doc for auth routing guidance: choose all-private mode only when public routes are the minority, keep auth policy in `src/lib/auth/auth_config.py`, protect that file with `excludeFiles` if customized, prefer auth-protected RPC logout from page or component UI, and keep dedicated signout routes for form-post or no-JavaScript fallbacks.
104
116
  - `state.md` is correct to warn that cross-request persistence depends on `request.state.session`, which is not bridged in the current `main.py`.
105
117
  - `routing.md`, `components.md`, `auth.md`, `fetch-data.md`, `cache.md`, `pulsepoint.md`, and `validation.md` should continue to be validated against the installed `casp` package before any behavior claims are changed.
106
118
 
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.html","not-found.html","error.html"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.html","src/app/index.html"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.html","src/app/index.html","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const n=e.substring(0,s+8).replace(/\\/g,"\\\\"),t=e.replace(new RegExp(`^${n}`),"").replace(/\\/g,"/");let i=`http://localhost/${t}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),a=t.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${a.startsWith("/")?a.substring(1):a}/`}}}async function updatePackageJson(e,s){const n=path.join(e,"package.json");if(checkExcludeFiles(n))return;const t=JSON.parse(fs.readFileSync(n,"utf8"));t.scripts={...t.scripts,projectName:"tsx settings/project-name.ts"};let i=[];s.tailwindcss&&(t.scripts={...t.scripts,tailwind:"postcss src/app/globals.css -o public/css/styles.css --watch","tailwind:build":"postcss src/app/globals.css -o public/css/styles.css"},i.push("tailwind")),s.typescript&&!s.backendOnly&&(t.scripts={...t.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},i.push("ts:watch")),s.mcp&&(t.scripts={...t.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...t.scripts};c.browserSync="tsx settings/bs-config.ts",c["browserSync:build"]="tsx settings/build.ts",c.dev=`npm-run-all projectName -p browserSync ${i.join(" ")}`;let a=["browserSync:build"];s.tailwindcss&&a.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&a.unshift("ts:build"),c.build=`npm-run-all ${a.join(" ")}`,t.scripts=c,t.type="module",fs.writeFileSync(n,JSON.stringify(t,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,n){const t=fs.existsSync(e),i=t&&fs.statSync(e);if(t&&i&&i.isDirectory()){const t=s.toLowerCase();if(!n.mcp&&t.includes("src\\lib\\mcp"))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\ts")||t.includes("\\ts\\")))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\vite-plugins")||t.includes("\\vite-plugins\\")||t.includes("\\vite-plugins")))return;if(n.backendOnly&&t.includes("public\\js")||n.backendOnly&&t.includes("public\\css")||n.backendOnly&&t.includes("public\\assets"))return;const i=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(t=>{copyRecursiveSync(path.join(e,t),path.join(s,t),n)})}else{if(checkExcludeFiles(s))return;if(!n.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")))return;if(!n.mcp&&s.includes("restart-mcp.ts"))return;if(n.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(n.backendOnly&&s.includes("layout.html"))return;if(n.tailwindcss&&s.includes("index.css"))return;if(!n.prisma&&s.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,n){s.forEach(({src:s,dest:t})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,t),n)})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const n=path.join(e,"src","app","layout.html");if(!checkExcludeFiles(n))try{let e=fs.readFileSync(n,"utf8"),t="";s.backendOnly||(s.tailwindcss||(t='\n <link href="/css/index.css" rel="stylesheet" />'),t+='\n <script type="module" src="/js/main.js"><\/script>');let i="";s.backendOnly||(i=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${t}`:t),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(n,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.html:"),e)}}async function createOrUpdateEnvFile(e,s){const n=path.join(e,".env");checkExcludeFiles(n)||fs.writeFileSync(n,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const n=e.replace(/\\/g,"/");return s.endsWith("/"+n)||s===n})}async function createDirectoryStructure(e,s){const n=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/AGENTS.md",dest:"/AGENTS.md"},{src:"/CLAUDE.md",dest:"/CLAUDE.md"}];s.tailwindcss&&n.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),s.typescript&&!s.backendOnly&&n.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const t=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"}];s.typescript&&!s.backendOnly&&t.push({src:"/ts",dest:"/ts"}),n.forEach(({src:s,dest:n})=>{const t=path.join(__dirname,s),i=path.join(e,n);if(checkExcludeFiles(i))return;const c=fs.readFileSync(t,"utf8");fs.writeFileSync(i,c,{flag:"w"})}),await executeCopy(e,t,s),await updatePackageJson(e,s),s.tailwindcss&&modifyPostcssConfig(e),!s.tailwindcss&&s.backendOnly||modifyLayoutPHP(e,s);const i=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins allowed for CORS (comma-separated)\nCORS_ALLOWED_ORIGINS=""\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Session & Security\nSESSION_LIFETIME_HOURS="7"\nMAX_CONTENT_LENGTH_MB="16"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i)}async function getAnswer(e={},s=!1){if(s)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,prisma:e.prisma??!1};if(e.starterKit){const s=e.starterKit;let n=null;if(STARTER_KITS[s]&&(n=STARTER_KITS[s]),n){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:n.features.backendOnly??!1,tailwindcss:n.features.tailwindcss??!1,prisma:n.features.prisma??!1,mcp:n.features.mcp??!1,typescript:n.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(t.backendOnly=!0),i.includes("--tailwindcss")&&(t.tailwindcss=!0),i.includes("--mcp")&&(t.mcp=!0),i.includes("--prisma")&&(t.prisma=!0),i.includes("--typescript")&&(t.typescript=!0),t}if(e.starterKitSource){const n={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,typescript:!1},t=process.argv.slice(2);return t.includes("--backend-only")&&(n.backendOnly=!0),t.includes("--tailwindcss")&&(n.tailwindcss=!0),t.includes("--mcp")&&(n.mcp=!0),t.includes("--prisma")&&(n.prisma=!0),t.includes("--typescript")&&(n.typescript=!0),n}}const n=[];e.projectName||n.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||n.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(n,{onCancel:t}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}));const a=await prompts(c,{onCancel:t});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:a.tailwindcss??e.tailwindcss??!1,typescript:a.typescript??e.typescript??!1,mcp:a.mcp??e.mcp??!1,prisma:a.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,s,n=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm uninstall ${n?"--save-dev":"--save"} ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,n)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>{try{const e=JSON.parse(t);s(e["dist-tags"].latest)}catch(e){n(new Error("Failed to parse JSON response"))}})}).on("error",e=>n(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const n=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),t=s.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!n||!t)return e.localeCompare(s);const i=n.slice(1,4).map(Number),c=t.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>c[e])return 1;if(i[e]<c[e])return-1}const a=n[4]??null,r=t[4]??null;return a&&!r?-1:!a&&r?1:a&&r?a.localeCompare(r):0}function getInstalledPackageInfo(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString(),n=s.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return n?{version:n[1],isLinked:s.includes(`${e}@`)&&s.includes("->")}:(console.error(`Package ${e} is not installed`),{version:null,isLinked:!1})}catch(e){return console.error(e instanceof Error?e.message:String(e)),{version:null,isLinked:!1}}}function isRunningFromNpxCache(e){const s=path.resolve(e).toLowerCase(),n=`${path.sep}_npx${path.sep}`.toLowerCase();return s.includes(n)}async function installNpmDependencies(e,s,n=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),console.log((n?"Installing development dependencies":"Installing dependencies")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm install ${n?"--save-dev":""} ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.2.4","@types/browser-sync":"2.29.1","@types/node":"25.6.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.7","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.10","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.2.4",tsx:"4.21.0",typescript:"6.0.3",vite:"8.0.8","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.18","caspian-utils":"0.0.x"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(s){const n=s;if("win32"===globalThis.process?.platform&&("EPERM"===n.code||"EACCES"===n.code)){try{spawnSync("cmd",["/c","attrib","-R","-H","-S","/S","/D",`${e}\\*`],{stdio:"ignore"})}catch{}return void spawnSync("cmd",["/c","rd","/s","/q",e],{stdio:"ignore"})}throw s}}async function setupStarterKit(e,s){if(!s.starterKit)return;let n=null;if(STARTER_KITS[s.starterKit]?n=STARTER_KITS[s.starterKit]:s.starterKitSource&&(n={id:s.starterKit,name:`Custom Starter Kit (${s.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:s.starterKitSource}}),n){if(console.log(chalk.green(`Setting up ${n.name}...`)),n.source)try{const t=n.source.branch?`git clone -b ${n.source.branch} --depth 1 ${n.source.url} "${e}"`:`git clone --depth 1 ${n.source.url} "${e}"`;execSync(t,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const i=path.join(e,"caspian.config.json");if(fs.existsSync(i))try{const n=JSON.parse(fs.readFileSync(i,"utf8")),t=e,c=bsConfigUrls(t);n.projectName=s.projectName,n.projectRootPath=t,n.bsTarget=c.bsTarget,n.bsPathRewrite=c.bsPathRewrite;const a=await fetchPackageVersion("create-caspian-app");n.version=n.version||a,fs.writeFileSync(i,JSON.stringify(n,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}n.customSetup&&await n.customSetup(e,s),console.log(chalk.green(`āœ“ ${n.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\nšŸš€ Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const n=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");n&&console.log(chalk.magenta(` Features: ${n}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,s,n){const t=spawnSync(e,s,{cwd:n,stdio:"inherit",shell:!1,encoding:"utf8"});if(t.error)throw t.error;if(0!==t.status)throw new Error(`Command failed (${e} ${s.join(" ")}), exit=${t.status}`)}function tryRunCmd(e,s,n){const t=spawnSync(e,s,{cwd:n,stdio:"ignore",shell:!1,encoding:"utf8"});return!t.error&&0===t.status}function tryInstallUv(e){console.log(chalk.blue("uv not found. Attempting to install uv..."));const s=[{cmd:"py",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python3",args:["-m","pip","install","--upgrade","uv"]}];for(const n of s)if(tryRunCmd(n.cmd,n.args,e))return!0;return!1}function resolveUvCommand(e){const s=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const n of s)if(tryRunCmd(n.cmd,[...n.argsPrefix,"--version"],e))return n;if(tryInstallUv(e))for(const n of s)if(tryRunCmd(n.cmd,[...n.argsPrefix,"--version"],e))return n;throw new Error("Could not find or install uv. Install uv and ensure `uv`, `py`, or `python` is available in PATH.")}function buildPythonDependencies(e){const s=["fastapi==0.136.0","uvicorn==0.45.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.14.3","slowapi==0.1.9","python-multipart==0.0.26","starsessions==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.0"];return e.tailwindcss&&s.push("tailwind-merge==0.3.3"),e.mcp&&s.push("fastmcp==3.2.4"),e.prisma&&(s.push("psycopg2-binary==2.9.12"),s.push("asyncpg==0.31.0"),s.push("aiosqlite==0.22.1"),s.push("aiomysql==0.3.2")),s}function ensurePyProjectExists(e){const s=path.join(e,"pyproject.toml");if(!fs.existsSync(s))throw new Error(`pyproject.toml not found at: ${s}`);let n=fs.readFileSync(s,"utf8");n=n.replace(/\r\n/g,"\n"),n.includes("package = false")||(n=n.includes("[tool.uv]")?n.replace("[tool.uv]","[tool.uv]\npackage = false"):`${n.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(s,n,"utf8")}async function ensurePythonVenvAndDeps(e,s){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: syncing dependencies with uv")),console.log(chalk.green("=========================\n")),console.log(chalk.blue("Preparing pyproject.toml...")),ensurePyProjectExists(e);const n=path.join(e,"requirements.txt");fs.existsSync(n)&&(fs.unlinkSync(n),console.log(chalk.gray("Removed legacy requirements.txt")));const t=resolveUvCommand(e),i=path.join(e,".venv");fs.existsSync(i)?console.log(chalk.blue("Existing .venv detected. Reusing it so uv sync can update dependencies without replacing the environment.")):(console.log(chalk.blue("Creating the virtual environment with uv...")),runCmd(t.cmd,[...t.argsPrefix,"venv",".venv"],e));const c=buildPythonDependencies(s);console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(t.cmd,[...t.argsPrefix,"add",...c],e),console.log(chalk.blue("Syncing dependencies...")),runCmd(t.cmd,[...t.argsPrefix,"sync","--exact"],e),console.log(chalk.green("\nāœ“ uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let n=e[0];const t=e.find(e=>e.startsWith("--starter-kit=")),i=t?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),a=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,o=!1;if(n){const t=process.cwd(),c=path.join(t,"caspian.config.json");if(i&&a){o=!0;const t={projectName:n,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(t,s)}else if(fs.existsSync(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const s=path.join(t,e);fs.existsSync(s)&&a.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:n,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:t};const o={projectName:n,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp};r=await getAnswer(o,s),null!==r&&(updateAnswer={projectName:n,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:t})}else{const t={projectName:n,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(t,s)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},s);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync("npm uninstall -g create-caspian-app",{stdio:"inherit"}),execSync("npm install -g create-caspian-app",{stdio:"inherit"})):execSync("npm install -g create-caspian-app",{stdio:"inherit"});const d=process.cwd();let u;if(n)if(o){const s=path.join(d,n);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,r),process.chdir(u);const t=path.join(u,"caspian.config.json");if(fs.existsSync(t)){const s=JSON.parse(fs.readFileSync(t,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),r={...r,backendOnly:s.backendOnly,tailwindcss:s.tailwindcss,typescript:s.typescript,mcp:s.mcp,prisma:s.prisma};let n=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&n.push(s.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:n??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),s=path.join(d,n),t=path.join(s,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(t)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}else fs.mkdirSync(r.projectName,{recursive:!0}),u=path.join(d,r.projectName),process.chdir(r.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("http-proxy-middleware"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python"),npmPkg("caspian-utils")];if(r.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano")),r.prisma&&execSync("npm install -g prisma-client-python@latest",{stdio:"inherit"}),r.typescript&&!r.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),r.starterKit&&!o&&await setupStarterKit(u,r),await installNpmDependencies(u,m,!0),n||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],s=[],n=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));return!!(n.dependencies&&n.dependencies[e]||n.devDependencies&&n.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano"].forEach(s=>{n(s)&&e.push(s)}),s.push("tailwind-merge")}if(r.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(s){console.warn(chalk.yellow(`Failed to delete ${e}: ${s}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),s.push("fastmcp")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(s=>{n(s)&&e.push(s)}),s.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const t=path.join(u,"settings","vite-plugins");fs.existsSync(t)&&(fs.rmSync(t,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(s=>{n(s)&&e.push(s)})}const t=(e=>Array.from(new Set(e)))(e);t.length>0&&(console.log(`Uninstalling npm packages: ${t.join(", ")}`),await uninstallNpmDependencies(u,t,!0)),s.length>0&&console.log(chalk.gray(`Python dependencies will be reconciled by uv sync --exact: ${s.join(", ")}`))}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),n={projectName:r.projectName,projectRootPath:e,bsTarget:s.bsTarget,bsPathRewrite:s.bsPathRewrite,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(n,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,r),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
2
+ import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.html","not-found.html","error.html"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,prisma:!1,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","src/app/layout.html","src/app/index.html"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/app/layout.html","src/app/index.html","public/js/main.js","src/app/globals.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,prisma:!0,mcp:!1},requiredFiles:["main.py","pyproject.toml"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!0},requiredFiles:["main.py",".prettierrc","pyproject.toml","postcss.config.js","src/lib/mcp"]}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return console.error("Invalid PROJECT_ROOT_PATH. The path does not contain \\htdocs\\"),{bsTarget:"",bsPathRewrite:{}};const n=e.substring(0,s+8).replace(/\\/g,"\\\\"),t=e.replace(new RegExp(`^${n}`),"").replace(/\\/g,"/");let i=`http://localhost/${t}`;i=i.endsWith("/")?i.slice(0,-1):i;const c=i.replace(/(?<!:)(\/\/+)/g,"/"),a=t.replace(/\/\/+/g,"/");return{bsTarget:`${c}/`,bsPathRewrite:{"^/":`/${a.startsWith("/")?a.substring(1):a}/`}}}async function updatePackageJson(e,s){const n=path.join(e,"package.json");if(checkExcludeFiles(n))return;const t=JSON.parse(fs.readFileSync(n,"utf8"));t.scripts={...t.scripts,projectName:"tsx settings/project-name.ts"};let i=[];s.tailwindcss&&(t.scripts={...t.scripts,tailwind:"postcss src/app/globals.css -o public/css/styles.css --watch","tailwind:build":"postcss src/app/globals.css -o public/css/styles.css"},i.push("tailwind")),s.typescript&&!s.backendOnly&&(t.scripts={...t.scripts,"ts:watch":"vite build --watch","ts:build":"vite build"},i.push("ts:watch")),s.mcp&&(t.scripts={...t.scripts,mcp:"tsx settings/restart-mcp.ts"},i.push("mcp"));let c={...t.scripts};c.browserSync="tsx settings/bs-config.ts",c["browserSync:build"]="tsx settings/build.ts",c.dev=`npm-run-all projectName -p browserSync ${i.join(" ")}`;let a=["browserSync:build"];s.tailwindcss&&a.unshift("tailwind:build"),s.typescript&&!s.backendOnly&&a.unshift("ts:build"),c.build=`npm-run-all ${a.join(" ")}`,t.scripts=c,t.type="module",fs.writeFileSync(n,JSON.stringify(t,null,2))}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,n){const t=fs.existsSync(e),i=t&&fs.statSync(e);if(t&&i&&i.isDirectory()){const t=s.toLowerCase();if(!n.mcp&&t.includes("src\\lib\\mcp"))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\ts")||t.includes("\\ts\\")))return;if((!n.typescript||n.backendOnly)&&(t.endsWith("\\vite-plugins")||t.includes("\\vite-plugins\\")||t.includes("\\vite-plugins")))return;if(n.backendOnly&&t.includes("public\\js")||n.backendOnly&&t.includes("public\\css")||n.backendOnly&&t.includes("public\\assets"))return;const i=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(i))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach(t=>{copyRecursiveSync(path.join(e,t),path.join(s,t),n)})}else{if(checkExcludeFiles(s))return;if(!n.tailwindcss&&(s.includes("globals.css")||s.includes("styles.css")))return;if(!n.mcp&&s.includes("restart-mcp.ts"))return;if(n.backendOnly&&nonBackendFiles.some(e=>s.includes(e)))return;if(n.backendOnly&&s.includes("layout.html"))return;if(n.tailwindcss&&s.includes("index.css"))return;if(!n.prisma&&s.includes("prisma-schema-config.json"))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,n){s.forEach(({src:s,dest:t})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,t),n)})}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const n=path.join(e,"src","app","layout.html");if(!checkExcludeFiles(n))try{let e=fs.readFileSync(n,"utf8"),t="";s.backendOnly||(s.tailwindcss||(t='\n <link href="/css/index.css" rel="stylesheet" />'),t+='\n <script type="module" src="/js/main.js"><\/script>');let i="";s.backendOnly||(i=s.tailwindcss?` <link href="/css/styles.css" rel="stylesheet" />${t}`:t),e=e.replace("</head>",`${i}\n</head>`),fs.writeFileSync(n,e,{flag:"w"})}catch(e){console.error(chalk.red("Error modifying layout.html:"),e)}}async function createOrUpdateEnvFile(e,s){const n=path.join(e,".env");checkExcludeFiles(n)||fs.writeFileSync(n,s,{flag:"w"})}function checkExcludeFiles(e){if(!updateAnswer?.isUpdate)return!1;const s=e.replace(/\\/g,"/");return!!updateAnswer?.excludeFilePath?.includes(s)||!!updateAnswer?.excludeFiles&&updateAnswer.excludeFiles.some(e=>{const n=e.replace(/\\/g,"/");return s.endsWith("/"+n)||s===n})}async function createDirectoryStructure(e,s){const n=[{src:"/main.py",dest:"/main.py"},{src:"/.prettierrc",dest:"/.prettierrc"},{src:"/pyproject.toml",dest:"/pyproject.toml"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"},{src:"/AGENTS.md",dest:"/AGENTS.md"},{src:"/CLAUDE.md",dest:"/CLAUDE.md"}];s.tailwindcss&&n.push({src:"/postcss.config.js",dest:"/postcss.config.js"}),s.typescript&&!s.backendOnly&&n.push({src:"/vite.config.ts",dest:"/vite.config.ts"});const t=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"},{src:"/public",dest:"/public"},{src:"/.github",dest:"/.github"}];s.typescript&&!s.backendOnly&&t.push({src:"/ts",dest:"/ts"}),n.forEach(({src:s,dest:n})=>{const t=path.join(__dirname,s),i=path.join(e,n);if(checkExcludeFiles(i))return;const c=fs.readFileSync(t,"utf8");fs.writeFileSync(i,c,{flag:"w"})}),await executeCopy(e,t,s),await updatePackageJson(e,s),s.tailwindcss&&modifyPostcssConfig(e),!s.tailwindcss&&s.backendOnly||modifyLayoutPHP(e,s);const i=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${generateAuthSecret()}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins allowed for CORS (comma-separated)\nCORS_ALLOWED_ORIGINS=""\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"\n\n# Session & Security\nSESSION_LIFETIME_HOURS="7"\nMAX_CONTENT_LENGTH_MB="16"\n\n# Rate Limiting\nRATE_LIMIT_DEFAULT="200 per minute"\nRATE_LIMIT_RPC="60 per minute"\nRATE_LIMIT_AUTH="60 per minute"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${i}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,i)}async function getAnswer(e={},s=!1){if(s)return{projectName:e.projectName??"my-app",backendOnly:e.backendOnly??!1,tailwindcss:e.tailwindcss??!1,typescript:e.typescript??!1,mcp:e.mcp??!1,prisma:e.prisma??!1};if(e.starterKit){const s=e.starterKit;let n=null;if(STARTER_KITS[s]&&(n=STARTER_KITS[s]),n){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:n.features.backendOnly??!1,tailwindcss:n.features.tailwindcss??!1,prisma:n.features.prisma??!1,mcp:n.features.mcp??!1,typescript:n.features.typescript??!1},i=process.argv.slice(2);return i.includes("--backend-only")&&(t.backendOnly=!0),i.includes("--tailwindcss")&&(t.tailwindcss=!0),i.includes("--mcp")&&(t.mcp=!0),i.includes("--prisma")&&(t.prisma=!0),i.includes("--typescript")&&(t.typescript=!0),t}if(e.starterKitSource){const n={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,prisma:!0,mcp:!1,typescript:!1},t=process.argv.slice(2);return t.includes("--backend-only")&&(n.backendOnly=!0),t.includes("--tailwindcss")&&(n.tailwindcss=!0),t.includes("--mcp")&&(n.mcp=!0),t.includes("--prisma")&&(n.prisma=!0),t.includes("--typescript")&&(n.typescript=!0),n}}const n=[];e.projectName||n.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||n.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{console.warn(chalk.red("Operation cancelled by the user.")),process.exit(0)},i=await prompts(n,{onCancel:t}),c=[];i.backendOnly??e.backendOnly??!1?(e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.typescript||c.push({type:"toggle",name:"typescript",message:`Would you like to use ${chalk.blue("TypeScript")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||c.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma ORM")}?`,initial:!1,active:"Yes",inactive:"No"}));const a=await prompts(c,{onCancel:t});return{projectName:i.projectName?String(i.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:i.backendOnly??e.backendOnly??!1,tailwindcss:a.tailwindcss??e.tailwindcss??!1,typescript:a.typescript??e.typescript??!1,mcp:a.mcp??e.mcp??!1,prisma:a.prisma??e.prisma??!1}}async function uninstallNpmDependencies(e,s,n=!1){console.log("Uninstalling Node dependencies:"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm uninstall ${n?"--save-dev":"--save"} ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise((s,n)=>{https.get(`https://registry.npmjs.org/${e}`,e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>{try{const e=JSON.parse(t);s(e["dist-tags"].latest)}catch(e){n(new Error("Failed to parse JSON response"))}})}).on("error",e=>n(e))})}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const n=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/),t=s.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);if(!n||!t)return e.localeCompare(s);const i=n.slice(1,4).map(Number),c=t.slice(1,4).map(Number);for(let e=0;e<i.length;e++){if(i[e]>c[e])return 1;if(i[e]<c[e])return-1}const a=n[4]??null,r=t[4]??null;return a&&!r?-1:!a&&r?1:a&&r?a.localeCompare(r):0}function getInstalledPackageInfo(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString(),n=s.match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)`));return n?{version:n[1],isLinked:s.includes(`${e}@`)&&s.includes("->")}:(console.error(`Package ${e} is not installed`),{version:null,isLinked:!1})}catch(e){return console.error(e instanceof Error?e.message:String(e)),{version:null,isLinked:!1}}}function isRunningFromNpxCache(e){const s=path.resolve(e).toLowerCase(),n=`${path.sep}_npx${path.sep}`.toLowerCase();return s.includes(n)}async function installNpmDependencies(e,s,n=!1){fs.existsSync(path.join(e,"package.json"))?console.log("Updating existing Node.js project..."):console.log("Initializing new Node.js project..."),fs.existsSync(path.join(e,"package.json"))||execSync("npm init -y",{stdio:"inherit",cwd:e}),console.log((n?"Installing development dependencies":"Installing dependencies")+":"),s.forEach(e=>console.log(`- ${chalk.blue(e)}`));const t=`npm install ${n?"--save-dev":""} ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}const npmPinnedVersions={"@tailwindcss/postcss":"4.2.4","@types/browser-sync":"2.29.1","@types/node":"25.6.0","@types/prompts":"2.4.9","browser-sync":"3.0.4",chalk:"5.6.2","chokidar-cli":"3.0.0",cssnano:"7.1.7","http-proxy-middleware":"3.0.5","npm-run-all":"4.1.5",postcss:"8.5.10","postcss-cli":"11.0.1",prompts:"2.4.2",tailwindcss:"4.2.4",tsx:"4.21.0",typescript:"6.0.3",vite:"8.0.8","fast-glob":"3.3.3","@lezer/common":"1.5.2","@lezer/python":"1.1.18","caspian-utils":"0.0.x"};function npmPkg(e){return npmPinnedVersions[e]?`${e}@${npmPinnedVersions[e]}`:e}function removeDirectorySafe(e){if(fs.existsSync(e))try{return void fs.rmSync(e,{recursive:!0,force:!0,maxRetries:5,retryDelay:250})}catch(s){const n=s;if("win32"===globalThis.process?.platform&&("EPERM"===n.code||"EACCES"===n.code)){try{spawnSync("cmd",["/c","attrib","-R","-H","-S","/S","/D",`${e}\\*`],{stdio:"ignore"})}catch{}return void spawnSync("cmd",["/c","rd","/s","/q",e],{stdio:"ignore"})}throw s}}async function setupStarterKit(e,s){if(!s.starterKit)return;let n=null;if(STARTER_KITS[s.starterKit]?n=STARTER_KITS[s.starterKit]:s.starterKitSource&&(n={id:s.starterKit,name:`Custom Starter Kit (${s.starterKit})`,description:"Custom starter kit from external source",features:{},requiredFiles:[],source:{type:"git",url:s.starterKitSource}}),n){if(console.log(chalk.green(`Setting up ${n.name}...`)),n.source)try{const t=n.source.branch?`git clone -b ${n.source.branch} --depth 1 ${n.source.url} "${e}"`:`git clone --depth 1 ${n.source.url} "${e}"`;execSync(t,{stdio:"inherit"});removeDirectorySafe(path.join(e,".git")),console.log(chalk.blue("Starter kit cloned successfully!"));const i=path.join(e,"caspian.config.json");if(fs.existsSync(i))try{const n=JSON.parse(fs.readFileSync(i,"utf8")),t=e,c=bsConfigUrls(t);n.projectName=s.projectName,n.projectRootPath=t,n.bsTarget=c.bsTarget,n.bsPathRewrite=c.bsPathRewrite;const a=await fetchPackageVersion("create-caspian-app");n.version=n.version||a,fs.writeFileSync(i,JSON.stringify(n,null,2)),console.log(chalk.green("Updated caspian.config.json with new project details"))}catch(e){console.warn(chalk.yellow("Failed to update caspian.config.json, will create new one"))}}catch(e){throw console.error(chalk.red(`Failed to setup starter kit: ${e}`)),e}n.customSetup&&await n.customSetup(e,s),console.log(chalk.green(`āœ“ ${n.name} setup complete!`))}else console.warn(chalk.yellow(`Starter kit '${s.starterKit}' not found. Skipping...`))}function showStarterKits(){console.log(chalk.blue("\nšŸš€ Available Starter Kits:\n")),Object.values(STARTER_KITS).forEach(e=>{const s=e.source?" (Custom)":" (Built-in)";console.log(chalk.green(` ${e.id}${chalk.gray(s)}`)),console.log(` ${e.name}`),console.log(chalk.gray(` ${e.description}`)),e.source&&console.log(chalk.cyan(` Source: ${e.source.url}`));const n=Object.entries(e.features).filter(([,e])=>!0===e).map(([e])=>e).join(", ");n&&console.log(chalk.magenta(` Features: ${n}`)),console.log()}),console.log(chalk.yellow("Usage:")),console.log(" npx create-caspian-app my-project --starter-kit=basic"),console.log(" npx create-caspian-app my-project --starter-kit=custom --starter-kit-source=https://github.com/user/repo"),console.log()}function runCmd(e,s,n){const t=spawnSync(e,s,{cwd:n,stdio:"inherit",shell:!1,encoding:"utf8"});if(t.error)throw t.error;if(0!==t.status)throw new Error(`Command failed (${e} ${s.join(" ")}), exit=${t.status}`)}function tryRunCmd(e,s,n){const t=spawnSync(e,s,{cwd:n,stdio:"ignore",shell:!1,encoding:"utf8"});return!t.error&&0===t.status}function tryInstallUv(e){console.log(chalk.blue("uv not found. Attempting to install uv..."));const s=[{cmd:"py",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python",args:["-m","pip","install","--upgrade","uv"]},{cmd:"python3",args:["-m","pip","install","--upgrade","uv"]}];for(const n of s)if(tryRunCmd(n.cmd,n.args,e))return!0;return!1}function resolveUvCommand(e){const s=[{cmd:"uv",argsPrefix:[]},{cmd:"py",argsPrefix:["-m","uv"]},{cmd:"python",argsPrefix:["-m","uv"]},{cmd:"python3",argsPrefix:["-m","uv"]}];for(const n of s)if(tryRunCmd(n.cmd,[...n.argsPrefix,"--version"],e))return n;if(tryInstallUv(e))for(const n of s)if(tryRunCmd(n.cmd,[...n.argsPrefix,"--version"],e))return n;throw new Error("Could not find or install uv. Install uv and ensure `uv`, `py`, or `python` is available in PATH.")}function buildPythonDependencies(e){const s=["fastapi==0.136.0","uvicorn==0.45.0","python-dotenv==1.2.2","jinja2==3.1.6","beautifulsoup4==4.14.3","slowapi==0.1.9","python-multipart==0.0.26","starsessions==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 ensurePyProjectExists(e){const s=path.join(e,"pyproject.toml");if(!fs.existsSync(s))throw new Error(`pyproject.toml not found at: ${s}`);let n=fs.readFileSync(s,"utf8");n=n.replace(/\r\n/g,"\n"),n.includes("package = false")||(n=n.includes("[tool.uv]")?n.replace("[tool.uv]","[tool.uv]\npackage = false"):`${n.trimEnd()}\n\n[tool.uv]\npackage = false\n`),fs.writeFileSync(s,n,"utf8")}async function ensurePythonVenvAndDeps(e,s){console.log(chalk.green("\n=========================")),console.log(chalk.green("Python setup: syncing dependencies with uv")),console.log(chalk.green("=========================\n")),console.log(chalk.blue("Preparing pyproject.toml...")),ensurePyProjectExists(e);const n=path.join(e,"requirements.txt");fs.existsSync(n)&&(fs.unlinkSync(n),console.log(chalk.gray("Removed legacy requirements.txt")));const t=resolveUvCommand(e),i=path.join(e,".venv");fs.existsSync(i)?console.log(chalk.blue("Existing .venv detected. Reusing it so uv sync can update dependencies without replacing the environment.")):(console.log(chalk.blue("Creating the virtual environment with uv...")),runCmd(t.cmd,[...t.argsPrefix,"venv",".venv"],e));const c=buildPythonDependencies(s);console.log(chalk.blue("Adding Python dependencies via uv add...")),runCmd(t.cmd,[...t.argsPrefix,"add",...c],e),console.log(chalk.blue("Syncing dependencies...")),runCmd(t.cmd,[...t.argsPrefix,"sync","--exact"],e),console.log(chalk.green("\nāœ“ uv environment ready and dependencies installed.\n"))}async function main(){try{const e=process.argv.slice(2),s=e.includes("-y");let n=e[0];const t=e.find(e=>e.startsWith("--starter-kit=")),i=t?.split("=")[1],c=e.find(e=>e.startsWith("--starter-kit-source=")),a=c?.split("=")[1];if(e.includes("--list-starter-kits"))return void showStarterKits();let r=null,o=!1;if(n){const t=process.cwd(),c=path.join(t,"caspian.config.json");if(i&&a){o=!0;const t={projectName:n,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(t,s)}else if(fs.existsSync(c)){const i=readJsonFile(c);let a=[];i.excludeFiles?.map(e=>{const s=path.join(t,e);fs.existsSync(s)&&a.push(s.replace(/\\/g,"/"))}),updateAnswer={projectName:n,backendOnly:i.backendOnly,tailwindcss:i.tailwindcss,mcp:i.mcp,prisma:i.prisma,typescript:i.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:t};const o={projectName:n,backendOnly:e.includes("--backend-only")||i.backendOnly,tailwindcss:e.includes("--tailwindcss")||i.tailwindcss,typescript:e.includes("--typescript")||i.typescript,prisma:e.includes("--prisma")||i.prisma,mcp:e.includes("--mcp")||i.mcp};r=await getAnswer(o,s),null!==r&&(updateAnswer={projectName:n,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,isUpdate:!0,componentScanDirs:i.componentScanDirs??[],excludeFiles:i.excludeFiles??[],excludeFilePath:a??[],filePath:t})}else{const t={projectName:n,starterKit:i,starterKitSource:a,backendOnly:e.includes("--backend-only"),tailwindcss:e.includes("--tailwindcss"),typescript:e.includes("--typescript"),mcp:e.includes("--mcp"),prisma:e.includes("--prisma")};r=await getAnswer(t,s)}if(null===r)return void console.log(chalk.red("Installation cancelled."))}else r=await getAnswer({},s);if(null===r)return void console.warn(chalk.red("Installation cancelled."));const l=await fetchPackageVersion("create-caspian-app"),p=getInstalledPackageInfo("create-caspian-app");isRunningFromNpxCache(__dirname)?console.log(chalk.gray("Skipping global create-caspian-app update because this command is running from an npx cache package.")):p.isLinked?console.log(chalk.gray("Skipping global create-caspian-app update because the global install is linked.")):p.version?-1===compareVersions(p.version,l)&&(execSync("npm uninstall -g create-caspian-app",{stdio:"inherit"}),execSync("npm install -g create-caspian-app",{stdio:"inherit"})):execSync("npm install -g create-caspian-app",{stdio:"inherit"});const d=process.cwd();let u;if(n)if(o){const s=path.join(d,n);fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,await setupStarterKit(u,r),process.chdir(u);const t=path.join(u,"caspian.config.json");if(fs.existsSync(t)){const s=JSON.parse(fs.readFileSync(t,"utf8"));e.includes("--backend-only")&&(s.backendOnly=!0),e.includes("--tailwindcss")&&(s.tailwindcss=!0),e.includes("--typescript")&&(s.typescript=!0),e.includes("--mcp")&&(s.mcp=!0),e.includes("--prisma")&&(s.prisma=!0),r={...r,backendOnly:s.backendOnly,tailwindcss:s.tailwindcss,typescript:s.typescript,mcp:s.mcp,prisma:s.prisma};let n=[];s.excludeFiles?.map(e=>{const s=path.join(u,e);fs.existsSync(s)&&n.push(s.replace(/\\/g,"/"))}),updateAnswer={...r,isUpdate:!0,componentScanDirs:s.componentScanDirs??[],excludeFiles:s.excludeFiles??[],excludeFilePath:n??[],filePath:u}}}else{const e=path.join(d,"caspian.config.json"),s=path.join(d,n),t=path.join(s,"caspian.config.json");fs.existsSync(e)?u=d:fs.existsSync(s)&&fs.existsSync(t)?(u=s,process.chdir(s)):(fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),u=s,process.chdir(s))}else fs.mkdirSync(r.projectName,{recursive:!0}),u=path.join(d,r.projectName),process.chdir(r.projectName);let m=[npmPkg("typescript"),npmPkg("@types/node"),npmPkg("tsx"),npmPkg("http-proxy-middleware"),npmPkg("chalk"),npmPkg("npm-run-all"),npmPkg("browser-sync"),npmPkg("@types/browser-sync"),npmPkg("@lezer/common"),npmPkg("@lezer/python"),npmPkg("caspian-utils")];if(r.prisma&&m.push(npmPkg("prompts"),npmPkg("@types/prompts")),r.tailwindcss&&m.push(npmPkg("tailwindcss"),npmPkg("postcss"),npmPkg("postcss-cli"),npmPkg("@tailwindcss/postcss"),npmPkg("cssnano")),r.prisma&&execSync("npm install -g prisma-client-python@latest",{stdio:"inherit"}),r.typescript&&!r.backendOnly&&m.push(npmPkg("vite"),npmPkg("fast-glob")),r.starterKit&&!o&&await setupStarterKit(u,r),await installNpmDependencies(u,m,!0),n||execSync("npx tsc --init",{stdio:"inherit"}),await createDirectoryStructure(u,r),r.prisma&&execSync("npx ppy init --caspian",{stdio:"inherit"}),updateAnswer?.isUpdate){const e=[],s=[],n=e=>{try{const s=path.join(u,"package.json");if(fs.existsSync(s)){const n=JSON.parse(fs.readFileSync(s,"utf8"));return!!(n.dependencies&&n.dependencies[e]||n.devDependencies&&n.devDependencies[e])}return!1}catch{return!1}};if(updateAnswer.backendOnly){nonBackendFiles.forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["js","css"].forEach(e=>{const s=path.join(u,"src","app",e);fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log(`${e} was deleted successfully.`))})}if(!updateAnswer.tailwindcss){["postcss.config.js"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});["tailwindcss","postcss","postcss-cli","@tailwindcss/postcss","cssnano"].forEach(s=>{n(s)&&e.push(s)}),s.push("tailwind-merge")}if(r.tailwindcss){const e=path.join(u,"public","css","index.css");if(fs.existsSync(e))try{fs.unlinkSync(e),console.log(`${e} was deleted successfully.`)}catch(s){console.warn(chalk.yellow(`Failed to delete ${e}: ${s}`))}}if(!updateAnswer.mcp){["restart-mcp.ts"].forEach(e=>{const s=path.join(u,"settings",e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const e=path.join(u,"src","lib","mcp");fs.existsSync(e)&&(fs.rmSync(e,{recursive:!0,force:!0}),console.log("MCP folder was deleted successfully.")),s.push("fastmcp")}if(!updateAnswer.prisma){["prisma","@prisma/client","@prisma/internals","better-sqlite3","@prisma/adapter-better-sqlite3","mariadb","@prisma/adapter-mariadb","pg","@prisma/adapter-pg","@types/pg"].forEach(s=>{n(s)&&e.push(s)}),s.push("psycopg2-binary","asyncpg","aiosqlite","aiomysql")}if(!updateAnswer.typescript||updateAnswer.backendOnly){["vite.config.ts"].forEach(e=>{const s=path.join(u,e);fs.existsSync(s)&&(fs.unlinkSync(s),console.log(`${e} was deleted successfully.`))});const s=path.join(u,"ts");fs.existsSync(s)&&(fs.rmSync(s,{recursive:!0,force:!0}),console.log("ts folder was deleted successfully."));const t=path.join(u,"settings","vite-plugins");fs.existsSync(t)&&(fs.rmSync(t,{recursive:!0,force:!0}),console.log("settings/vite-plugins folder was deleted successfully."));["vite","fast-glob"].forEach(s=>{n(s)&&e.push(s)})}const t=(e=>Array.from(new Set(e)))(e);t.length>0&&(console.log(`Uninstalling npm packages: ${t.join(", ")}`),await uninstallNpmDependencies(u,t,!0)),s.length>0&&console.log(chalk.gray(`Python dependencies will be reconciled by uv sync --exact: ${s.join(", ")}`))}if(!o||!fs.existsSync(path.join(u,"caspian.config.json"))){const e=u.replace(/\\/g,"\\"),s=bsConfigUrls(e),n={projectName:r.projectName,projectRootPath:e,bsTarget:s.bsTarget,bsPathRewrite:s.bsPathRewrite,backendOnly:r.backendOnly,tailwindcss:r.tailwindcss,mcp:r.mcp,prisma:r.prisma,typescript:r.typescript,version:l,componentScanDirs:updateAnswer?.componentScanDirs??["src"],excludeFiles:updateAnswer?.excludeFiles??[]};fs.writeFileSync(path.join(u,"caspian.config.json"),JSON.stringify(n,null,2),{flag:"w"})}await ensurePythonVenvAndDeps(u,r),console.log("\n=========================\n"),console.log(`${chalk.green("Success!")} Caspian project successfully created in ${chalk.green(u.replace(/\\/g,"/"))}!`),console.log("\n=========================")}catch(e){console.error("Error while creating the project:",e),process.exit(1)}}main();
@@ -8,16 +8,25 @@ def build_auth_settings() -> AuthSettings:
8
8
 
9
9
  Keep secrets (AUTH_SECRET, AUTH_COOKIE_NAME) in .env.
10
10
  Keep app-level session settings in .env (SESSION_LIFETIME_HOURS, etc).
11
+ Decide route privacy here at app setup time instead of changing Caspian core files.
12
+
13
+ Rule of thumb:
14
+ - If most routes should require auth, set is_all_routes_private=True and list only the public exceptions.
15
+ - If many routes should stay public, keep is_all_routes_private=False and list only the protected routes.
11
16
  """
12
17
 
13
18
  return AuthSettings(
14
19
  # Token settings
15
20
  default_token_validity="1h",
21
+ # Sliding-session refresh only matters when the request flow calls auth.refresh_session().
16
22
  token_auto_refresh=False,
17
23
 
18
24
  # Route protection
25
+ # This app-owned starter config begins public-first; switch to True only when most routes require auth.
26
+ # Use all-private mode when only a few routes should stay public.
19
27
  is_all_routes_private=False,
20
28
  public_routes=["/"],
29
+ # Sign-in and signup stay public by default; only change this when the app explicitly needs it.
21
30
  auth_routes=["/signin", "/signup"],
22
31
  private_routes=[], # unused when all-routes-private is True
23
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-caspian-app",
3
- "version": "0.2.0-beta.32",
3
+ "version": "0.2.0-beta.34",
4
4
  "description": "Scaffold a new Caspian project (FastAPI-powered reactive Python framework).",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",