create-lacspace-app 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +51 -0
  2. package/README.md +67 -0
  3. package/dist/index.js +456 -0
  4. package/package.json +17 -0
package/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence — a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ <div align="center">
2
+
3
+ # create-lacspace-app
4
+
5
+ **Scaffold a beautiful, production-ready Next.js app in one command — pick a template, start gorgeous.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/create-lacspace-app?color=%2316a34a&label=npm)](https://www.npmjs.com/package/create-lacspace-app)
8
+ [![license](https://img.shields.io/npm/l/create-lacspace-app?color=green)](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
9
+
10
+ </div>
11
+
12
+ > `create-next-app` gives you a blank page. This gives you a **finished-looking app** — a polished template, styled with Tailwind, already wired with the Lacspace libraries (SEO metadata + JSON-LD, security headers, sitemap & robots). Choose your kind of site and you're running in seconds.
13
+
14
+ ## Use it
15
+
16
+ ```bash
17
+ npm create lacspace-app@latest my-app
18
+ # or
19
+ npx create-lacspace-app my-app --template business
20
+ ```
21
+
22
+ It asks for a project name and a template, scaffolds the project, installs dependencies and initialises git.
23
+
24
+ ## Templates
25
+
26
+ | Key | What you get |
27
+ | --- | --- |
28
+ | **personal** | A sleek personal / developer portfolio with a projects grid |
29
+ | **business** | A professional company / agency site with services + CTA |
30
+ | **ecommerce** | A modern storefront home with a featured product grid |
31
+ | **saas** | A high-converting SaaS landing page with features + pricing |
32
+
33
+ Every template is a real Next.js 15 App Router + Tailwind v4 project — dark, modern, responsive, with a gradient accent you can theme.
34
+
35
+ ## Batteries already wired
36
+
37
+ - **`lib/site.ts`** — one [`@lacspace/seo`](https://www.npmjs.com/package/@lacspace/seo) `defineSite()` config drives your `<title>`, canonical, Open Graph, Twitter card and JSON-LD
38
+ - **`next.config.mjs`** — hardened security headers via [`@lacspace/headers`](https://www.npmjs.com/package/@lacspace/headers)
39
+ - **`app/robots.txt` + `app/sitemap.xml`** — generated from your site config with [`@lacspace/robots`](https://www.npmjs.com/package/@lacspace/robots) & [`@lacspace/sitemap`](https://www.npmjs.com/package/@lacspace/sitemap)
40
+
41
+ Edit `lib/site.ts` and `app/page.tsx` and you're off.
42
+
43
+ ## Options
44
+
45
+ | Flag | Meaning |
46
+ | --- | --- |
47
+ | `-t, --template <key>` | `personal` \| `business` \| `ecommerce` \| `saas` |
48
+ | `--pm <npm\|pnpm\|yarn\|bun>` | package manager (default npm) |
49
+ | `--no-install` | skip installing dependencies |
50
+ | `--no-git` | skip git init |
51
+ | `-y, --yes` | accept defaults (needs a project name) |
52
+
53
+ ## Licensing
54
+
55
+ Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — MIT-equivalent freedoms. Use it in personal and commercial projects at no cost; just keep the notice. The apps you generate are yours. See the **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
56
+
57
+ ---
58
+
59
+ <div align="center">
60
+
61
+ **Part of the Lacspace ecosystem — zero-dependency, isomorphic TypeScript packages.**
62
+
63
+ [All packages ↗](https://lacspace.com/packages) · [npm org ↗](https://www.npmjs.com/org/lacspace) · [Licence Centre ↗](https://lacspace.com/licenses) · [GitHub ↗](https://github.com/lacspace/npm-packages)
64
+
65
+ </div>
66
+
67
+ <div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · Lacspace Free Licence · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
package/dist/index.js ADDED
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readdirSync, mkdirSync, writeFileSync } from 'fs';
3
+ import { resolve, basename, join, dirname } from 'path';
4
+ import { createInterface } from 'readline/promises';
5
+ import { stdout, exit, argv, stdin, cwd } from 'process';
6
+ import { spawnSync } from 'child_process';
7
+
8
+ var C = {
9
+ reset: "\x1B[0m",
10
+ bold: "\x1B[1m",
11
+ dim: "\x1B[2m",
12
+ green: "\x1B[32m",
13
+ cyan: "\x1B[36m",
14
+ yellow: "\x1B[33m",
15
+ red: "\x1B[31m",
16
+ magenta: "\x1B[35m"
17
+ };
18
+ var c = (k, s) => `${C[k]}${s}${C.reset}`;
19
+ var TEMPLATES = [
20
+ { key: "personal", label: "Personal portfolio", description: "A sleek personal / developer portfolio with projects and contact.", accent: ["#6366f1", "#a855f7"], siteName: "Your Name", siteDescription: "Developer, designer & maker. Selected work and writing." },
21
+ { key: "business", label: "Business site", description: "A professional company / agency site with services and a CTA.", accent: ["#2563eb", "#06b6d4"], siteName: "Acme Studio", siteDescription: "We design and build digital products that grow businesses." },
22
+ { key: "ecommerce", label: "E-commerce storefront", description: "A modern product storefront home with a featured grid.", accent: ["#0d9488", "#84cc16"], siteName: "Acme Store", siteDescription: "Beautiful things, thoughtfully made. Free shipping worldwide." },
23
+ { key: "saas", label: "SaaS landing", description: "A high-converting SaaS landing page with features and pricing.", accent: ["#7c3aed", "#ec4899"], siteName: "Acme Cloud", siteDescription: "The all-in-one platform your team will love. Ship faster." }
24
+ ];
25
+ var pkgJson = (ctx) => JSON.stringify({
26
+ name: ctx.name,
27
+ version: "0.1.0",
28
+ private: true,
29
+ scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" },
30
+ dependencies: {
31
+ next: "^15.1.0",
32
+ react: "^19.0.0",
33
+ "react-dom": "^19.0.0",
34
+ "@lacspace/seo": "^1.5.0",
35
+ "@lacspace/headers": "^1.1.1",
36
+ "@lacspace/robots": "^1.2.0",
37
+ "@lacspace/sitemap": "^1.1.0"
38
+ },
39
+ devDependencies: {
40
+ typescript: "^5.7.0",
41
+ "@types/node": "^22.10.0",
42
+ "@types/react": "^19.0.0",
43
+ "@types/react-dom": "^19.0.0",
44
+ tailwindcss: "^4.0.0",
45
+ "@tailwindcss/postcss": "^4.0.0",
46
+ postcss: "^8.4.49"
47
+ }
48
+ }, null, 2) + "\n";
49
+ var tsconfig = () => JSON.stringify({
50
+ compilerOptions: {
51
+ target: "ES2022",
52
+ lib: ["dom", "dom.iterable", "esnext"],
53
+ allowJs: true,
54
+ skipLibCheck: true,
55
+ strict: true,
56
+ noEmit: true,
57
+ esModuleInterop: true,
58
+ module: "esnext",
59
+ moduleResolution: "bundler",
60
+ resolveJsonModule: true,
61
+ isolatedModules: true,
62
+ jsx: "preserve",
63
+ incremental: true,
64
+ plugins: [{ name: "next" }],
65
+ paths: { "@/*": ["./*"] }
66
+ },
67
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
68
+ exclude: ["node_modules"]
69
+ }, null, 2) + "\n";
70
+ var nextConfig = () => `import { toNextHeaders } from "@lacspace/headers";
71
+
72
+ /** @type {import('next').NextConfig} */
73
+ const nextConfig = {
74
+ async headers() {
75
+ // Hardened security headers (HSTS, CSP, X-Frame-Options, \u2026) from @lacspace/headers
76
+ return toNextHeaders();
77
+ },
78
+ };
79
+
80
+ export default nextConfig;
81
+ `;
82
+ var postcss = () => `export default {
83
+ plugins: { "@tailwindcss/postcss": {} },
84
+ };
85
+ `;
86
+ var gitignore = () => `node_modules
87
+ .next
88
+ out
89
+ .env*
90
+ !.env.example
91
+ *.log
92
+ .DS_Store
93
+ `;
94
+ var globalsCss = (ctx) => `@import "tailwindcss";
95
+
96
+ :root {
97
+ --accent-from: ${ctx.template.accent[0]};
98
+ --accent-to: ${ctx.template.accent[1]};
99
+ }
100
+
101
+ html { scroll-behavior: smooth; }
102
+ body { background: #0a0a0f; color: #e5e7eb; -webkit-font-smoothing: antialiased; }
103
+
104
+ .gradient-text {
105
+ background: linear-gradient(120deg, var(--accent-from), var(--accent-to));
106
+ -webkit-background-clip: text; background-clip: text; color: transparent;
107
+ }
108
+ .gradient-bg { background: linear-gradient(120deg, var(--accent-from), var(--accent-to)); }
109
+ `;
110
+ var siteTs = (ctx) => `import { defineSite } from "@lacspace/seo";
111
+
112
+ /** Your site's SEO configuration \u2014 set once, used everywhere. */
113
+ export const site = defineSite({
114
+ name: ${JSON.stringify(ctx.template.siteName)},
115
+ url: "https://example.com",
116
+ description: ${JSON.stringify(ctx.template.siteDescription)},
117
+ // twitter: "yourhandle",
118
+ // ogImage: "/og",
119
+ });
120
+ `;
121
+ var layout = (ctx) => `import type { Metadata } from "next";
122
+ import { site } from "@/lib/site";
123
+ import "./globals.css";
124
+
125
+ export const metadata: Metadata = site.meta({ title: ${JSON.stringify(ctx.template.siteName)} });
126
+
127
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
128
+ return (
129
+ <html lang="en">
130
+ <body>
131
+ {children}
132
+ <script
133
+ type="application/ld+json"
134
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(site.rootJsonLd()) }}
135
+ />
136
+ </body>
137
+ </html>
138
+ );
139
+ }
140
+ `;
141
+ var robotsTs = () => `import { robotsForSite } from "@lacspace/robots";
142
+ import { site } from "@/lib/site";
143
+
144
+ export function GET() {
145
+ return new Response(robotsForSite(site.config), { headers: { "content-type": "text/plain" } });
146
+ }
147
+ `;
148
+ var sitemapTs = () => `import { sitemapForSite } from "@lacspace/sitemap";
149
+ import { site } from "@/lib/site";
150
+
151
+ export function GET() {
152
+ const xml = sitemapForSite(site.config, ["/", "/about", "/contact"]);
153
+ return new Response(xml, { headers: { "content-type": "application/xml" } });
154
+ }
155
+ `;
156
+ var readme = (ctx) => `# ${ctx.name}
157
+
158
+ A Next.js app scaffolded with [create-lacspace-app](https://www.npmjs.com/package/create-lacspace-app) \u2014 template: **${ctx.template.label}**.
159
+
160
+ Pre-wired with the Lacspace libraries:
161
+ - [\`@lacspace/seo\`](https://www.npmjs.com/package/@lacspace/seo) \u2014 metadata + JSON-LD via \`lib/site.ts\` (edit it once, it flows everywhere)
162
+ - [\`@lacspace/headers\`](https://www.npmjs.com/package/@lacspace/headers) \u2014 security headers in \`next.config.mjs\`
163
+ - [\`@lacspace/robots\`](https://www.npmjs.com/package/@lacspace/robots) + [\`@lacspace/sitemap\`](https://www.npmjs.com/package/@lacspace/sitemap) \u2014 \`app/robots.txt\` & \`app/sitemap.xml\`
164
+
165
+ ## Getting started
166
+
167
+ \`\`\`bash
168
+ npm install
169
+ npm run dev
170
+ \`\`\`
171
+
172
+ Open [http://localhost:3000](http://localhost:3000). Edit \`app/page.tsx\` and \`lib/site.ts\`.
173
+
174
+ Built with [Lacspace](https://lacspace.com/packages).
175
+ `;
176
+ var NAV = (name, links) => `<header className="sticky top-0 z-40 backdrop-blur border-b border-white/10">
177
+ <nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
178
+ <span className="text-lg font-black gradient-text">${name}</span>
179
+ <div className="hidden gap-8 text-sm text-white/60 sm:flex">
180
+ ${links.map((l) => `<a href="#${l.toLowerCase()}" className="hover:text-white">${l}</a>`).join("\n ")}
181
+ </div>
182
+ </nav>
183
+ </header>`;
184
+ var FOOTER = (name) => `<footer className="border-t border-white/10 py-10 text-center text-sm text-white/40">
185
+ \xA9 {new Date().getFullYear()} ${name}. Built with{" "}
186
+ <a href="https://lacspace.com/packages" className="text-white/70 hover:text-white">Lacspace</a>.
187
+ </footer>`;
188
+ function homePage(ctx) {
189
+ const n = ctx.template.siteName;
190
+ const shell = (inner) => `export default function Home() {
191
+ return (
192
+ <main className="min-h-screen">
193
+ ${inner}
194
+ </main>
195
+ );
196
+ }
197
+ `;
198
+ if (ctx.template.key === "personal") {
199
+ return shell(`${NAV(n, ["Work", "About", "Contact"])}
200
+ <section className="mx-auto max-w-3xl px-6 py-28 text-center">
201
+ <p className="mb-4 text-sm font-semibold uppercase tracking-widest text-white/40">Portfolio</p>
202
+ <h1 className="text-5xl font-black leading-tight sm:text-7xl">Hi, I'm <span className="gradient-text">${n}</span>.</h1>
203
+ <p className="mx-auto mt-6 max-w-xl text-lg text-white/60">${ctx.template.siteDescription}</p>
204
+ <div className="mt-10 flex justify-center gap-4">
205
+ <a href="#work" className="gradient-bg rounded-full px-6 py-3 font-semibold text-black">View my work</a>
206
+ <a href="#contact" className="rounded-full border border-white/20 px-6 py-3 font-semibold hover:bg-white/5">Get in touch</a>
207
+ </div>
208
+ </section>
209
+ <section id="work" className="mx-auto max-w-6xl px-6 py-16">
210
+ <h2 className="mb-10 text-3xl font-bold">Selected work</h2>
211
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
212
+ {[1, 2, 3, 4, 5, 6].map((i) => (
213
+ <div key={i} className="group rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition hover:-translate-y-1 hover:border-white/20">
214
+ <div className="mb-4 h-32 rounded-xl gradient-bg opacity-80" />
215
+ <h3 className="font-semibold">Project {i}</h3>
216
+ <p className="mt-1 text-sm text-white/50">A short description of what you built and the impact it had.</p>
217
+ </div>
218
+ ))}
219
+ </div>
220
+ </section>
221
+ ${FOOTER(n)}`);
222
+ }
223
+ if (ctx.template.key === "business") {
224
+ return shell(`${NAV(n, ["Services", "Work", "Contact"])}
225
+ <section className="mx-auto max-w-4xl px-6 py-28 text-center">
226
+ <h1 className="text-5xl font-black leading-tight sm:text-6xl">We build products <span className="gradient-text">that grow businesses</span>.</h1>
227
+ <p className="mx-auto mt-6 max-w-2xl text-lg text-white/60">${ctx.template.siteDescription}</p>
228
+ <a href="#contact" className="mt-10 inline-block gradient-bg rounded-full px-8 py-3 font-semibold text-black">Start a project</a>
229
+ </section>
230
+ <section id="services" className="mx-auto max-w-6xl px-6 py-16">
231
+ <h2 className="mb-10 text-center text-3xl font-bold">What we do</h2>
232
+ <div className="grid gap-6 md:grid-cols-3">
233
+ {[
234
+ { t: "Strategy", d: "Positioning, research and a roadmap that ships." },
235
+ { t: "Design", d: "Brand and product design that people remember." },
236
+ { t: "Engineering", d: "Fast, reliable software built to last." },
237
+ ].map((s) => (
238
+ <div key={s.t} className="rounded-2xl border border-white/10 bg-white/[0.02] p-8">
239
+ <h3 className="text-xl font-bold gradient-text">{s.t}</h3>
240
+ <p className="mt-2 text-white/60">{s.d}</p>
241
+ </div>
242
+ ))}
243
+ </div>
244
+ </section>
245
+ <section id="contact" className="mx-auto max-w-3xl px-6 py-24 text-center">
246
+ <h2 className="text-3xl font-bold">Let's work together</h2>
247
+ <p className="mt-3 text-white/60">Tell us about your project and we'll get back within a day.</p>
248
+ <a href="mailto:hello@example.com" className="mt-8 inline-block rounded-full border border-white/20 px-8 py-3 font-semibold hover:bg-white/5">hello@example.com</a>
249
+ </section>
250
+ ${FOOTER(n)}`);
251
+ }
252
+ if (ctx.template.key === "ecommerce") {
253
+ return shell(`${NAV(n, ["Shop", "About", "Cart"])}
254
+ <section className="mx-auto max-w-5xl px-6 py-24 text-center">
255
+ <h1 className="text-5xl font-black leading-tight sm:text-6xl"><span className="gradient-text">${n}</span></h1>
256
+ <p className="mx-auto mt-6 max-w-xl text-lg text-white/60">${ctx.template.siteDescription}</p>
257
+ <a href="#shop" className="mt-10 inline-block gradient-bg rounded-full px-8 py-3 font-semibold text-black">Shop the collection</a>
258
+ </section>
259
+ <section id="shop" className="mx-auto max-w-6xl px-6 py-16">
260
+ <h2 className="mb-10 text-3xl font-bold">Featured</h2>
261
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
262
+ {[
263
+ { n: "Aurora Lamp", p: "$89" }, { n: "Terra Mug", p: "$24" },
264
+ { n: "Linen Throw", p: "$65" }, { n: "Oak Stand", p: "$120" },
265
+ ].map((prod) => (
266
+ <div key={prod.n} className="group rounded-2xl border border-white/10 bg-white/[0.02] p-4 transition hover:-translate-y-1">
267
+ <div className="mb-4 aspect-square rounded-xl gradient-bg opacity-80" />
268
+ <div className="flex items-center justify-between">
269
+ <h3 className="font-semibold">{prod.n}</h3>
270
+ <span className="text-white/60">{prod.p}</span>
271
+ </div>
272
+ <button className="mt-3 w-full rounded-lg border border-white/15 py-2 text-sm font-medium hover:bg-white/5">Add to cart</button>
273
+ </div>
274
+ ))}
275
+ </div>
276
+ </section>
277
+ ${FOOTER(n)}`);
278
+ }
279
+ return shell(`${NAV(n, ["Features", "Pricing", "Sign in"])}
280
+ <section className="mx-auto max-w-4xl px-6 py-28 text-center">
281
+ <p className="mb-4 inline-block rounded-full border border-white/15 px-4 py-1 text-xs font-semibold text-white/60">New \xB7 v1.0</p>
282
+ <h1 className="text-5xl font-black leading-tight sm:text-6xl">Ship faster with <span className="gradient-text">${n}</span></h1>
283
+ <p className="mx-auto mt-6 max-w-2xl text-lg text-white/60">${ctx.template.siteDescription}</p>
284
+ <div className="mt-10 flex justify-center gap-4">
285
+ <a href="#pricing" className="gradient-bg rounded-full px-8 py-3 font-semibold text-black">Start free</a>
286
+ <a href="#features" className="rounded-full border border-white/20 px-8 py-3 font-semibold hover:bg-white/5">See features</a>
287
+ </div>
288
+ </section>
289
+ <section id="features" className="mx-auto max-w-6xl px-6 py-16">
290
+ <div className="grid gap-6 md:grid-cols-3">
291
+ {[
292
+ { t: "Fast", d: "Built on the edge \u2014 global by default." },
293
+ { t: "Secure", d: "Hardened headers and auth out of the box." },
294
+ { t: "Scalable", d: "From your first user to your millionth." },
295
+ ].map((f) => (
296
+ <div key={f.t} className="rounded-2xl border border-white/10 bg-white/[0.02] p-8">
297
+ <h3 className="text-xl font-bold gradient-text">{f.t}</h3>
298
+ <p className="mt-2 text-white/60">{f.d}</p>
299
+ </div>
300
+ ))}
301
+ </div>
302
+ </section>
303
+ <section id="pricing" className="mx-auto max-w-md px-6 py-24 text-center">
304
+ <div className="rounded-3xl border border-white/10 bg-white/[0.02] p-10">
305
+ <h2 className="text-3xl font-bold">Pro</h2>
306
+ <p className="mt-2 text-5xl font-black gradient-text">$29<span className="text-lg text-white/40">/mo</span></p>
307
+ <a href="#" className="mt-8 inline-block w-full gradient-bg rounded-full px-8 py-3 font-semibold text-black">Get started</a>
308
+ </div>
309
+ </section>
310
+ ${FOOTER(n)}`);
311
+ }
312
+ function buildFiles(ctx) {
313
+ return {
314
+ "package.json": pkgJson(ctx),
315
+ "tsconfig.json": tsconfig(),
316
+ "next.config.mjs": nextConfig(),
317
+ "postcss.config.mjs": postcss(),
318
+ ".gitignore": gitignore(),
319
+ "README.md": readme(ctx),
320
+ "lib/site.ts": siteTs(ctx),
321
+ "app/layout.tsx": layout(ctx),
322
+ "app/globals.css": globalsCss(ctx),
323
+ "app/page.tsx": homePage(ctx),
324
+ "app/robots.txt/route.ts": robotsTs(),
325
+ "app/sitemap.xml/route.ts": sitemapTs()
326
+ };
327
+ }
328
+ function parseArgs(list) {
329
+ const a = { yes: false, install: true, git: true, pm: "npm", help: false };
330
+ for (let i = 0; i < list.length; i++) {
331
+ const arg = list[i];
332
+ const next = () => list[++i] ?? "";
333
+ if (arg === "-t" || arg === "--template") a.template = next();
334
+ else if (arg === "-y" || arg === "--yes") a.yes = true;
335
+ else if (arg === "--no-install") a.install = false;
336
+ else if (arg === "--no-git") a.git = false;
337
+ else if (arg === "--pm") a.pm = next();
338
+ else if (arg === "-h" || arg === "--help") a.help = true;
339
+ else if (arg.startsWith("--template=")) a.template = arg.slice(11);
340
+ else if (!arg.startsWith("-") && !a.name) a.name = arg;
341
+ }
342
+ return a;
343
+ }
344
+ var HELP = `
345
+ ${c("bold", "create-lacspace-app")} \u2014 scaffold a beautiful Next.js app, Lacspace-wired
346
+
347
+ ${c("bold", "Usage")}
348
+ npm create lacspace-app@latest <name> [options]
349
+ npx create-lacspace-app <name> --template <key>
350
+
351
+ ${c("bold", "Templates")}
352
+ ${TEMPLATES.map((t) => ` ${t.key.padEnd(10)} ${t.description}`).join("\n")}
353
+
354
+ ${c("bold", "Options")}
355
+ -t, --template <key> Template (${TEMPLATES.map((t) => t.key).join(" | ")})
356
+ --pm <npm|pnpm|yarn|bun> Package manager (default npm)
357
+ --no-install Skip installing dependencies
358
+ --no-git Skip git init
359
+ -y, --yes Accept defaults (needs <name>)
360
+ -h, --help Show this help
361
+ `;
362
+ async function main() {
363
+ const args = parseArgs(argv.slice(2));
364
+ if (args.help) {
365
+ stdout.write(HELP + "\n");
366
+ return;
367
+ }
368
+ stdout.write(`
369
+ ${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
370
+
371
+ `);
372
+ let name = args.name;
373
+ let templateKey = args.template;
374
+ if (!args.yes) {
375
+ const rl = createInterface({ input: stdin, output: stdout });
376
+ try {
377
+ if (!name) name = (await rl.question(`${c("green", "?")} Project name ${c("dim", "(my-app)")}: `)).trim() || "my-app";
378
+ if (!templateKey) {
379
+ stdout.write(`
380
+ Choose a template:
381
+ `);
382
+ TEMPLATES.forEach((t, i) => stdout.write(` ${c("cyan", String(i + 1))}. ${c("bold", t.label)} ${c("dim", "\u2014 " + t.description)}
383
+ `));
384
+ const ans = (await rl.question(`
385
+ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
386
+ const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
387
+ templateKey = TEMPLATES[idx]?.key ?? "personal";
388
+ }
389
+ } finally {
390
+ rl.close();
391
+ }
392
+ }
393
+ name = name ?? "my-app";
394
+ const template = TEMPLATES.find((t) => t.key === templateKey) ?? TEMPLATES[0];
395
+ const dir = resolve(cwd(), name);
396
+ const projectName = basename(dir).toLowerCase().replace(/[^a-z0-9-_]/g, "-");
397
+ if (existsSync(dir) && readdirSync(dir).length > 0) {
398
+ stdout.write(c("red", `
399
+ \u2717 Directory "${name}" already exists and is not empty.
400
+
401
+ `));
402
+ exit(1);
403
+ return;
404
+ }
405
+ const files = buildFiles({ name: projectName, template });
406
+ for (const [rel, content] of Object.entries(files)) {
407
+ const full = join(dir, rel);
408
+ mkdirSync(dirname(full), { recursive: true });
409
+ writeFileSync(full, content);
410
+ }
411
+ stdout.write(`
412
+ ${c("green", "\u2714")} Created ${c("bold", name)} ${c("dim", `(${template.label})`)}
413
+ `);
414
+ for (const rel of Object.keys(files)) stdout.write(` ${c("dim", "+ " + rel)}
415
+ `);
416
+ if (args.git) {
417
+ const r = spawnSync("git", ["init", "-q"], { cwd: dir, stdio: "ignore" });
418
+ if (r.status === 0) stdout.write(`
419
+ ${c("green", "\u2714")} Initialised git
420
+ `);
421
+ }
422
+ if (args.install) {
423
+ stdout.write(`
424
+ ${c("cyan", "\u25F7")} Installing dependencies with ${args.pm}\u2026
425
+ `);
426
+ const install = args.pm === "yarn" ? [] : ["install"];
427
+ const r = spawnSync(args.pm, install, { cwd: dir, stdio: "inherit" });
428
+ if (r.status !== 0) stdout.write(c("yellow", `
429
+ ! Install failed \u2014 run "${args.pm} install" yourself.
430
+ `));
431
+ }
432
+ const run = args.pm === "npm" ? "npm run dev" : `${args.pm} dev`;
433
+ stdout.write(`
434
+ ${c("bold", "Done! Next steps")}
435
+ `);
436
+ stdout.write(` ${c("cyan", `cd ${name}`)}
437
+ `);
438
+ if (!args.install) stdout.write(` ${c("cyan", `${args.pm} install`)}
439
+ `);
440
+ stdout.write(` ${c("cyan", run)}
441
+ `);
442
+ stdout.write(`
443
+ Edit ${c("cyan", "lib/site.ts")} (your SEO config) and ${c("cyan", "app/page.tsx")}.
444
+ `);
445
+ stdout.write(`
446
+ ${c("green", "Happy building!")} ${c("dim", "https://lacspace.com/packages")}
447
+
448
+ `);
449
+ }
450
+ main().catch((err) => {
451
+ stdout.write(c("red", `
452
+ \u2717 ${err instanceof Error ? err.message : String(err)}
453
+
454
+ `));
455
+ exit(1);
456
+ });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "create-lacspace-app",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — personal portfolio, business site, e-commerce or SaaS landing — pre-wired with SEO metadata + JSON-LD, security headers, sitemap and robots. Like create-next-app, but you start gorgeous.",
5
+ "type": "module",
6
+ "bin": { "create-lacspace-app": "./dist/index.js" },
7
+ "files": ["dist"],
8
+ "scripts": { "build": "tsup", "prepublishOnly": "npm run build" },
9
+ "keywords": ["create-lacspace-app", "nextjs", "next-app", "starter", "template", "scaffold", "boilerplate", "portfolio-template", "ecommerce-template", "saas-template", "cli", "create-next-app-alternative", "typescript"],
10
+ "author": "Lacspace <contact@lacspace.com>",
11
+ "license": "SEE LICENSE IN LICENSE",
12
+ "homepage": "https://lacspace.com/packages",
13
+ "repository": { "type": "git", "url": "git+https://github.com/lacspace/npm-packages.git", "directory": "create-lacspace-app" },
14
+ "bugs": { "url": "https://github.com/lacspace/npm-packages/issues" },
15
+ "engines": { "node": ">=18" },
16
+ "publishConfig": { "access": "public" }
17
+ }