create-loumi-app 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,28 +1,30 @@
1
1
  #!/usr/bin/env node
2
- import {Command}from'commander';import*as o from'@clack/prompts';import p from'chalk';import y from'path';import f from'fs-extra';import h from'ora';import {execa}from'execa';import {fileURLToPath}from'url';import te from'sort-package-json';async function P(e){o.intro(p.bgBlue.white(" create-loumi-app "));let t=await o.text({message:"Projektname?",placeholder:"my-loumi-app",initialValue:e,validate:i=>{if(!i)return "Projektname ist erforderlich";if(!/^[a-z0-9-_]+$/i.test(i))return "Nur Buchstaben, Zahlen, Bindestriche und Unterstriche erlaubt"}});if(o.isCancel(t))return o.cancel("Abgebrochen."),null;let a=await o.multiselect({message:"Welche Add-ons m\xF6chtest du? (Space zum Ausw\xE4hlen)",options:[{value:"convex",label:"Convex",hint:"Backend, DB, Real-time + TanStack Query"},{value:"better-auth",label:"Better Auth",hint:"Authentication (Email/Password, OAuth) via Convex"},{value:"sanity",label:"Sanity",hint:"Headless CMS (Portable Text) + Blog-Routes"},{value:"resend",label:"Resend",hint:"E-Mail Versand (Transactional Emails)"},{value:"seo",label:"Advanced SEO",hint:"JSON-LD, Schema.org, robots.txt, llms.txt"}],required:false});if(o.isCancel(a))return o.cancel("Abgebrochen."),null;let r=await o.confirm({message:"Dependencies installieren?",initialValue:true});if(o.isCancel(r))return o.cancel("Abgebrochen."),null;let n=await o.confirm({message:"Git Repository initialisieren?",initialValue:true});return o.isCancel(n)?(o.cancel("Abgebrochen."),null):{projectName:t,addons:a||[],installDeps:r,initGit:n}}var Z=fileURLToPath(import.meta.url),ee=y.dirname(Z);function T(e){return y.join(ee,"..","template",e)}async function l(e,t){let a=T(e);await f.copy(a,t,{overwrite:true});}async function I(e,t,a){let r=y.join(e,t),n=y.join(e,a);await f.pathExists(r)&&await f.move(r,n,{overwrite:true});}async function b(e,t,a){if(!await f.pathExists(e))return;let n=(await f.readFile(e,"utf-8")).replaceAll(t,a);await f.writeFile(e,n);}async function c(e,t){await f.pathExists(e)?await f.appendFile(e,t):await f.writeFile(e,t);}var N={react:"^19.2.0","react-dom":"^19.2.0","@tanstack/react-router":"^1.158.0","@tanstack/react-start":"^1.158.0",tailwindcss:"^4.1.0","@tailwindcss/vite":"^4.1.0",typescript:"^5.9.0",vite:"^7.3.0","@vitejs/plugin-react":"^5.1.0","@cloudflare/vite-plugin":"^1.23.0",wrangler:"^4.63.0","@cloudflare/workers-types":"^4.20260206.0","@cloudflare/unenv-preset":"^2.12.0",convex:"^1.31.0","@convex-dev/react-query":"^0.1.0","@tanstack/react-query":"^5.90.0","@tanstack/react-router-ssr-query":"^1.158.0","@sanity/client":"^7.14.0","@sanity/image-url":"^2.0.0","@portabletext/react":"^6.0.0","better-auth":"^1.4.18","@convex-dev/better-auth":"^0.10.10",resend:"^6.9.0","@types/react":"^19.2.0","@types/react-dom":"^19.2.0"};function ie(e){return N[e]}function d(e){let t={};for(let a of e)t[a]=ie(a);return t}async function u(e,{dependencies:t={},devDependencies:a={},scripts:r={}}){let n=y.join(e,"package.json"),i=await f.readJson(n);i.dependencies={...i.dependencies,...t},i.devDependencies={...i.devDependencies,...a},i.scripts={...i.scripts,...r};let m=te(JSON.stringify(i,null,2));await f.writeFile(n,m+`
3
- `);}var se=["react","react-dom","@tanstack/react-router","@tanstack/react-start","tailwindcss","@tailwindcss/vite"],oe=["typescript","vite","@vitejs/plugin-react","@cloudflare/vite-plugin","wrangler","@cloudflare/workers-types","@cloudflare/unenv-preset","@types/react","@types/react-dom"];async function U(e,t){await l("base",e),await I(e,"_gitignore",".gitignore");let a={name:t,version:"0.1.0",private:true,type:"module",scripts:{dev:"vite dev",build:"vite build",start:"vite preview",preview:"wrangler dev",deploy:"vite build && wrangler deploy"},dependencies:{},devDependencies:{}};await f.writeJson(y.join(e,"package.json"),a,{spaces:2}),await u(e,{dependencies:d(se),devDependencies:d(oe)}),await b(y.join(e,"wrangler.jsonc"),"PROJECT_NAME",t);}var re=["convex","@convex-dev/react-query","@tanstack/react-query","@tanstack/react-router-ssr-query"],ce=`
2
+ import {Command}from'commander';import*as r from'@clack/prompts';import p from'chalk';import w from'path';import v from'fs-extra';import m from'ora';import {execa}from'execa';import {fileURLToPath}from'url';import ie from'sort-package-json';async function I(e){r.intro(p.bgBlue.white(" create-loumi-app "));let t=await r.text({message:"Projektname?",placeholder:"my-loumi-app",initialValue:e,validate:i=>{if(!i)return "Projektname ist erforderlich";if(!/^[a-z0-9-_]+$/i.test(i))return "Nur Buchstaben, Zahlen, Bindestriche und Unterstriche erlaubt"}});if(r.isCancel(t))return r.cancel("Abgebrochen."),null;let a=await r.multiselect({message:"Welche Add-ons m\xF6chtest du? (Space zum Ausw\xE4hlen)",options:[{value:"convex",label:"Convex",hint:"Backend, DB, Real-time + TanStack Query"},{value:"better-auth",label:"Better Auth",hint:"Authentication (Email/Password, OAuth) via Convex"},{value:"sanity",label:"Sanity",hint:"Headless CMS (Portable Text) + Blog-Routes"},{value:"resend",label:"Resend",hint:"E-Mail Versand (Transactional Emails)"},{value:"seo",label:"Advanced SEO",hint:"JSON-LD, Schema.org, robots.txt, llms.txt"},{value:"cloudflare",label:"Cloudflare Workers",hint:"Deploy auf Cloudflare Workers statt Netlify"}],required:false});if(r.isCancel(a))return r.cancel("Abgebrochen."),null;let o=await r.confirm({message:"Dependencies installieren?",initialValue:true});if(r.isCancel(o))return r.cancel("Abgebrochen."),null;let n=await r.confirm({message:"Git Repository initialisieren?",initialValue:true});return r.isCancel(n)?(r.cancel("Abgebrochen."),null):{projectName:t,addons:a||[],installDeps:o,initGit:n}}var te=fileURLToPath(import.meta.url),ne=w.dirname(te);function A(e){return w.join(ne,"..","template",e)}async function c(e,t){let a=A(e);await v.copy(a,t,{overwrite:true});}async function D(e,t,a){let o=w.join(e,t),n=w.join(e,a);await v.pathExists(o)&&await v.move(o,n,{overwrite:true});}async function y(e,t,a){if(!await v.pathExists(e))return;let n=(await v.readFile(e,"utf-8")).replaceAll(t,a);await v.writeFile(e,n);}async function l(e,t){await v.pathExists(e)?await v.appendFile(e,t):await v.writeFile(e,t);}var O={react:"^19.2.0","react-dom":"^19.2.0","@tanstack/react-router":"^1.158.0","@tanstack/react-start":"^1.158.0",tailwindcss:"^4.1.0","@tailwindcss/vite":"^4.1.0",typescript:"^5.9.0",vite:"^7.3.0","@vitejs/plugin-react":"^5.1.0","@netlify/vite-plugin-tanstack-start":"^1.0.0","@cloudflare/vite-plugin":"^1.23.0",wrangler:"^4.63.0","@cloudflare/workers-types":"^4.20260206.0","@cloudflare/unenv-preset":"^2.12.0",convex:"^1.31.0","@convex-dev/react-query":"^0.1.0","@tanstack/react-query":"^5.90.0","@tanstack/react-router-ssr-query":"^1.158.0","@sanity/client":"^7.14.0","@sanity/image-url":"^2.0.0","@portabletext/react":"^6.0.0","better-auth":"^1.4.18","@convex-dev/better-auth":"^0.10.10",resend:"^6.9.0","@types/react":"^19.2.0","@types/react-dom":"^19.2.0"};function se(e){return O[e]}function d(e){let t={};for(let a of e)t[a]=se(a);return t}async function u(e,{dependencies:t={},devDependencies:a={},scripts:o={}}){let n=w.join(e,"package.json"),i=await v.readJson(n);i.dependencies={...i.dependencies,...t},i.devDependencies={...i.devDependencies,...a},i.scripts={...i.scripts,...o};let g=ie(JSON.stringify(i,null,2));await v.writeFile(n,g+`
3
+ `);}var le=["react","react-dom","@tanstack/react-router","@tanstack/react-start","tailwindcss","@tailwindcss/vite"],ce=["typescript","vite","@vitejs/plugin-react","@netlify/vite-plugin-tanstack-start","@types/react","@types/react-dom"];async function L(e,t){await c("base",e),await D(e,"_gitignore",".gitignore");let a={name:t,version:"0.1.0",private:true,type:"module",scripts:{dev:"vite dev",build:"vite build",start:"vite preview",deploy:"netlify deploy --build --prod"},dependencies:{},devDependencies:{}};await v.writeJson(w.join(e,"package.json"),a,{spaces:2}),await u(e,{dependencies:d(le),devDependencies:d(ce)});}var de=["convex","@convex-dev/react-query","@tanstack/react-query","@tanstack/react-router-ssr-query"],pe=`
4
4
  # Convex
