create-lacspace-app 2.7.0 → 2.9.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/README.md +12 -0
- package/dist/index.js +169 -13
- package/dist/lib.cjs +177 -12
- package/dist/lib.d.cts +30 -1
- package/dist/lib.d.ts +30 -1
- package/dist/lib.js +176 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,18 @@ npx create-lacspace-app my-app --template saas --fullstack
|
|
|
20
20
|
|
|
21
21
|
You choose the *kind* of site you're building. It writes a **real Next.js 15 + React 19 + Tailwind v4 app** — not a hello-world, but a genuinely **polished, modern site**: a fluid `clamp()` type scale, tight display headings, a refined light **and** dark palette, glass chrome, soft layered shadows, a smooth logo marquee, animated counters, scroll reveals and a shimmering primary CTA — every page filled in, an SEO stack wired end-to-end, and a **26-component UI kit** you can drop in anywhere.
|
|
22
22
|
|
|
23
|
+
> **New in v2.9 — guided & discoverable.** Made for newcomers:
|
|
24
|
+
> - **Guided start** — the interactive prompt now offers a **recipe** first ("what are you building?"), so you can ship a whole product without knowing the flags.
|
|
25
|
+
> - **`list`** — `npx create-lacspace-app list` shows every template, add-on and recipe. **`explain <name>`** describes what any add-on or recipe gives you (packages, next steps, docs link).
|
|
26
|
+
> - **`--dry-run`** prints the exact files a command would create without writing anything. `--brand "#hex"` is an alias for `--theme`.
|
|
27
|
+
|
|
28
|
+
> **New in v2.8 — recipes.** Scaffold a whole *kind of product* in one command with `--recipe <key>` (a curated template + full-stack mode + add-on stack):
|
|
29
|
+
> - **`ai-saas`** → SaaS + accounts + payments + AI chat + analytics
|
|
30
|
+
> - **`store`** → e-commerce + eSewa/Khalti checkout + email + analytics
|
|
31
|
+
> - **`blog`** → blog + content + search · **`docs-ai`** → docs + RAG + search · **`internal-tool`** → dashboard + auth + analytics + email
|
|
32
|
+
>
|
|
33
|
+
> `npx create-lacspace-app my-app --recipe ai-saas`. Explicit `--template`/`--with`/`--fullstack` still merge on top. Also on the lib API: `listRecipes()` / `getRecipe()`.
|
|
34
|
+
|
|
23
35
|
> **New in v2.7 — payments & email add-ons.**
|
|
24
36
|
> - **`payments`** — a checkout wired to **eSewa & Khalti**: orders, integer-safe money (`@lacspace/money`), and the signed eSewa flow that **works end-to-end in test mode with NO credentials**. Khalti activates when you add `KHALTI_SECRET`.
|
|
25
37
|
> - **`email`** — transactional email: a ready mail service (`@lacspace/mailer`) with beautiful templates (`@lacspace/email-templates`) + address validation. **Logs emails to the console until you add SMTP** — so it runs out-of-the-box, then delivers for real with one env change.
|
package/dist/index.js
CHANGED
|
@@ -29,14 +29,15 @@ var TEMPLATES = [
|
|
|
29
29
|
{ key: "marketplace", label: "Marketplace / commerce", description: "A real storefront wired to the Lacspace commerce packages \u2014 cart, checkout, tax, shipping, orders, invoices and Nepal payments.", accent: ["#0d9488", "#6366f1"], siteName: "LSBazaar", siteDescription: "A modern storefront \u2014 cart to checkout, wired end to end." }
|
|
30
30
|
];
|
|
31
31
|
function resolveContext(options = {}) {
|
|
32
|
-
const
|
|
32
|
+
const recipe = options.recipe ? RECIPES.find((r) => r.key === String(options.recipe).toLowerCase()) : void 0;
|
|
33
|
+
const base = TEMPLATES.find((t) => t.key === (options.template ?? recipe?.template)) ?? TEMPLATES[0];
|
|
33
34
|
const accent = resolveAccent(options.theme);
|
|
34
35
|
const template = accent ? { ...base, accent } : base;
|
|
35
36
|
const raw = options.name ?? "my-app";
|
|
36
37
|
const seg = raw.split(/[\\/]/).filter(Boolean).pop() ?? "my-app";
|
|
37
38
|
const name = seg.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/^-+|-+$/g, "") || "my-app";
|
|
38
|
-
const features = normalizeFeatures(options.features);
|
|
39
|
-
let mode = options.mode === "dynamic" ? "dynamic" : "static";
|
|
39
|
+
const features = normalizeFeatures([...recipe?.features ?? [], ...options.features ?? []]);
|
|
40
|
+
let mode = options.mode === "dynamic" ? "dynamic" : options.mode === "static" ? "static" : recipe?.mode ?? "static";
|
|
40
41
|
if (mode === "static" && features.some((f) => f.requiresBackend)) mode = "dynamic";
|
|
41
42
|
return { name, template, features, mode };
|
|
42
43
|
}
|
|
@@ -5252,6 +5253,13 @@ var faqSection = (ctx) => {
|
|
|
5252
5253
|
</div>
|
|
5253
5254
|
</section>`;
|
|
5254
5255
|
};
|
|
5256
|
+
var RECIPES = [
|
|
5257
|
+
{ key: "ai-saas", label: "AI SaaS", description: "SaaS landing + accounts + payments + a streaming AI chat + analytics.", template: "saas", mode: "dynamic", features: ["auth-pages", "payments", "ai-chat", "analytics"] },
|
|
5258
|
+
{ key: "store", label: "Online store", description: "E-commerce storefront + eSewa/Khalti checkout + transactional email + analytics.", template: "ecommerce", mode: "dynamic", features: ["payments", "email", "analytics"] },
|
|
5259
|
+
{ key: "blog", label: "Blog", description: "A blog with a Markdown content section, RSS/llms.txt and instant search.", template: "blog", features: ["content", "search"] },
|
|
5260
|
+
{ key: "internal-tool", label: "Internal tool", description: "An admin dashboard with accounts (2FA), analytics and email \u2014 full-stack.", template: "dashboard", mode: "dynamic", features: ["auth-pages", "analytics", "email"] },
|
|
5261
|
+
{ key: "docs-ai", label: "AI docs", description: "A documentation site with chat-with-your-docs RAG and instant search.", template: "docs", features: ["rag", "search"] }
|
|
5262
|
+
];
|
|
5255
5263
|
var contentLib = () => `import fs from "node:fs";
|
|
5256
5264
|
import path from "node:path";
|
|
5257
5265
|
import { parseFrontmatter, markdownToHtml, excerpt } from "@lacspace/markdown";
|
|
@@ -7209,7 +7217,7 @@ function backendFiles(ctx) {
|
|
|
7209
7217
|
}
|
|
7210
7218
|
var splitList = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
7211
7219
|
function parseArgs(list) {
|
|
7212
|
-
const a = { features: [], yes: false, install: true, git: true, pm: "npm", help: false };
|
|
7220
|
+
const a = { features: [], yes: false, install: true, git: true, pm: "npm", help: false, dryRun: false };
|
|
7213
7221
|
for (let i = 0; i < list.length; i++) {
|
|
7214
7222
|
const arg = list[i];
|
|
7215
7223
|
const next = () => list[++i] ?? "";
|
|
@@ -7217,8 +7225,9 @@ function parseArgs(list) {
|
|
|
7217
7225
|
else if (arg === "-y" || arg === "--yes") a.yes = true;
|
|
7218
7226
|
else if (arg === "--no-install") a.install = false;
|
|
7219
7227
|
else if (arg === "--no-git") a.git = false;
|
|
7228
|
+
else if (arg === "--dry-run" || arg === "--dry") a.dryRun = true;
|
|
7220
7229
|
else if (arg === "--pm") a.pm = next();
|
|
7221
|
-
else if (arg === "--theme" || arg === "--accent") a.theme = next();
|
|
7230
|
+
else if (arg === "--theme" || arg === "--accent" || arg === "--brand") a.theme = next();
|
|
7222
7231
|
else if (arg === "--with" || arg === "--features") a.features.push(...splitList(next()));
|
|
7223
7232
|
else if (arg === "--fullstack" || arg === "--full-stack" || arg === "--dynamic") a.mode = "dynamic";
|
|
7224
7233
|
else if (arg === "--static") a.mode = "static";
|
|
@@ -7228,10 +7237,13 @@ function parseArgs(list) {
|
|
|
7228
7237
|
} else if (arg.startsWith("--mode=")) {
|
|
7229
7238
|
const m = arg.slice(7).toLowerCase();
|
|
7230
7239
|
a.mode = m === "dynamic" ? "dynamic" : "static";
|
|
7231
|
-
} else if (arg === "
|
|
7240
|
+
} else if (arg === "--recipe") a.recipe = next();
|
|
7241
|
+
else if (arg.startsWith("--recipe=")) a.recipe = arg.slice(9);
|
|
7242
|
+
else if (arg === "-h" || arg === "--help") a.help = true;
|
|
7232
7243
|
else if (arg.startsWith("--template=")) a.template = arg.slice(11);
|
|
7233
7244
|
else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
|
|
7234
7245
|
else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
|
|
7246
|
+
else if (arg.startsWith("--brand=")) a.theme = arg.slice(8);
|
|
7235
7247
|
else if (arg.startsWith("--with=")) a.features.push(...splitList(arg.slice(7)));
|
|
7236
7248
|
else if (arg.startsWith("--features=")) a.features.push(...splitList(arg.slice(11)));
|
|
7237
7249
|
else if (!arg.startsWith("-") && !a.name) a.name = arg;
|
|
@@ -7322,6 +7334,8 @@ ${c("bold", "Usage")}
|
|
|
7322
7334
|
npm create lacspace-app@latest <name> [options]
|
|
7323
7335
|
npx create-lacspace-app <name> --template <key>
|
|
7324
7336
|
npx create-lacspace-app add <section...> ${c("dim", "# grow an existing app")}
|
|
7337
|
+
npx create-lacspace-app list ${c("dim", "# templates, add-ons & recipes")}
|
|
7338
|
+
npx create-lacspace-app explain <name> ${c("dim", "# what an add-on/recipe gives you")}
|
|
7325
7339
|
|
|
7326
7340
|
${c("bold", "Templates")}
|
|
7327
7341
|
${TEMPLATES.map((t) => ` ${t.key.padEnd(10)} ${t.description}`).join("\n")}
|
|
@@ -7332,17 +7346,23 @@ ${c("bold", "Options")}
|
|
|
7332
7346
|
MongoDB/Redis backend (JWT auth + CRUD) + shared types
|
|
7333
7347
|
${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
|
|
7334
7348
|
--with <a,b> Feature add-ons, comma-separated (alias --features)
|
|
7335
|
-
--
|
|
7349
|
+
--recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
|
|
7350
|
+
--theme <name|hex> Accent: a preset, a "#hex", or "from,to" (alias --brand)
|
|
7336
7351
|
--pm <npm|pnpm|yarn|bun> Package manager (default npm)
|
|
7352
|
+
--dry-run Print the files that would be created; write nothing
|
|
7337
7353
|
--no-install Skip installing dependencies
|
|
7338
7354
|
--no-git Skip git init
|
|
7339
7355
|
-y, --yes Accept defaults (needs <name>)
|
|
7340
7356
|
-h, --help Show this help
|
|
7341
7357
|
|
|
7342
7358
|
${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
|
|
7343
|
-
${FEATURES.map((f) => ` ${f.key.padEnd(
|
|
7359
|
+
${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
|
|
7344
7360
|
${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
|
|
7345
7361
|
|
|
7362
|
+
${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
|
|
7363
|
+
${RECIPES.map((r) => ` ${r.key.padEnd(14)} ${r.description}`).join("\n")}
|
|
7364
|
+
${c("dim", "e.g. npx create-lacspace-app my-app --recipe ai-saas")}
|
|
7365
|
+
|
|
7346
7366
|
${c("bold", "Themes")} ${c("dim", "(--theme)")}
|
|
7347
7367
|
${Object.keys(THEMES).join(" \xB7 ")}
|
|
7348
7368
|
${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
|
|
@@ -7705,12 +7725,103 @@ ${c("bold", "Use them")} \u2014 import into any page:
|
|
|
7705
7725
|
return;
|
|
7706
7726
|
}
|
|
7707
7727
|
}
|
|
7728
|
+
function runList() {
|
|
7729
|
+
stdout.write(`
|
|
7730
|
+
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 what you can build")}
|
|
7731
|
+
`);
|
|
7732
|
+
stdout.write(`
|
|
7733
|
+
${c("bold", "Templates")} ${c("dim", "(-t / --template)")}
|
|
7734
|
+
`);
|
|
7735
|
+
for (const t of TEMPLATES) stdout.write(` ${c("cyan", t.key.padEnd(12))} ${c("dim", t.description)}
|
|
7736
|
+
`);
|
|
7737
|
+
stdout.write(`
|
|
7738
|
+
${c("bold", "Add-ons")} ${c("dim", "(--with) \u2014 free & keyless")}
|
|
7739
|
+
`);
|
|
7740
|
+
for (const f of FEATURES) stdout.write(` ${c("cyan", f.key.padEnd(12))} ${c("dim", f.description)}${f.requiresBackend ? c("green", " [full-stack]") : ""}
|
|
7741
|
+
`);
|
|
7742
|
+
stdout.write(`
|
|
7743
|
+
${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
|
|
7744
|
+
`);
|
|
7745
|
+
for (const r of RECIPES) stdout.write(` ${c("cyan", r.key.padEnd(14))} ${c("dim", r.description)}
|
|
7746
|
+
`);
|
|
7747
|
+
stdout.write(`
|
|
7748
|
+
${c("dim", "Learn one:")} ${c("cyan", "npx create-lacspace-app explain <name>")}
|
|
7749
|
+
|
|
7750
|
+
`);
|
|
7751
|
+
}
|
|
7752
|
+
function runExplain(rawNames) {
|
|
7753
|
+
const name = rawNames.find((n) => !n.startsWith("-"))?.toLowerCase();
|
|
7754
|
+
if (!name) {
|
|
7755
|
+
stdout.write(`Usage: ${c("cyan", "npx create-lacspace-app explain <template|add-on|recipe>")}
|
|
7756
|
+
|
|
7757
|
+
`);
|
|
7758
|
+
runList();
|
|
7759
|
+
return;
|
|
7760
|
+
}
|
|
7761
|
+
const feat = FEATURES.find((f) => f.key === name);
|
|
7762
|
+
const recipe = RECIPES.find((r) => r.key === name);
|
|
7763
|
+
const tmpl = TEMPLATES.find((t) => t.key === name);
|
|
7764
|
+
stdout.write("\n");
|
|
7765
|
+
if (feat) {
|
|
7766
|
+
stdout.write(`${c("bold", c("magenta", "add-on: " + feat.key))} ${feat.requiresBackend ? c("green", "[full-stack]") : c("dim", "[frontend]")}
|
|
7767
|
+
|
|
7768
|
+
${feat.description}
|
|
7769
|
+
`);
|
|
7770
|
+
const deps = { ...feat.deps, ...feat.backend?.({ name: "app", template: TEMPLATES[0], features: [], mode: "dynamic" })?.deps ?? {} };
|
|
7771
|
+
if (Object.keys(deps).length) stdout.write(`
|
|
7772
|
+
${c("bold", "Packages")}: ${Object.keys(deps).map((d) => c("cyan", d)).join(", ")}
|
|
7773
|
+
`);
|
|
7774
|
+
stdout.write(`
|
|
7775
|
+
${c("bold", "Next steps")}:
|
|
7776
|
+
`);
|
|
7777
|
+
for (const s of feat.nextSteps) stdout.write(` ${c("dim", "\u2022")} ${s}
|
|
7778
|
+
`);
|
|
7779
|
+
if (feat.learn) stdout.write(`
|
|
7780
|
+
Learn more \u2192 ${c("cyan", feat.learn)}
|
|
7781
|
+
`);
|
|
7782
|
+
stdout.write(`
|
|
7783
|
+
Add it: ${c("cyan", "npx create-lacspace-app my-app --with " + feat.key)}
|
|
7784
|
+
|
|
7785
|
+
`);
|
|
7786
|
+
} else if (recipe) {
|
|
7787
|
+
stdout.write(`${c("bold", c("magenta", "recipe: " + recipe.key))}
|
|
7788
|
+
|
|
7789
|
+
${recipe.description}
|
|
7790
|
+
|
|
7791
|
+
${c("bold", "Template")}: ${c("cyan", recipe.template)}${recipe.mode ? c("dim", " \xB7 " + recipe.mode) : ""}
|
|
7792
|
+
${c("bold", "Add-ons")}: ${recipe.features.map((f) => c("cyan", f)).join(", ")}
|
|
7793
|
+
|
|
7794
|
+
Use it: ${c("cyan", "npx create-lacspace-app my-app --recipe " + recipe.key)}
|
|
7795
|
+
|
|
7796
|
+
`);
|
|
7797
|
+
} else if (tmpl) {
|
|
7798
|
+
stdout.write(`${c("bold", c("magenta", "template: " + tmpl.key))}
|
|
7799
|
+
|
|
7800
|
+
${tmpl.description}
|
|
7801
|
+
|
|
7802
|
+
Use it: ${c("cyan", "npx create-lacspace-app my-app --template " + tmpl.key)}
|
|
7803
|
+
|
|
7804
|
+
`);
|
|
7805
|
+
} else {
|
|
7806
|
+
stdout.write(c("yellow", `Unknown "${name}".
|
|
7807
|
+
`));
|
|
7808
|
+
runList();
|
|
7809
|
+
}
|
|
7810
|
+
}
|
|
7708
7811
|
async function main() {
|
|
7709
7812
|
const raw = argv.slice(2);
|
|
7710
7813
|
if (raw[0] === "add") {
|
|
7711
7814
|
runAdd(raw.slice(1));
|
|
7712
7815
|
return;
|
|
7713
7816
|
}
|
|
7817
|
+
if (raw[0] === "list" || raw[0] === "ls") {
|
|
7818
|
+
runList();
|
|
7819
|
+
return;
|
|
7820
|
+
}
|
|
7821
|
+
if (raw[0] === "explain" || raw[0] === "info") {
|
|
7822
|
+
runExplain(raw.slice(1));
|
|
7823
|
+
return;
|
|
7824
|
+
}
|
|
7714
7825
|
const args = parseArgs(raw);
|
|
7715
7826
|
if (args.help) {
|
|
7716
7827
|
stdout.write(HELP + "\n");
|
|
@@ -7719,15 +7830,42 @@ async function main() {
|
|
|
7719
7830
|
stdout.write(`
|
|
7720
7831
|
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
|
|
7721
7832
|
|
|
7833
|
+
`);
|
|
7834
|
+
const recipe = args.recipe ? RECIPES.find((r) => r.key === args.recipe.toLowerCase()) : void 0;
|
|
7835
|
+
if (args.recipe && !recipe) stdout.write(c("yellow", ` ! Unknown recipe "${args.recipe}" \u2014 ignoring. Try: ${RECIPES.map((r) => r.key).join(", ")}
|
|
7836
|
+
`));
|
|
7837
|
+
if (recipe) stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", recipe.key)} ${c("dim", "\u2014 " + recipe.description)}
|
|
7722
7838
|
`);
|
|
7723
7839
|
let name = args.name;
|
|
7724
|
-
let templateKey = args.template;
|
|
7725
|
-
let mode = args.mode ?? "static";
|
|
7726
|
-
const featureKeys = [...args.features];
|
|
7840
|
+
let templateKey = args.template ?? recipe?.template;
|
|
7841
|
+
let mode = args.mode ?? recipe?.mode ?? "static";
|
|
7842
|
+
const featureKeys = [...recipe?.features ?? [], ...args.features];
|
|
7843
|
+
let usedRecipe = Boolean(recipe);
|
|
7727
7844
|
if (!args.yes && stdin.isTTY) {
|
|
7728
7845
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
7729
7846
|
try {
|
|
7730
7847
|
if (!name) name = (await rl.question(`${c("green", "?")} Project name ${c("dim", "(my-app)")}: `)).trim() || "my-app";
|
|
7848
|
+
if (!usedRecipe && !templateKey && featureKeys.length === 0) {
|
|
7849
|
+
stdout.write(`
|
|
7850
|
+
Start from a recipe? ${c("dim", "(a template + add-ons, ready to go)")}
|
|
7851
|
+
`);
|
|
7852
|
+
RECIPES.forEach((r, i) => stdout.write(` ${c("cyan", String(i + 1))}. ${c("bold", r.label)} ${c("dim", "\u2014 " + r.description)}
|
|
7853
|
+
`));
|
|
7854
|
+
stdout.write(` ${c("cyan", "0")}. ${c("dim", "No recipe \u2014 I'll choose a template myself")}
|
|
7855
|
+
`);
|
|
7856
|
+
const ans = (await rl.question(`
|
|
7857
|
+
${c("green", "?")} Recipe ${c("dim", "(0)")}: `)).trim() || "0";
|
|
7858
|
+
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : RECIPES.findIndex((r) => r.key === ans);
|
|
7859
|
+
const chosen = ans === "0" ? void 0 : RECIPES[idx];
|
|
7860
|
+
if (chosen) {
|
|
7861
|
+
usedRecipe = true;
|
|
7862
|
+
templateKey = chosen.template;
|
|
7863
|
+
mode = chosen.mode ?? mode;
|
|
7864
|
+
for (const k of chosen.features) if (!featureKeys.includes(k)) featureKeys.push(k);
|
|
7865
|
+
stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", chosen.key)}
|
|
7866
|
+
`);
|
|
7867
|
+
}
|
|
7868
|
+
}
|
|
7731
7869
|
if (!templateKey) {
|
|
7732
7870
|
stdout.write(`
|
|
7733
7871
|
Choose a template:
|
|
@@ -7739,7 +7877,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
|
|
|
7739
7877
|
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
|
|
7740
7878
|
templateKey = TEMPLATES[idx]?.key ?? "personal";
|
|
7741
7879
|
}
|
|
7742
|
-
if (args.mode === void 0) {
|
|
7880
|
+
if (args.mode === void 0 && !usedRecipe) {
|
|
7743
7881
|
stdout.write(`
|
|
7744
7882
|
What kind of app?
|
|
7745
7883
|
`);
|
|
@@ -7792,7 +7930,25 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
|
|
|
7792
7930
|
return;
|
|
7793
7931
|
}
|
|
7794
7932
|
const features = normalizeFeatures(featureKeys);
|
|
7933
|
+
if (mode === "static" && features.some((f) => f.requiresBackend)) {
|
|
7934
|
+
mode = "dynamic";
|
|
7935
|
+
stdout.write(` ${c("dim", "\u2191 a selected add-on needs a backend \u2014 building full-stack")}
|
|
7936
|
+
`);
|
|
7937
|
+
}
|
|
7795
7938
|
const files = buildFiles({ name: projectName, template, features, mode });
|
|
7939
|
+
if (args.dryRun) {
|
|
7940
|
+
const keys = Object.keys(files).sort();
|
|
7941
|
+
stdout.write(`
|
|
7942
|
+
${c("bold", "Dry run")} ${c("dim", `\u2014 ${keys.length} files (${mode}${features.length ? ", +" + features.map((f) => f.key).join(",") : ""}), nothing written`)}
|
|
7943
|
+
`);
|
|
7944
|
+
for (const rel of keys) stdout.write(` ${c("dim", "+ " + rel)}
|
|
7945
|
+
`);
|
|
7946
|
+
stdout.write(`
|
|
7947
|
+
${c("dim", "Remove --dry-run to create the project.")}
|
|
7948
|
+
|
|
7949
|
+
`);
|
|
7950
|
+
return;
|
|
7951
|
+
}
|
|
7796
7952
|
for (const [rel, content] of Object.entries(files)) {
|
|
7797
7953
|
const full = join(dir, rel);
|
|
7798
7954
|
mkdirSync(dirname(full), { recursive: true });
|
|
@@ -7880,4 +8036,4 @@ if (invokedAsCli) {
|
|
|
7880
8036
|
});
|
|
7881
8037
|
}
|
|
7882
8038
|
|
|
7883
|
-
export { FEATURES, SECTIONS, TEMPLATES, applyFeatures, buildFiles, normalizeFeatures, resolveContext };
|
|
8039
|
+
export { FEATURES, RECIPES, SECTIONS, TEMPLATES, applyFeatures, buildFiles, normalizeFeatures, resolveContext };
|
package/dist/lib.cjs
CHANGED
|
@@ -33,14 +33,15 @@ var TEMPLATES = [
|
|
|
33
33
|
{ key: "marketplace", label: "Marketplace / commerce", description: "A real storefront wired to the Lacspace commerce packages \u2014 cart, checkout, tax, shipping, orders, invoices and Nepal payments.", accent: ["#0d9488", "#6366f1"], siteName: "LSBazaar", siteDescription: "A modern storefront \u2014 cart to checkout, wired end to end." }
|
|
34
34
|
];
|
|
35
35
|
function resolveContext(options = {}) {
|
|
36
|
-
const
|
|
36
|
+
const recipe = options.recipe ? RECIPES.find((r) => r.key === String(options.recipe).toLowerCase()) : void 0;
|
|
37
|
+
const base = TEMPLATES.find((t) => t.key === (options.template ?? recipe?.template)) ?? TEMPLATES[0];
|
|
37
38
|
const accent = resolveAccent(options.theme);
|
|
38
39
|
const template = accent ? { ...base, accent } : base;
|
|
39
40
|
const raw = options.name ?? "my-app";
|
|
40
41
|
const seg = raw.split(/[\\/]/).filter(Boolean).pop() ?? "my-app";
|
|
41
42
|
const name = seg.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/^-+|-+$/g, "") || "my-app";
|
|
42
|
-
const features = normalizeFeatures(options.features);
|
|
43
|
-
let mode = options.mode === "dynamic" ? "dynamic" : "static";
|
|
43
|
+
const features = normalizeFeatures([...recipe?.features ?? [], ...options.features ?? []]);
|
|
44
|
+
let mode = options.mode === "dynamic" ? "dynamic" : options.mode === "static" ? "static" : recipe?.mode ?? "static";
|
|
44
45
|
if (mode === "static" && features.some((f) => f.requiresBackend)) mode = "dynamic";
|
|
45
46
|
return { name, template, features, mode };
|
|
46
47
|
}
|
|
@@ -5256,6 +5257,13 @@ var faqSection = (ctx) => {
|
|
|
5256
5257
|
</div>
|
|
5257
5258
|
</section>`;
|
|
5258
5259
|
};
|
|
5260
|
+
var RECIPES = [
|
|
5261
|
+
{ key: "ai-saas", label: "AI SaaS", description: "SaaS landing + accounts + payments + a streaming AI chat + analytics.", template: "saas", mode: "dynamic", features: ["auth-pages", "payments", "ai-chat", "analytics"] },
|
|
5262
|
+
{ key: "store", label: "Online store", description: "E-commerce storefront + eSewa/Khalti checkout + transactional email + analytics.", template: "ecommerce", mode: "dynamic", features: ["payments", "email", "analytics"] },
|
|
5263
|
+
{ key: "blog", label: "Blog", description: "A blog with a Markdown content section, RSS/llms.txt and instant search.", template: "blog", features: ["content", "search"] },
|
|
5264
|
+
{ key: "internal-tool", label: "Internal tool", description: "An admin dashboard with accounts (2FA), analytics and email \u2014 full-stack.", template: "dashboard", mode: "dynamic", features: ["auth-pages", "analytics", "email"] },
|
|
5265
|
+
{ key: "docs-ai", label: "AI docs", description: "A documentation site with chat-with-your-docs RAG and instant search.", template: "docs", features: ["rag", "search"] }
|
|
5266
|
+
];
|
|
5259
5267
|
var contentLib = () => `import fs from "node:fs";
|
|
5260
5268
|
import path from "node:path";
|
|
5261
5269
|
import { parseFrontmatter, markdownToHtml, excerpt } from "@lacspace/markdown";
|
|
@@ -7213,7 +7221,7 @@ function backendFiles(ctx) {
|
|
|
7213
7221
|
}
|
|
7214
7222
|
var splitList = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
7215
7223
|
function parseArgs(list) {
|
|
7216
|
-
const a = { features: [], yes: false, install: true, git: true, pm: "npm", help: false };
|
|
7224
|
+
const a = { features: [], yes: false, install: true, git: true, pm: "npm", help: false, dryRun: false };
|
|
7217
7225
|
for (let i = 0; i < list.length; i++) {
|
|
7218
7226
|
const arg = list[i];
|
|
7219
7227
|
const next = () => list[++i] ?? "";
|
|
@@ -7221,8 +7229,9 @@ function parseArgs(list) {
|
|
|
7221
7229
|
else if (arg === "-y" || arg === "--yes") a.yes = true;
|
|
7222
7230
|
else if (arg === "--no-install") a.install = false;
|
|
7223
7231
|
else if (arg === "--no-git") a.git = false;
|
|
7232
|
+
else if (arg === "--dry-run" || arg === "--dry") a.dryRun = true;
|
|
7224
7233
|
else if (arg === "--pm") a.pm = next();
|
|
7225
|
-
else if (arg === "--theme" || arg === "--accent") a.theme = next();
|
|
7234
|
+
else if (arg === "--theme" || arg === "--accent" || arg === "--brand") a.theme = next();
|
|
7226
7235
|
else if (arg === "--with" || arg === "--features") a.features.push(...splitList(next()));
|
|
7227
7236
|
else if (arg === "--fullstack" || arg === "--full-stack" || arg === "--dynamic") a.mode = "dynamic";
|
|
7228
7237
|
else if (arg === "--static") a.mode = "static";
|
|
@@ -7232,10 +7241,13 @@ function parseArgs(list) {
|
|
|
7232
7241
|
} else if (arg.startsWith("--mode=")) {
|
|
7233
7242
|
const m = arg.slice(7).toLowerCase();
|
|
7234
7243
|
a.mode = m === "dynamic" ? "dynamic" : "static";
|
|
7235
|
-
} else if (arg === "
|
|
7244
|
+
} else if (arg === "--recipe") a.recipe = next();
|
|
7245
|
+
else if (arg.startsWith("--recipe=")) a.recipe = arg.slice(9);
|
|
7246
|
+
else if (arg === "-h" || arg === "--help") a.help = true;
|
|
7236
7247
|
else if (arg.startsWith("--template=")) a.template = arg.slice(11);
|
|
7237
7248
|
else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
|
|
7238
7249
|
else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
|
|
7250
|
+
else if (arg.startsWith("--brand=")) a.theme = arg.slice(8);
|
|
7239
7251
|
else if (arg.startsWith("--with=")) a.features.push(...splitList(arg.slice(7)));
|
|
7240
7252
|
else if (arg.startsWith("--features=")) a.features.push(...splitList(arg.slice(11)));
|
|
7241
7253
|
else if (!arg.startsWith("-") && !a.name) a.name = arg;
|
|
@@ -7326,6 +7338,8 @@ ${c("bold", "Usage")}
|
|
|
7326
7338
|
npm create lacspace-app@latest <name> [options]
|
|
7327
7339
|
npx create-lacspace-app <name> --template <key>
|
|
7328
7340
|
npx create-lacspace-app add <section...> ${c("dim", "# grow an existing app")}
|
|
7341
|
+
npx create-lacspace-app list ${c("dim", "# templates, add-ons & recipes")}
|
|
7342
|
+
npx create-lacspace-app explain <name> ${c("dim", "# what an add-on/recipe gives you")}
|
|
7329
7343
|
|
|
7330
7344
|
${c("bold", "Templates")}
|
|
7331
7345
|
${TEMPLATES.map((t) => ` ${t.key.padEnd(10)} ${t.description}`).join("\n")}
|
|
@@ -7336,17 +7350,23 @@ ${c("bold", "Options")}
|
|
|
7336
7350
|
MongoDB/Redis backend (JWT auth + CRUD) + shared types
|
|
7337
7351
|
${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
|
|
7338
7352
|
--with <a,b> Feature add-ons, comma-separated (alias --features)
|
|
7339
|
-
--
|
|
7353
|
+
--recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
|
|
7354
|
+
--theme <name|hex> Accent: a preset, a "#hex", or "from,to" (alias --brand)
|
|
7340
7355
|
--pm <npm|pnpm|yarn|bun> Package manager (default npm)
|
|
7356
|
+
--dry-run Print the files that would be created; write nothing
|
|
7341
7357
|
--no-install Skip installing dependencies
|
|
7342
7358
|
--no-git Skip git init
|
|
7343
7359
|
-y, --yes Accept defaults (needs <name>)
|
|
7344
7360
|
-h, --help Show this help
|
|
7345
7361
|
|
|
7346
7362
|
${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
|
|
7347
|
-
${FEATURES.map((f) => ` ${f.key.padEnd(
|
|
7363
|
+
${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
|
|
7348
7364
|
${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
|
|
7349
7365
|
|
|
7366
|
+
${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
|
|
7367
|
+
${RECIPES.map((r) => ` ${r.key.padEnd(14)} ${r.description}`).join("\n")}
|
|
7368
|
+
${c("dim", "e.g. npx create-lacspace-app my-app --recipe ai-saas")}
|
|
7369
|
+
|
|
7350
7370
|
${c("bold", "Themes")} ${c("dim", "(--theme)")}
|
|
7351
7371
|
${Object.keys(THEMES).join(" \xB7 ")}
|
|
7352
7372
|
${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
|
|
@@ -7709,12 +7729,103 @@ ${c("bold", "Use them")} \u2014 import into any page:
|
|
|
7709
7729
|
return;
|
|
7710
7730
|
}
|
|
7711
7731
|
}
|
|
7732
|
+
function runList() {
|
|
7733
|
+
process$1.stdout.write(`
|
|
7734
|
+
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 what you can build")}
|
|
7735
|
+
`);
|
|
7736
|
+
process$1.stdout.write(`
|
|
7737
|
+
${c("bold", "Templates")} ${c("dim", "(-t / --template)")}
|
|
7738
|
+
`);
|
|
7739
|
+
for (const t of TEMPLATES) process$1.stdout.write(` ${c("cyan", t.key.padEnd(12))} ${c("dim", t.description)}
|
|
7740
|
+
`);
|
|
7741
|
+
process$1.stdout.write(`
|
|
7742
|
+
${c("bold", "Add-ons")} ${c("dim", "(--with) \u2014 free & keyless")}
|
|
7743
|
+
`);
|
|
7744
|
+
for (const f of FEATURES) process$1.stdout.write(` ${c("cyan", f.key.padEnd(12))} ${c("dim", f.description)}${f.requiresBackend ? c("green", " [full-stack]") : ""}
|
|
7745
|
+
`);
|
|
7746
|
+
process$1.stdout.write(`
|
|
7747
|
+
${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
|
|
7748
|
+
`);
|
|
7749
|
+
for (const r of RECIPES) process$1.stdout.write(` ${c("cyan", r.key.padEnd(14))} ${c("dim", r.description)}
|
|
7750
|
+
`);
|
|
7751
|
+
process$1.stdout.write(`
|
|
7752
|
+
${c("dim", "Learn one:")} ${c("cyan", "npx create-lacspace-app explain <name>")}
|
|
7753
|
+
|
|
7754
|
+
`);
|
|
7755
|
+
}
|
|
7756
|
+
function runExplain(rawNames) {
|
|
7757
|
+
const name = rawNames.find((n) => !n.startsWith("-"))?.toLowerCase();
|
|
7758
|
+
if (!name) {
|
|
7759
|
+
process$1.stdout.write(`Usage: ${c("cyan", "npx create-lacspace-app explain <template|add-on|recipe>")}
|
|
7760
|
+
|
|
7761
|
+
`);
|
|
7762
|
+
runList();
|
|
7763
|
+
return;
|
|
7764
|
+
}
|
|
7765
|
+
const feat = FEATURES.find((f) => f.key === name);
|
|
7766
|
+
const recipe = RECIPES.find((r) => r.key === name);
|
|
7767
|
+
const tmpl = TEMPLATES.find((t) => t.key === name);
|
|
7768
|
+
process$1.stdout.write("\n");
|
|
7769
|
+
if (feat) {
|
|
7770
|
+
process$1.stdout.write(`${c("bold", c("magenta", "add-on: " + feat.key))} ${feat.requiresBackend ? c("green", "[full-stack]") : c("dim", "[frontend]")}
|
|
7771
|
+
|
|
7772
|
+
${feat.description}
|
|
7773
|
+
`);
|
|
7774
|
+
const deps = { ...feat.deps, ...feat.backend?.({ name: "app", template: TEMPLATES[0], features: [], mode: "dynamic" })?.deps ?? {} };
|
|
7775
|
+
if (Object.keys(deps).length) process$1.stdout.write(`
|
|
7776
|
+
${c("bold", "Packages")}: ${Object.keys(deps).map((d) => c("cyan", d)).join(", ")}
|
|
7777
|
+
`);
|
|
7778
|
+
process$1.stdout.write(`
|
|
7779
|
+
${c("bold", "Next steps")}:
|
|
7780
|
+
`);
|
|
7781
|
+
for (const s of feat.nextSteps) process$1.stdout.write(` ${c("dim", "\u2022")} ${s}
|
|
7782
|
+
`);
|
|
7783
|
+
if (feat.learn) process$1.stdout.write(`
|
|
7784
|
+
Learn more \u2192 ${c("cyan", feat.learn)}
|
|
7785
|
+
`);
|
|
7786
|
+
process$1.stdout.write(`
|
|
7787
|
+
Add it: ${c("cyan", "npx create-lacspace-app my-app --with " + feat.key)}
|
|
7788
|
+
|
|
7789
|
+
`);
|
|
7790
|
+
} else if (recipe) {
|
|
7791
|
+
process$1.stdout.write(`${c("bold", c("magenta", "recipe: " + recipe.key))}
|
|
7792
|
+
|
|
7793
|
+
${recipe.description}
|
|
7794
|
+
|
|
7795
|
+
${c("bold", "Template")}: ${c("cyan", recipe.template)}${recipe.mode ? c("dim", " \xB7 " + recipe.mode) : ""}
|
|
7796
|
+
${c("bold", "Add-ons")}: ${recipe.features.map((f) => c("cyan", f)).join(", ")}
|
|
7797
|
+
|
|
7798
|
+
Use it: ${c("cyan", "npx create-lacspace-app my-app --recipe " + recipe.key)}
|
|
7799
|
+
|
|
7800
|
+
`);
|
|
7801
|
+
} else if (tmpl) {
|
|
7802
|
+
process$1.stdout.write(`${c("bold", c("magenta", "template: " + tmpl.key))}
|
|
7803
|
+
|
|
7804
|
+
${tmpl.description}
|
|
7805
|
+
|
|
7806
|
+
Use it: ${c("cyan", "npx create-lacspace-app my-app --template " + tmpl.key)}
|
|
7807
|
+
|
|
7808
|
+
`);
|
|
7809
|
+
} else {
|
|
7810
|
+
process$1.stdout.write(c("yellow", `Unknown "${name}".
|
|
7811
|
+
`));
|
|
7812
|
+
runList();
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7712
7815
|
async function main() {
|
|
7713
7816
|
const raw = process$1.argv.slice(2);
|
|
7714
7817
|
if (raw[0] === "add") {
|
|
7715
7818
|
runAdd(raw.slice(1));
|
|
7716
7819
|
return;
|
|
7717
7820
|
}
|
|
7821
|
+
if (raw[0] === "list" || raw[0] === "ls") {
|
|
7822
|
+
runList();
|
|
7823
|
+
return;
|
|
7824
|
+
}
|
|
7825
|
+
if (raw[0] === "explain" || raw[0] === "info") {
|
|
7826
|
+
runExplain(raw.slice(1));
|
|
7827
|
+
return;
|
|
7828
|
+
}
|
|
7718
7829
|
const args = parseArgs(raw);
|
|
7719
7830
|
if (args.help) {
|
|
7720
7831
|
process$1.stdout.write(HELP + "\n");
|
|
@@ -7723,15 +7834,42 @@ async function main() {
|
|
|
7723
7834
|
process$1.stdout.write(`
|
|
7724
7835
|
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
|
|
7725
7836
|
|
|
7837
|
+
`);
|
|
7838
|
+
const recipe = args.recipe ? RECIPES.find((r) => r.key === args.recipe.toLowerCase()) : void 0;
|
|
7839
|
+
if (args.recipe && !recipe) process$1.stdout.write(c("yellow", ` ! Unknown recipe "${args.recipe}" \u2014 ignoring. Try: ${RECIPES.map((r) => r.key).join(", ")}
|
|
7840
|
+
`));
|
|
7841
|
+
if (recipe) process$1.stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", recipe.key)} ${c("dim", "\u2014 " + recipe.description)}
|
|
7726
7842
|
`);
|
|
7727
7843
|
let name = args.name;
|
|
7728
|
-
let templateKey = args.template;
|
|
7729
|
-
let mode = args.mode ?? "static";
|
|
7730
|
-
const featureKeys = [...args.features];
|
|
7844
|
+
let templateKey = args.template ?? recipe?.template;
|
|
7845
|
+
let mode = args.mode ?? recipe?.mode ?? "static";
|
|
7846
|
+
const featureKeys = [...recipe?.features ?? [], ...args.features];
|
|
7847
|
+
let usedRecipe = Boolean(recipe);
|
|
7731
7848
|
if (!args.yes && process$1.stdin.isTTY) {
|
|
7732
7849
|
const rl = promises.createInterface({ input: process$1.stdin, output: process$1.stdout });
|
|
7733
7850
|
try {
|
|
7734
7851
|
if (!name) name = (await rl.question(`${c("green", "?")} Project name ${c("dim", "(my-app)")}: `)).trim() || "my-app";
|
|
7852
|
+
if (!usedRecipe && !templateKey && featureKeys.length === 0) {
|
|
7853
|
+
process$1.stdout.write(`
|
|
7854
|
+
Start from a recipe? ${c("dim", "(a template + add-ons, ready to go)")}
|
|
7855
|
+
`);
|
|
7856
|
+
RECIPES.forEach((r, i) => process$1.stdout.write(` ${c("cyan", String(i + 1))}. ${c("bold", r.label)} ${c("dim", "\u2014 " + r.description)}
|
|
7857
|
+
`));
|
|
7858
|
+
process$1.stdout.write(` ${c("cyan", "0")}. ${c("dim", "No recipe \u2014 I'll choose a template myself")}
|
|
7859
|
+
`);
|
|
7860
|
+
const ans = (await rl.question(`
|
|
7861
|
+
${c("green", "?")} Recipe ${c("dim", "(0)")}: `)).trim() || "0";
|
|
7862
|
+
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : RECIPES.findIndex((r) => r.key === ans);
|
|
7863
|
+
const chosen = ans === "0" ? void 0 : RECIPES[idx];
|
|
7864
|
+
if (chosen) {
|
|
7865
|
+
usedRecipe = true;
|
|
7866
|
+
templateKey = chosen.template;
|
|
7867
|
+
mode = chosen.mode ?? mode;
|
|
7868
|
+
for (const k of chosen.features) if (!featureKeys.includes(k)) featureKeys.push(k);
|
|
7869
|
+
process$1.stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", chosen.key)}
|
|
7870
|
+
`);
|
|
7871
|
+
}
|
|
7872
|
+
}
|
|
7735
7873
|
if (!templateKey) {
|
|
7736
7874
|
process$1.stdout.write(`
|
|
7737
7875
|
Choose a template:
|
|
@@ -7743,7 +7881,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
|
|
|
7743
7881
|
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
|
|
7744
7882
|
templateKey = TEMPLATES[idx]?.key ?? "personal";
|
|
7745
7883
|
}
|
|
7746
|
-
if (args.mode === void 0) {
|
|
7884
|
+
if (args.mode === void 0 && !usedRecipe) {
|
|
7747
7885
|
process$1.stdout.write(`
|
|
7748
7886
|
What kind of app?
|
|
7749
7887
|
`);
|
|
@@ -7796,7 +7934,25 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
|
|
|
7796
7934
|
return;
|
|
7797
7935
|
}
|
|
7798
7936
|
const features = normalizeFeatures(featureKeys);
|
|
7937
|
+
if (mode === "static" && features.some((f) => f.requiresBackend)) {
|
|
7938
|
+
mode = "dynamic";
|
|
7939
|
+
process$1.stdout.write(` ${c("dim", "\u2191 a selected add-on needs a backend \u2014 building full-stack")}
|
|
7940
|
+
`);
|
|
7941
|
+
}
|
|
7799
7942
|
const files = buildFiles({ name: projectName, template, features, mode });
|
|
7943
|
+
if (args.dryRun) {
|
|
7944
|
+
const keys = Object.keys(files).sort();
|
|
7945
|
+
process$1.stdout.write(`
|
|
7946
|
+
${c("bold", "Dry run")} ${c("dim", `\u2014 ${keys.length} files (${mode}${features.length ? ", +" + features.map((f) => f.key).join(",") : ""}), nothing written`)}
|
|
7947
|
+
`);
|
|
7948
|
+
for (const rel of keys) process$1.stdout.write(` ${c("dim", "+ " + rel)}
|
|
7949
|
+
`);
|
|
7950
|
+
process$1.stdout.write(`
|
|
7951
|
+
${c("dim", "Remove --dry-run to create the project.")}
|
|
7952
|
+
|
|
7953
|
+
`);
|
|
7954
|
+
return;
|
|
7955
|
+
}
|
|
7800
7956
|
for (const [rel, content] of Object.entries(files)) {
|
|
7801
7957
|
const full = path.join(dir, rel);
|
|
7802
7958
|
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
@@ -7907,6 +8063,13 @@ function getFeature(key) {
|
|
|
7907
8063
|
const f = FEATURES.find((x) => x.key === key);
|
|
7908
8064
|
return f ? { ...f } : void 0;
|
|
7909
8065
|
}
|
|
8066
|
+
function listRecipes() {
|
|
8067
|
+
return RECIPES.map((r) => ({ ...r, features: [...r.features] }));
|
|
8068
|
+
}
|
|
8069
|
+
function getRecipe(key) {
|
|
8070
|
+
const r = RECIPES.find((x) => x.key === key);
|
|
8071
|
+
return r ? { ...r, features: [...r.features] } : void 0;
|
|
8072
|
+
}
|
|
7910
8073
|
function generateProject(options = {}) {
|
|
7911
8074
|
return buildFiles(resolveContext(options));
|
|
7912
8075
|
}
|
|
@@ -7924,9 +8087,11 @@ async function scaffold(options = {}) {
|
|
|
7924
8087
|
|
|
7925
8088
|
exports.generateProject = generateProject;
|
|
7926
8089
|
exports.getFeature = getFeature;
|
|
8090
|
+
exports.getRecipe = getRecipe;
|
|
7927
8091
|
exports.getSection = getSection;
|
|
7928
8092
|
exports.getTemplate = getTemplate;
|
|
7929
8093
|
exports.listFeatures = listFeatures;
|
|
8094
|
+
exports.listRecipes = listRecipes;
|
|
7930
8095
|
exports.listSections = listSections;
|
|
7931
8096
|
exports.listTemplates = listTemplates;
|
|
7932
8097
|
exports.scaffold = scaffold;
|
package/dist/lib.d.cts
CHANGED
|
@@ -104,6 +104,28 @@ interface GenerateOptions {
|
|
|
104
104
|
* changes.
|
|
105
105
|
*/
|
|
106
106
|
mode?: "static" | "dynamic";
|
|
107
|
+
/**
|
|
108
|
+
* A one-shot **recipe** (see {@link RECIPES}) — a curated bundle of template +
|
|
109
|
+
* mode + feature add-ons for a whole kind of product, e.g. `"ai-saas"` or
|
|
110
|
+
* `"store"`. Explicit `template`/`features`/`mode` still win and are merged on
|
|
111
|
+
* top. Omit for no recipe.
|
|
112
|
+
*/
|
|
113
|
+
recipe?: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A one-command **recipe**: a curated template + mode + add-on stack for a whole
|
|
117
|
+
* kind of product. `npx create-lacspace-app my-app --recipe ai-saas`.
|
|
118
|
+
*/
|
|
119
|
+
interface RecipeDef {
|
|
120
|
+
key: string;
|
|
121
|
+
label: string;
|
|
122
|
+
description: string;
|
|
123
|
+
/** The template this recipe starts from. */
|
|
124
|
+
template: string;
|
|
125
|
+
/** Force full-stack when the stack needs a backend (add-ons can also force it). */
|
|
126
|
+
mode?: "static" | "dynamic";
|
|
127
|
+
/** The add-ons this recipe layers on. */
|
|
128
|
+
features: string[];
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
/** A project file map: relative path → file contents. */
|
|
@@ -131,6 +153,13 @@ declare function getSection(name: string): string | undefined;
|
|
|
131
153
|
declare function listFeatures(): FeatureDef[];
|
|
132
154
|
/** Look up one feature add-on's definition by key, or `undefined`. */
|
|
133
155
|
declare function getFeature(key: string): FeatureDef | undefined;
|
|
156
|
+
/**
|
|
157
|
+
* The built-in one-shot recipes (see {@link RecipeDef}) — curated template + mode +
|
|
158
|
+
* add-on bundles you can pass as `options.recipe` or request with `--recipe <key>`.
|
|
159
|
+
*/
|
|
160
|
+
declare function listRecipes(): RecipeDef[];
|
|
161
|
+
/** Look up one recipe's definition by key, or `undefined`. */
|
|
162
|
+
declare function getRecipe(key: string): RecipeDef | undefined;
|
|
134
163
|
/**
|
|
135
164
|
* Generate a full project as an in-memory file map (relative path → contents).
|
|
136
165
|
*
|
|
@@ -162,4 +191,4 @@ interface ScaffoldResult {
|
|
|
162
191
|
*/
|
|
163
192
|
declare function scaffold(options?: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
164
193
|
|
|
165
|
-
export { type FeatureBackend, type FeatureDef, type FeatureRoute, type GenerateOptions, type ProjectFiles, type ScaffoldOptions, type ScaffoldResult, type TemplateDef, generateProject, getFeature, getSection, getTemplate, listFeatures, listSections, listTemplates, scaffold, templateKeys, templates };
|
|
194
|
+
export { type FeatureBackend, type FeatureDef, type FeatureRoute, type GenerateOptions, type ProjectFiles, type RecipeDef, type ScaffoldOptions, type ScaffoldResult, type TemplateDef, generateProject, getFeature, getRecipe, getSection, getTemplate, listFeatures, listRecipes, listSections, listTemplates, scaffold, templateKeys, templates };
|
package/dist/lib.d.ts
CHANGED
|
@@ -104,6 +104,28 @@ interface GenerateOptions {
|
|
|
104
104
|
* changes.
|
|
105
105
|
*/
|
|
106
106
|
mode?: "static" | "dynamic";
|
|
107
|
+
/**
|
|
108
|
+
* A one-shot **recipe** (see {@link RECIPES}) — a curated bundle of template +
|
|
109
|
+
* mode + feature add-ons for a whole kind of product, e.g. `"ai-saas"` or
|
|
110
|
+
* `"store"`. Explicit `template`/`features`/`mode` still win and are merged on
|
|
111
|
+
* top. Omit for no recipe.
|
|
112
|
+
*/
|
|
113
|
+
recipe?: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A one-command **recipe**: a curated template + mode + add-on stack for a whole
|
|
117
|
+
* kind of product. `npx create-lacspace-app my-app --recipe ai-saas`.
|
|
118
|
+
*/
|
|
119
|
+
interface RecipeDef {
|
|
120
|
+
key: string;
|
|
121
|
+
label: string;
|
|
122
|
+
description: string;
|
|
123
|
+
/** The template this recipe starts from. */
|
|
124
|
+
template: string;
|
|
125
|
+
/** Force full-stack when the stack needs a backend (add-ons can also force it). */
|
|
126
|
+
mode?: "static" | "dynamic";
|
|
127
|
+
/** The add-ons this recipe layers on. */
|
|
128
|
+
features: string[];
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
/** A project file map: relative path → file contents. */
|
|
@@ -131,6 +153,13 @@ declare function getSection(name: string): string | undefined;
|
|
|
131
153
|
declare function listFeatures(): FeatureDef[];
|
|
132
154
|
/** Look up one feature add-on's definition by key, or `undefined`. */
|
|
133
155
|
declare function getFeature(key: string): FeatureDef | undefined;
|
|
156
|
+
/**
|
|
157
|
+
* The built-in one-shot recipes (see {@link RecipeDef}) — curated template + mode +
|
|
158
|
+
* add-on bundles you can pass as `options.recipe` or request with `--recipe <key>`.
|
|
159
|
+
*/
|
|
160
|
+
declare function listRecipes(): RecipeDef[];
|
|
161
|
+
/** Look up one recipe's definition by key, or `undefined`. */
|
|
162
|
+
declare function getRecipe(key: string): RecipeDef | undefined;
|
|
134
163
|
/**
|
|
135
164
|
* Generate a full project as an in-memory file map (relative path → contents).
|
|
136
165
|
*
|
|
@@ -162,4 +191,4 @@ interface ScaffoldResult {
|
|
|
162
191
|
*/
|
|
163
192
|
declare function scaffold(options?: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
164
193
|
|
|
165
|
-
export { type FeatureBackend, type FeatureDef, type FeatureRoute, type GenerateOptions, type ProjectFiles, type ScaffoldOptions, type ScaffoldResult, type TemplateDef, generateProject, getFeature, getSection, getTemplate, listFeatures, listSections, listTemplates, scaffold, templateKeys, templates };
|
|
194
|
+
export { type FeatureBackend, type FeatureDef, type FeatureRoute, type GenerateOptions, type ProjectFiles, type RecipeDef, type ScaffoldOptions, type ScaffoldResult, type TemplateDef, generateProject, getFeature, getRecipe, getSection, getTemplate, listFeatures, listRecipes, listSections, listTemplates, scaffold, templateKeys, templates };
|
package/dist/lib.js
CHANGED
|
@@ -30,14 +30,15 @@ var TEMPLATES = [
|
|
|
30
30
|
{ key: "marketplace", label: "Marketplace / commerce", description: "A real storefront wired to the Lacspace commerce packages \u2014 cart, checkout, tax, shipping, orders, invoices and Nepal payments.", accent: ["#0d9488", "#6366f1"], siteName: "LSBazaar", siteDescription: "A modern storefront \u2014 cart to checkout, wired end to end." }
|
|
31
31
|
];
|
|
32
32
|
function resolveContext(options = {}) {
|
|
33
|
-
const
|
|
33
|
+
const recipe = options.recipe ? RECIPES.find((r) => r.key === String(options.recipe).toLowerCase()) : void 0;
|
|
34
|
+
const base = TEMPLATES.find((t) => t.key === (options.template ?? recipe?.template)) ?? TEMPLATES[0];
|
|
34
35
|
const accent = resolveAccent(options.theme);
|
|
35
36
|
const template = accent ? { ...base, accent } : base;
|
|
36
37
|
const raw = options.name ?? "my-app";
|
|
37
38
|
const seg = raw.split(/[\\/]/).filter(Boolean).pop() ?? "my-app";
|
|
38
39
|
const name = seg.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/^-+|-+$/g, "") || "my-app";
|
|
39
|
-
const features = normalizeFeatures(options.features);
|
|
40
|
-
let mode = options.mode === "dynamic" ? "dynamic" : "static";
|
|
40
|
+
const features = normalizeFeatures([...recipe?.features ?? [], ...options.features ?? []]);
|
|
41
|
+
let mode = options.mode === "dynamic" ? "dynamic" : options.mode === "static" ? "static" : recipe?.mode ?? "static";
|
|
41
42
|
if (mode === "static" && features.some((f) => f.requiresBackend)) mode = "dynamic";
|
|
42
43
|
return { name, template, features, mode };
|
|
43
44
|
}
|
|
@@ -5253,6 +5254,13 @@ var faqSection = (ctx) => {
|
|
|
5253
5254
|
</div>
|
|
5254
5255
|
</section>`;
|
|
5255
5256
|
};
|
|
5257
|
+
var RECIPES = [
|
|
5258
|
+
{ key: "ai-saas", label: "AI SaaS", description: "SaaS landing + accounts + payments + a streaming AI chat + analytics.", template: "saas", mode: "dynamic", features: ["auth-pages", "payments", "ai-chat", "analytics"] },
|
|
5259
|
+
{ key: "store", label: "Online store", description: "E-commerce storefront + eSewa/Khalti checkout + transactional email + analytics.", template: "ecommerce", mode: "dynamic", features: ["payments", "email", "analytics"] },
|
|
5260
|
+
{ key: "blog", label: "Blog", description: "A blog with a Markdown content section, RSS/llms.txt and instant search.", template: "blog", features: ["content", "search"] },
|
|
5261
|
+
{ key: "internal-tool", label: "Internal tool", description: "An admin dashboard with accounts (2FA), analytics and email \u2014 full-stack.", template: "dashboard", mode: "dynamic", features: ["auth-pages", "analytics", "email"] },
|
|
5262
|
+
{ key: "docs-ai", label: "AI docs", description: "A documentation site with chat-with-your-docs RAG and instant search.", template: "docs", features: ["rag", "search"] }
|
|
5263
|
+
];
|
|
5256
5264
|
var contentLib = () => `import fs from "node:fs";
|
|
5257
5265
|
import path from "node:path";
|
|
5258
5266
|
import { parseFrontmatter, markdownToHtml, excerpt } from "@lacspace/markdown";
|
|
@@ -7210,7 +7218,7 @@ function backendFiles(ctx) {
|
|
|
7210
7218
|
}
|
|
7211
7219
|
var splitList = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
7212
7220
|
function parseArgs(list) {
|
|
7213
|
-
const a = { features: [], yes: false, install: true, git: true, pm: "npm", help: false };
|
|
7221
|
+
const a = { features: [], yes: false, install: true, git: true, pm: "npm", help: false, dryRun: false };
|
|
7214
7222
|
for (let i = 0; i < list.length; i++) {
|
|
7215
7223
|
const arg = list[i];
|
|
7216
7224
|
const next = () => list[++i] ?? "";
|
|
@@ -7218,8 +7226,9 @@ function parseArgs(list) {
|
|
|
7218
7226
|
else if (arg === "-y" || arg === "--yes") a.yes = true;
|
|
7219
7227
|
else if (arg === "--no-install") a.install = false;
|
|
7220
7228
|
else if (arg === "--no-git") a.git = false;
|
|
7229
|
+
else if (arg === "--dry-run" || arg === "--dry") a.dryRun = true;
|
|
7221
7230
|
else if (arg === "--pm") a.pm = next();
|
|
7222
|
-
else if (arg === "--theme" || arg === "--accent") a.theme = next();
|
|
7231
|
+
else if (arg === "--theme" || arg === "--accent" || arg === "--brand") a.theme = next();
|
|
7223
7232
|
else if (arg === "--with" || arg === "--features") a.features.push(...splitList(next()));
|
|
7224
7233
|
else if (arg === "--fullstack" || arg === "--full-stack" || arg === "--dynamic") a.mode = "dynamic";
|
|
7225
7234
|
else if (arg === "--static") a.mode = "static";
|
|
@@ -7229,10 +7238,13 @@ function parseArgs(list) {
|
|
|
7229
7238
|
} else if (arg.startsWith("--mode=")) {
|
|
7230
7239
|
const m = arg.slice(7).toLowerCase();
|
|
7231
7240
|
a.mode = m === "dynamic" ? "dynamic" : "static";
|
|
7232
|
-
} else if (arg === "
|
|
7241
|
+
} else if (arg === "--recipe") a.recipe = next();
|
|
7242
|
+
else if (arg.startsWith("--recipe=")) a.recipe = arg.slice(9);
|
|
7243
|
+
else if (arg === "-h" || arg === "--help") a.help = true;
|
|
7233
7244
|
else if (arg.startsWith("--template=")) a.template = arg.slice(11);
|
|
7234
7245
|
else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
|
|
7235
7246
|
else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
|
|
7247
|
+
else if (arg.startsWith("--brand=")) a.theme = arg.slice(8);
|
|
7236
7248
|
else if (arg.startsWith("--with=")) a.features.push(...splitList(arg.slice(7)));
|
|
7237
7249
|
else if (arg.startsWith("--features=")) a.features.push(...splitList(arg.slice(11)));
|
|
7238
7250
|
else if (!arg.startsWith("-") && !a.name) a.name = arg;
|
|
@@ -7323,6 +7335,8 @@ ${c("bold", "Usage")}
|
|
|
7323
7335
|
npm create lacspace-app@latest <name> [options]
|
|
7324
7336
|
npx create-lacspace-app <name> --template <key>
|
|
7325
7337
|
npx create-lacspace-app add <section...> ${c("dim", "# grow an existing app")}
|
|
7338
|
+
npx create-lacspace-app list ${c("dim", "# templates, add-ons & recipes")}
|
|
7339
|
+
npx create-lacspace-app explain <name> ${c("dim", "# what an add-on/recipe gives you")}
|
|
7326
7340
|
|
|
7327
7341
|
${c("bold", "Templates")}
|
|
7328
7342
|
${TEMPLATES.map((t) => ` ${t.key.padEnd(10)} ${t.description}`).join("\n")}
|
|
@@ -7333,17 +7347,23 @@ ${c("bold", "Options")}
|
|
|
7333
7347
|
MongoDB/Redis backend (JWT auth + CRUD) + shared types
|
|
7334
7348
|
${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
|
|
7335
7349
|
--with <a,b> Feature add-ons, comma-separated (alias --features)
|
|
7336
|
-
--
|
|
7350
|
+
--recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
|
|
7351
|
+
--theme <name|hex> Accent: a preset, a "#hex", or "from,to" (alias --brand)
|
|
7337
7352
|
--pm <npm|pnpm|yarn|bun> Package manager (default npm)
|
|
7353
|
+
--dry-run Print the files that would be created; write nothing
|
|
7338
7354
|
--no-install Skip installing dependencies
|
|
7339
7355
|
--no-git Skip git init
|
|
7340
7356
|
-y, --yes Accept defaults (needs <name>)
|
|
7341
7357
|
-h, --help Show this help
|
|
7342
7358
|
|
|
7343
7359
|
${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
|
|
7344
|
-
${FEATURES.map((f) => ` ${f.key.padEnd(
|
|
7360
|
+
${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
|
|
7345
7361
|
${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
|
|
7346
7362
|
|
|
7363
|
+
${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
|
|
7364
|
+
${RECIPES.map((r) => ` ${r.key.padEnd(14)} ${r.description}`).join("\n")}
|
|
7365
|
+
${c("dim", "e.g. npx create-lacspace-app my-app --recipe ai-saas")}
|
|
7366
|
+
|
|
7347
7367
|
${c("bold", "Themes")} ${c("dim", "(--theme)")}
|
|
7348
7368
|
${Object.keys(THEMES).join(" \xB7 ")}
|
|
7349
7369
|
${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
|
|
@@ -7706,12 +7726,103 @@ ${c("bold", "Use them")} \u2014 import into any page:
|
|
|
7706
7726
|
return;
|
|
7707
7727
|
}
|
|
7708
7728
|
}
|
|
7729
|
+
function runList() {
|
|
7730
|
+
stdout.write(`
|
|
7731
|
+
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 what you can build")}
|
|
7732
|
+
`);
|
|
7733
|
+
stdout.write(`
|
|
7734
|
+
${c("bold", "Templates")} ${c("dim", "(-t / --template)")}
|
|
7735
|
+
`);
|
|
7736
|
+
for (const t of TEMPLATES) stdout.write(` ${c("cyan", t.key.padEnd(12))} ${c("dim", t.description)}
|
|
7737
|
+
`);
|
|
7738
|
+
stdout.write(`
|
|
7739
|
+
${c("bold", "Add-ons")} ${c("dim", "(--with) \u2014 free & keyless")}
|
|
7740
|
+
`);
|
|
7741
|
+
for (const f of FEATURES) stdout.write(` ${c("cyan", f.key.padEnd(12))} ${c("dim", f.description)}${f.requiresBackend ? c("green", " [full-stack]") : ""}
|
|
7742
|
+
`);
|
|
7743
|
+
stdout.write(`
|
|
7744
|
+
${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
|
|
7745
|
+
`);
|
|
7746
|
+
for (const r of RECIPES) stdout.write(` ${c("cyan", r.key.padEnd(14))} ${c("dim", r.description)}
|
|
7747
|
+
`);
|
|
7748
|
+
stdout.write(`
|
|
7749
|
+
${c("dim", "Learn one:")} ${c("cyan", "npx create-lacspace-app explain <name>")}
|
|
7750
|
+
|
|
7751
|
+
`);
|
|
7752
|
+
}
|
|
7753
|
+
function runExplain(rawNames) {
|
|
7754
|
+
const name = rawNames.find((n) => !n.startsWith("-"))?.toLowerCase();
|
|
7755
|
+
if (!name) {
|
|
7756
|
+
stdout.write(`Usage: ${c("cyan", "npx create-lacspace-app explain <template|add-on|recipe>")}
|
|
7757
|
+
|
|
7758
|
+
`);
|
|
7759
|
+
runList();
|
|
7760
|
+
return;
|
|
7761
|
+
}
|
|
7762
|
+
const feat = FEATURES.find((f) => f.key === name);
|
|
7763
|
+
const recipe = RECIPES.find((r) => r.key === name);
|
|
7764
|
+
const tmpl = TEMPLATES.find((t) => t.key === name);
|
|
7765
|
+
stdout.write("\n");
|
|
7766
|
+
if (feat) {
|
|
7767
|
+
stdout.write(`${c("bold", c("magenta", "add-on: " + feat.key))} ${feat.requiresBackend ? c("green", "[full-stack]") : c("dim", "[frontend]")}
|
|
7768
|
+
|
|
7769
|
+
${feat.description}
|
|
7770
|
+
`);
|
|
7771
|
+
const deps = { ...feat.deps, ...feat.backend?.({ name: "app", template: TEMPLATES[0], features: [], mode: "dynamic" })?.deps ?? {} };
|
|
7772
|
+
if (Object.keys(deps).length) stdout.write(`
|
|
7773
|
+
${c("bold", "Packages")}: ${Object.keys(deps).map((d) => c("cyan", d)).join(", ")}
|
|
7774
|
+
`);
|
|
7775
|
+
stdout.write(`
|
|
7776
|
+
${c("bold", "Next steps")}:
|
|
7777
|
+
`);
|
|
7778
|
+
for (const s of feat.nextSteps) stdout.write(` ${c("dim", "\u2022")} ${s}
|
|
7779
|
+
`);
|
|
7780
|
+
if (feat.learn) stdout.write(`
|
|
7781
|
+
Learn more \u2192 ${c("cyan", feat.learn)}
|
|
7782
|
+
`);
|
|
7783
|
+
stdout.write(`
|
|
7784
|
+
Add it: ${c("cyan", "npx create-lacspace-app my-app --with " + feat.key)}
|
|
7785
|
+
|
|
7786
|
+
`);
|
|
7787
|
+
} else if (recipe) {
|
|
7788
|
+
stdout.write(`${c("bold", c("magenta", "recipe: " + recipe.key))}
|
|
7789
|
+
|
|
7790
|
+
${recipe.description}
|
|
7791
|
+
|
|
7792
|
+
${c("bold", "Template")}: ${c("cyan", recipe.template)}${recipe.mode ? c("dim", " \xB7 " + recipe.mode) : ""}
|
|
7793
|
+
${c("bold", "Add-ons")}: ${recipe.features.map((f) => c("cyan", f)).join(", ")}
|
|
7794
|
+
|
|
7795
|
+
Use it: ${c("cyan", "npx create-lacspace-app my-app --recipe " + recipe.key)}
|
|
7796
|
+
|
|
7797
|
+
`);
|
|
7798
|
+
} else if (tmpl) {
|
|
7799
|
+
stdout.write(`${c("bold", c("magenta", "template: " + tmpl.key))}
|
|
7800
|
+
|
|
7801
|
+
${tmpl.description}
|
|
7802
|
+
|
|
7803
|
+
Use it: ${c("cyan", "npx create-lacspace-app my-app --template " + tmpl.key)}
|
|
7804
|
+
|
|
7805
|
+
`);
|
|
7806
|
+
} else {
|
|
7807
|
+
stdout.write(c("yellow", `Unknown "${name}".
|
|
7808
|
+
`));
|
|
7809
|
+
runList();
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7709
7812
|
async function main() {
|
|
7710
7813
|
const raw = argv.slice(2);
|
|
7711
7814
|
if (raw[0] === "add") {
|
|
7712
7815
|
runAdd(raw.slice(1));
|
|
7713
7816
|
return;
|
|
7714
7817
|
}
|
|
7818
|
+
if (raw[0] === "list" || raw[0] === "ls") {
|
|
7819
|
+
runList();
|
|
7820
|
+
return;
|
|
7821
|
+
}
|
|
7822
|
+
if (raw[0] === "explain" || raw[0] === "info") {
|
|
7823
|
+
runExplain(raw.slice(1));
|
|
7824
|
+
return;
|
|
7825
|
+
}
|
|
7715
7826
|
const args = parseArgs(raw);
|
|
7716
7827
|
if (args.help) {
|
|
7717
7828
|
stdout.write(HELP + "\n");
|
|
@@ -7720,15 +7831,42 @@ async function main() {
|
|
|
7720
7831
|
stdout.write(`
|
|
7721
7832
|
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
|
|
7722
7833
|
|
|
7834
|
+
`);
|
|
7835
|
+
const recipe = args.recipe ? RECIPES.find((r) => r.key === args.recipe.toLowerCase()) : void 0;
|
|
7836
|
+
if (args.recipe && !recipe) stdout.write(c("yellow", ` ! Unknown recipe "${args.recipe}" \u2014 ignoring. Try: ${RECIPES.map((r) => r.key).join(", ")}
|
|
7837
|
+
`));
|
|
7838
|
+
if (recipe) stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", recipe.key)} ${c("dim", "\u2014 " + recipe.description)}
|
|
7723
7839
|
`);
|
|
7724
7840
|
let name = args.name;
|
|
7725
|
-
let templateKey = args.template;
|
|
7726
|
-
let mode = args.mode ?? "static";
|
|
7727
|
-
const featureKeys = [...args.features];
|
|
7841
|
+
let templateKey = args.template ?? recipe?.template;
|
|
7842
|
+
let mode = args.mode ?? recipe?.mode ?? "static";
|
|
7843
|
+
const featureKeys = [...recipe?.features ?? [], ...args.features];
|
|
7844
|
+
let usedRecipe = Boolean(recipe);
|
|
7728
7845
|
if (!args.yes && stdin.isTTY) {
|
|
7729
7846
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
7730
7847
|
try {
|
|
7731
7848
|
if (!name) name = (await rl.question(`${c("green", "?")} Project name ${c("dim", "(my-app)")}: `)).trim() || "my-app";
|
|
7849
|
+
if (!usedRecipe && !templateKey && featureKeys.length === 0) {
|
|
7850
|
+
stdout.write(`
|
|
7851
|
+
Start from a recipe? ${c("dim", "(a template + add-ons, ready to go)")}
|
|
7852
|
+
`);
|
|
7853
|
+
RECIPES.forEach((r, i) => stdout.write(` ${c("cyan", String(i + 1))}. ${c("bold", r.label)} ${c("dim", "\u2014 " + r.description)}
|
|
7854
|
+
`));
|
|
7855
|
+
stdout.write(` ${c("cyan", "0")}. ${c("dim", "No recipe \u2014 I'll choose a template myself")}
|
|
7856
|
+
`);
|
|
7857
|
+
const ans = (await rl.question(`
|
|
7858
|
+
${c("green", "?")} Recipe ${c("dim", "(0)")}: `)).trim() || "0";
|
|
7859
|
+
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : RECIPES.findIndex((r) => r.key === ans);
|
|
7860
|
+
const chosen = ans === "0" ? void 0 : RECIPES[idx];
|
|
7861
|
+
if (chosen) {
|
|
7862
|
+
usedRecipe = true;
|
|
7863
|
+
templateKey = chosen.template;
|
|
7864
|
+
mode = chosen.mode ?? mode;
|
|
7865
|
+
for (const k of chosen.features) if (!featureKeys.includes(k)) featureKeys.push(k);
|
|
7866
|
+
stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", chosen.key)}
|
|
7867
|
+
`);
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7732
7870
|
if (!templateKey) {
|
|
7733
7871
|
stdout.write(`
|
|
7734
7872
|
Choose a template:
|
|
@@ -7740,7 +7878,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
|
|
|
7740
7878
|
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
|
|
7741
7879
|
templateKey = TEMPLATES[idx]?.key ?? "personal";
|
|
7742
7880
|
}
|
|
7743
|
-
if (args.mode === void 0) {
|
|
7881
|
+
if (args.mode === void 0 && !usedRecipe) {
|
|
7744
7882
|
stdout.write(`
|
|
7745
7883
|
What kind of app?
|
|
7746
7884
|
`);
|
|
@@ -7793,7 +7931,25 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
|
|
|
7793
7931
|
return;
|
|
7794
7932
|
}
|
|
7795
7933
|
const features = normalizeFeatures(featureKeys);
|
|
7934
|
+
if (mode === "static" && features.some((f) => f.requiresBackend)) {
|
|
7935
|
+
mode = "dynamic";
|
|
7936
|
+
stdout.write(` ${c("dim", "\u2191 a selected add-on needs a backend \u2014 building full-stack")}
|
|
7937
|
+
`);
|
|
7938
|
+
}
|
|
7796
7939
|
const files = buildFiles({ name: projectName, template, features, mode });
|
|
7940
|
+
if (args.dryRun) {
|
|
7941
|
+
const keys = Object.keys(files).sort();
|
|
7942
|
+
stdout.write(`
|
|
7943
|
+
${c("bold", "Dry run")} ${c("dim", `\u2014 ${keys.length} files (${mode}${features.length ? ", +" + features.map((f) => f.key).join(",") : ""}), nothing written`)}
|
|
7944
|
+
`);
|
|
7945
|
+
for (const rel of keys) stdout.write(` ${c("dim", "+ " + rel)}
|
|
7946
|
+
`);
|
|
7947
|
+
stdout.write(`
|
|
7948
|
+
${c("dim", "Remove --dry-run to create the project.")}
|
|
7949
|
+
|
|
7950
|
+
`);
|
|
7951
|
+
return;
|
|
7952
|
+
}
|
|
7797
7953
|
for (const [rel, content] of Object.entries(files)) {
|
|
7798
7954
|
const full = join(dir, rel);
|
|
7799
7955
|
mkdirSync(dirname(full), { recursive: true });
|
|
@@ -7904,6 +8060,13 @@ function getFeature(key) {
|
|
|
7904
8060
|
const f = FEATURES.find((x) => x.key === key);
|
|
7905
8061
|
return f ? { ...f } : void 0;
|
|
7906
8062
|
}
|
|
8063
|
+
function listRecipes() {
|
|
8064
|
+
return RECIPES.map((r) => ({ ...r, features: [...r.features] }));
|
|
8065
|
+
}
|
|
8066
|
+
function getRecipe(key) {
|
|
8067
|
+
const r = RECIPES.find((x) => x.key === key);
|
|
8068
|
+
return r ? { ...r, features: [...r.features] } : void 0;
|
|
8069
|
+
}
|
|
7907
8070
|
function generateProject(options = {}) {
|
|
7908
8071
|
return buildFiles(resolveContext(options));
|
|
7909
8072
|
}
|
|
@@ -7919,4 +8082,4 @@ async function scaffold(options = {}) {
|
|
|
7919
8082
|
return { dir: target, files: Object.keys(files) };
|
|
7920
8083
|
}
|
|
7921
8084
|
|
|
7922
|
-
export { generateProject, getFeature, getSection, getTemplate, listFeatures, listSections, listTemplates, scaffold, templateKeys, templates };
|
|
8085
|
+
export { generateProject, getFeature, getRecipe, getSection, getTemplate, listFeatures, listRecipes, listSections, listTemplates, scaffold, templateKeys, templates };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lacspace-app",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — portfolio, business, e-commerce, SaaS, blog, docs, dashboard, restaurant or marketplace — pre-wired with SEO, security headers, sitemap and robots. Use it as a CLI (like create-next-app, but you start gorgeous) or import it as a library to generate projects programmatically.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|