create-lacspace-app 2.6.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 +11 -0
- package/dist/index.js +459 -10
- package/dist/lib.cjs +467 -9
- package/dist/lib.d.cts +30 -1
- package/dist/lib.d.ts +30 -1
- package/dist/lib.js +466 -10
- package/package.json +1 -1
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
|
}
|
|
@@ -2213,6 +2214,71 @@ var FEATURES = [
|
|
|
2213
2214
|
"It's cookieless and stores no personal data \u2014 privacy-first by default."
|
|
2214
2215
|
],
|
|
2215
2216
|
learn: "https://developer.lacspace.com/packages/analytics-lite"
|
|
2217
|
+
},
|
|
2218
|
+
{
|
|
2219
|
+
key: "payments",
|
|
2220
|
+
label: "Payments (Nepal)",
|
|
2221
|
+
description: "A checkout wired to eSewa & Khalti \u2014 orders, the signed eSewa flow (works in TEST with NO credentials), Khalti when keyed, integer-safe money. Full-stack.",
|
|
2222
|
+
requiresBackend: true,
|
|
2223
|
+
deps: {},
|
|
2224
|
+
files: () => ({
|
|
2225
|
+
"app/checkout/page.tsx": checkoutPage(),
|
|
2226
|
+
"app/checkout/success/page.tsx": checkoutSuccessPage(),
|
|
2227
|
+
"app/checkout/failed/page.tsx": checkoutFailedPage()
|
|
2228
|
+
}),
|
|
2229
|
+
backend: () => ({
|
|
2230
|
+
deps: { "@lacspace/esewa": "^1.2.0", "@lacspace/khalti": "^1.1.0", "@lacspace/money": "^1.1.0" },
|
|
2231
|
+
files: {
|
|
2232
|
+
"src/models/order.ts": ordersModel(),
|
|
2233
|
+
"src/routes/checkout.ts": checkoutRoutes()
|
|
2234
|
+
},
|
|
2235
|
+
routes: [{ path: "/checkout", handler: "checkoutRoutes", auth: true, importLine: 'import checkoutRoutes from "./checkout.js";' }],
|
|
2236
|
+
env: {
|
|
2237
|
+
ESEWA_MERCHANT_CODE: "eSewa merchant/product code (blank = eSewa TEST sandbox).",
|
|
2238
|
+
ESEWA_SECRET: "eSewa secret key (blank = TEST sandbox \u2014 payments work end-to-end in test).",
|
|
2239
|
+
KHALTI_SECRET: "Khalti secret key (required to enable Khalti; blank shows a 'configure Khalti' message)."
|
|
2240
|
+
}
|
|
2241
|
+
}),
|
|
2242
|
+
nextSteps: [
|
|
2243
|
+
"Sign in, then open http://localhost:3000/checkout and pay with eSewa \u2014 it works in TEST mode with no credentials.",
|
|
2244
|
+
"Enable Khalti by setting KHALTI_SECRET in .env (get a test key from https://khalti.com).",
|
|
2245
|
+
"Go live: set ESEWA_MERCHANT_CODE + ESEWA_SECRET (and a live KHALTI_SECRET)."
|
|
2246
|
+
],
|
|
2247
|
+
learn: "https://developer.lacspace.com/packages/esewa"
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
key: "email",
|
|
2251
|
+
label: "Email",
|
|
2252
|
+
description: "Transactional email \u2014 a ready mail service (@lacspace/mailer) with beautiful templates + address validation. Logs to the console until you add SMTP. Full-stack.",
|
|
2253
|
+
requiresBackend: true,
|
|
2254
|
+
deps: {},
|
|
2255
|
+
files: () => ({ "app/email-test/page.tsx": emailTestPage() }),
|
|
2256
|
+
backend: (ctx) => ({
|
|
2257
|
+
deps: {
|
|
2258
|
+
"@lacspace/mailer": "^1.2.0",
|
|
2259
|
+
"@lacspace/email-templates": "^1.1.0",
|
|
2260
|
+
"@lacspace/email-validate": "^1.1.0"
|
|
2261
|
+
},
|
|
2262
|
+
files: {
|
|
2263
|
+
"src/mail/mailer.ts": mailerLib(ctx),
|
|
2264
|
+
"src/routes/email.ts": emailRoutes()
|
|
2265
|
+
},
|
|
2266
|
+
routes: [{ path: "/email", handler: "emailRoutes", auth: true, importLine: 'import emailRoutes from "./email.js";' }],
|
|
2267
|
+
env: {
|
|
2268
|
+
SMTP_HOST: "SMTP host (blank in dev \u2014 emails are logged to the API console instead of sent).",
|
|
2269
|
+
SMTP_PORT: "SMTP port (default 587).",
|
|
2270
|
+
SMTP_SECURE: "true for implicit TLS on port 465; otherwise false.",
|
|
2271
|
+
SMTP_USER: "SMTP username.",
|
|
2272
|
+
SMTP_PASS: "SMTP password.",
|
|
2273
|
+
SMTP_FROM: 'Default From address, e.g. "Acme <no-reply@acme.com>".'
|
|
2274
|
+
}
|
|
2275
|
+
}),
|
|
2276
|
+
nextSteps: [
|
|
2277
|
+
"Sign in, then open http://localhost:3000/email-test and send yourself a sample email.",
|
|
2278
|
+
"With no SMTP_* set, the email is printed to the API console (dev). Set SMTP_* in .env to send for real.",
|
|
2279
|
+
"Reuse the helpers in backend/src/mail/mailer.ts: sendWelcome / sendVerify / sendReset."
|
|
2280
|
+
],
|
|
2281
|
+
learn: "https://developer.lacspace.com/packages/mailer"
|
|
2216
2282
|
}
|
|
2217
2283
|
];
|
|
2218
2284
|
var aiChatRoute = (_ctx) => `import { resolveConfig } from "@lacspace/providers";
|
|
@@ -5191,6 +5257,13 @@ var faqSection = (ctx) => {
|
|
|
5191
5257
|
</div>
|
|
5192
5258
|
</section>`;
|
|
5193
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
|
+
];
|
|
5194
5267
|
var contentLib = () => `import fs from "node:fs";
|
|
5195
5268
|
import path from "node:path";
|
|
5196
5269
|
import { parseFrontmatter, markdownToHtml, excerpt } from "@lacspace/markdown";
|
|
@@ -5831,6 +5904,365 @@ export default function AnalyticsPage() {
|
|
|
5831
5904
|
);
|
|
5832
5905
|
}
|
|
5833
5906
|
`;
|
|
5907
|
+
var ordersModel = () => `import mongoose from "mongoose";
|
|
5908
|
+
import { uuidv7 } from "@lacspace/id";
|
|
5909
|
+
|
|
5910
|
+
export interface OrderDoc {
|
|
5911
|
+
_id: string;
|
|
5912
|
+
userId: string;
|
|
5913
|
+
label: string;
|
|
5914
|
+
amountPaisa: number;
|
|
5915
|
+
currency: string;
|
|
5916
|
+
status: "pending" | "paid" | "failed";
|
|
5917
|
+
gateway: string;
|
|
5918
|
+
ref: string;
|
|
5919
|
+
createdAt: Date;
|
|
5920
|
+
updatedAt: Date;
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5923
|
+
const schema = new mongoose.Schema<OrderDoc>(
|
|
5924
|
+
{
|
|
5925
|
+
_id: { type: String, default: () => uuidv7() },
|
|
5926
|
+
userId: { type: String, required: true, index: true },
|
|
5927
|
+
label: { type: String, required: true },
|
|
5928
|
+
amountPaisa: { type: Number, required: true },
|
|
5929
|
+
currency: { type: String, default: "NPR" },
|
|
5930
|
+
status: { type: String, enum: ["pending", "paid", "failed"], default: "pending" },
|
|
5931
|
+
gateway: { type: String, default: "" },
|
|
5932
|
+
ref: { type: String, default: "" },
|
|
5933
|
+
},
|
|
5934
|
+
{ timestamps: true },
|
|
5935
|
+
);
|
|
5936
|
+
|
|
5937
|
+
export const Order =
|
|
5938
|
+
(mongoose.models.Order as mongoose.Model<OrderDoc>) ?? mongoose.model<OrderDoc>("Order", schema);
|
|
5939
|
+
`;
|
|
5940
|
+
var checkoutRoutes = () => `import express from "express";
|
|
5941
|
+
import {
|
|
5942
|
+
buildForm, verifyResponse, paisaToRupees, generateTransactionUuid,
|
|
5943
|
+
ESEWA_TEST_SECRET, ESEWA_TEST_PRODUCT_CODE,
|
|
5944
|
+
} from "@lacspace/esewa";
|
|
5945
|
+
import { initiate, lookup } from "@lacspace/khalti";
|
|
5946
|
+
import { Money, formatBasic } from "@lacspace/money";
|
|
5947
|
+
import { v } from "@lacspace/validate";
|
|
5948
|
+
import { asyncHandler, HttpError } from "../http.js";
|
|
5949
|
+
import { env } from "../env.js";
|
|
5950
|
+
import { Order } from "../models/order.js";
|
|
5951
|
+
|
|
5952
|
+
// how this works: create an order, then start a gateway payment. eSewa works in TEST
|
|
5953
|
+
// mode with the package's baked-in sandbox keys (no credentials!). Khalti needs a real
|
|
5954
|
+
// secret. After the customer returns, the frontend calls /verify \u2014 never trust the
|
|
5955
|
+
// redirect alone, always confirm server-side.
|
|
5956
|
+
const router = express.Router();
|
|
5957
|
+
const web = () => env.CORS_ORIGIN.split(",")[0]!.trim();
|
|
5958
|
+
|
|
5959
|
+
const CreateInput = v.object({ label: v.string().min(1).max(120), amount: v.number().positive() });
|
|
5960
|
+
|
|
5961
|
+
// POST /checkout \u2014 create a pending order (amount in RUPEES from the UI \u2192 stored as paisa).
|
|
5962
|
+
router.post("/", asyncHandler(async (req, res) => {
|
|
5963
|
+
const { label, amount } = CreateInput.parse(req.body);
|
|
5964
|
+
const amountPaisa = Money.of(amount, "NPR").toMinor();
|
|
5965
|
+
const order = await Order.create({ userId: req.user!.sub, label, amountPaisa });
|
|
5966
|
+
res.status(201).json({ id: String(order._id), label, amountPaisa, display: formatBasic(Money.fromMinor(amountPaisa, "NPR")) });
|
|
5967
|
+
}));
|
|
5968
|
+
|
|
5969
|
+
// POST /checkout/:id/esewa \u2014 build the signed eSewa form to auto-POST from the browser.
|
|
5970
|
+
router.post("/:id/esewa", asyncHandler(async (req, res) => {
|
|
5971
|
+
const order = await Order.findOne({ _id: req.params.id, userId: req.user!.sub });
|
|
5972
|
+
if (!order) throw new HttpError(404, "Order not found");
|
|
5973
|
+
const secret = process.env.ESEWA_SECRET ?? ESEWA_TEST_SECRET;
|
|
5974
|
+
const productCode = process.env.ESEWA_MERCHANT_CODE ?? ESEWA_TEST_PRODUCT_CODE;
|
|
5975
|
+
const esewaEnv: "test" | "prod" = process.env.ESEWA_SECRET ? "prod" : "test";
|
|
5976
|
+
const uuid = generateTransactionUuid();
|
|
5977
|
+
order.ref = uuid;
|
|
5978
|
+
order.gateway = "esewa";
|
|
5979
|
+
await order.save();
|
|
5980
|
+
const form = await buildForm(
|
|
5981
|
+
{
|
|
5982
|
+
amount: paisaToRupees(order.amountPaisa),
|
|
5983
|
+
transactionUuid: uuid,
|
|
5984
|
+
productCode,
|
|
5985
|
+
successUrl: web() + "/checkout/success?orderId=" + String(order._id),
|
|
5986
|
+
failureUrl: web() + "/checkout/failed?orderId=" + String(order._id),
|
|
5987
|
+
},
|
|
5988
|
+
{ secret, env: esewaEnv },
|
|
5989
|
+
);
|
|
5990
|
+
res.json(form);
|
|
5991
|
+
}));
|
|
5992
|
+
|
|
5993
|
+
// POST /checkout/:id/esewa/verify \u2014 verify the base64 \`data\` eSewa returned; mark paid.
|
|
5994
|
+
router.post("/:id/esewa/verify", asyncHandler(async (req, res) => {
|
|
5995
|
+
const { data } = v.object({ data: v.string().min(1) }).parse(req.body);
|
|
5996
|
+
const order = await Order.findOne({ _id: req.params.id, userId: req.user!.sub });
|
|
5997
|
+
if (!order) throw new HttpError(404, "Order not found");
|
|
5998
|
+
const secret = process.env.ESEWA_SECRET ?? ESEWA_TEST_SECRET;
|
|
5999
|
+
const result = await verifyResponse(data, secret);
|
|
6000
|
+
const status = String((result.data as { status?: string }).status ?? "");
|
|
6001
|
+
if (!result.valid || status !== "COMPLETE") throw new HttpError(400, "Payment could not be verified");
|
|
6002
|
+
order.status = "paid";
|
|
6003
|
+
await order.save();
|
|
6004
|
+
res.json({ id: String(order._id), status: "paid" });
|
|
6005
|
+
}));
|
|
6006
|
+
|
|
6007
|
+
// POST /checkout/:id/khalti \u2014 start a Khalti payment (needs KHALTI_SECRET, else 501).
|
|
6008
|
+
router.post("/:id/khalti", asyncHandler(async (req, res) => {
|
|
6009
|
+
const secretKey = process.env.KHALTI_SECRET;
|
|
6010
|
+
if (!secretKey) throw new HttpError(501, "Set KHALTI_SECRET in .env to enable Khalti (see .env.example).");
|
|
6011
|
+
const order = await Order.findOne({ _id: req.params.id, userId: req.user!.sub });
|
|
6012
|
+
if (!order) throw new HttpError(404, "Order not found");
|
|
6013
|
+
order.gateway = "khalti";
|
|
6014
|
+
await order.save();
|
|
6015
|
+
const r = await initiate(
|
|
6016
|
+
{
|
|
6017
|
+
return_url: web() + "/checkout/success?orderId=" + String(order._id),
|
|
6018
|
+
website_url: web(),
|
|
6019
|
+
amount: order.amountPaisa,
|
|
6020
|
+
purchase_order_id: String(order._id),
|
|
6021
|
+
purchase_order_name: order.label,
|
|
6022
|
+
},
|
|
6023
|
+
{ secretKey, env: "test" },
|
|
6024
|
+
);
|
|
6025
|
+
order.ref = r.pidx;
|
|
6026
|
+
await order.save();
|
|
6027
|
+
res.json({ paymentUrl: r.payment_url, pidx: r.pidx });
|
|
6028
|
+
}));
|
|
6029
|
+
|
|
6030
|
+
// POST /checkout/:id/khalti/verify \u2014 authoritative lookup by pidx; mark paid.
|
|
6031
|
+
router.post("/:id/khalti/verify", asyncHandler(async (req, res) => {
|
|
6032
|
+
const secretKey = process.env.KHALTI_SECRET;
|
|
6033
|
+
if (!secretKey) throw new HttpError(501, "Khalti is not configured");
|
|
6034
|
+
const { pidx } = v.object({ pidx: v.string().min(1) }).parse(req.body);
|
|
6035
|
+
const order = await Order.findOne({ _id: req.params.id, userId: req.user!.sub });
|
|
6036
|
+
if (!order) throw new HttpError(404, "Order not found");
|
|
6037
|
+
const r = await lookup(pidx, { secretKey, env: "test" });
|
|
6038
|
+
if (r.status !== "Completed") throw new HttpError(400, "Payment status: " + String(r.status));
|
|
6039
|
+
order.status = "paid";
|
|
6040
|
+
await order.save();
|
|
6041
|
+
res.json({ id: String(order._id), status: "paid" });
|
|
6042
|
+
}));
|
|
6043
|
+
|
|
6044
|
+
export default router;
|
|
6045
|
+
`;
|
|
6046
|
+
var checkoutPage = () => `"use client";
|
|
6047
|
+
import { useState } from "react";
|
|
6048
|
+
import { getToken } from "@/lib/api";
|
|
6049
|
+
|
|
6050
|
+
const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
6051
|
+
const msgOf = (e: unknown) => (e instanceof Error ? e.message : "Something went wrong");
|
|
6052
|
+
|
|
6053
|
+
async function call<T>(path: string, body: unknown): Promise<T> {
|
|
6054
|
+
const token = getToken();
|
|
6055
|
+
const res = await fetch(API + path, {
|
|
6056
|
+
method: "POST",
|
|
6057
|
+
headers: { "Content-Type": "application/json", ...(token ? { Authorization: "Bearer " + token } : {}) },
|
|
6058
|
+
body: JSON.stringify(body),
|
|
6059
|
+
});
|
|
6060
|
+
const data: unknown = await res.json().catch(() => ({}));
|
|
6061
|
+
if (!res.ok) throw new Error((data as { error?: string }).error ?? "Request failed");
|
|
6062
|
+
return data as T;
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
export default function CheckoutPage() {
|
|
6066
|
+
const [label, setLabel] = useState("Pro plan");
|
|
6067
|
+
const [amount, setAmount] = useState(1000);
|
|
6068
|
+
const [orderId, setOrderId] = useState<string | null>(null);
|
|
6069
|
+
const [msg, setMsg] = useState<string | null>(null);
|
|
6070
|
+
|
|
6071
|
+
async function createOrder(e: React.FormEvent) {
|
|
6072
|
+
e.preventDefault();
|
|
6073
|
+
try {
|
|
6074
|
+
const o = await call<{ id: string; display: string }>("/checkout", { label, amount });
|
|
6075
|
+
setOrderId(o.id);
|
|
6076
|
+
setMsg("Order " + o.display + " created \u2014 choose how to pay.");
|
|
6077
|
+
} catch (err) { setMsg(msgOf(err)); }
|
|
6078
|
+
}
|
|
6079
|
+
async function payEsewa() {
|
|
6080
|
+
if (!orderId) return;
|
|
6081
|
+
try {
|
|
6082
|
+
const form = await call<{ action: string; fields: Record<string, string> }>("/checkout/" + orderId + "/esewa", {});
|
|
6083
|
+
const f = document.createElement("form");
|
|
6084
|
+
f.method = "POST";
|
|
6085
|
+
f.action = form.action;
|
|
6086
|
+
for (const [k, val] of Object.entries(form.fields)) {
|
|
6087
|
+
const i = document.createElement("input");
|
|
6088
|
+
i.type = "hidden"; i.name = k; i.value = val;
|
|
6089
|
+
f.appendChild(i);
|
|
6090
|
+
}
|
|
6091
|
+
document.body.appendChild(f);
|
|
6092
|
+
f.submit();
|
|
6093
|
+
} catch (err) { setMsg(msgOf(err)); }
|
|
6094
|
+
}
|
|
6095
|
+
async function payKhalti() {
|
|
6096
|
+
if (!orderId) return;
|
|
6097
|
+
try {
|
|
6098
|
+
const r = await call<{ paymentUrl: string }>("/checkout/" + orderId + "/khalti", {});
|
|
6099
|
+
window.location.href = r.paymentUrl;
|
|
6100
|
+
} catch (err) { setMsg(msgOf(err)); }
|
|
6101
|
+
}
|
|
6102
|
+
|
|
6103
|
+
return (
|
|
6104
|
+
<main className="mx-auto max-w-md px-6 py-16">
|
|
6105
|
+
<h1 className="text-2xl font-bold">Checkout</h1>
|
|
6106
|
+
<p className="mt-1 text-sm text-muted">Sign in first, then create an order and pay.</p>
|
|
6107
|
+
<form onSubmit={createOrder} className="mt-6 space-y-3 rounded-2xl border border-hairline p-5">
|
|
6108
|
+
<input value={label} onChange={(e) => setLabel(e.target.value)} placeholder="What are you buying?" className="w-full rounded-xl border border-hairline bg-surface px-4 py-2 outline-none" />
|
|
6109
|
+
<input type="number" min={10} value={amount} onChange={(e) => setAmount(Number(e.target.value))} placeholder="Amount (NPR)" className="w-full rounded-xl border border-hairline bg-surface px-4 py-2 outline-none" />
|
|
6110
|
+
<button className="w-full rounded-full gradient-bg px-4 py-2 font-semibold on-accent">Create order</button>
|
|
6111
|
+
</form>
|
|
6112
|
+
{msg && <p className="mt-4 text-sm text-muted">{msg}</p>}
|
|
6113
|
+
{orderId && (
|
|
6114
|
+
<div className="mt-6 space-y-3">
|
|
6115
|
+
<button onClick={payEsewa} className="w-full rounded-full border border-hairline px-4 py-3 font-semibold transition hover:bg-surface">Pay with eSewa <span className="text-muted">(works in test)</span></button>
|
|
6116
|
+
<button onClick={payKhalti} className="w-full rounded-full border border-hairline px-4 py-3 font-semibold transition hover:bg-surface">Pay with Khalti</button>
|
|
6117
|
+
</div>
|
|
6118
|
+
)}
|
|
6119
|
+
<p className="mt-6 text-xs text-muted">eSewa works out-of-the-box in test mode. Khalti needs KHALTI_SECRET in .env.</p>
|
|
6120
|
+
</main>
|
|
6121
|
+
);
|
|
6122
|
+
}
|
|
6123
|
+
`;
|
|
6124
|
+
var checkoutSuccessPage = () => `"use client";
|
|
6125
|
+
import { useEffect, useState } from "react";
|
|
6126
|
+
import Link from "next/link";
|
|
6127
|
+
import { getToken } from "@/lib/api";
|
|
6128
|
+
|
|
6129
|
+
const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
6130
|
+
|
|
6131
|
+
export default function CheckoutSuccessPage() {
|
|
6132
|
+
const [status, setStatus] = useState<"verifying" | "paid" | "failed">("verifying");
|
|
6133
|
+
const [detail, setDetail] = useState("");
|
|
6134
|
+
|
|
6135
|
+
useEffect(() => {
|
|
6136
|
+
const q = new URLSearchParams(window.location.search);
|
|
6137
|
+
const orderId = q.get("orderId");
|
|
6138
|
+
const data = q.get("data"); // eSewa returns a base64 \`data\` param
|
|
6139
|
+
const pidx = q.get("pidx"); // Khalti returns \`pidx\`
|
|
6140
|
+
const token = getToken();
|
|
6141
|
+
if (!orderId || !token) { setStatus("failed"); setDetail("Missing order or session \u2014 please sign in."); return; }
|
|
6142
|
+
(async () => {
|
|
6143
|
+
let path = ""; let body: Record<string, string> = {};
|
|
6144
|
+
if (data) { path = "/checkout/" + orderId + "/esewa/verify"; body = { data }; }
|
|
6145
|
+
else if (pidx) { path = "/checkout/" + orderId + "/khalti/verify"; body = { pidx }; }
|
|
6146
|
+
else { setStatus("failed"); setDetail("No payment token was returned."); return; }
|
|
6147
|
+
try {
|
|
6148
|
+
const res = await fetch(API + path, {
|
|
6149
|
+
method: "POST",
|
|
6150
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer " + token },
|
|
6151
|
+
body: JSON.stringify(body),
|
|
6152
|
+
});
|
|
6153
|
+
if (!res.ok) { const e: unknown = await res.json().catch(() => ({})); throw new Error((e as { error?: string }).error ?? "Verification failed"); }
|
|
6154
|
+
setStatus("paid");
|
|
6155
|
+
} catch (e) { setStatus("failed"); setDetail(e instanceof Error ? e.message : "Verification failed"); }
|
|
6156
|
+
})();
|
|
6157
|
+
}, []);
|
|
6158
|
+
|
|
6159
|
+
return (
|
|
6160
|
+
<main className="mx-auto max-w-md px-6 py-24 text-center">
|
|
6161
|
+
{status === "verifying" && <p className="text-muted">Verifying your payment\u2026</p>}
|
|
6162
|
+
{status === "paid" && (<><h1 className="text-3xl font-bold">Payment successful \u{1F389}</h1><p className="mt-2 text-muted">Thank you \u2014 your order is paid.</p></>)}
|
|
6163
|
+
{status === "failed" && (<><h1 className="text-2xl font-bold">Payment not completed</h1><p className="mt-2 text-muted">{detail}</p></>)}
|
|
6164
|
+
<Link href="/checkout" className="mt-6 inline-block rounded-full border border-hairline px-4 py-2 text-sm">Back to checkout</Link>
|
|
6165
|
+
</main>
|
|
6166
|
+
);
|
|
6167
|
+
}
|
|
6168
|
+
`;
|
|
6169
|
+
var checkoutFailedPage = () => `import Link from "next/link";
|
|
6170
|
+
|
|
6171
|
+
export default function CheckoutFailedPage() {
|
|
6172
|
+
return (
|
|
6173
|
+
<main className="mx-auto max-w-md px-6 py-24 text-center">
|
|
6174
|
+
<h1 className="text-2xl font-bold">Payment cancelled</h1>
|
|
6175
|
+
<p className="mt-2 text-muted">Your payment was not completed.</p>
|
|
6176
|
+
<Link href="/checkout" className="mt-6 inline-block rounded-full border border-hairline px-4 py-2 text-sm">Try again</Link>
|
|
6177
|
+
</main>
|
|
6178
|
+
);
|
|
6179
|
+
}
|
|
6180
|
+
`;
|
|
6181
|
+
var mailerLib = (ctx) => `import { createTransport, createJsonTransport, mailerFromEnv, type Transport } from "@lacspace/mailer";
|
|
6182
|
+
import { welcomeEmail, verifyEmail, passwordResetEmail, toPlainText } from "@lacspace/email-templates";
|
|
6183
|
+
|
|
6184
|
+
// how this works: if SMTP_* env vars are set, it sends real email over SMTP; otherwise
|
|
6185
|
+
// it logs the full message to the console (so the app runs with NO credentials). Adding
|
|
6186
|
+
// SMTP_* later switches to real delivery with no code change.
|
|
6187
|
+
export const transport: Transport = process.env.SMTP_HOST
|
|
6188
|
+
? createTransport(mailerFromEnv())
|
|
6189
|
+
: createJsonTransport((json) => console.log("\\n[email:dev] set SMTP_* in .env to deliver for real:\\n" + json + "\\n"));
|
|
6190
|
+
|
|
6191
|
+
const FROM = process.env.SMTP_FROM ?? ${JSON.stringify(ctx.name + " <no-reply@example.com>")};
|
|
6192
|
+
const brand = { brandName: ${JSON.stringify(ctx.name)} };
|
|
6193
|
+
|
|
6194
|
+
export function sendWelcome(to: string, name?: string) {
|
|
6195
|
+
const html = welcomeEmail({ name, message: "Thanks for joining " + brand.brandName + "!", ...brand });
|
|
6196
|
+
return transport.send({ from: FROM, to, subject: "Welcome to " + brand.brandName, html, text: toPlainText(html) });
|
|
6197
|
+
}
|
|
6198
|
+
export function sendVerify(to: string, verifyUrl: string) {
|
|
6199
|
+
const html = verifyEmail({ verifyUrl, expiresMinutes: 30, ...brand });
|
|
6200
|
+
return transport.send({ from: FROM, to, subject: "Verify your email", html, text: toPlainText(html) });
|
|
6201
|
+
}
|
|
6202
|
+
export function sendReset(to: string, resetUrl: string) {
|
|
6203
|
+
const html = passwordResetEmail({ resetUrl, expiresMinutes: 30, ...brand });
|
|
6204
|
+
return transport.send({ from: FROM, to, subject: "Reset your password", html, text: toPlainText(html) });
|
|
6205
|
+
}
|
|
6206
|
+
`;
|
|
6207
|
+
var emailRoutes = () => `import express from "express";
|
|
6208
|
+
import { validateEmail } from "@lacspace/email-validate";
|
|
6209
|
+
import { asyncHandler, HttpError } from "../http.js";
|
|
6210
|
+
import { sendWelcome } from "../mail/mailer.js";
|
|
6211
|
+
|
|
6212
|
+
const router = express.Router();
|
|
6213
|
+
|
|
6214
|
+
// POST /email/test \u2014 send a sample welcome email (validates the address first).
|
|
6215
|
+
router.post("/test", asyncHandler(async (req, res) => {
|
|
6216
|
+
const to = String((req.body ?? {}).to ?? "").trim();
|
|
6217
|
+
const check = validateEmail(to);
|
|
6218
|
+
if (!check.valid) throw new HttpError(400, check.reason ?? "Invalid email address");
|
|
6219
|
+
const result = await sendWelcome(check.normalized ?? to);
|
|
6220
|
+
res.json({ ok: true, messageId: result.messageId });
|
|
6221
|
+
}));
|
|
6222
|
+
|
|
6223
|
+
export default router;
|
|
6224
|
+
`;
|
|
6225
|
+
var emailTestPage = () => `"use client";
|
|
6226
|
+
import { useState } from "react";
|
|
6227
|
+
import { getToken } from "@/lib/api";
|
|
6228
|
+
|
|
6229
|
+
const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
6230
|
+
|
|
6231
|
+
export default function EmailTestPage() {
|
|
6232
|
+
const [to, setTo] = useState("");
|
|
6233
|
+
const [msg, setMsg] = useState<string | null>(null);
|
|
6234
|
+
const [busy, setBusy] = useState(false);
|
|
6235
|
+
|
|
6236
|
+
async function send(e: React.FormEvent) {
|
|
6237
|
+
e.preventDefault();
|
|
6238
|
+
setBusy(true); setMsg(null);
|
|
6239
|
+
try {
|
|
6240
|
+
const token = getToken();
|
|
6241
|
+
const res = await fetch(API + "/email/test", {
|
|
6242
|
+
method: "POST",
|
|
6243
|
+
headers: { "Content-Type": "application/json", ...(token ? { Authorization: "Bearer " + token } : {}) },
|
|
6244
|
+
body: JSON.stringify({ to }),
|
|
6245
|
+
});
|
|
6246
|
+
const data: unknown = await res.json().catch(() => ({}));
|
|
6247
|
+
if (!res.ok) throw new Error((data as { error?: string }).error ?? "Failed to send");
|
|
6248
|
+
setMsg("Sent! If no SMTP is configured, check the API console for the email.");
|
|
6249
|
+
} catch (err) { setMsg(err instanceof Error ? err.message : "Failed"); }
|
|
6250
|
+
finally { setBusy(false); }
|
|
6251
|
+
}
|
|
6252
|
+
|
|
6253
|
+
return (
|
|
6254
|
+
<main className="mx-auto max-w-md px-6 py-16">
|
|
6255
|
+
<h1 className="text-2xl font-bold">Send a test email</h1>
|
|
6256
|
+
<p className="mt-1 text-muted">With no SMTP configured, the email is logged to the API console.</p>
|
|
6257
|
+
<form onSubmit={send} className="mt-6 space-y-3">
|
|
6258
|
+
<input type="email" required value={to} onChange={(e) => setTo(e.target.value)} placeholder="you@example.com" className="w-full rounded-xl border border-hairline bg-surface px-4 py-3 outline-none" />
|
|
6259
|
+
<button disabled={busy} className="w-full rounded-full gradient-bg px-4 py-3 font-semibold on-accent disabled:opacity-60">{busy ? "Sending\u2026" : "Send test email"}</button>
|
|
6260
|
+
</form>
|
|
6261
|
+
{msg && <p className="mt-4 text-sm text-muted">{msg}</p>}
|
|
6262
|
+
</main>
|
|
6263
|
+
);
|
|
6264
|
+
}
|
|
6265
|
+
`;
|
|
5834
6266
|
var scope = (ctx) => `@${ctx.name}`;
|
|
5835
6267
|
var typesPkgJson = (ctx) => JSON.stringify({
|
|
5836
6268
|
name: `${scope(ctx)}/types`,
|
|
@@ -6808,7 +7240,9 @@ function parseArgs(list) {
|
|
|
6808
7240
|
} else if (arg.startsWith("--mode=")) {
|
|
6809
7241
|
const m = arg.slice(7).toLowerCase();
|
|
6810
7242
|
a.mode = m === "dynamic" ? "dynamic" : "static";
|
|
6811
|
-
} else if (arg === "
|
|
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;
|
|
6812
7246
|
else if (arg.startsWith("--template=")) a.template = arg.slice(11);
|
|
6813
7247
|
else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
|
|
6814
7248
|
else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
|
|
@@ -6912,6 +7346,7 @@ ${c("bold", "Options")}
|
|
|
6912
7346
|
MongoDB/Redis backend (JWT auth + CRUD) + shared types
|
|
6913
7347
|
${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
|
|
6914
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(" | ")})
|
|
6915
7350
|
--theme <name|hex> Accent: a preset, a "#hex", or "from,to" (e.g. --theme lacspace)
|
|
6916
7351
|
--pm <npm|pnpm|yarn|bun> Package manager (default npm)
|
|
6917
7352
|
--no-install Skip installing dependencies
|
|
@@ -6920,9 +7355,13 @@ ${c("bold", "Options")}
|
|
|
6920
7355
|
-h, --help Show this help
|
|
6921
7356
|
|
|
6922
7357
|
${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
|
|
6923
|
-
${FEATURES.map((f) => ` ${f.key.padEnd(
|
|
7358
|
+
${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
|
|
6924
7359
|
${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
|
|
6925
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
|
+
|
|
6926
7365
|
${c("bold", "Themes")} ${c("dim", "(--theme)")}
|
|
6927
7366
|
${Object.keys(THEMES).join(" \xB7 ")}
|
|
6928
7367
|
${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
|
|
@@ -7299,11 +7738,16 @@ async function main() {
|
|
|
7299
7738
|
process$1.stdout.write(`
|
|
7300
7739
|
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
|
|
7301
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)}
|
|
7302
7746
|
`);
|
|
7303
7747
|
let name = args.name;
|
|
7304
|
-
let templateKey = args.template;
|
|
7305
|
-
let mode = args.mode ?? "static";
|
|
7306
|
-
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];
|
|
7307
7751
|
if (!args.yes && process$1.stdin.isTTY) {
|
|
7308
7752
|
const rl = promises.createInterface({ input: process$1.stdin, output: process$1.stdout });
|
|
7309
7753
|
try {
|
|
@@ -7319,7 +7763,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
|
|
|
7319
7763
|
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
|
|
7320
7764
|
templateKey = TEMPLATES[idx]?.key ?? "personal";
|
|
7321
7765
|
}
|
|
7322
|
-
if (args.mode === void 0) {
|
|
7766
|
+
if (args.mode === void 0 && !recipe) {
|
|
7323
7767
|
process$1.stdout.write(`
|
|
7324
7768
|
What kind of app?
|
|
7325
7769
|
`);
|
|
@@ -7372,6 +7816,11 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
|
|
|
7372
7816
|
return;
|
|
7373
7817
|
}
|
|
7374
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
|
+
}
|
|
7375
7824
|
const files = buildFiles({ name: projectName, template, features, mode });
|
|
7376
7825
|
for (const [rel, content] of Object.entries(files)) {
|
|
7377
7826
|
const full = path.join(dir, rel);
|
|
@@ -7483,6 +7932,13 @@ function getFeature(key) {
|
|
|
7483
7932
|
const f = FEATURES.find((x) => x.key === key);
|
|
7484
7933
|
return f ? { ...f } : void 0;
|
|
7485
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
|
+
}
|
|
7486
7942
|
function generateProject(options = {}) {
|
|
7487
7943
|
return buildFiles(resolveContext(options));
|
|
7488
7944
|
}
|
|
@@ -7500,9 +7956,11 @@ async function scaffold(options = {}) {
|
|
|
7500
7956
|
|
|
7501
7957
|
exports.generateProject = generateProject;
|
|
7502
7958
|
exports.getFeature = getFeature;
|
|
7959
|
+
exports.getRecipe = getRecipe;
|
|
7503
7960
|
exports.getSection = getSection;
|
|
7504
7961
|
exports.getTemplate = getTemplate;
|
|
7505
7962
|
exports.listFeatures = listFeatures;
|
|
7963
|
+
exports.listRecipes = listRecipes;
|
|
7506
7964
|
exports.listSections = listSections;
|
|
7507
7965
|
exports.listTemplates = listTemplates;
|
|
7508
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 };
|