create-lacspace-app 2.7.0 → 2.8.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 CHANGED
@@ -20,6 +20,13 @@ 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.8 — recipes.** Scaffold a whole *kind of product* in one command with `--recipe <key>` (a curated template + full-stack mode + add-on stack):
24
+ > - **`ai-saas`** → SaaS + accounts + payments + AI chat + analytics
25
+ > - **`store`** → e-commerce + eSewa/Khalti checkout + email + analytics
26
+ > - **`blog`** → blog + content + search · **`docs-ai`** → docs + RAG + search · **`internal-tool`** → dashboard + auth + analytics + email
27
+ >
28
+ > `npx create-lacspace-app my-app --recipe ai-saas`. Explicit `--template`/`--with`/`--fullstack` still merge on top. Also on the lib API: `listRecipes()` / `getRecipe()`.
29
+
23
30
  > **New in v2.7 — payments & email add-ons.**
24
31
  > - **`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
32
  > - **`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 base = TEMPLATES.find((t) => t.key === options.template) ?? TEMPLATES[0];
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";
@@ -7228,7 +7236,9 @@ function parseArgs(list) {
7228
7236
  } else if (arg.startsWith("--mode=")) {
7229
7237
  const m = arg.slice(7).toLowerCase();
7230
7238
  a.mode = m === "dynamic" ? "dynamic" : "static";
7231
- } else if (arg === "-h" || arg === "--help") a.help = true;
7239
+ } else if (arg === "--recipe") a.recipe = next();
7240
+ else if (arg.startsWith("--recipe=")) a.recipe = arg.slice(9);
7241
+ else if (arg === "-h" || arg === "--help") a.help = true;
7232
7242
  else if (arg.startsWith("--template=")) a.template = arg.slice(11);
7233
7243
  else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
7234
7244
  else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
@@ -7332,6 +7342,7 @@ ${c("bold", "Options")}
7332
7342
  MongoDB/Redis backend (JWT auth + CRUD) + shared types