5
5
  # Run "npx convex dev" to generate these values
6
6
  CONVEX_DEPLOYMENT=
7
7
  VITE_CONVEX_URL=
8
- `,le="\n\n## Convex (Backend / Database)\n\n- **Real-time database** with automatic WebSocket subscriptions\n- Convex functions live in `convex/` directory (schema, queries, mutations)\n- Schema defined in `convex/schema.ts` using `defineSchema`, `defineTable`, `v` validators\n- Queries/mutations exported from `convex/*.ts` using `query()` and `mutation()`\n- Client integration via `@convex-dev/react-query` \u2014 bridges Convex into TanStack Query cache\n- Router uses `setupRouterSsrQueryIntegration` from `@tanstack/react-router-ssr-query`\n- SSR data loading: `context.queryClient.ensureQueryData(convexQuery(api.module.fn, args))`\n- Client-side reading: `useSuspenseQuery(convexQuery(api.module.fn, args))`\n- Mutations: `useConvexMutation(api.module.fn)` or `useMutation` from react-query\n- `_generated/` folder is auto-generated \u2014 never edit manually\n- Run `npx convex dev` to start Convex dev server and generate types\n";async function B(e){await l("addons/convex",e),await u(e,{dependencies:d(re)}),await c(y.join(e,".env.local"),ce),await c(y.join(e,"CLAUDE.md"),le);}var pe=["better-auth","@convex-dev/better-auth"],ue=`
8
+ `,ue="\n\n## Convex (Backend / Database)\n\n- **Real-time database** with automatic WebSocket subscriptions\n- Convex functions live in `convex/` directory (schema, queries, mutations)\n- Schema defined in `convex/schema.ts` using `defineSchema`, `defineTable`, `v` validators\n- Queries/mutations exported from `convex/*.ts` using `query()` and `mutation()`\n- Client integration via `@convex-dev/react-query` \u2014 bridges Convex into TanStack Query cache\n- Router uses `setupRouterSsrQueryIntegration` from `@tanstack/react-router-ssr-query`\n- SSR data loading: `context.queryClient.ensureQueryData(convexQuery(api.module.fn, args))`\n- Client-side reading: `useSuspenseQuery(convexQuery(api.module.fn, args))`\n- Mutations: `useConvexMutation(api.module.fn)` or `useMutation` from react-query\n- `_generated/` folder is auto-generated \u2014 never edit manually\n- Run `npx convex dev` to start Convex dev server and generate types\n";async function B(e){await c("addons/convex",e),await u(e,{dependencies:d(de)}),await l(w.join(e,".env.local"),pe),await l(w.join(e,"CLAUDE.md"),ue);}var ge=["better-auth","@convex-dev/better-auth"],fe=`
9
9
  # Better Auth
10
10
  # Set BETTER_AUTH_SECRET on Convex: npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
11
11
  # Set SITE_URL on Convex: npx convex env set SITE_URL=http://localhost:3000
12
12
  VITE_CONVEX_SITE_URL=
13
13
  VITE_SITE_URL=http://localhost:3000
14
- `,me='\n\n## Better Auth (Authentication)\n\n- Uses `@convex-dev/better-auth` \u2014 Better Auth runs INSIDE Convex (single user table, no external DB)\n- Auth tables (user, session, account) managed by the Convex component \u2014 NOT in your schema\n- Server config: `convex/auth.ts` \u2014 `authComponent.adapter(ctx)` bridges Better Auth to Convex DB\n- Client: `src/lib/auth-client.ts` \u2014 `createAuthClient` with `convexClient()` plugin\n- Server utils: `src/lib/auth-server.ts` \u2014 `getToken`, `fetchAuthQuery`, etc.\n- API route: `src/routes/api.auth.$.ts` \u2014 catch-all handler for Better Auth endpoints\n- Root route uses `beforeLoad` to inject `isAuthenticated` + `token` into route context\n- Route protection: `if (!context.isAuthenticated) throw redirect({ to: "/login" })`\n- Client-side auth state: use `Authenticated`, `Unauthenticated`, `useConvexAuth()` from `convex/react`\n- Sign-in/up: `authClient.signIn.email({ email, password })` (client-side only)\n- Sign-out: use `location.reload()` in onSuccess callback (required with `expectAuth: true`)\n- Convex env vars (set via CLI, not .env.local): `BETTER_AUTH_SECRET`, `SITE_URL`\n- Do NOT create a separate user table in your Convex schema \u2014 use triggers for app-specific user data\n';async function j(e,t){t.includes("convex")||console.log(p.yellow(`
14
+ `,ve='\n\n## Better Auth (Authentication)\n\n- Uses `@convex-dev/better-auth` \u2014 Better Auth runs INSIDE Convex (single user table, no external DB)\n- Auth tables (user, session, account) managed by the Convex component \u2014 NOT in your schema\n- Server config: `convex/auth.ts` \u2014 `authComponent.adapter(ctx)` bridges Better Auth to Convex DB\n- Client: `src/lib/auth-client.ts` \u2014 `createAuthClient` with `convexClient()` plugin\n- Server utils: `src/lib/auth-server.ts` \u2014 `getToken`, `fetchAuthQuery`, etc.\n- API route: `src/routes/api.auth.$.ts` \u2014 catch-all handler for Better Auth endpoints\n- Root route uses `beforeLoad` to inject `isAuthenticated` + `token` into route context\n- Route protection: `if (!context.isAuthenticated) throw redirect({ to: "/login" })`\n- Client-side auth state: use `Authenticated`, `Unauthenticated`, `useConvexAuth()` from `convex/react`\n- Sign-in/up: `authClient.signIn.email({ email, password })` (client-side only)\n- Sign-out: use `location.reload()` in onSuccess callback (required with `expectAuth: true`)\n- Convex env vars (set via CLI, not .env.local): `BETTER_AUTH_SECRET`, `SITE_URL`\n- Do NOT create a separate user table in your Convex schema \u2014 use triggers for app-specific user data\n';async function J(e,t){t.includes("convex")||console.log(p.yellow(`
15
15
  \u26A0 Better Auth ben\xF6tigt Convex. Convex wird automatisch mit installiert.
16
- `)),await l("addons/better-auth",e),await u(e,{dependencies:d(pe)}),await c(y.join(e,".env.local"),ue),await c(y.join(e,"CLAUDE.md"),me);}var ge=["@sanity/client","@sanity/image-url","@portabletext/react"],fe=`
16
+ `)),await c("addons/better-auth",e),await u(e,{dependencies:d(ge)}),await l(w.join(e,".env.local"),fe),await l(w.join(e,"CLAUDE.md"),ve);}var he=["@sanity/client","@sanity/image-url","@portabletext/react"],ye=`
17
17
  # Sanity
18
18
  VITE_SANITY_PROJECT_ID=
19
19
  VITE_SANITY_DATASET=production
20
20
  VITE_SANITY_API_VERSION=2025-01-01
21
- `,he='\n\n## Sanity (Headless CMS)\n\n- Client configured in `src/lib/sanity.ts` using `@sanity/client`\n- Content fetched via GROQ queries in route loaders: `sanityClient.fetch(QUERY, params)`\n- Blog routes: `/blog` (layout), `/blog/` (list), `/blog/$slug` (detail)\n- Rich text rendered with `<PortableText value={body} />` from `@portabletext/react`\n- Images: use `urlFor(source).width(x).url()` helper from `src/lib/sanity.ts`\n- Sanity schemas in `sanity/schemaTypes/` \u2014 documents and objects\n- SEO object type available on documents (`metaTitle`, `metaDescription`, `ogImage`)\n- GROQ cheat sheet: `*[_type == "post"]{title, slug}`, `| order(publishedAt desc)`\n- Set `VITE_SANITY_PROJECT_ID` in `.env.local` before running\n';async function J(e){await l("addons/sanity",e),await u(e,{dependencies:d(ge)}),await c(y.join(e,".env.local"),fe),await c(y.join(e,"CLAUDE.md"),he);}var ve=["resend"],ye=`
21
+ `,we='\n\n## Sanity (Headless CMS)\n\n- Client configured in `src/lib/sanity.ts` using `@sanity/client`\n- Content fetched via GROQ queries in route loaders: `sanityClient.fetch(QUERY, params)`\n- Blog routes: `/blog` (layout), `/blog/` (list), `/blog/$slug` (detail)\n- Rich text rendered with `<PortableText value={body} />` from `@portabletext/react`\n- Images: use `urlFor(source).width(x).url()` helper from `src/lib/sanity.ts`\n- Sanity schemas in `sanity/schemaTypes/` \u2014 documents and objects\n- SEO object type available on documents (`metaTitle`, `metaDescription`, `ogImage`)\n- GROQ cheat sheet: `*[_type == "post"]{title, slug}`, `| order(publishedAt desc)`\n- Set `VITE_SANITY_PROJECT_ID` in `.env.local` before running\n';async function W(e){await c("addons/sanity",e),await u(e,{dependencies:d(he)}),await l(w.join(e,".env.local"),ye),await l(w.join(e,"CLAUDE.md"),we);}var xe=["resend"],be=`
22
22
  # Resend
23
23
  RESEND_API_KEY=
24
- `,we="\n\n## Resend (Email)\n\n- Client configured in `src/lib/email.ts` using the `resend` SDK\n- `sendEmail({ to, subject, html })` helper \u2014 call from server-side code only (loaders, API routes)\n- `RESEND_API_KEY` must be set in `.env.local` (not `VITE_` prefixed \u2014 server-only)\n- Default `from` is `onboarding@resend.dev` \u2014 change to your verified domain\n- For React email templates, consider adding `@react-email/components`\n";async function $(e){await l("addons/resend",e),await u(e,{dependencies:d(ve)}),await c(y.join(e,".env.local"),ye),await c(y.join(e,"CLAUDE.md"),we);}var xe='\n\n## Advanced SEO\n\n- `seo()` helper in `src/lib/seo.ts` \u2014 generates meta + links arrays for `head()`\n Usage: `head: () => ({ ...seo({ title: "Page", description: "..." }) })`\n- JSON-LD helpers in `src/lib/schema-org.ts`: `organizationSchema`, `webSiteSchema`, `articleSchema`, `breadcrumbSchema`\n- `public/robots.txt` \u2014 update Sitemap URL before deploying\n- `public/llms.txt` \u2014 LLM-readable site description, update with actual content\n- Always set `title` and `description` on every route via `head()`\n- Use `canonicalUrl` for pages accessible at multiple URLs\n- Use `noIndex: true` for pages that should not be indexed\n';async function G(e,t){await l("addons/seo",e),await b(y.join(e,"public","llms.txt"),"PROJECT_NAME",t),await b(y.join(e,"public","robots.txt"),"PROJECT_DOMAIN",`${t}.workers.dev`),await c(y.join(e,"CLAUDE.md"),xe);}async function M(e){try{return await execa("git",["init"],{cwd:e}),await execa("git",["add","-A"],{cwd:e}),await execa("git",["commit","-m","Initial commit from create-loumi-app"],{cwd:e}),!0}catch{return false}}var be=["convex","better-auth","sanity","resend","seo"];async function Q(e,t){let a=y.join(e,".agents","skills"),r=y.join(e,".claude","skills");await f.ensureDir(a),await f.ensureDir(r);let n=0,i=0,m=["base",...t.filter(w=>be.includes(w))];for(let w of m){let x=T(y.join("skills",w));if(!await f.pathExists(x))continue;let E=await f.readdir(x);for(let s of E)try{let g=y.join(x,s),H=y.join(a,s);await f.copy(g,H,{overwrite:!0});let k=y.join(r,s),K=y.join("..","..",".agents","skills",s);await f.pathExists(k)&&await f.remove(k),await f.symlink(K,k),n++;}catch{i++;}}return {installed:n,failed:i}}async function _(e){let{projectName:t,installDeps:a,initGit:r}=e,n=[...e.addons],i=y.resolve(process.cwd(),t);await f.pathExists(i)&&(await f.readdir(i)).length>0&&(console.log(p.red(`
24
+ `,Ee="\n\n## Resend (Email)\n\n- Client configured in `src/lib/email.ts` using the `resend` SDK\n- `sendEmail({ to, subject, html })` helper \u2014 call from server-side code only (loaders, API routes)\n- `RESEND_API_KEY` must be set in `.env.local` (not `VITE_` prefixed \u2014 server-only)\n- Default `from` is `onboarding@resend.dev` \u2014 change to your verified domain\n- For React email templates, consider adding `@react-email/components`\n";async function q(e){await c("addons/resend",e),await u(e,{dependencies:d(xe)}),await l(w.join(e,".env.local"),be),await l(w.join(e,"CLAUDE.md"),Ee);}var ke='\n\n## Advanced SEO\n\n- `seo()` helper in `src/lib/seo.ts` \u2014 generates meta + links arrays for `head()`\n Usage: `head: () => ({ ...seo({ title: "Page", description: "..." }) })`\n- JSON-LD helpers in `src/lib/schema-org.ts`: `organizationSchema`, `webSiteSchema`, `articleSchema`, `breadcrumbSchema`\n- `public/robots.txt` \u2014 update Sitemap URL before deploying\n- `public/llms.txt` \u2014 LLM-readable site description, update with actual content\n- Always set `title` and `description` on every route via `head()`\n- Use `canonicalUrl` for pages accessible at multiple URLs\n- Use `noIndex: true` for pages that should not be indexed\n';async function G(e,t,a){await c("addons/seo",e),await y(w.join(e,"public","llms.txt"),"PROJECT_NAME",t);let o=a.includes("cloudflare")?`${t}.workers.dev`:`${t}.netlify.app`;await y(w.join(e,"public","robots.txt"),"PROJECT_DOMAIN",o),await l(w.join(e,"CLAUDE.md"),ke);}var Se=["@cloudflare/vite-plugin","wrangler","@cloudflare/workers-types","@cloudflare/unenv-preset"],Ce="\n\n## Cloudflare Workers\n\n- **Hosting:** Cloudflare Workers (Wrangler, `wrangler.jsonc`)\n- `wrangler dev` \u2014 lokaler Preview mit Cloudflare Workers Runtime\n- `wrangler deploy` \u2014 Deploy auf Cloudflare Workers\n- `vite.config.ts` nutzt `@cloudflare/vite-plugin` statt Netlify-Plugin\n- Environment Variables \xFCber Wrangler Dashboard oder `wrangler secret put`\n";async function M(e,t){await c("addons/cloudflare",e);let a=w.join(e,"netlify.toml");await v.pathExists(a)&&await v.remove(a),await u(e,{devDependencies:d(Se)});let o=w.join(e,"package.json"),n=await v.readJson(o);n.scripts.preview="wrangler dev",n.scripts.deploy="vite build && wrangler deploy",delete n.devDependencies["@netlify/vite-plugin-tanstack-start"],await v.writeJson(o,n,{spaces:2});let i=t.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[^a-z0-9-]/gi,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase();await y(w.join(e,"wrangler.jsonc"),"PROJECT_NAME",i),await y(w.join(e,"CLAUDE.md"),"- **Hosting:** Netlify (`netlify.toml`)","- **Hosting:** Cloudflare Workers (`wrangler.jsonc`)"),await y(w.join(e,"CLAUDE.md"),`pnpm start # Preview production build
25
+ pnpm deploy # Build + deploy to Netlify`,`pnpm preview # Preview with Wrangler (local Cloudflare)
26
+ pnpm deploy # Build + deploy to Cloudflare Workers`),await l(w.join(e,"CLAUDE.md"),Ce);}async function Q(e){try{return await execa("git",["init"],{cwd:e}),await execa("git",["add","-A"],{cwd:e}),await execa("git",["commit","-m","Initial commit from create-loumi-app"],{cwd:e}),!0}catch{return false}}var Te=["convex","better-auth","sanity","resend","seo"];async function z(e,t){let a=w.join(e,".agents","skills"),o=w.join(e,".claude","skills");await v.ensureDir(a),await v.ensureDir(o);let n=0,i=0,g=["base",...t.filter(x=>Te.includes(x))];for(let x of g){let b=A(w.join("skills",x));if(!await v.pathExists(b))continue;let k=await v.readdir(b);for(let s of k)try{let f=w.join(b,s),K=w.join(a,s);await v.copy(f,K,{overwrite:!0});let T=w.join(o,s),X=w.join("..","..",".agents","skills",s);await v.pathExists(T)&&await v.remove(T),await v.symlink(X,T),n++;}catch{i++;}}return {installed:n,failed:i}}async function N(e){let{projectName:t,installDeps:a,initGit:o}=e,n=[...e.addons],i=w.resolve(process.cwd(),t);await v.pathExists(i)&&(await v.readdir(i)).length>0&&(console.log(p.red(`
25
27
  Error: Verzeichnis "${t}" ist nicht leer.
26
- `)),process.exit(1)),await f.ensureDir(i),n.includes("better-auth")&&!n.includes("convex")&&n.push("convex");let m=h("Base Template kopieren...").start();if(await U(i,t),m.succeed("Base Template erstellt"),n.includes("convex")){let s=h("Convex einrichten...").start();await B(i),s.succeed("Convex eingerichtet");}if(n.includes("better-auth")){let s=h("Better Auth einrichten...").start();await j(i,n),s.succeed("Better Auth eingerichtet");}if(n.includes("sanity")){let s=h("Sanity einrichten...").start();await J(i),s.succeed("Sanity eingerichtet");}if(n.includes("resend")){let s=h("Resend einrichten...").start();await $(i),s.succeed("Resend eingerichtet");}if(n.includes("seo")){let s=h("Advanced SEO einrichten...").start();await G(i,t),s.succeed("Advanced SEO eingerichtet");}let w=h("Claude Skills installieren...").start(),{installed:x,failed:E}=await Q(i,n);if(E===0?w.succeed(`${x} Claude Skills installiert`):w.warn(`${x} Skills installiert, ${E} fehlgeschlagen`),a){let s=h("Dependencies installieren...").start();try{let g=Se();try{await execa(g,["install"],{cwd:i});}catch{await execa(g,["install","--ignore-scripts"],{cwd:i});}s.succeed(`Dependencies installiert (${g})`);}catch(g){s.fail("Dependencies konnten nicht installiert werden"),g instanceof Error&&console.log(p.yellow(` ${g.message}
28
+ `)),process.exit(1)),await v.ensureDir(i),n.includes("better-auth")&&!n.includes("convex")&&n.push("convex");let g=m("Base Template kopieren...").start();if(await L(i,t),g.succeed("Base Template erstellt"),n.includes("convex")){let s=m("Convex einrichten...").start();await B(i),s.succeed("Convex eingerichtet");}if(n.includes("better-auth")){let s=m("Better Auth einrichten...").start();await J(i,n),s.succeed("Better Auth eingerichtet");}if(n.includes("sanity")){let s=m("Sanity einrichten...").start();await W(i),s.succeed("Sanity eingerichtet");}if(n.includes("resend")){let s=m("Resend einrichten...").start();await q(i),s.succeed("Resend eingerichtet");}if(n.includes("seo")){let s=m("Advanced SEO einrichten...").start();await G(i,t,n),s.succeed("Advanced SEO eingerichtet");}if(n.includes("cloudflare")){let s=m("Cloudflare Workers einrichten...").start();await M(i,t),s.succeed("Cloudflare Workers eingerichtet");}let x=m("Claude Skills installieren...").start(),{installed:b,failed:k}=await z(i,n);if(k===0?x.succeed(`${b} Claude Skills installiert`):x.warn(`${b} Skills installiert, ${k} fehlgeschlagen`),a){let s=m("Dependencies installieren...").start();try{let f=Pe();try{await execa(f,["install"],{cwd:i});}catch{await execa(f,["install","--ignore-scripts"],{cwd:i});}s.succeed(`Dependencies installiert (${f})`);}catch(f){s.fail("Dependencies konnten nicht installiert werden"),f instanceof Error&&console.log(p.yellow(` ${f.message}
27
29
  `)),console.log(p.yellow(` F\xFChre manuell 'npm install' aus.
28
- `));}}if(r){let s=h("Git initialisieren...").start();await M(i)?s.succeed("Git Repository initialisiert"):s.fail("Git konnte nicht initialisiert werden");}console.log(),console.log(p.green("\u2714 Projekt erstellt!")),console.log(),console.log(" N\xE4chste Schritte:"),console.log(p.cyan(` cd ${t}`)),a||console.log(p.cyan(" npm install")),n.includes("convex")&&console.log(p.cyan(" npx convex dev")),console.log(p.cyan(" npm run dev")),console.log(),n.includes("sanity")&&console.log(p.dim(" Vergiss nicht VITE_SANITY_PROJECT_ID in .env.local zu setzen!")),n.includes("better-auth")&&console.log(p.dim(" Better Auth Setup: npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)")),n.includes("resend")&&console.log(p.dim(" Vergiss nicht RESEND_API_KEY in .env.local zu setzen!")),console.log();}function Se(){let e=process.env.npm_config_user_agent;if(e){if(e.startsWith("pnpm"))return "pnpm";if(e.startsWith("yarn"))return "yarn";if(e.startsWith("bun"))return "bun"}return "npm"}var z=new Command;z.name("create-loumi-app").description("Scaffold a new Loumi web project").version("1.0.0").argument("[project-name]","Name of the project").option("--skip-install","Skip dependency installation").option("--add-ons <addons>","Comma-separated list of add-ons (convex,better-auth,sanity,resend,seo)").action(async(e,t)=>{if(e&&t.addOns!==void 0){let r=t.addOns.split(",").map(m=>m.trim().toLowerCase()).filter(Boolean),n=["convex","better-auth","sanity","resend","seo"],i=r.filter(m=>!n.includes(m));i.length>0&&(console.error(`Unknown add-ons: ${i.join(", ")}`),console.error(`Valid add-ons: ${n.join(", ")}`),process.exit(1)),await _({projectName:e,addons:r,installDeps:!t.skipInstall,initGit:true});return}let a=await P(e);a||process.exit(0),await _(a);});z.parse();
30
+ `));}}if(o){let s=m("Git initialisieren...").start();await Q(i)?s.succeed("Git Repository initialisiert"):s.fail("Git konnte nicht initialisiert werden");}console.log(),console.log(p.green("\u2714 Projekt erstellt!")),console.log(),console.log(" N\xE4chste Schritte:"),console.log(p.cyan(` cd ${t}`)),a||console.log(p.cyan(" npm install")),n.includes("convex")&&console.log(p.cyan(" npx convex dev")),console.log(p.cyan(" npm run dev")),console.log(),n.includes("cloudflare")?console.log(p.dim(" Hosting: Cloudflare Workers (wrangler dev / wrangler deploy)")):console.log(p.dim(" Hosting: Netlify (netlify deploy --build --prod)")),n.includes("sanity")&&console.log(p.dim(" Vergiss nicht VITE_SANITY_PROJECT_ID in .env.local zu setzen!")),n.includes("better-auth")&&console.log(p.dim(" Better Auth Setup: npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)")),n.includes("resend")&&console.log(p.dim(" Vergiss nicht RESEND_API_KEY in .env.local zu setzen!")),console.log();}function Pe(){let e=process.env.npm_config_user_agent;if(e){if(e.startsWith("pnpm"))return "pnpm";if(e.startsWith("yarn"))return "yarn";if(e.startsWith("bun"))return "bun"}return "npm"}var Y=new Command;Y.name("create-loumi-app").description("Scaffold a new Loumi web project").version("1.0.0").argument("[project-name]","Name of the project").option("--skip-install","Skip dependency installation").option("--add-ons <addons>","Comma-separated list of add-ons (convex,better-auth,sanity,resend,seo,cloudflare)").action(async(e,t)=>{if(e&&t.addOns!==void 0){let o=t.addOns.split(",").map(g=>g.trim().toLowerCase()).filter(Boolean),n=["convex","better-auth","sanity","resend","seo","cloudflare"],i=o.filter(g=>!n.includes(g));i.length>0&&(console.error(`Unknown add-ons: ${i.join(", ")}`),console.error(`Valid add-ons: ${n.join(", ")}`),process.exit(1)),await N({projectName:e,addons:o,installDeps:!t.skipInstall,initGit:true});return}let a=await I(e);a||process.exit(0),await N(a);});Y.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loumi-app",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "CLI tool to scaffold Loumi web projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from "vite";
2
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
3
+ import { cloudflare } from "@cloudflare/vite-plugin";
4
+ import tailwindcss from "@tailwindcss/vite";
5
+ import viteReact from "@vitejs/plugin-react";
6
+
7
+ export default defineConfig({
8
+ plugins: [
9
+ cloudflare({ inspectorPort: false, viteEnvironment: { name: "ssr" } }),
10
+ tanstackStart(),
11
+ tailwindcss(),
12
+ viteReact(),
13
+ ],
14
+ });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "node_modules/wrangler/config-schema.json",
3
3
  "name": "PROJECT_NAME",
4
- "compatibility_date": "2026-02-06",
4
+ "compatibility_date": "2026-02-05",
5
5
  "compatibility_flags": ["nodejs_compat"],
6
6
  "main": "@tanstack/react-start/server-entry"
7
7
  }
@@ -5,7 +5,7 @@
5
5
  - **Framework:** TanStack Start (React, file-based routing)
6
6
  - **Router:** TanStack Router (`src/routes/` directory, `createFileRoute`)
7
7
  - **Styling:** Tailwind CSS v4 (Vite plugin, `@import "tailwindcss"`, `@theme` for tokens)
8
- - **Hosting:** Cloudflare Workers (Wrangler, `wrangler.jsonc`)
8
+ - **Hosting:** Netlify (`netlify.toml`)
9
9
  - **Build:** Vite (`vite.config.ts`)
10
10
  - **Language:** TypeScript (strict mode)
11
11
 
@@ -36,6 +36,6 @@ src/
36
36
  ```bash
37
37
  pnpm dev # Start dev server (Vite)
38
38
  pnpm build # Production build
39
- pnpm preview # Preview with Wrangler (local Cloudflare)
40
- pnpm deploy # Build + deploy to Cloudflare Workers
39
+ pnpm start # Preview production build
40
+ pnpm deploy # Build + deploy to Netlify
41
41
  ```
@@ -0,0 +1,3 @@
1
+ [build]
2
+ command = "vite build"
3
+ publish = "dist/client"
@@ -1,14 +1,9 @@
1
1
  import { defineConfig } from "vite";
2
2
  import { tanstackStart } from "@tanstack/react-start/plugin/vite";
3
- import { cloudflare } from "@cloudflare/vite-plugin";
4
3
  import tailwindcss from "@tailwindcss/vite";
5
4
  import viteReact from "@vitejs/plugin-react";
5
+ import netlify from "@netlify/vite-plugin-tanstack-start";
6
6
 
7
7
  export default defineConfig({
8
- plugins: [
9
- cloudflare({ viteEnvironment: { name: "ssr" } }),
10
- tanstackStart(),
11
- tailwindcss(),
12
- viteReact(),
13
- ],
8
+ plugins: [tanstackStart(), tailwindcss(), viteReact(), netlify()],
14
9
  });