create-lacspace-app 2.8.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 CHANGED
@@ -20,6 +20,11 @@ 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
+
23
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):
24
29
  > - **`ai-saas`** → SaaS + accounts + payments + AI chat + analytics
25
30
  > - **`store`** → e-commerce + eSewa/Khalti checkout + email + analytics
package/dist/index.js CHANGED
@@ -7217,7 +7217,7 @@ function backendFiles(ctx) {
7217
7217
  }
7218
7218
  var splitList = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
7219
7219
  function parseArgs(list) {
7220
- 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 };
7221
7221
  for (let i = 0; i < list.length; i++) {
7222
7222
  const arg = list[i];
7223
7223
  const next = () => list[++i] ?? "";
@@ -7225,8 +7225,9 @@ function parseArgs(list) {
7225
7225
  else if (arg === "-y" || arg === "--yes") a.yes = true;
7226
7226
  else if (arg === "--no-install") a.install = false;
7227
7227
  else if (arg === "--no-git") a.git = false;
7228
+ else if (arg === "--dry-run" || arg === "--dry") a.dryRun = true;
7228
7229
  else if (arg === "--pm") a.pm = next();
7229
- else if (arg === "--theme" || arg === "--accent") a.theme = next();
7230
+ else if (arg === "--theme" || arg === "--accent" || arg === "--brand") a.theme = next();
7230
7231
  else if (arg === "--with" || arg === "--features") a.features.push(...splitList(next()));
7231
7232
  else if (arg === "--fullstack" || arg === "--full-stack" || arg === "--dynamic") a.mode = "dynamic";
7232
7233
  else if (arg === "--static") a.mode = "static";
@@ -7242,6 +7243,7 @@ function parseArgs(list) {
7242
7243
  else if (arg.startsWith("--template=")) a.template = arg.slice(11);
7243
7244
  else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
7244
7245
  else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
7246
+ else if (arg.startsWith("--brand=")) a.theme = arg.slice(8);
7245
7247
  else if (arg.startsWith("--with=")) a.features.push(...splitList(arg.slice(7)));
7246
7248
  else if (arg.startsWith("--features=")) a.features.push(...splitList(arg.slice(11)));
7247
7249
  else if (!arg.startsWith("-") && !a.name) a.name = arg;
@@ -7332,6 +7334,8 @@ ${c("bold", "Usage")}
7332
7334
  npm create lacspace-app@latest <name> [options]
7333
7335
  npx create-lacspace-app <name> --template <key>
7334
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")}
7335
7339
 
7336
7340
  ${c("bold", "Templates")}
7337
7341
  ${TEMPLATES.map((t) => ` ${t.key.padEnd(10)} ${t.description}`).join("\n")}
@@ -7343,8 +7347,9 @@ ${c("bold", "Options")}
7343
7347
  ${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
7344
7348
  --with <a,b> Feature add-ons, comma-separated (alias --features)
7345
7349
  --recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
7346
- --theme <name|hex> Accent: a preset, a "#hex", or "from,to" (e.g. --theme lacspace)
7350
+ --theme <name|hex> Accent: a preset, a "#hex", or "from,to" (alias --brand)
7347
7351
  --pm <npm|pnpm|yarn|bun> Package manager (default npm)
7352
+ --dry-run Print the files that would be created; write nothing
7348
7353
  --no-install Skip installing dependencies
7349
7354
  --no-git Skip git init
7350
7355
  -y, --yes Accept defaults (needs <name>)
@@ -7720,12 +7725,103 @@ ${c("bold", "Use them")} \u2014 import into any page:
7720
7725
  return;
7721
7726
  }
7722
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
+ }
7723
7811
  async function main() {
7724
7812
  const raw = argv.slice(2);
7725
7813
  if (raw[0] === "add") {
7726
7814
  runAdd(raw.slice(1));
7727
7815
  return;
7728
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
+ }
7729
7825
  const args = parseArgs(raw);
7730
7826
  if (args.help) {
7731
7827
  stdout.write(HELP + "\n");
@@ -7744,10 +7840,32 @@ ${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a go
7744
7840
  let templateKey = args.template ?? recipe?.template;
7745
7841
  let mode = args.mode ?? recipe?.mode ?? "static";
7746
7842
  const featureKeys = [...recipe?.features ?? [], ...args.features];
7843
+ let usedRecipe = Boolean(recipe);
7747
7844
  if (!args.yes && stdin.isTTY) {
7748
7845
  const rl = createInterface({ input: stdin, output: stdout });
7749
7846
  try {
7750
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
+ }
7751
7869
  if (!templateKey) {
7752
7870
  stdout.write(`
7753
7871
  Choose a template:
@@ -7759,7 +7877,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
7759
7877
  const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
7760
7878
  templateKey = TEMPLATES[idx]?.key ?? "personal";
7761
7879
  }
7762
- if (args.mode === void 0 && !recipe) {
7880
+ if (args.mode === void 0 && !usedRecipe) {
7763
7881
  stdout.write(`
7764
7882
  What kind of app?
7765
7883
  `);
@@ -7818,6 +7936,19 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
7818
7936
  `);
7819
7937
  }
7820
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
+ }
7821
7952
  for (const [rel, content] of Object.entries(files)) {
7822
7953
  const full = join(dir, rel);
7823
7954
  mkdirSync(dirname(full), { recursive: true });
package/dist/lib.cjs CHANGED
@@ -7221,7 +7221,7 @@ function backendFiles(ctx) {
7221
7221
  }
7222
7222
  var splitList = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
7223
7223
  function parseArgs(list) {
7224
- 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 };
7225
7225
  for (let i = 0; i < list.length; i++) {
7226
7226
  const arg = list[i];
7227
7227
  const next = () => list[++i] ?? "";
@@ -7229,8 +7229,9 @@ function parseArgs(list) {
7229
7229
  else if (arg === "-y" || arg === "--yes") a.yes = true;
7230
7230
  else if (arg === "--no-install") a.install = false;
7231
7231
  else if (arg === "--no-git") a.git = false;
7232
+ else if (arg === "--dry-run" || arg === "--dry") a.dryRun = true;
7232
7233
  else if (arg === "--pm") a.pm = next();
7233
- else if (arg === "--theme" || arg === "--accent") a.theme = next();
7234
+ else if (arg === "--theme" || arg === "--accent" || arg === "--brand") a.theme = next();
7234
7235
  else if (arg === "--with" || arg === "--features") a.features.push(...splitList(next()));
7235
7236
  else if (arg === "--fullstack" || arg === "--full-stack" || arg === "--dynamic") a.mode = "dynamic";
7236
7237
  else if (arg === "--static") a.mode = "static";
@@ -7246,6 +7247,7 @@ function parseArgs(list) {
7246
7247
  else if (arg.startsWith("--template=")) a.template = arg.slice(11);
7247
7248
  else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
7248
7249
  else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
7250
+ else if (arg.startsWith("--brand=")) a.theme = arg.slice(8);
7249
7251
  else if (arg.startsWith("--with=")) a.features.push(...splitList(arg.slice(7)));
7250
7252
  else if (arg.startsWith("--features=")) a.features.push(...splitList(arg.slice(11)));
7251
7253
  else if (!arg.startsWith("-") && !a.name) a.name = arg;
@@ -7336,6 +7338,8 @@ ${c("bold", "Usage")}
7336
7338
  npm create lacspace-app@latest <name> [options]
7337
7339
  npx create-lacspace-app <name> --template <key>
7338
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")}
7339
7343
 
7340
7344
  ${c("bold", "Templates")}
7341
7345
  ${TEMPLATES.map((t) => ` ${t.key.padEnd(10)} ${t.description}`).join("\n")}
@@ -7347,8 +7351,9 @@ ${c("bold", "Options")}
7347
7351
  ${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
7348
7352
  --with <a,b> Feature add-ons, comma-separated (alias --features)
7349
7353
  --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" (e.g. --theme lacspace)
7354
+ --theme <name|hex> Accent: a preset, a "#hex", or "from,to" (alias --brand)
7351
7355
  --pm <npm|pnpm|yarn|bun> Package manager (default npm)
7356
+ --dry-run Print the files that would be created; write nothing
7352
7357
  --no-install Skip installing dependencies
7353
7358
  --no-git Skip git init
7354
7359
  -y, --yes Accept defaults (needs <name>)
@@ -7724,12 +7729,103 @@ ${c("bold", "Use them")} \u2014 import into any page:
7724
7729
  return;
7725
7730
  }
7726
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
+ }
7727
7815
  async function main() {
7728
7816
  const raw = process$1.argv.slice(2);
7729
7817
  if (raw[0] === "add") {
7730
7818
  runAdd(raw.slice(1));
7731
7819
  return;
7732
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
+ }
7733
7829
  const args = parseArgs(raw);
7734
7830
  if (args.help) {
7735
7831
  process$1.stdout.write(HELP + "\n");
@@ -7748,10 +7844,32 @@ ${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a go
7748
7844
  let templateKey = args.template ?? recipe?.template;
7749
7845
  let mode = args.mode ?? recipe?.mode ?? "static";
7750
7846
  const featureKeys = [...recipe?.features ?? [], ...args.features];
7847
+ let usedRecipe = Boolean(recipe);
7751
7848
  if (!args.yes && process$1.stdin.isTTY) {
7752
7849
  const rl = promises.createInterface({ input: process$1.stdin, output: process$1.stdout });
7753
7850
  try {
7754
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
+ }
7755
7873
  if (!templateKey) {
7756
7874
  process$1.stdout.write(`
7757
7875
  Choose a template:
@@ -7763,7 +7881,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
7763
7881
  const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
7764
7882
  templateKey = TEMPLATES[idx]?.key ?? "personal";
7765
7883
  }
7766
- if (args.mode === void 0 && !recipe) {
7884
+ if (args.mode === void 0 && !usedRecipe) {
7767
7885
  process$1.stdout.write(`
7768
7886
  What kind of app?
7769
7887
  `);
@@ -7822,6 +7940,19 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
7822
7940
  `);
7823
7941
  }
7824
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
+ }
7825
7956
  for (const [rel, content] of Object.entries(files)) {
7826
7957
  const full = path.join(dir, rel);
7827
7958
  fs.mkdirSync(path.dirname(full), { recursive: true });
package/dist/lib.js CHANGED
@@ -7218,7 +7218,7 @@ function backendFiles(ctx) {
7218
7218
  }
7219
7219
  var splitList = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
7220
7220
  function parseArgs(list) {
7221
- 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 };
7222
7222
  for (let i = 0; i < list.length; i++) {
7223
7223
  const arg = list[i];
7224
7224
  const next = () => list[++i] ?? "";
@@ -7226,8 +7226,9 @@ function parseArgs(list) {
7226
7226
  else if (arg === "-y" || arg === "--yes") a.yes = true;
7227
7227
  else if (arg === "--no-install") a.install = false;
7228
7228
  else if (arg === "--no-git") a.git = false;
7229
+ else if (arg === "--dry-run" || arg === "--dry") a.dryRun = true;
7229
7230
  else if (arg === "--pm") a.pm = next();
7230
- else if (arg === "--theme" || arg === "--accent") a.theme = next();
7231
+ else if (arg === "--theme" || arg === "--accent" || arg === "--brand") a.theme = next();
7231
7232
  else if (arg === "--with" || arg === "--features") a.features.push(...splitList(next()));
7232
7233
  else if (arg === "--fullstack" || arg === "--full-stack" || arg === "--dynamic") a.mode = "dynamic";
7233
7234
  else if (arg === "--static") a.mode = "static";
@@ -7243,6 +7244,7 @@ function parseArgs(list) {
7243
7244
  else if (arg.startsWith("--template=")) a.template = arg.slice(11);
7244
7245
  else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
7245
7246
  else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
7247
+ else if (arg.startsWith("--brand=")) a.theme = arg.slice(8);
7246
7248
  else if (arg.startsWith("--with=")) a.features.push(...splitList(arg.slice(7)));
7247
7249
  else if (arg.startsWith("--features=")) a.features.push(...splitList(arg.slice(11)));
7248
7250
  else if (!arg.startsWith("-") && !a.name) a.name = arg;
@@ -7333,6 +7335,8 @@ ${c("bold", "Usage")}
7333
7335
  npm create lacspace-app@latest <name> [options]
7334
7336
  npx create-lacspace-app <name> --template <key>
7335
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")}
7336
7340
 
7337
7341
  ${c("bold", "Templates")}
7338
7342
  ${TEMPLATES.map((t) => ` ${t.key.padEnd(10)} ${t.description}`).join("\n")}
@@ -7344,8 +7348,9 @@ ${c("bold", "Options")}
7344
7348
  ${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
7345
7349
  --with <a,b> Feature add-ons, comma-separated (alias --features)
7346
7350
  --recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
7347
- --theme <name|hex> Accent: a preset, a "#hex", or "from,to" (e.g. --theme lacspace)
7351
+ --theme <name|hex> Accent: a preset, a "#hex", or "from,to" (alias --brand)
7348
7352
  --pm <npm|pnpm|yarn|bun> Package manager (default npm)
7353
+ --dry-run Print the files that would be created; write nothing
7349
7354
  --no-install Skip installing dependencies
7350
7355
  --no-git Skip git init
7351
7356
  -y, --yes Accept defaults (needs <name>)
@@ -7721,12 +7726,103 @@ ${c("bold", "Use them")} \u2014 import into any page:
7721
7726
  return;
7722
7727
  }
7723
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
+ }
7724
7812
  async function main() {
7725
7813
  const raw = argv.slice(2);
7726
7814
  if (raw[0] === "add") {
7727
7815
  runAdd(raw.slice(1));
7728
7816
  return;
7729
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
+ }
7730
7826
  const args = parseArgs(raw);
7731
7827
  if (args.help) {
7732
7828
  stdout.write(HELP + "\n");
@@ -7745,10 +7841,32 @@ ${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a go
7745
7841
  let templateKey = args.template ?? recipe?.template;
7746
7842
  let mode = args.mode ?? recipe?.mode ?? "static";
7747
7843
  const featureKeys = [...recipe?.features ?? [], ...args.features];
7844
+ let usedRecipe = Boolean(recipe);
7748
7845
  if (!args.yes && stdin.isTTY) {
7749
7846
  const rl = createInterface({ input: stdin, output: stdout });
7750
7847
  try {
7751
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
+ }
7752
7870
  if (!templateKey) {
7753
7871
  stdout.write(`
7754
7872
  Choose a template:
@@ -7760,7 +7878,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
7760
7878
  const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
7761
7879
  templateKey = TEMPLATES[idx]?.key ?? "personal";
7762
7880
  }
7763
- if (args.mode === void 0 && !recipe) {
7881
+ if (args.mode === void 0 && !usedRecipe) {
7764
7882
  stdout.write(`
7765
7883
  What kind of app?
7766
7884
  `);
@@ -7819,6 +7937,19 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
7819
7937
  `);
7820
7938
  }
7821
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
+ }
7822
7953
  for (const [rel, content] of Object.entries(files)) {
7823
7954
  const full = join(dir, rel);
7824
7955
  mkdirSync(dirname(full), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lacspace-app",
3
- "version": "2.8.0",
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": {