7333
7343
  ${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
7334
7344
  --with <a,b> Feature add-ons, comma-separated (alias --features)
7345
+ --recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
7335
7346
  --theme <name|hex> Accent: a preset, a "#hex", or "from,to" (e.g. --theme lacspace)
7336
7347
  --pm <npm|pnpm|yarn|bun> Package manager (default npm)
7337
7348
  --no-install Skip installing dependencies
@@ -7340,9 +7351,13 @@ ${c("bold", "Options")}
7340
7351
  -h, --help Show this help
7341
7352
 
7342
7353
  ${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
7343
- ${FEATURES.map((f) => ` ${f.key.padEnd(10)} ${f.description}`).join("\n")}
7354
+ ${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
7344
7355
  ${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
7345
7356
 
7357
+ ${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
7358
+ ${RECIPES.map((r) => ` ${r.key.padEnd(14)} ${r.description}`).join("\n")}
7359
+ ${c("dim", "e.g. npx create-lacspace-app my-app --recipe ai-saas")}
7360
+
7346
7361
  ${c("bold", "Themes")} ${c("dim", "(--theme)")}
7347
7362
  ${Object.keys(THEMES).join(" \xB7 ")}
7348
7363
  ${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
@@ -7719,11 +7734,16 @@ async function main() {
7719
7734
  stdout.write(`
7720
7735
  ${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
7721
7736
 
7737
+ `);
7738
+ const recipe = args.recipe ? RECIPES.find((r) => r.key === args.recipe.toLowerCase()) : void 0;
7739
+ if (args.recipe && !recipe) stdout.write(c("yellow", ` ! Unknown recipe "${args.recipe}" \u2014 ignoring. Try: ${RECIPES.map((r) => r.key).join(", ")}
7740
+ `));
7741
+ if (recipe) stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", recipe.key)} ${c("dim", "\u2014 " + recipe.description)}
7722
7742
  `);
7723
7743
  let name = args.name;
7724
- let templateKey = args.template;
7725
- let mode = args.mode ?? "static";
7726
- const featureKeys = [...args.features];
7744
+ let templateKey = args.template ?? recipe?.template;
7745
+ let mode = args.mode ?? recipe?.mode ?? "static";
7746
+ const featureKeys = [...recipe?.features ?? [], ...args.features];
7727
7747
  if (!args.yes && stdin.isTTY) {
7728
7748
  const rl = createInterface({ input: stdin, output: stdout });
7729
7749
  try {
@@ -7739,7 +7759,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
7739
7759
  const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
7740
7760
  templateKey = TEMPLATES[idx]?.key ?? "personal";
7741
7761
  }
7742
- if (args.mode === void 0) {
7762
+ if (args.mode === void 0 && !recipe) {
7743
7763
  stdout.write(`
7744
7764
  What kind of app?
7745
7765
  `);
@@ -7792,6 +7812,11 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
7792
7812
  return;
7793
7813
  }
7794
7814
  const features = normalizeFeatures(featureKeys);
7815
+ if (mode === "static" && features.some((f) => f.requiresBackend)) {
7816
+ mode = "dynamic";
7817
+ stdout.write(` ${c("dim", "\u2191 a selected add-on needs a backend \u2014 building full-stack")}
7818
+ `);
7819
+ }
7795
7820
  const files = buildFiles({ name: projectName, template, features, mode });
7796
7821
  for (const [rel, content] of Object.entries(files)) {
7797
7822
  const full = join(dir, rel);
@@ -7880,4 +7905,4 @@ if (invokedAsCli) {
7880
7905
  });
7881
7906
  }
7882
7907
 
7883
- export { FEATURES, SECTIONS, TEMPLATES, applyFeatures, buildFiles, normalizeFeatures, resolveContext };
7908
+ 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 base = TEMPLATES.find((t) => t.key === options.template) ?? TEMPLATES[0];
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";
@@ -7232,7 +7240,9 @@ function parseArgs(list) {
7232
7240
  } else if (arg.startsWith("--mode=")) {
7233
7241
  const m = arg.slice(7).toLowerCase();
7234
7242
  a.mode = m === "dynamic" ? "dynamic" : "static";
7235
- } else if (arg === "-h" || arg === "--help") a.help = true;
7243
+ } else if (arg === "--recipe") a.recipe = next();
7244
+ else if (arg.startsWith("--recipe=")) a.recipe = arg.slice(9);
7245
+ else if (arg === "-h" || arg === "--help") a.help = true;
7236
7246
  else if (arg.startsWith("--template=")) a.template = arg.slice(11);
7237
7247
  else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
7238
7248
  else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
@@ -7336,6 +7346,7 @@ ${c("bold", "Options")}
7336
7346
  MongoDB/Redis backend (JWT auth + CRUD) + shared types
7337
7347
  ${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
7338
7348
  --with <a,b> Feature add-ons, comma-separated (alias --features)
7349
+ --recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
7339
7350
  --theme <name|hex> Accent: a preset, a "#hex", or "from,to" (e.g. --theme lacspace)
7340
7351
  --pm <npm|pnpm|yarn|bun> Package manager (default npm)
7341
7352
  --no-install Skip installing dependencies
@@ -7344,9 +7355,13 @@ ${c("bold", "Options")}
7344
7355
  -h, --help Show this help
7345
7356
 
7346
7357
  ${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
7347
- ${FEATURES.map((f) => ` ${f.key.padEnd(10)} ${f.description}`).join("\n")}
7358
+ ${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
7348
7359
  ${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
7349
7360
 
7361
+ ${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
7362
+ ${RECIPES.map((r) => ` ${r.key.padEnd(14)} ${r.description}`).join("\n")}
7363
+ ${c("dim", "e.g. npx create-lacspace-app my-app --recipe ai-saas")}
7364
+
7350
7365
  ${c("bold", "Themes")} ${c("dim", "(--theme)")}
7351
7366
  ${Object.keys(THEMES).join(" \xB7 ")}
7352
7367
  ${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
@@ -7723,11 +7738,16 @@ async function main() {
7723
7738
  process$1.stdout.write(`
7724
7739
  ${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
7725
7740
 
7741
+ `);
7742
+ const recipe = args.recipe ? RECIPES.find((r) => r.key === args.recipe.toLowerCase()) : void 0;
7743
+ if (args.recipe && !recipe) process$1.stdout.write(c("yellow", ` ! Unknown recipe "${args.recipe}" \u2014 ignoring. Try: ${RECIPES.map((r) => r.key).join(", ")}
7744
+ `));
7745
+ if (recipe) process$1.stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", recipe.key)} ${c("dim", "\u2014 " + recipe.description)}
7726
7746
  `);
7727
7747
  let name = args.name;
7728
- let templateKey = args.template;
7729
- let mode = args.mode ?? "static";
7730
- const featureKeys = [...args.features];
7748
+ let templateKey = args.template ?? recipe?.template;
7749
+ let mode = args.mode ?? recipe?.mode ?? "static";
7750
+ const featureKeys = [...recipe?.features ?? [], ...args.features];
7731
7751
  if (!args.yes && process$1.stdin.isTTY) {
7732
7752
  const rl = promises.createInterface({ input: process$1.stdin, output: process$1.stdout });
7733
7753
  try {
@@ -7743,7 +7763,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
7743
7763
  const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
7744
7764
  templateKey = TEMPLATES[idx]?.key ?? "personal";
7745
7765
  }
7746
- if (args.mode === void 0) {
7766
+ if (args.mode === void 0 && !recipe) {
7747
7767
  process$1.stdout.write(`
7748
7768
  What kind of app?
7749
7769
  `);
@@ -7796,6 +7816,11 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
7796
7816
  return;
7797
7817
  }
7798
7818
  const features = normalizeFeatures(featureKeys);
7819
+ if (mode === "static" && features.some((f) => f.requiresBackend)) {
7820
+ mode = "dynamic";
7821
+ process$1.stdout.write(` ${c("dim", "\u2191 a selected add-on needs a backend \u2014 building full-stack")}
7822
+ `);
7823
+ }
7799
7824
  const files = buildFiles({ name: projectName, template, features, mode });
7800
7825
  for (const [rel, content] of Object.entries(files)) {
7801
7826
  const full = path.join(dir, rel);
@@ -7907,6 +7932,13 @@ function getFeature(key) {
7907
7932
  const f = FEATURES.find((x) => x.key === key);
7908
7933
  return f ? { ...f } : void 0;
7909
7934
  }
7935
+ function listRecipes() {
7936
+ return RECIPES.map((r) => ({ ...r, features: [...r.features] }));
7937
+ }
7938
+ function getRecipe(key) {
7939
+ const r = RECIPES.find((x) => x.key === key);
7940
+ return r ? { ...r, features: [...r.features] } : void 0;
7941
+ }
7910
7942
  function generateProject(options = {}) {
7911
7943
  return buildFiles(resolveContext(options));
7912
7944
  }
@@ -7924,9 +7956,11 @@ async function scaffold(options = {}) {
7924
7956
 
7925
7957
  exports.generateProject = generateProject;
7926
7958
  exports.getFeature = getFeature;
7959
+ exports.getRecipe = getRecipe;
7927
7960
  exports.getSection = getSection;
7928
7961
  exports.getTemplate = getTemplate;
7929
7962
  exports.listFeatures = listFeatures;
7963
+ exports.listRecipes = listRecipes;
7930
7964
  exports.listSections = listSections;
7931
7965
  exports.listTemplates = listTemplates;
7932
7966
  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 base = TEMPLATES.find((t) => t.key === options.template) ?? TEMPLATES[0];
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";
@@ -7229,7 +7237,9 @@ function parseArgs(list) {
7229
7237
  } else if (arg.startsWith("--mode=")) {
7230
7238
  const m = arg.slice(7).toLowerCase();
7231
7239
  a.mode = m === "dynamic" ? "dynamic" : "static";
7232
- } else if (arg === "-h" || arg === "--help") a.help = true;
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;
7233
7243
  else if (arg.startsWith("--template=")) a.template = arg.slice(11);
7234
7244
  else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
7235
7245
  else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
@@ -7333,6 +7343,7 @@ ${c("bold", "Options")}
7333
7343
  MongoDB/Redis backend (JWT auth + CRUD) + shared types
7334
7344
  ${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
7335
7345
  --with <a,b> Feature add-ons, comma-separated (alias --features)
7346
+ --recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
7336
7347
  --theme <name|hex> Accent: a preset, a "#hex", or "from,to" (e.g. --theme lacspace)
7337
7348
  --pm <npm|pnpm|yarn|bun> Package manager (default npm)
7338
7349
  --no-install Skip installing dependencies
@@ -7341,9 +7352,13 @@ ${c("bold", "Options")}
7341
7352
  -h, --help Show this help
7342
7353
 
7343
7354
  ${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
7344
- ${FEATURES.map((f) => ` ${f.key.padEnd(10)} ${f.description}`).join("\n")}
7355
+ ${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
7345
7356
  ${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
7346
7357
 
7358
+ ${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
7359
+ ${RECIPES.map((r) => ` ${r.key.padEnd(14)} ${r.description}`).join("\n")}
7360
+ ${c("dim", "e.g. npx create-lacspace-app my-app --recipe ai-saas")}
7361
+
7347
7362
  ${c("bold", "Themes")} ${c("dim", "(--theme)")}
7348
7363
  ${Object.keys(THEMES).join(" \xB7 ")}
7349
7364
  ${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
@@ -7720,11 +7735,16 @@ async function main() {
7720
7735
  stdout.write(`
7721
7736
  ${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
7722
7737
 
7738
+ `);
7739
+ const recipe = args.recipe ? RECIPES.find((r) => r.key === args.recipe.toLowerCase()) : void 0;
7740
+ if (args.recipe && !recipe) stdout.write(c("yellow", ` ! Unknown recipe "${args.recipe}" \u2014 ignoring. Try: ${RECIPES.map((r) => r.key).join(", ")}
7741
+ `));
7742
+ if (recipe) stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", recipe.key)} ${c("dim", "\u2014 " + recipe.description)}
7723
7743
  `);
7724
7744
  let name = args.name;
7725
- let templateKey = args.template;
7726
- let mode = args.mode ?? "static";
7727
- const featureKeys = [...args.features];
7745
+ let templateKey = args.template ?? recipe?.template;
7746
+ let mode = args.mode ?? recipe?.mode ?? "static";
7747
+ const featureKeys = [...recipe?.features ?? [], ...args.features];
7728
7748
  if (!args.yes && stdin.isTTY) {
7729
7749
  const rl = createInterface({ input: stdin, output: stdout });
7730
7750
  try {
@@ -7740,7 +7760,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
7740
7760
  const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
7741
7761
  templateKey = TEMPLATES[idx]?.key ?? "personal";
7742
7762
  }
7743
- if (args.mode === void 0) {
7763
+ if (args.mode === void 0 && !recipe) {
7744
7764
  stdout.write(`
7745
7765
  What kind of app?
7746
7766
  `);
@@ -7793,6 +7813,11 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
7793
7813
  return;
7794
7814
  }
7795
7815
  const features = normalizeFeatures(featureKeys);
7816
+ if (mode === "static" && features.some((f) => f.requiresBackend)) {
7817
+ mode = "dynamic";
7818
+ stdout.write(` ${c("dim", "\u2191 a selected add-on needs a backend \u2014 building full-stack")}
7819
+ `);
7820
+ }
7796
7821
  const files = buildFiles({ name: projectName, template, features, mode });
7797
7822
  for (const [rel, content] of Object.entries(files)) {
7798
7823
  const full = join(dir, rel);
@@ -7904,6 +7929,13 @@ function getFeature(key) {
7904
7929
  const f = FEATURES.find((x) => x.key === key);
7905
7930
  return f ? { ...f } : void 0;
7906
7931
  }
7932
+ function listRecipes() {
7933
+ return RECIPES.map((r) => ({ ...r, features: [...r.features] }));
7934
+ }
7935
+ function getRecipe(key) {
7936
+ const r = RECIPES.find((x) => x.key === key);
7937
+ return r ? { ...r, features: [...r.features] } : void 0;
7938
+ }
7907
7939
  function generateProject(options = {}) {
7908
7940
  return buildFiles(resolveContext(options));
7909
7941
  }
@@ -7919,4 +7951,4 @@ async function scaffold(options = {}) {
7919
7951
  return { dir: target, files: Object.keys(files) };
7920
7952
  }
7921
7953
 
7922
- export { generateProject, getFeature, getSection, getTemplate, listFeatures, listSections, listTemplates, scaffold, templateKeys, templates };
7954
+ 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.7.0",
3
+ "version": "2.8.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": {