create-nymbal-app 0.0.0 → 0.2.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/bin/create-nymbal-app.mjs +9 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +419 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -7
- package/scaffold/project/astro/src/pages/index.astro +57 -0
- package/scaffold/templates/astro/README.md +26 -0
- package/scaffold/templates/astro/_dot.env.example +6 -0
- package/scaffold/templates/astro/_dot.gitignore +16 -0
- package/scaffold/templates/astro/_dot.node-version +1 -0
- package/scaffold/templates/astro/_dot.npmrc +1 -0
- package/scaffold/templates/astro/astro.config.mjs +14 -0
- package/scaffold/templates/astro/package.json +59 -0
- package/scaffold/templates/astro/src/components/AddressForm.astro +107 -0
- package/scaffold/templates/astro/src/components/Breadcrumb.astro +20 -0
- package/scaffold/templates/astro/src/components/CategoryNav.astro +27 -0
- package/scaffold/templates/astro/src/components/CheckoutSteps.astro +29 -0
- package/scaffold/templates/astro/src/components/Footer.astro +108 -0
- package/scaffold/templates/astro/src/components/Header.astro +192 -0
- package/scaffold/templates/astro/src/components/HeroSection.astro +17 -0
- package/scaffold/templates/astro/src/components/ImageGallery.astro +42 -0
- package/scaffold/templates/astro/src/components/NewsletterSignup.astro +39 -0
- package/scaffold/templates/astro/src/components/OrderSummary.astro +53 -0
- package/scaffold/templates/astro/src/components/Pagination.astro +13 -0
- package/scaffold/templates/astro/src/components/PriceDisplay.astro +25 -0
- package/scaffold/templates/astro/src/components/ProductCard.astro +44 -0
- package/scaffold/templates/astro/src/components/RelatedProducts.astro +28 -0
- package/scaffold/templates/astro/src/components/ReviewForm.astro +100 -0
- package/scaffold/templates/astro/src/components/ReviewList.astro +41 -0
- package/scaffold/templates/astro/src/components/SkipNav.astro +3 -0
- package/scaffold/templates/astro/src/components/SortDropdown.astro +26 -0
- package/scaffold/templates/astro/src/components/StockBadge.astro +32 -0
- package/scaffold/templates/astro/src/env.d.ts +2 -0
- package/scaffold/templates/astro/src/layouts/AccountLayout.astro +168 -0
- package/scaffold/templates/astro/src/layouts/BaseLayout.astro +55 -0
- package/scaffold/templates/astro/src/lib/api.ts +89 -0
- package/scaffold/templates/astro/src/lib/client.ts +13 -0
- package/scaffold/templates/astro/src/lib/format.ts +17 -0
- package/scaffold/templates/astro/src/lib/order-status.ts +14 -0
- package/scaffold/templates/astro/src/lib/seo.ts +62 -0
- package/scaffold/templates/astro/src/pages/404.astro +18 -0
- package/scaffold/templates/astro/src/pages/500.astro +17 -0
- package/scaffold/templates/astro/src/pages/account/addresses.astro +110 -0
- package/scaffold/templates/astro/src/pages/account/index.astro +67 -0
- package/scaffold/templates/astro/src/pages/account/login.astro +55 -0
- package/scaffold/templates/astro/src/pages/account/orders/[orderNumber].astro +108 -0
- package/scaffold/templates/astro/src/pages/account/orders.astro +81 -0
- package/scaffold/templates/astro/src/pages/account/profile.astro +131 -0
- package/scaffold/templates/astro/src/pages/account/register.astro +94 -0
- package/scaffold/templates/astro/src/pages/cart.astro +16 -0
- package/scaffold/templates/astro/src/pages/category/[slug].astro +73 -0
- package/scaffold/templates/astro/src/pages/checkout.astro +100 -0
- package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -0
- package/scaffold/templates/astro/src/pages/fragments/header.astro +5 -0
- package/scaffold/templates/astro/src/pages/fragments/nav.astro +9 -0
- package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +14 -0
- package/scaffold/templates/astro/src/pages/index.astro +49 -0
- package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +127 -0
- package/scaffold/templates/astro/src/pages/product/[slug].astro +107 -0
- package/scaffold/templates/astro/src/pages/products/index.astro +62 -0
- package/scaffold/templates/astro/src/pages/search.astro +37 -0
- package/scaffold/templates/astro/src/scripts/account-guard.ts +22 -0
- package/scaffold/templates/astro/src/scripts/auth-forms.ts +89 -0
- package/scaffold/templates/astro/src/scripts/cart-page.ts +143 -0
- package/scaffold/templates/astro/src/scripts/checkout.ts +277 -0
- package/scaffold/templates/astro/src/scripts/image-gallery.ts +22 -0
- package/scaffold/templates/astro/src/scripts/init-client.ts +16 -0
- package/scaffold/templates/astro/src/scripts/order-status.ts +27 -0
- package/scaffold/templates/astro/src/scripts/pdp.ts +67 -0
- package/scaffold/templates/astro/src/scripts/plp.ts +109 -0
- package/scaffold/templates/astro/src/scripts/search.ts +145 -0
- package/scaffold/templates/astro/src/styles/global.css +362 -0
- package/scaffold/templates/astro/src/styles/pages/account.css +331 -0
- package/scaffold/templates/astro/src/styles/pages/cart.css +243 -0
- package/scaffold/templates/astro/src/styles/pages/checkout.css +270 -0
- package/scaffold/templates/astro/src/styles/pages/confirmation.css +142 -0
- package/scaffold/templates/astro/src/styles/pages/error.css +69 -0
- package/scaffold/templates/astro/src/styles/pages/home.css +269 -0
- package/scaffold/templates/astro/src/styles/pages/pdp.css +272 -0
- package/scaffold/templates/astro/src/styles/pages/plp.css +213 -0
- package/scaffold/templates/astro/src/styles/pages/search.css +152 -0
- package/scaffold/templates/astro/tsconfig.json +3 -0
- package/scaffold/templates/nextjs/README.md +26 -0
- package/scaffold/templates/nextjs/_dot.env.example +6 -0
- package/scaffold/templates/nextjs/_dot.gitignore +17 -0
- package/scaffold/templates/nextjs/_dot.node-version +1 -0
- package/scaffold/templates/nextjs/_dot.npmrc +1 -0
- package/scaffold/templates/nextjs/app/account/addresses/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/account/layout.tsx +57 -0
- package/scaffold/templates/nextjs/app/account/login/page.tsx +23 -0
- package/scaffold/templates/nextjs/app/account/orders/[orderNumber]/page.tsx +192 -0
- package/scaffold/templates/nextjs/app/account/orders/page.tsx +123 -0
- package/scaffold/templates/nextjs/app/account/page.tsx +59 -0
- package/scaffold/templates/nextjs/app/account/profile/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/account/register/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/cart/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +58 -0
- package/scaffold/templates/nextjs/app/checkout/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/error.tsx +49 -0
- package/scaffold/templates/nextjs/app/globals.css +473 -0
- package/scaffold/templates/nextjs/app/layout.tsx +29 -0
- package/scaffold/templates/nextjs/app/not-found.tsx +38 -0
- package/scaffold/templates/nextjs/app/order/[orderNumber]/page.tsx +127 -0
- package/scaffold/templates/nextjs/app/page.tsx +63 -0
- package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +111 -0
- package/scaffold/templates/nextjs/app/products/page.tsx +40 -0
- package/scaffold/templates/nextjs/app/search/page.tsx +24 -0
- package/scaffold/templates/nextjs/components/client/AddressManager.tsx +181 -0
- package/scaffold/templates/nextjs/components/client/AuthGuard.tsx +55 -0
- package/scaffold/templates/nextjs/components/client/CartDrawerToggle.tsx +39 -0
- package/scaffold/templates/nextjs/components/client/CartPage.tsx +120 -0
- package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +523 -0
- package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +68 -0
- package/scaffold/templates/nextjs/components/client/LoginForm.tsx +89 -0
- package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +12 -0
- package/scaffold/templates/nextjs/components/client/NewsletterSignup.tsx +74 -0
- package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +24 -0
- package/scaffold/templates/nextjs/components/client/OrderStatusRefresh.tsx +48 -0
- package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +148 -0
- package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +61 -0
- package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +112 -0
- package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +131 -0
- package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +116 -0
- package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +27 -0
- package/scaffold/templates/nextjs/components/client/SearchResults.tsx +141 -0
- package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +35 -0
- package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +36 -0
- package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +29 -0
- package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +60 -0
- package/scaffold/templates/nextjs/components/server/Footer.module.css +64 -0
- package/scaffold/templates/nextjs/components/server/Footer.tsx +79 -0
- package/scaffold/templates/nextjs/components/server/Header.module.css +147 -0
- package/scaffold/templates/nextjs/components/server/Header.tsx +102 -0
- package/scaffold/templates/nextjs/components/server/HeroSection.tsx +18 -0
- package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +79 -0
- package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +31 -0
- package/scaffold/templates/nextjs/components/server/ProductCard.tsx +59 -0
- package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +31 -0
- package/scaffold/templates/nextjs/components/server/ReviewList.tsx +62 -0
- package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -0
- package/scaffold/templates/nextjs/components/server/StockBadge.tsx +19 -0
- package/scaffold/templates/nextjs/lib/api.ts +89 -0
- package/scaffold/templates/nextjs/lib/client.ts +23 -0
- package/scaffold/templates/nextjs/lib/format.ts +7 -0
- package/scaffold/templates/nextjs/lib/order-status.ts +14 -0
- package/scaffold/templates/nextjs/lib/seo.ts +73 -0
- package/scaffold/templates/nextjs/next.config.mjs +9 -0
- package/scaffold/templates/nextjs/package.json +64 -0
- package/scaffold/templates/nextjs/styles/pages/account.module.css +377 -0
- package/scaffold/templates/nextjs/styles/pages/cart.module.css +240 -0
- package/scaffold/templates/nextjs/styles/pages/checkout.module.css +404 -0
- package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +237 -0
- package/scaffold/templates/nextjs/styles/pages/error.module.css +132 -0
- package/scaffold/templates/nextjs/styles/pages/home.module.css +285 -0
- package/scaffold/templates/nextjs/styles/pages/pdp.module.css +319 -0
- package/scaffold/templates/nextjs/styles/pages/plp.module.css +280 -0
- package/scaffold/templates/nextjs/styles/pages/search.module.css +192 -0
- package/scaffold/templates/nextjs/tsconfig.json +41 -0
- package/README.md +0 -4
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type PackageManager = 'npm' | 'pnpm' | 'yarn-classic' | 'yarn-berry' | 'bun';
|
|
2
|
+
interface Answers {
|
|
3
|
+
name: string;
|
|
4
|
+
directory: string;
|
|
5
|
+
currency: 'GBP' | 'USD' | 'EUR';
|
|
6
|
+
template: 'astro' | 'nextjs';
|
|
7
|
+
seedDemo: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Runs a command in `cwd` (with `env` when given, else the process environment) and rejects on a non-zero exit.
|
|
11
|
+
* Injected into `main` so tests do not shell out.
|
|
12
|
+
*/
|
|
13
|
+
type Runner = (cmd: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv) => Promise<void>;
|
|
14
|
+
interface MainDeps {
|
|
15
|
+
run?: Runner;
|
|
16
|
+
}
|
|
17
|
+
declare function detectPackageManager(): PackageManager;
|
|
18
|
+
declare const TEMPLATES: readonly ["astro", "nextjs"];
|
|
19
|
+
declare const CURRENCIES: readonly ["GBP", "USD", "EUR"];
|
|
20
|
+
interface CliFlags {
|
|
21
|
+
name?: string;
|
|
22
|
+
template?: (typeof TEMPLATES)[number];
|
|
23
|
+
currency?: (typeof CURRENCIES)[number];
|
|
24
|
+
yes: boolean;
|
|
25
|
+
/** Present (false) only with `--no-install`. */
|
|
26
|
+
install?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** A command-line flag that is missing its value or names a value outside the allowed set. */
|
|
29
|
+
declare class CliFlagError extends Error {
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
/** Parses process.argv (node and script first): --name, --template and --currency take `--flag v` or `--flag=v`. */
|
|
33
|
+
declare function parseCliFlags(argv: string[]): CliFlags;
|
|
34
|
+
declare function main(argv?: string[], deps?: MainDeps): Promise<void>;
|
|
35
|
+
declare function scaffold(dir: string, answers: Answers): Promise<void>;
|
|
36
|
+
|
|
37
|
+
export { CliFlagError, type CliFlags, type MainDeps, type PackageManager, type Runner, detectPackageManager, main, parseCliFlags, scaffold };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { cp, readFile, mkdir, readdir, rm, writeFile } from "fs/promises";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { dirname, resolve } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { spawn, spawnSync } from "child_process";
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import { cancel, confirm, intro, isCancel, note, outro, select, text } from "@clack/prompts";
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
var SCAFFOLD_ROOT = resolve(HERE, "..", "scaffold");
|
|
12
|
+
var API_PORT = 3001;
|
|
13
|
+
function newAdminCredentials() {
|
|
14
|
+
return { email: "admin@nymbal.local", password: randomBytes(16).toString("base64url") };
|
|
15
|
+
}
|
|
16
|
+
async function writeAdminEnv(dir, admin) {
|
|
17
|
+
const lines = [
|
|
18
|
+
"# The admin account the seed creates. Keep this file out of version control (.gitignore lists it).",
|
|
19
|
+
`NYMBAL_ADMIN_EMAIL=${admin.email}`,
|
|
20
|
+
`NYMBAL_ADMIN_PASSWORD=${admin.password}`,
|
|
21
|
+
""
|
|
22
|
+
];
|
|
23
|
+
await writeFile(resolve(dir, ".env"), lines.join("\n"), { encoding: "utf8", mode: 384 });
|
|
24
|
+
}
|
|
25
|
+
function adminSignIn(admin) {
|
|
26
|
+
return [
|
|
27
|
+
`Admin: http://localhost:${API_PORT}/admin/`,
|
|
28
|
+
` email ${admin.email}`,
|
|
29
|
+
` password ${admin.password} (saved in .env)`
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
function packageManagerField() {
|
|
33
|
+
const token = (process.env["npm_config_user_agent"] ?? "").split(" ")[0] ?? "";
|
|
34
|
+
const match = /^(npm|pnpm|yarn|bun)\/(\d+\.\d+\.\d+\S*)$/.exec(token);
|
|
35
|
+
if (match) return `${match[1]}@${match[2]}`;
|
|
36
|
+
const npm = spawnSync("npm", ["-v"], { encoding: "utf8", shell: process.platform === "win32" });
|
|
37
|
+
const version = npm.status === 0 ? npm.stdout.trim() : "";
|
|
38
|
+
return version ? `npm@${version}` : void 0;
|
|
39
|
+
}
|
|
40
|
+
function detectPackageManager() {
|
|
41
|
+
const ua = process.env["npm_config_user_agent"];
|
|
42
|
+
if (!ua) return "npm";
|
|
43
|
+
const token = ua.split(" ")[0] ?? "";
|
|
44
|
+
const slash = token.indexOf("/");
|
|
45
|
+
const pm = slash >= 0 ? token.slice(0, slash) : token;
|
|
46
|
+
const version = slash >= 0 ? token.slice(slash + 1) : "";
|
|
47
|
+
if (pm === "pnpm") return "pnpm";
|
|
48
|
+
if (pm === "bun") return "bun";
|
|
49
|
+
if (pm === "yarn") {
|
|
50
|
+
const major = parseInt(version.split(".")[0] ?? "1", 10);
|
|
51
|
+
return major < 2 ? "yarn-classic" : "yarn-berry";
|
|
52
|
+
}
|
|
53
|
+
return "npm";
|
|
54
|
+
}
|
|
55
|
+
function installCmd(pm) {
|
|
56
|
+
switch (pm) {
|
|
57
|
+
case "pnpm":
|
|
58
|
+
return ["pnpm", ["install"]];
|
|
59
|
+
case "bun":
|
|
60
|
+
return ["bun", ["install"]];
|
|
61
|
+
case "yarn-classic":
|
|
62
|
+
case "yarn-berry":
|
|
63
|
+
return ["yarn", ["install"]];
|
|
64
|
+
default:
|
|
65
|
+
return ["npm", ["install"]];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function seedCmd(pm) {
|
|
69
|
+
switch (pm) {
|
|
70
|
+
case "pnpm":
|
|
71
|
+
return ["pnpm", ["exec", "nymbal", "seed"]];
|
|
72
|
+
case "bun":
|
|
73
|
+
return ["bunx", ["nymbal", "seed"]];
|
|
74
|
+
case "yarn-classic":
|
|
75
|
+
return ["yarn", ["run", "nymbal", "seed"]];
|
|
76
|
+
case "yarn-berry":
|
|
77
|
+
return ["yarn", ["exec", "nymbal", "seed"]];
|
|
78
|
+
default:
|
|
79
|
+
return ["npx", ["nymbal", "seed"]];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function devCmd(pm) {
|
|
83
|
+
switch (pm) {
|
|
84
|
+
case "pnpm":
|
|
85
|
+
return "pnpm dev";
|
|
86
|
+
case "bun":
|
|
87
|
+
return "bun dev";
|
|
88
|
+
case "yarn-classic":
|
|
89
|
+
case "yarn-berry":
|
|
90
|
+
return "yarn dev";
|
|
91
|
+
default:
|
|
92
|
+
return "npm run dev";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
var TEMPLATES = ["astro", "nextjs"];
|
|
96
|
+
var CURRENCIES = ["GBP", "USD", "EUR"];
|
|
97
|
+
var CliFlagError = class extends Error {
|
|
98
|
+
constructor(message) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = "CliFlagError";
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
function oneOf(flag, value, allowed) {
|
|
104
|
+
if (allowed.includes(value)) return value;
|
|
105
|
+
throw new CliFlagError(`Unknown ${flag} "${value}". Allowed: ${allowed.join(", ")}`);
|
|
106
|
+
}
|
|
107
|
+
function parseCliFlags(argv) {
|
|
108
|
+
const flags = { yes: false };
|
|
109
|
+
const args = argv.slice(2);
|
|
110
|
+
let positional;
|
|
111
|
+
for (let i = 0; i < args.length; i++) {
|
|
112
|
+
const arg = args[i];
|
|
113
|
+
if (arg === "--yes" || arg === "-y") {
|
|
114
|
+
flags.yes = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (arg === "--no-install") {
|
|
118
|
+
flags.install = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const match = /^--(name|template|currency)(?:=(.*))?$/s.exec(arg);
|
|
122
|
+
if (match) {
|
|
123
|
+
const flag = match[1];
|
|
124
|
+
let value = match[2];
|
|
125
|
+
if (value === void 0) {
|
|
126
|
+
const next = args[i + 1];
|
|
127
|
+
if (next === void 0 || next.startsWith("-")) throw new CliFlagError(`--${flag} needs a value`);
|
|
128
|
+
value = next;
|
|
129
|
+
i++;
|
|
130
|
+
}
|
|
131
|
+
if (flag === "name") flags.name = value;
|
|
132
|
+
else if (flag === "template") flags.template = oneOf("--template", value, TEMPLATES);
|
|
133
|
+
else flags.currency = oneOf("--currency", value, CURRENCIES);
|
|
134
|
+
} else if (!arg.startsWith("-") && positional === void 0) {
|
|
135
|
+
positional = arg;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (flags.name === void 0 && positional !== void 0) flags.name = positional;
|
|
139
|
+
return flags;
|
|
140
|
+
}
|
|
141
|
+
async function main(argv = process.argv, deps = {}) {
|
|
142
|
+
const run = deps.run ?? runCmd;
|
|
143
|
+
let flags;
|
|
144
|
+
try {
|
|
145
|
+
flags = parseCliFlags(argv);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (!(err instanceof CliFlagError)) throw err;
|
|
148
|
+
process.stderr.write(`Error: ${err.message}
|
|
149
|
+
`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const pm = detectPackageManager();
|
|
153
|
+
if (flags.yes) {
|
|
154
|
+
if (!flags.name) {
|
|
155
|
+
process.stderr.write("Error: --yes requires --name or a positional name argument\n");
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const name2 = flags.name.trim();
|
|
159
|
+
const answers2 = {
|
|
160
|
+
name: name2,
|
|
161
|
+
directory: toDirectoryName(name2),
|
|
162
|
+
currency: flags.currency ?? "GBP",
|
|
163
|
+
template: flags.template ?? "astro",
|
|
164
|
+
seedDemo: true
|
|
165
|
+
};
|
|
166
|
+
const targetDir2 = resolve(process.cwd(), answers2.directory);
|
|
167
|
+
if (existsSync(targetDir2)) {
|
|
168
|
+
const entries = await readdir(targetDir2);
|
|
169
|
+
if (entries.length > 0) {
|
|
170
|
+
process.stderr.write(`Error: Directory ${answers2.directory} already exists and is not empty.
|
|
171
|
+
`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
await mkdir(targetDir2, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
await scaffold(targetDir2, answers2);
|
|
178
|
+
const admin2 = newAdminCredentials();
|
|
179
|
+
await writeAdminEnv(targetDir2, admin2);
|
|
180
|
+
process.stdout.write(`Created ${answers2.directory}
|
|
181
|
+
`);
|
|
182
|
+
if (flags.install !== false) {
|
|
183
|
+
await installAndSeed(targetDir2, pm, answers2.seedDemo, run, admin2);
|
|
184
|
+
} else {
|
|
185
|
+
process.stdout.write(`Next: cd ${answers2.directory} && ${installCmd(pm)[0]} install
|
|
186
|
+
`);
|
|
187
|
+
}
|
|
188
|
+
process.stdout.write(`${adminSignIn(admin2).join("\n")}
|
|
189
|
+
`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
intro(pc.cyan(pc.bold("\u25C6 create-nymbal-app")));
|
|
193
|
+
const name = flags.name ? flags.name : await text({
|
|
194
|
+
message: "Store name",
|
|
195
|
+
placeholder: "My Store",
|
|
196
|
+
validate: (v) => !v || v.trim().length === 0 ? "Required" : void 0
|
|
197
|
+
});
|
|
198
|
+
if (isCancel(name)) return cancel("Cancelled");
|
|
199
|
+
const currencyOptions = [
|
|
200
|
+
{ value: "GBP", label: "GBP \u2014 British Pound" },
|
|
201
|
+
{ value: "USD", label: "USD \u2014 US Dollar" },
|
|
202
|
+
{ value: "EUR", label: "EUR \u2014 Euro" }
|
|
203
|
+
];
|
|
204
|
+
const currency = await select({
|
|
205
|
+
message: "Store currency",
|
|
206
|
+
options: currencyOptions,
|
|
207
|
+
initialValue: "GBP"
|
|
208
|
+
});
|
|
209
|
+
if (isCancel(currency)) return cancel("Cancelled");
|
|
210
|
+
const templateOptions = [
|
|
211
|
+
{ value: "astro", label: "Astro (recommended \u2014 performance-first)" },
|
|
212
|
+
{ value: "nextjs", label: "Next.js (migration-friendly)" }
|
|
213
|
+
];
|
|
214
|
+
const template = await select({
|
|
215
|
+
message: "Storefront template",
|
|
216
|
+
options: templateOptions,
|
|
217
|
+
initialValue: "astro"
|
|
218
|
+
});
|
|
219
|
+
if (isCancel(template)) return cancel("Cancelled");
|
|
220
|
+
const seedDemo = await confirm({
|
|
221
|
+
message: "Generate demo content?",
|
|
222
|
+
initialValue: true
|
|
223
|
+
});
|
|
224
|
+
if (isCancel(seedDemo)) return cancel("Cancelled");
|
|
225
|
+
const shouldInstall = flags.install === false ? false : await confirm({
|
|
226
|
+
message: "Install dependencies now?",
|
|
227
|
+
initialValue: true
|
|
228
|
+
});
|
|
229
|
+
if (isCancel(shouldInstall)) return cancel("Cancelled");
|
|
230
|
+
const answers = {
|
|
231
|
+
name: String(name).trim(),
|
|
232
|
+
directory: toDirectoryName(String(name).trim()),
|
|
233
|
+
currency,
|
|
234
|
+
template,
|
|
235
|
+
seedDemo
|
|
236
|
+
};
|
|
237
|
+
const targetDir = resolve(process.cwd(), answers.directory);
|
|
238
|
+
if (existsSync(targetDir)) {
|
|
239
|
+
const entries = await readdir(targetDir);
|
|
240
|
+
if (entries.length > 0) {
|
|
241
|
+
cancel(`Directory ${answers.directory} already exists and is not empty.`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
await mkdir(targetDir, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
note(pc.dim(`Scaffolding into ${targetDir}`), "Creating project");
|
|
248
|
+
await scaffold(targetDir, answers);
|
|
249
|
+
const admin = newAdminCredentials();
|
|
250
|
+
await writeAdminEnv(targetDir, admin);
|
|
251
|
+
if (shouldInstall) {
|
|
252
|
+
await installAndSeed(targetDir, pm, answers.seedDemo, run, admin);
|
|
253
|
+
}
|
|
254
|
+
const cmd = devCmd(pm);
|
|
255
|
+
const nextSteps = [
|
|
256
|
+
` ${pc.cyan("cd")} ${answers.directory}`
|
|
257
|
+
];
|
|
258
|
+
if (!shouldInstall) {
|
|
259
|
+
const [ic] = installCmd(pm);
|
|
260
|
+
nextSteps.push(` ${pc.cyan(ic + " install")}`);
|
|
261
|
+
}
|
|
262
|
+
nextSteps.push(` ${pc.cyan(cmd)}`);
|
|
263
|
+
outro(
|
|
264
|
+
[pc.green(`\u2713 Created ${answers.directory}`), "", ...nextSteps, "", ...adminSignIn(admin)].join(
|
|
265
|
+
"\n"
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
async function installAndSeed(dir, pm, seedDemo, run, admin) {
|
|
270
|
+
const [installBin, installArgs] = installCmd(pm);
|
|
271
|
+
process.stdout.write(`Running ${installBin} install (this may take a moment)...
|
|
272
|
+
`);
|
|
273
|
+
await run(installBin, installArgs, dir);
|
|
274
|
+
if (seedDemo) {
|
|
275
|
+
process.stdout.write("Seeding demo content...\n");
|
|
276
|
+
const [seedBin, seedArgs] = seedCmd(pm);
|
|
277
|
+
await run(seedBin, seedArgs, dir, {
|
|
278
|
+
...process.env,
|
|
279
|
+
NYMBAL_ADMIN_EMAIL: admin.email,
|
|
280
|
+
NYMBAL_ADMIN_PASSWORD: admin.password
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function scaffold(dir, answers) {
|
|
285
|
+
await copyScaffoldDir(resolve(SCAFFOLD_ROOT, "templates", answers.template), dir);
|
|
286
|
+
const projectLayer = resolve(SCAFFOLD_ROOT, "project", answers.template);
|
|
287
|
+
if (existsSync(projectLayer)) {
|
|
288
|
+
await copyScaffoldDir(projectLayer, dir);
|
|
289
|
+
}
|
|
290
|
+
await writeFile(resolve(dir, "nymbal.config.ts"), renderConfig(answers), "utf8");
|
|
291
|
+
await patchPackageJson(dir, answers.directory);
|
|
292
|
+
}
|
|
293
|
+
async function patchPackageJson(dir, name) {
|
|
294
|
+
const pkgPath = resolve(dir, "package.json");
|
|
295
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
296
|
+
pkg["name"] = name;
|
|
297
|
+
pkg["private"] = true;
|
|
298
|
+
const packageManager = packageManagerField();
|
|
299
|
+
if (packageManager) pkg["packageManager"] = packageManager;
|
|
300
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
301
|
+
}
|
|
302
|
+
async function copyScaffoldDir(src, dest) {
|
|
303
|
+
if (!existsSync(src)) {
|
|
304
|
+
throw new Error(`Scaffold directory not found: ${src}`);
|
|
305
|
+
}
|
|
306
|
+
await cp(src, dest, {
|
|
307
|
+
recursive: true,
|
|
308
|
+
filter: (source) => !source.endsWith("node_modules")
|
|
309
|
+
});
|
|
310
|
+
await renameDotFiles(dest);
|
|
311
|
+
}
|
|
312
|
+
async function renameDotFiles(dir) {
|
|
313
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
if (!entry.isDirectory() && entry.name.startsWith("_dot.")) {
|
|
316
|
+
const full = resolve(dir, entry.name);
|
|
317
|
+
const renamed = resolve(dir, "." + entry.name.slice(5));
|
|
318
|
+
await writeFile(renamed, await readFile(full));
|
|
319
|
+
await rm(full);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function renderConfig(answers) {
|
|
324
|
+
return `import { defineConfig, env } from '@nymbalhq/config'
|
|
325
|
+
|
|
326
|
+
export default defineConfig({
|
|
327
|
+
store: {
|
|
328
|
+
name: '${escape(answers.name)}',
|
|
329
|
+
currency: '${answers.currency}',
|
|
330
|
+
locale: '${defaultLocale(answers.currency)}',
|
|
331
|
+
timezone: '${defaultTimezone(answers.currency)}',
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
template: '${answers.template}',
|
|
335
|
+
|
|
336
|
+
infrastructure: {
|
|
337
|
+
cloud: 'local',
|
|
338
|
+
commandStore: 'sqlite',
|
|
339
|
+
documentStore: 'in-memory',
|
|
340
|
+
eventBus: 'in-process',
|
|
341
|
+
compute: 'in-process',
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
security: {
|
|
345
|
+
adapter: 'middleware',
|
|
346
|
+
rateLimit: {
|
|
347
|
+
'/checkout': { requests: 10, window: '60s', action: 'block' },
|
|
348
|
+
'/auth/*': { requests: 5, window: '60s', action: 'challenge' },
|
|
349
|
+
'/api/*': { requests: 100, window: '60s', action: 'throttle' },
|
|
350
|
+
'/webhooks/*': { requests: 200, window: '60s', action: 'throttle' },
|
|
351
|
+
},
|
|
352
|
+
botProtection: { mode: 'managed', allowList: ['googlebot', 'bingbot', 'stripe-webhooks'] },
|
|
353
|
+
headers: {
|
|
354
|
+
hsts: true,
|
|
355
|
+
contentSecurityPolicy: 'strict',
|
|
356
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
357
|
+
xFrameOptions: 'deny',
|
|
358
|
+
},
|
|
359
|
+
csrf: true,
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
commerce: {
|
|
363
|
+
payments: { provider: 'native' },
|
|
364
|
+
email: { provider: 'native' },
|
|
365
|
+
reviews: { provider: 'native' },
|
|
366
|
+
search: { provider: 'native' },
|
|
367
|
+
analytics: { provider: 'native' },
|
|
368
|
+
shipping: { provider: 'native' },
|
|
369
|
+
tax: { provider: 'native' },
|
|
370
|
+
ai: { provider: 'anthropic', config: { apiKey: env('ANTHROPIC_API_KEY', 'anthropic-placeholder') } },
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
http: {
|
|
374
|
+
adapter: 'fastify',
|
|
375
|
+
port: ${API_PORT},
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
deployment: {
|
|
379
|
+
strategy: 'standard',
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
features: {
|
|
383
|
+
staging: false,
|
|
384
|
+
heartbeat: false,
|
|
385
|
+
autoUpdates: false,
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
function defaultLocale(currency) {
|
|
391
|
+
return currency === "USD" ? "en-US" : "en-GB";
|
|
392
|
+
}
|
|
393
|
+
function defaultTimezone(currency) {
|
|
394
|
+
return currency === "USD" ? "America/New_York" : "Europe/London";
|
|
395
|
+
}
|
|
396
|
+
function escape(v) {
|
|
397
|
+
return v.replace(/'/g, "\\'");
|
|
398
|
+
}
|
|
399
|
+
function toDirectoryName(name) {
|
|
400
|
+
return name.toLowerCase().replace(/[^a-z0-9-_ ]/g, "").trim().replace(/\s+/g, "-");
|
|
401
|
+
}
|
|
402
|
+
var runCmd = (cmd, args, cwd, env = process.env) => {
|
|
403
|
+
return new Promise((resolve2, reject) => {
|
|
404
|
+
const child = spawn(cmd, args, { cwd, env, stdio: "inherit", shell: false });
|
|
405
|
+
child.on("error", reject);
|
|
406
|
+
child.on("exit", (code) => {
|
|
407
|
+
if (code === 0) return resolve2();
|
|
408
|
+
reject(new Error(`${cmd} ${args.join(" ")} exited with ${code}`));
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
export {
|
|
413
|
+
CliFlagError,
|
|
414
|
+
detectPackageManager,
|
|
415
|
+
main,
|
|
416
|
+
parseCliFlags,
|
|
417
|
+
scaffold
|
|
418
|
+
};
|
|
419
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { cp, readFile, mkdir, readdir, rm, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { spawn, spawnSync } from 'node:child_process'\nimport { randomBytes } from 'node:crypto'\nimport { cancel, confirm, intro, isCancel, note, outro, select, text } from '@clack/prompts'\nimport pc from 'picocolors'\n\nconst HERE = dirname(fileURLToPath(import.meta.url))\nconst SCAFFOLD_ROOT = resolve(HERE, '..', 'scaffold')\n\nexport type PackageManager = 'npm' | 'pnpm' | 'yarn-classic' | 'yarn-berry' | 'bun'\n\ninterface Answers {\n name: string\n directory: string\n currency: 'GBP' | 'USD' | 'EUR'\n template: 'astro' | 'nextjs'\n seedDemo: boolean\n}\n\n/**\n * Runs a command in `cwd` (with `env` when given, else the process environment) and rejects on a non-zero exit.\n * Injected into `main` so tests do not shell out.\n */\nexport type Runner = (\n cmd: string,\n args: string[],\n cwd: string,\n env?: NodeJS.ProcessEnv,\n) => Promise<void>\n\n/** The port `renderConfig` gives the API, which serves the admin at /admin/. */\nconst API_PORT = 3001\n\ninterface AdminCredentials {\n email: string\n password: string\n}\n\n/** A fresh admin password per project (128 bits, base64url), so no two stores share the seed's default. */\nfunction newAdminCredentials(): AdminCredentials {\n return { email: 'admin@nymbal.local', password: randomBytes(16).toString('base64url') }\n}\n\n/** `.env` holds the admin credentials the seed (and a first `nymbal dev`) creates the admin from. */\nasync function writeAdminEnv(dir: string, admin: AdminCredentials): Promise<void> {\n const lines = [\n '# The admin account the seed creates. Keep this file out of version control (.gitignore lists it).',\n `NYMBAL_ADMIN_EMAIL=${admin.email}`,\n `NYMBAL_ADMIN_PASSWORD=${admin.password}`,\n '',\n ]\n await writeFile(resolve(dir, '.env'), lines.join('\\n'), { encoding: 'utf8', mode: 0o600 })\n}\n\nfunction adminSignIn(admin: AdminCredentials): string[] {\n return [\n `Admin: http://localhost:${API_PORT}/admin/`,\n ` email ${admin.email}`,\n ` password ${admin.password} (saved in .env)`,\n ]\n}\n\nexport interface MainDeps {\n run?: Runner\n}\n\n/**\n * The `packageManager` field for the package manager that launched us (`npm_config_user_agent`), read when called.\n * Without a usable user agent it is the installed npm; undefined when npm cannot be run either.\n */\nfunction packageManagerField(): string | undefined {\n const token = (process.env['npm_config_user_agent'] ?? '').split(' ')[0] ?? ''\n const match = /^(npm|pnpm|yarn|bun)\\/(\\d+\\.\\d+\\.\\d+\\S*)$/.exec(token)\n if (match) return `${match[1]}@${match[2]}`\n const npm = spawnSync('npm', ['-v'], { encoding: 'utf8', shell: process.platform === 'win32' })\n const version = npm.status === 0 ? npm.stdout.trim() : ''\n return version ? `npm@${version}` : undefined\n}\n\nexport function detectPackageManager(): PackageManager {\n const ua = process.env['npm_config_user_agent']\n if (!ua) return 'npm'\n const token = ua.split(' ')[0] ?? ''\n const slash = token.indexOf('/')\n const pm = slash >= 0 ? token.slice(0, slash) : token\n const version = slash >= 0 ? token.slice(slash + 1) : ''\n if (pm === 'pnpm') return 'pnpm'\n if (pm === 'bun') return 'bun'\n if (pm === 'yarn') {\n const major = parseInt(version.split('.')[0] ?? '1', 10)\n return major < 2 ? 'yarn-classic' : 'yarn-berry'\n }\n return 'npm'\n}\n\nfunction installCmd(pm: PackageManager): [string, string[]] {\n switch (pm) {\n case 'pnpm': return ['pnpm', ['install']]\n case 'bun': return ['bun', ['install']]\n case 'yarn-classic':\n case 'yarn-berry': return ['yarn', ['install']]\n default: return ['npm', ['install']]\n }\n}\n\nfunction seedCmd(pm: PackageManager): [string, string[]] {\n switch (pm) {\n case 'pnpm': return ['pnpm', ['exec', 'nymbal', 'seed']]\n case 'bun': return ['bunx', ['nymbal', 'seed']]\n case 'yarn-classic': return ['yarn', ['run', 'nymbal', 'seed']]\n case 'yarn-berry': return ['yarn', ['exec', 'nymbal', 'seed']]\n default: return ['npx', ['nymbal', 'seed']]\n }\n}\n\nfunction devCmd(pm: PackageManager): string {\n switch (pm) {\n case 'pnpm': return 'pnpm dev'\n case 'bun': return 'bun dev'\n case 'yarn-classic':\n case 'yarn-berry': return 'yarn dev'\n default: return 'npm run dev'\n }\n}\n\nconst TEMPLATES = ['astro', 'nextjs'] as const\nconst CURRENCIES = ['GBP', 'USD', 'EUR'] as const\n\nexport interface CliFlags {\n name?: string\n template?: (typeof TEMPLATES)[number]\n currency?: (typeof CURRENCIES)[number]\n yes: boolean\n /** Present (false) only with `--no-install`. */\n install?: boolean\n}\n\n/** A command-line flag that is missing its value or names a value outside the allowed set. */\nexport class CliFlagError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'CliFlagError'\n }\n}\n\nfunction oneOf<T extends string>(flag: string, value: string, allowed: readonly T[]): T {\n if ((allowed as readonly string[]).includes(value)) return value as T\n throw new CliFlagError(`Unknown ${flag} \"${value}\". Allowed: ${allowed.join(', ')}`)\n}\n\n/** Parses process.argv (node and script first): --name, --template and --currency take `--flag v` or `--flag=v`. */\nexport function parseCliFlags(argv: string[]): CliFlags {\n const flags: CliFlags = { yes: false }\n const args = argv.slice(2)\n let positional: string | undefined\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]!\n if (arg === '--yes' || arg === '-y') {\n flags.yes = true\n continue\n }\n if (arg === '--no-install') {\n flags.install = false\n continue\n }\n const match = /^--(name|template|currency)(?:=(.*))?$/s.exec(arg)\n if (match) {\n const flag = match[1]!\n let value = match[2]\n if (value === undefined) {\n const next = args[i + 1]\n if (next === undefined || next.startsWith('-')) throw new CliFlagError(`--${flag} needs a value`)\n value = next\n i++\n }\n if (flag === 'name') flags.name = value\n else if (flag === 'template') flags.template = oneOf('--template', value, TEMPLATES)\n else flags.currency = oneOf('--currency', value, CURRENCIES)\n } else if (!arg.startsWith('-') && positional === undefined) {\n positional = arg\n }\n }\n if (flags.name === undefined && positional !== undefined) flags.name = positional\n return flags\n}\n\nexport async function main(\n argv: string[] = process.argv,\n deps: MainDeps = {},\n): Promise<void> {\n const run = deps.run ?? runCmd\n let flags: CliFlags\n try {\n flags = parseCliFlags(argv)\n } catch (err) {\n if (!(err instanceof CliFlagError)) throw err\n process.stderr.write(`Error: ${err.message}\\n`)\n process.exit(1)\n }\n\n const pm = detectPackageManager()\n\n if (flags.yes) {\n if (!flags.name) {\n process.stderr.write('Error: --yes requires --name or a positional name argument\\n')\n process.exit(1)\n }\n const name = flags.name.trim()\n const answers: Answers = {\n name,\n directory: toDirectoryName(name),\n currency: flags.currency ?? 'GBP',\n template: flags.template ?? 'astro',\n seedDemo: true,\n }\n const targetDir = resolve(process.cwd(), answers.directory)\n if (existsSync(targetDir)) {\n const entries = await readdir(targetDir)\n if (entries.length > 0) {\n process.stderr.write(`Error: Directory ${answers.directory} already exists and is not empty.\\n`)\n process.exit(1)\n }\n } else {\n await mkdir(targetDir, { recursive: true })\n }\n await scaffold(targetDir, answers)\n const admin = newAdminCredentials()\n await writeAdminEnv(targetDir, admin)\n process.stdout.write(`Created ${answers.directory}\\n`)\n if (flags.install !== false) {\n await installAndSeed(targetDir, pm, answers.seedDemo, run, admin)\n } else {\n process.stdout.write(`Next: cd ${answers.directory} && ${installCmd(pm)[0]} install\\n`)\n }\n process.stdout.write(`${adminSignIn(admin).join('\\n')}\\n`)\n return\n }\n\n intro(pc.cyan(pc.bold('◆ create-nymbal-app')))\n\n const name = flags.name\n ? flags.name\n : ((await text({\n message: 'Store name',\n placeholder: 'My Store',\n validate: (v) => (!v || v.trim().length === 0 ? 'Required' : undefined),\n })) as string | symbol)\n\n if (isCancel(name)) return cancel('Cancelled')\n\n type Currency = 'GBP' | 'USD' | 'EUR'\n const currencyOptions: { value: Currency; label: string }[] = [\n { value: 'GBP', label: 'GBP — British Pound' },\n { value: 'USD', label: 'USD — US Dollar' },\n { value: 'EUR', label: 'EUR — Euro' },\n ]\n const currency = await select({\n message: 'Store currency',\n options: currencyOptions,\n initialValue: 'GBP' as Currency,\n })\n\n if (isCancel(currency)) return cancel('Cancelled')\n\n type Template = 'astro' | 'nextjs'\n const templateOptions: { value: Template; label: string }[] = [\n { value: 'astro', label: 'Astro (recommended — performance-first)' },\n { value: 'nextjs', label: 'Next.js (migration-friendly)' },\n ]\n const template = await select({\n message: 'Storefront template',\n options: templateOptions,\n initialValue: 'astro' as Template,\n })\n\n if (isCancel(template)) return cancel('Cancelled')\n\n const seedDemo = (await confirm({\n message: 'Generate demo content?',\n initialValue: true,\n })) as boolean | symbol\n\n if (isCancel(seedDemo)) return cancel('Cancelled')\n\n const shouldInstall =\n flags.install === false\n ? false\n : ((await confirm({\n message: 'Install dependencies now?',\n initialValue: true,\n })) as boolean | symbol)\n\n if (isCancel(shouldInstall)) return cancel('Cancelled')\n\n const answers: Answers = {\n name: String(name).trim(),\n directory: toDirectoryName(String(name).trim()),\n currency: currency as 'GBP' | 'USD' | 'EUR',\n template: template as 'astro' | 'nextjs',\n seedDemo: seedDemo as boolean,\n }\n\n const targetDir = resolve(process.cwd(), answers.directory)\n if (existsSync(targetDir)) {\n const entries = await readdir(targetDir)\n if (entries.length > 0) {\n cancel(`Directory ${answers.directory} already exists and is not empty.`)\n process.exit(1)\n }\n } else {\n await mkdir(targetDir, { recursive: true })\n }\n\n note(pc.dim(`Scaffolding into ${targetDir}`), 'Creating project')\n\n await scaffold(targetDir, answers)\n const admin = newAdminCredentials()\n await writeAdminEnv(targetDir, admin)\n\n if (shouldInstall) {\n await installAndSeed(targetDir, pm, answers.seedDemo, run, admin)\n }\n\n const cmd = devCmd(pm)\n const nextSteps = [\n ` ${pc.cyan('cd')} ${answers.directory}`,\n ]\n if (!shouldInstall) {\n const [ic] = installCmd(pm)\n nextSteps.push(` ${pc.cyan(ic + ' install')}`)\n }\n nextSteps.push(` ${pc.cyan(cmd)}`)\n\n outro(\n [pc.green(`✓ Created ${answers.directory}`), '', ...nextSteps, '', ...adminSignIn(admin)].join(\n '\\n',\n ),\n )\n}\n\n/** Install with the detected package manager, then seed through the project's own `nymbal` bin. */\nasync function installAndSeed(\n dir: string,\n pm: PackageManager,\n seedDemo: boolean,\n run: Runner,\n admin: AdminCredentials,\n): Promise<void> {\n const [installBin, installArgs] = installCmd(pm)\n process.stdout.write(`Running ${installBin} install (this may take a moment)...\\n`)\n await run(installBin, installArgs, dir)\n if (seedDemo) {\n process.stdout.write('Seeding demo content...\\n')\n const [seedBin, seedArgs] = seedCmd(pm)\n // Explicit, so a value exported in the shell cannot replace the one in .env and the outro.\n await run(seedBin, seedArgs, dir, {\n ...process.env,\n NYMBAL_ADMIN_EMAIL: admin.email,\n NYMBAL_ADMIN_PASSWORD: admin.password,\n })\n }\n}\n\nexport async function scaffold(dir: string, answers: Answers): Promise<void> {\n // Copy the chosen template directly into dir (flat — no storefront/ subdirectory)\n await copyScaffoldDir(resolve(SCAFFOLD_ROOT, 'templates', answers.template), dir)\n\n // Overlay the project layer (scaffold/project/<template>) on top of the template\n // defaults. Project files WIN on path collision — this is the override model:\n // anything in the project layer shadows the template default at the same path.\n // scaffold/project/ lives outside scaffold/templates/, so sync-scaffold's\n // rm -rf of scaffold/templates/* never deletes it.\n const projectLayer = resolve(SCAFFOLD_ROOT, 'project', answers.template)\n if (existsSync(projectLayer)) {\n await copyScaffoldDir(projectLayer, dir)\n }\n\n // Write nymbal.config.ts (answers-dependent, so generated at runtime)\n await writeFile(resolve(dir, 'nymbal.config.ts'), renderConfig(answers), 'utf8')\n\n // Patch the copied package.json: set name to the project directory name\n await patchPackageJson(dir, answers.directory)\n}\n\nasync function patchPackageJson(dir: string, name: string): Promise<void> {\n const pkgPath = resolve(dir, 'package.json')\n const pkg = JSON.parse(await readFile(pkgPath, 'utf8')) as Record<string, unknown>\n pkg['name'] = name\n pkg['private'] = true\n const packageManager = packageManagerField()\n if (packageManager) pkg['packageManager'] = packageManager\n await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\\n', 'utf8')\n}\n\nasync function copyScaffoldDir(src: string, dest: string): Promise<void> {\n if (!existsSync(src)) {\n throw new Error(`Scaffold directory not found: ${src}`)\n }\n await cp(src, dest, {\n recursive: true,\n filter: (source) => !source.endsWith('node_modules'),\n })\n await renameDotFiles(dest)\n}\n\nasync function renameDotFiles(dir: string): Promise<void> {\n // Non-recursive: only rename _dot.* files at the project root.\n // All shared dotfiles (_dot.gitignore, _dot.env.example) live at template root.\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (!entry.isDirectory() && entry.name.startsWith('_dot.')) {\n const full = resolve(dir, entry.name)\n const renamed = resolve(dir, '.' + entry.name.slice(5))\n await writeFile(renamed, await readFile(full))\n await rm(full)\n }\n }\n}\n\nfunction renderConfig(answers: Answers): string {\n return `import { defineConfig, env } from '@nymbalhq/config'\n\nexport default defineConfig({\n store: {\n name: '${escape(answers.name)}',\n currency: '${answers.currency}',\n locale: '${defaultLocale(answers.currency)}',\n timezone: '${defaultTimezone(answers.currency)}',\n },\n\n template: '${answers.template}',\n\n infrastructure: {\n cloud: 'local',\n commandStore: 'sqlite',\n documentStore: 'in-memory',\n eventBus: 'in-process',\n compute: 'in-process',\n },\n\n security: {\n adapter: 'middleware',\n rateLimit: {\n '/checkout': { requests: 10, window: '60s', action: 'block' },\n '/auth/*': { requests: 5, window: '60s', action: 'challenge' },\n '/api/*': { requests: 100, window: '60s', action: 'throttle' },\n '/webhooks/*': { requests: 200, window: '60s', action: 'throttle' },\n },\n botProtection: { mode: 'managed', allowList: ['googlebot', 'bingbot', 'stripe-webhooks'] },\n headers: {\n hsts: true,\n contentSecurityPolicy: 'strict',\n referrerPolicy: 'strict-origin-when-cross-origin',\n xFrameOptions: 'deny',\n },\n csrf: true,\n },\n\n commerce: {\n payments: { provider: 'native' },\n email: { provider: 'native' },\n reviews: { provider: 'native' },\n search: { provider: 'native' },\n analytics: { provider: 'native' },\n shipping: { provider: 'native' },\n tax: { provider: 'native' },\n ai: { provider: 'anthropic', config: { apiKey: env('ANTHROPIC_API_KEY', 'anthropic-placeholder') } },\n },\n\n http: {\n adapter: 'fastify',\n port: ${API_PORT},\n },\n\n deployment: {\n strategy: 'standard',\n },\n\n features: {\n staging: false,\n heartbeat: false,\n autoUpdates: false,\n },\n})\n`\n}\n\nfunction defaultLocale(currency: Answers['currency']): string {\n return currency === 'USD' ? 'en-US' : 'en-GB'\n}\n\nfunction defaultTimezone(currency: Answers['currency']): string {\n return currency === 'USD' ? 'America/New_York' : 'Europe/London'\n}\n\nfunction escape(v: string): string {\n return v.replace(/'/g, \"\\\\'\")\n}\n\nfunction toDirectoryName(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9-_ ]/g, '')\n .trim()\n .replace(/\\s+/g, '-')\n}\n\nconst runCmd: Runner = (cmd, args, cwd, env = process.env) => {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { cwd, env, stdio: 'inherit', shell: false })\n child.on('error', reject)\n child.on('exit', (code) => {\n if (code === 0) return resolve()\n reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}`))\n })\n })\n}\n"],"mappings":";AAAA,SAAS,IAAI,UAAU,OAAO,SAAS,IAAI,iBAAiB;AAC5D,SAAS,kBAAkB;AAC3B,SAAS,SAAS,eAAe;AACjC,SAAS,qBAAqB;AAC9B,SAAS,OAAO,iBAAiB;AACjC,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,SAAS,OAAO,UAAU,MAAM,OAAO,QAAQ,YAAY;AAC5E,OAAO,QAAQ;AAEf,IAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,IAAM,gBAAgB,QAAQ,MAAM,MAAM,UAAU;AAwBpD,IAAM,WAAW;AAQjB,SAAS,sBAAwC;AAC/C,SAAO,EAAE,OAAO,sBAAsB,UAAU,YAAY,EAAE,EAAE,SAAS,WAAW,EAAE;AACxF;AAGA,eAAe,cAAc,KAAa,OAAwC;AAChF,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA,sBAAsB,MAAM,KAAK;AAAA,IACjC,yBAAyB,MAAM,QAAQ;AAAA,IACvC;AAAA,EACF;AACA,QAAM,UAAU,QAAQ,KAAK,MAAM,GAAG,MAAM,KAAK,IAAI,GAAG,EAAE,UAAU,QAAQ,MAAM,IAAM,CAAC;AAC3F;AAEA,SAAS,YAAY,OAAmC;AACtD,SAAO;AAAA,IACL,2BAA2B,QAAQ;AAAA,IACnC,eAAe,MAAM,KAAK;AAAA,IAC1B,eAAe,MAAM,QAAQ;AAAA,EAC/B;AACF;AAUA,SAAS,sBAA0C;AACjD,QAAM,SAAS,QAAQ,IAAI,uBAAuB,KAAK,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK;AAC5E,QAAM,QAAQ,4CAA4C,KAAK,KAAK;AACpE,MAAI,MAAO,QAAO,GAAG,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;AACzC,QAAM,MAAM,UAAU,OAAO,CAAC,IAAI,GAAG,EAAE,UAAU,QAAQ,OAAO,QAAQ,aAAa,QAAQ,CAAC;AAC9F,QAAM,UAAU,IAAI,WAAW,IAAI,IAAI,OAAO,KAAK,IAAI;AACvD,SAAO,UAAU,OAAO,OAAO,KAAK;AACtC;AAEO,SAAS,uBAAuC;AACrD,QAAM,KAAK,QAAQ,IAAI,uBAAuB;AAC9C,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,QAAQ,GAAG,MAAM,GAAG,EAAE,CAAC,KAAK;AAClC,QAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,QAAM,KAAK,SAAS,IAAI,MAAM,MAAM,GAAG,KAAK,IAAI;AAChD,QAAM,UAAU,SAAS,IAAI,MAAM,MAAM,QAAQ,CAAC,IAAI;AACtD,MAAI,OAAO,OAAQ,QAAO;AAC1B,MAAI,OAAO,MAAO,QAAO;AACzB,MAAI,OAAO,QAAQ;AACjB,UAAM,QAAQ,SAAS,QAAQ,MAAM,GAAG,EAAE,CAAC,KAAK,KAAK,EAAE;AACvD,WAAO,QAAQ,IAAI,iBAAiB;AAAA,EACtC;AACA,SAAO;AACT;AAEA,SAAS,WAAW,IAAwC;AAC1D,UAAQ,IAAI;AAAA,IACV,KAAK;AAAQ,aAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;AAAA,IACxC,KAAK;AAAO,aAAO,CAAC,OAAO,CAAC,SAAS,CAAC;AAAA,IACtC,KAAK;AAAA,IACL,KAAK;AAAc,aAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;AAAA,IAC9C;AAAS,aAAO,CAAC,OAAO,CAAC,SAAS,CAAC;AAAA,EACrC;AACF;AAEA,SAAS,QAAQ,IAAwC;AACvD,UAAQ,IAAI;AAAA,IACV,KAAK;AAAQ,aAAO,CAAC,QAAQ,CAAC,QAAQ,UAAU,MAAM,CAAC;AAAA,IACvD,KAAK;AAAO,aAAO,CAAC,QAAQ,CAAC,UAAU,MAAM,CAAC;AAAA,IAC9C,KAAK;AAAgB,aAAO,CAAC,QAAQ,CAAC,OAAO,UAAU,MAAM,CAAC;AAAA,IAC9D,KAAK;AAAc,aAAO,CAAC,QAAQ,CAAC,QAAQ,UAAU,MAAM,CAAC;AAAA,IAC7D;AAAS,aAAO,CAAC,OAAO,CAAC,UAAU,MAAM,CAAC;AAAA,EAC5C;AACF;AAEA,SAAS,OAAO,IAA4B;AAC1C,UAAQ,IAAI;AAAA,IACV,KAAK;AAAQ,aAAO;AAAA,IACpB,KAAK;AAAO,aAAO;AAAA,IACnB,KAAK;AAAA,IACL,KAAK;AAAc,aAAO;AAAA,IAC1B;AAAS,aAAO;AAAA,EAClB;AACF;AAEA,IAAM,YAAY,CAAC,SAAS,QAAQ;AACpC,IAAM,aAAa,CAAC,OAAO,OAAO,KAAK;AAYhC,IAAM,eAAN,cAA2B,MAAM;AAAA,EACtC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEA,SAAS,MAAwB,MAAc,OAAe,SAA0B;AACtF,MAAK,QAA8B,SAAS,KAAK,EAAG,QAAO;AAC3D,QAAM,IAAI,aAAa,WAAW,IAAI,KAAK,KAAK,eAAe,QAAQ,KAAK,IAAI,CAAC,EAAE;AACrF;AAGO,SAAS,cAAc,MAA0B;AACtD,QAAM,QAAkB,EAAE,KAAK,MAAM;AACrC,QAAM,OAAO,KAAK,MAAM,CAAC;AACzB,MAAI;AACJ,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,MAAM,KAAK,CAAC;AAClB,QAAI,QAAQ,WAAW,QAAQ,MAAM;AACnC,YAAM,MAAM;AACZ;AAAA,IACF;AACA,QAAI,QAAQ,gBAAgB;AAC1B,YAAM,UAAU;AAChB;AAAA,IACF;AACA,UAAM,QAAQ,0CAA0C,KAAK,GAAG;AAChE,QAAI,OAAO;AACT,YAAM,OAAO,MAAM,CAAC;AACpB,UAAI,QAAQ,MAAM,CAAC;AACnB,UAAI,UAAU,QAAW;AACvB,cAAM,OAAO,KAAK,IAAI,CAAC;AACvB,YAAI,SAAS,UAAa,KAAK,WAAW,GAAG,EAAG,OAAM,IAAI,aAAa,KAAK,IAAI,gBAAgB;AAChG,gBAAQ;AACR;AAAA,MACF;AACA,UAAI,SAAS,OAAQ,OAAM,OAAO;AAAA,eACzB,SAAS,WAAY,OAAM,WAAW,MAAM,cAAc,OAAO,SAAS;AAAA,UAC9E,OAAM,WAAW,MAAM,cAAc,OAAO,UAAU;AAAA,IAC7D,WAAW,CAAC,IAAI,WAAW,GAAG,KAAK,eAAe,QAAW;AAC3D,mBAAa;AAAA,IACf;AAAA,EACF;AACA,MAAI,MAAM,SAAS,UAAa,eAAe,OAAW,OAAM,OAAO;AACvE,SAAO;AACT;AAEA,eAAsB,KACpB,OAAiB,QAAQ,MACzB,OAAiB,CAAC,GACH;AACf,QAAM,MAAM,KAAK,OAAO;AACxB,MAAI;AACJ,MAAI;AACF,YAAQ,cAAc,IAAI;AAAA,EAC5B,SAAS,KAAK;AACZ,QAAI,EAAE,eAAe,cAAe,OAAM;AAC1C,YAAQ,OAAO,MAAM,UAAU,IAAI,OAAO;AAAA,CAAI;AAC9C,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,KAAK,qBAAqB;AAEhC,MAAI,MAAM,KAAK;AACb,QAAI,CAAC,MAAM,MAAM;AACf,cAAQ,OAAO,MAAM,8DAA8D;AACnF,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,UAAMA,QAAO,MAAM,KAAK,KAAK;AAC7B,UAAMC,WAAmB;AAAA,MACvB,MAAAD;AAAA,MACA,WAAW,gBAAgBA,KAAI;AAAA,MAC/B,UAAU,MAAM,YAAY;AAAA,MAC5B,UAAU,MAAM,YAAY;AAAA,MAC5B,UAAU;AAAA,IACZ;AACA,UAAME,aAAY,QAAQ,QAAQ,IAAI,GAAGD,SAAQ,SAAS;AAC1D,QAAI,WAAWC,UAAS,GAAG;AACzB,YAAM,UAAU,MAAM,QAAQA,UAAS;AACvC,UAAI,QAAQ,SAAS,GAAG;AACtB,gBAAQ,OAAO,MAAM,oBAAoBD,SAAQ,SAAS;AAAA,CAAqC;AAC/F,gBAAQ,KAAK,CAAC;AAAA,MAChB;AAAA,IACF,OAAO;AACL,YAAM,MAAMC,YAAW,EAAE,WAAW,KAAK,CAAC;AAAA,IAC5C;AACA,UAAM,SAASA,YAAWD,QAAO;AACjC,UAAME,SAAQ,oBAAoB;AAClC,UAAM,cAAcD,YAAWC,MAAK;AACpC,YAAQ,OAAO,MAAM,WAAWF,SAAQ,SAAS;AAAA,CAAI;AACrD,QAAI,MAAM,YAAY,OAAO;AAC3B,YAAM,eAAeC,YAAW,IAAID,SAAQ,UAAU,KAAKE,MAAK;AAAA,IAClE,OAAO;AACL,cAAQ,OAAO,MAAM,YAAYF,SAAQ,SAAS,OAAO,WAAW,EAAE,EAAE,CAAC,CAAC;AAAA,CAAY;AAAA,IACxF;AACA,YAAQ,OAAO,MAAM,GAAG,YAAYE,MAAK,EAAE,KAAK,IAAI,CAAC;AAAA,CAAI;AACzD;AAAA,EACF;AAEA,QAAM,GAAG,KAAK,GAAG,KAAK,0BAAqB,CAAC,CAAC;AAE7C,QAAM,OAAO,MAAM,OACf,MAAM,OACJ,MAAM,KAAK;AAAA,IACX,SAAS;AAAA,IACT,aAAa;AAAA,IACb,UAAU,CAAC,MAAO,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,IAAI,aAAa;AAAA,EAC/D,CAAC;AAEL,MAAI,SAAS,IAAI,EAAG,QAAO,OAAO,WAAW;AAG7C,QAAM,kBAAwD;AAAA,IAC5D,EAAE,OAAO,OAAO,OAAO,2BAAsB;AAAA,IAC7C,EAAE,OAAO,OAAO,OAAO,uBAAkB;AAAA,IACzC,EAAE,OAAO,OAAO,OAAO,kBAAa;AAAA,EACtC;AACA,QAAM,WAAW,MAAM,OAAO;AAAA,IAC5B,SAAS;AAAA,IACT,SAAS;AAAA,IACT,cAAc;AAAA,EAChB,CAAC;AAED,MAAI,SAAS,QAAQ,EAAG,QAAO,OAAO,WAAW;AAGjD,QAAM,kBAAwD;AAAA,IAC5D,EAAE,OAAO,SAAS,OAAO,+CAA0C;AAAA,IACnE,EAAE,OAAO,UAAU,OAAO,+BAA+B;AAAA,EAC3D;AACA,QAAM,WAAW,MAAM,OAAO;AAAA,IAC5B,SAAS;AAAA,IACT,SAAS;AAAA,IACT,cAAc;AAAA,EAChB,CAAC;AAED,MAAI,SAAS,QAAQ,EAAG,QAAO,OAAO,WAAW;AAEjD,QAAM,WAAY,MAAM,QAAQ;AAAA,IAC9B,SAAS;AAAA,IACT,cAAc;AAAA,EAChB,CAAC;AAED,MAAI,SAAS,QAAQ,EAAG,QAAO,OAAO,WAAW;AAEjD,QAAM,gBACJ,MAAM,YAAY,QACd,QACE,MAAM,QAAQ;AAAA,IACd,SAAS;AAAA,IACT,cAAc;AAAA,EAChB,CAAC;AAEP,MAAI,SAAS,aAAa,EAAG,QAAO,OAAO,WAAW;AAEtD,QAAM,UAAmB;AAAA,IACvB,MAAM,OAAO,IAAI,EAAE,KAAK;AAAA,IACxB,WAAW,gBAAgB,OAAO,IAAI,EAAE,KAAK,CAAC;AAAA,IAC9C;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,YAAY,QAAQ,QAAQ,IAAI,GAAG,QAAQ,SAAS;AAC1D,MAAI,WAAW,SAAS,GAAG;AACzB,UAAM,UAAU,MAAM,QAAQ,SAAS;AACvC,QAAI,QAAQ,SAAS,GAAG;AACtB,aAAO,aAAa,QAAQ,SAAS,mCAAmC;AACxE,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF,OAAO;AACL,UAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAAA,EAC5C;AAEA,OAAK,GAAG,IAAI,oBAAoB,SAAS,EAAE,GAAG,kBAAkB;AAEhE,QAAM,SAAS,WAAW,OAAO;AACjC,QAAM,QAAQ,oBAAoB;AAClC,QAAM,cAAc,WAAW,KAAK;AAEpC,MAAI,eAAe;AACjB,UAAM,eAAe,WAAW,IAAI,QAAQ,UAAU,KAAK,KAAK;AAAA,EAClE;AAEA,QAAM,MAAM,OAAO,EAAE;AACrB,QAAM,YAAY;AAAA,IAChB,KAAK,GAAG,KAAK,IAAI,CAAC,IAAI,QAAQ,SAAS;AAAA,EACzC;AACA,MAAI,CAAC,eAAe;AAClB,UAAM,CAAC,EAAE,IAAI,WAAW,EAAE;AAC1B,cAAU,KAAK,KAAK,GAAG,KAAK,KAAK,UAAU,CAAC,EAAE;AAAA,EAChD;AACA,YAAU,KAAK,KAAK,GAAG,KAAK,GAAG,CAAC,EAAE;AAElC;AAAA,IACE,CAAC,GAAG,MAAM,kBAAa,QAAQ,SAAS,EAAE,GAAG,IAAI,GAAG,WAAW,IAAI,GAAG,YAAY,KAAK,CAAC,EAAE;AAAA,MACxF;AAAA,IACF;AAAA,EACF;AACF;AAGA,eAAe,eACb,KACA,IACA,UACA,KACA,OACe;AACf,QAAM,CAAC,YAAY,WAAW,IAAI,WAAW,EAAE;AAC/C,UAAQ,OAAO,MAAM,WAAW,UAAU;AAAA,CAAwC;AAClF,QAAM,IAAI,YAAY,aAAa,GAAG;AACtC,MAAI,UAAU;AACZ,YAAQ,OAAO,MAAM,2BAA2B;AAChD,UAAM,CAAC,SAAS,QAAQ,IAAI,QAAQ,EAAE;AAEtC,UAAM,IAAI,SAAS,UAAU,KAAK;AAAA,MAChC,GAAG,QAAQ;AAAA,MACX,oBAAoB,MAAM;AAAA,MAC1B,uBAAuB,MAAM;AAAA,IAC/B,CAAC;AAAA,EACH;AACF;AAEA,eAAsB,SAAS,KAAa,SAAiC;AAE3E,QAAM,gBAAgB,QAAQ,eAAe,aAAa,QAAQ,QAAQ,GAAG,GAAG;AAOhF,QAAM,eAAe,QAAQ,eAAe,WAAW,QAAQ,QAAQ;AACvE,MAAI,WAAW,YAAY,GAAG;AAC5B,UAAM,gBAAgB,cAAc,GAAG;AAAA,EACzC;AAGA,QAAM,UAAU,QAAQ,KAAK,kBAAkB,GAAG,aAAa,OAAO,GAAG,MAAM;AAG/E,QAAM,iBAAiB,KAAK,QAAQ,SAAS;AAC/C;AAEA,eAAe,iBAAiB,KAAa,MAA6B;AACxE,QAAM,UAAU,QAAQ,KAAK,cAAc;AAC3C,QAAM,MAAM,KAAK,MAAM,MAAM,SAAS,SAAS,MAAM,CAAC;AACtD,MAAI,MAAM,IAAI;AACd,MAAI,SAAS,IAAI;AACjB,QAAM,iBAAiB,oBAAoB;AAC3C,MAAI,eAAgB,KAAI,gBAAgB,IAAI;AAC5C,QAAM,UAAU,SAAS,KAAK,UAAU,KAAK,MAAM,CAAC,IAAI,MAAM,MAAM;AACtE;AAEA,eAAe,gBAAgB,KAAa,MAA6B;AACvE,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,UAAM,IAAI,MAAM,iCAAiC,GAAG,EAAE;AAAA,EACxD;AACA,QAAM,GAAG,KAAK,MAAM;AAAA,IAClB,WAAW;AAAA,IACX,QAAQ,CAAC,WAAW,CAAC,OAAO,SAAS,cAAc;AAAA,EACrD,CAAC;AACD,QAAM,eAAe,IAAI;AAC3B;AAEA,eAAe,eAAe,KAA4B;AAGxD,QAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAC1D,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,YAAY,KAAK,MAAM,KAAK,WAAW,OAAO,GAAG;AAC1D,YAAM,OAAO,QAAQ,KAAK,MAAM,IAAI;AACpC,YAAM,UAAU,QAAQ,KAAK,MAAM,MAAM,KAAK,MAAM,CAAC,CAAC;AACtD,YAAM,UAAU,SAAS,MAAM,SAAS,IAAI,CAAC;AAC7C,YAAM,GAAG,IAAI;AAAA,IACf;AAAA,EACF;AACF;AAEA,SAAS,aAAa,SAA0B;AAC9C,SAAO;AAAA;AAAA;AAAA;AAAA,aAII,OAAO,QAAQ,IAAI,CAAC;AAAA,iBAChB,QAAQ,QAAQ;AAAA,eAClB,cAAc,QAAQ,QAAQ,CAAC;AAAA,iBAC7B,gBAAgB,QAAQ,QAAQ,CAAC;AAAA;AAAA;AAAA,eAGnC,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAyCnB,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcpB;AAEA,SAAS,cAAc,UAAuC;AAC5D,SAAO,aAAa,QAAQ,UAAU;AACxC;AAEA,SAAS,gBAAgB,UAAuC;AAC9D,SAAO,aAAa,QAAQ,qBAAqB;AACnD;AAEA,SAAS,OAAO,GAAmB;AACjC,SAAO,EAAE,QAAQ,MAAM,KAAK;AAC9B;AAEA,SAAS,gBAAgB,MAAsB;AAC7C,SAAO,KACJ,YAAY,EACZ,QAAQ,iBAAiB,EAAE,EAC3B,KAAK,EACL,QAAQ,QAAQ,GAAG;AACxB;AAEA,IAAM,SAAiB,CAAC,KAAK,MAAM,KAAK,MAAM,QAAQ,QAAQ;AAC5D,SAAO,IAAI,QAAQ,CAACC,UAAS,WAAW;AACtC,UAAM,QAAQ,MAAM,KAAK,MAAM,EAAE,KAAK,KAAK,OAAO,WAAW,OAAO,MAAM,CAAC;AAC3E,UAAM,GAAG,SAAS,MAAM;AACxB,UAAM,GAAG,QAAQ,CAAC,SAAS;AACzB,UAAI,SAAS,EAAG,QAAOA,SAAQ;AAC/B,aAAO,IAAI,MAAM,GAAG,GAAG,IAAI,KAAK,KAAK,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC;AAAA,IAClE,CAAC;AAAA,EACH,CAAC;AACH;","names":["name","answers","targetDir","admin","resolve"]}
|
package/package.json
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nymbal-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scaffold a new Nymbal commerce project",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/nymbalhq/nymbal.git",
|
|
9
|
+
"directory": "packages/create-app"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"create-nymbal-app": "./bin/create-nymbal-app.mjs"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"dist",
|
|
26
|
+
"scaffold"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@clack/prompts": "^0.7.0",
|
|
30
|
+
"picocolors": "^1.1.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsup": "^8.3.5",
|
|
34
|
+
"typescript": "^5.6.3",
|
|
35
|
+
"vitest": "^3.0.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup && pnpm sync:scaffold",
|
|
39
|
+
"dev": "tsup --watch",
|
|
40
|
+
"sync:scaffold": "node ./scripts/sync-scaffold.mjs",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../styles/pages/home.css'
|
|
3
|
+
import BaseLayout from '../layouts/BaseLayout.astro'
|
|
4
|
+
import HeroSection from '../components/HeroSection.astro'
|
|
5
|
+
import CategoryNav from '../components/CategoryNav.astro'
|
|
6
|
+
import ProductCard from '../components/ProductCard.astro'
|
|
7
|
+
import NewsletterSignup from '../components/NewsletterSignup.astro'
|
|
8
|
+
import { fetchProducts, fetchCategories } from '../lib/api'
|
|
9
|
+
import { organizationJsonLd } from '../lib/seo'
|
|
10
|
+
|
|
11
|
+
const [productResult, categories] = await Promise.all([
|
|
12
|
+
fetchProducts({ limit: 8 }),
|
|
13
|
+
fetchCategories(),
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
const products = productResult.items
|
|
17
|
+
const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<BaseLayout title="Nymbal — Modern Storefront" jsonLd={jsonLd}>
|
|
21
|
+
{/* Marks this page as the project-scaffolded override of the template default.
|
|
22
|
+
Rendered as a real (hidden) element rather than an HTML comment because the
|
|
23
|
+
Astro compiler strips bare comments that lead a component fragment, which
|
|
24
|
+
would silently break the override smoke check. */}
|
|
25
|
+
<div hidden data-nymbal-marker="nymbal:project-override homepage"></div>
|
|
26
|
+
<HeroSection />
|
|
27
|
+
|
|
28
|
+
<section class="section container">
|
|
29
|
+
<p class="section-tagline" style="text-align: center; color: var(--nymbal-color-text-muted, #71717a);">
|
|
30
|
+
Welcome to your store — this homepage is yours to customise.
|
|
31
|
+
</p>
|
|
32
|
+
<h2 class="section-title">Shop by Category</h2>
|
|
33
|
+
<CategoryNav categories={categories} />
|
|
34
|
+
</section>
|
|
35
|
+
|
|
36
|
+
<section class="featured-section section">
|
|
37
|
+
<div class="container">
|
|
38
|
+
<div class="featured-header">
|
|
39
|
+
<h2 class="section-title" style="margin-bottom: 0;">Our Featured Picks</h2>
|
|
40
|
+
<a href="/products" class="featured-link">View all</a>
|
|
41
|
+
</div>
|
|
42
|
+
{products.length > 0 ? (
|
|
43
|
+
<div class="product-grid">
|
|
44
|
+
{products.map((product) => (
|
|
45
|
+
<ProductCard product={product} />
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
) : (
|
|
49
|
+
<p style="text-align: center; color: var(--nymbal-color-text-muted, #71717a);">
|
|
50
|
+
Products coming soon.
|
|
51
|
+
</p>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
</section>
|
|
55
|
+
|
|
56
|
+
<NewsletterSignup />
|
|
57
|
+
</BaseLayout>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Your Nymbal Store
|
|
2
|
+
|
|
3
|
+
A commerce project scaffolded with [create-nymbal-app](https://github.com/nymbal/nymbal).
|
|
4
|
+
|
|
5
|
+
## Scripts
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pnpm dev # start Astro dev server
|
|
9
|
+
pnpm build # production build
|
|
10
|
+
pnpm preview # preview production build locally
|
|
11
|
+
pnpm migrate # run database migrations
|
|
12
|
+
pnpm seed # re-seed demo content
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
All configuration lives in `nymbal.config.ts`. Adapters, template, security, and deployment
|
|
18
|
+
settings are centralized there — the CLI and every package reads from the same source of truth.
|
|
19
|
+
|
|
20
|
+
## Swapping the command store
|
|
21
|
+
|
|
22
|
+
For local development you get SQLite out of the box. To move to Postgres:
|
|
23
|
+
|
|
24
|
+
1. Set `DATABASE_URL=postgres://…` in `.env`.
|
|
25
|
+
2. Change `infrastructure.commandStore` to `'postgres'` in `nymbal.config.ts`.
|
|
26
|
+
3. Run `pnpm migrate`.
|