create-brainerce-store 1.68.0 → 1.72.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 +31 -10
- package/dist/index.js +197 -105
- package/messages/en.json +63 -3
- package/messages/he.json +63 -3
- package/package.json +1 -1
- package/templates/nextjs/base/TRANSLATIONS.md +14 -7
- package/templates/nextjs/base/src/app/checkout/page.tsx +1074 -1017
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +21 -2
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +1 -1
- package/templates/nextjs/base/src/app/register/page.tsx +67 -64
- package/templates/nextjs/base/src/components/account/order-history.tsx +25 -39
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +30 -11
- package/templates/nextjs/base/src/components/account/profile-section.tsx +303 -226
- package/templates/nextjs/base/src/components/auth/register-form.tsx +326 -245
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +306 -294
- package/templates/nextjs/base/src/components/checkout/date-picker.tsx +13 -1
- package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +61 -21
- package/templates/nextjs/base/src/components/shared/birthday-picker.tsx +258 -0
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +71 -2
- package/templates/nextjs/base/src/core/lib/auth.ts +155 -154
- package/templates/nextjs/base/src/core/lib/birthday.ts +74 -0
- package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +5 -16
- package/templates/nextjs/base/src/core/lib/store-info.ts +10 -0
- package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +3 -6
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +19 -1
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +42 -6
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +52 -10
- package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +143 -0
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +18 -2
- package/templates/nextjs/base/src/ui/product/back-in-stock-form.tsx +173 -0
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +484 -455
- package/templates/nextjs/base/src/ui/product/review-form.tsx +136 -12
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +139 -108
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +21 -3
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +19 -1
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +44 -7
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +52 -10
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +155 -142
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +500 -477
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +135 -11
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +179 -148
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +15 -1
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +38 -4
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +54 -11
- package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +122 -0
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +87 -83
- package/templates/nextjs/ui-canvas/product/back-in-stock-form.tsx +151 -0
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +373 -352
- package/templates/nextjs/ui-canvas/product/review-form.tsx +129 -11
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +127 -96
package/README.md
CHANGED
|
@@ -44,19 +44,40 @@ Full details: [TEMPLATE-ARCHITECTURE.md](./TEMPLATE-ARCHITECTURE.md).
|
|
|
44
44
|
|
|
45
45
|
## Flags
|
|
46
46
|
|
|
47
|
-
| Flag
|
|
48
|
-
|
|
|
49
|
-
| `--connection-id <id>` | Brainerce vibe-coded connection ID (`vc_*`)
|
|
50
|
-
| `--canvas`
|
|
51
|
-
| `--language <lang>`
|
|
52
|
-
| `--pkg-manager <pm>`
|
|
53
|
-
| `--framework <fw>`
|
|
54
|
-
| `--api-url <url>`
|
|
55
|
-
| `--no-git`
|
|
56
|
-
| `--no-install`
|
|
47
|
+
| Flag | Description | Default |
|
|
48
|
+
| ---------------------- | ------------------------------------------------------ | ------------------ |
|
|
49
|
+
| `--connection-id <id>` | Brainerce vibe-coded connection ID (`vc_*`) | prompted |
|
|
50
|
+
| `--canvas` | Bare unstyled `src/ui/` skeletons for AI-driven design | off |
|
|
51
|
+
| `--language <lang>` | Store language (`en`, `he`) | fetched from store |
|
|
52
|
+
| `--pkg-manager <pm>` | `npm`, `pnpm`, `yarn`, `bun` | auto-detected |
|
|
53
|
+
| `--framework <fw>` | `nextjs` (Vite/Remix coming) | `nextjs` |
|
|
54
|
+
| `--api-url <url>` | Brainerce API base URL (or `BRAINERCE_API_URL` env) | auto-discovered |
|
|
55
|
+
| `--no-git` | Skip git initialization | git on |
|
|
56
|
+
| `--no-install` | Skip dependency installation | install on |
|
|
57
57
|
|
|
58
58
|
Pass `.` as the project name to scaffold into the current directory.
|
|
59
59
|
|
|
60
|
+
## Reading the connection
|
|
61
|
+
|
|
62
|
+
Before scaffolding, the CLI reads the channel from `/api/vc/<id>/info` so the
|
|
63
|
+
generated project gets the real store name, currency and language rather than
|
|
64
|
+
guesses. It walks production first, then staging, and stops at the first
|
|
65
|
+
environment that answers. When it has to fall back, it says so and names the
|
|
66
|
+
URL the project will be wired to.
|
|
67
|
+
|
|
68
|
+
That endpoint checks the request's `Origin` against the channel's **Domain**
|
|
69
|
+
and **Allowed Origins**, and the match counts the **port**. A Test channel with
|
|
70
|
+
an empty Domain box accepts anything; one with `localhost:3000` recorded will
|
|
71
|
+
refuse a bare `http://localhost`. The CLI therefore presents
|
|
72
|
+
`http://localhost:3000` first and plain `http://localhost` second, and reports
|
|
73
|
+
what every environment said rather than only the last one. A `403` from
|
|
74
|
+
production must never surface as "not found on staging".
|
|
75
|
+
|
|
76
|
+
A channel it cannot read is a hard stop, not a warning. Build-time
|
|
77
|
+
`NEXT_PUBLIC_*` values are inlined into the client bundle and cannot be
|
|
78
|
+
corrected at deploy time, so guessing `USD` / `en` here would ship a storefront
|
|
79
|
+
quoting the wrong currency.
|
|
80
|
+
|
|
60
81
|
## Requirements
|
|
61
82
|
|
|
62
83
|
Node `^20.19.0 || ^22.13.0 || >=24.0.0`.
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.72.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
|
@@ -206,7 +206,36 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
|
206
206
|
// dashboard's "orders from ChatGPT/Google/…" reporting. No template code
|
|
207
207
|
// depends on it (fully automatic), but scaffolds should ship the SDK that
|
|
208
208
|
// actually reports attribution.
|
|
209
|
-
|
|
209
|
+
// 1.60 adds marketing.subscribe() — confirmed opt-in for a storefront
|
|
210
|
+
// newsletter signup. The scaffolded footer's signup form calls it, so an
|
|
211
|
+
// older SDK fails type-check at scaffold time.
|
|
212
|
+
// 1.61 adds stockAlerts.subscribe() plus two fields the scaffolded product
|
|
213
|
+
// page reads to decide whether to offer it at all —
|
|
214
|
+
// StoreInfo.stockAlertsEnabled (the merchant's switch) and
|
|
215
|
+
// InventoryInfo.backorderMode (an alert on a backorderable item is a
|
|
216
|
+
// no-op). All three are referenced by the generated back-in-stock form, so
|
|
217
|
+
// an older SDK fails type-check at scaffold time.
|
|
218
|
+
// 1.62 adds the availability clock. The scaffolded DATE/DATETIME checkout
|
|
219
|
+
// pickers pass `{ timezone }` as a third argument to isCalendarDateAllowed()/
|
|
220
|
+
// computeAvailableSlots()/getBusinessHoursForDate() — without it the
|
|
221
|
+
// leadTimeMinutes/cutoffTime/maxDaysAhead bounds are skipped and the picker
|
|
222
|
+
// offers days the API then rejects — and an older SDK types those helpers
|
|
223
|
+
// with two parameters, so the scaffold fails type-check outright.
|
|
224
|
+
// 1.63 adds customer photos on reviews. The scaffolded review form calls
|
|
225
|
+
// uploadReviewPhoto() and passes `imageKeys` on submit, reads `photos` off
|
|
226
|
+
// getMyProductReview() to decide whether to render a picker at all, and the
|
|
227
|
+
// reviews list renders `review.images`. None of those exist on an older SDK,
|
|
228
|
+
// so the scaffold fails type-check outright.
|
|
229
|
+
// 2.0 corrects OrderStatus to the twelve canonical UPPERCASE values. The API
|
|
230
|
+
// has always returned them that way; the 1.x union declared six lowercase
|
|
231
|
+
// ones, so the scaffolded order timeline and order history looked their
|
|
232
|
+
// status maps up with a value that could never match and rendered every
|
|
233
|
+
// order as "Pending". Both components are now keyed on the real twelve, as
|
|
234
|
+
// `Record<OrderStatus, ...>`, so a 1.x SDK fails type-check on excess and
|
|
235
|
+
// missing keys. 2.0 also stops forgotPassword sending `resetUrl`, which the
|
|
236
|
+
// API rejects with 400 under forbidNonWhitelisted, so a scaffold on 1.x has
|
|
237
|
+
// a broken password reset in the browser.
|
|
238
|
+
brainerce: "^2.0.0",
|
|
210
239
|
"isomorphic-dompurify": "^3.8.0"
|
|
211
240
|
});
|
|
212
241
|
|
|
@@ -280,16 +309,18 @@ function detectInvokingPackageManager() {
|
|
|
280
309
|
// src/cli.ts
|
|
281
310
|
async function runInteractive(defaults) {
|
|
282
311
|
const questions = [];
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
312
|
+
if (!defaults.projectName) {
|
|
313
|
+
questions.push({
|
|
314
|
+
type: "text",
|
|
315
|
+
name: "projectName",
|
|
316
|
+
message: "Project name:",
|
|
317
|
+
initial: defaults.projectNameSuggestion || "",
|
|
318
|
+
validate: (value) => {
|
|
319
|
+
const error = validateProjectName(value);
|
|
320
|
+
return error || true;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
293
324
|
if (!defaults.connectionId) {
|
|
294
325
|
questions.push({
|
|
295
326
|
type: "text",
|
|
@@ -314,37 +345,42 @@ async function runInteractive(defaults) {
|
|
|
314
345
|
initial: 0
|
|
315
346
|
});
|
|
316
347
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
348
|
+
if (!defaults.pkgManager) {
|
|
349
|
+
questions.push({
|
|
350
|
+
type: "select",
|
|
351
|
+
name: "pkgManager",
|
|
352
|
+
message: "Package manager:",
|
|
353
|
+
choices: [
|
|
354
|
+
{
|
|
355
|
+
title: `pnpm${detectInvokingPackageManager() === "pnpm" ? " (detected)" : ""}`,
|
|
356
|
+
value: "pnpm"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
title: `npm${detectInvokingPackageManager() === "npm" ? " (detected)" : ""}`,
|
|
360
|
+
value: "npm"
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
title: `yarn${detectInvokingPackageManager() === "yarn" ? " (detected)" : ""}`,
|
|
364
|
+
value: "yarn"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
title: `bun${detectInvokingPackageManager() === "bun" ? " (detected)" : ""}`,
|
|
368
|
+
value: "bun"
|
|
369
|
+
}
|
|
370
|
+
],
|
|
371
|
+
initial: ["pnpm", "npm", "yarn", "bun"].indexOf(detectInvokingPackageManager())
|
|
372
|
+
});
|
|
373
|
+
}
|
|
341
374
|
const response = await (0, import_prompts.default)(questions, {
|
|
342
375
|
onCancel: () => {
|
|
343
376
|
throw new Error("PROMPT_CANCELLED");
|
|
344
377
|
}
|
|
345
378
|
});
|
|
346
379
|
return {
|
|
347
|
-
|
|
380
|
+
// The typed answer wins. This read used to be the other way round, so a
|
|
381
|
+
// name typed at the prompt was silently replaced by the slug offered as
|
|
382
|
+
// its placeholder.
|
|
383
|
+
projectName: response.projectName || defaults.projectName || "",
|
|
348
384
|
connectionId: defaults.connectionId || response.connectionId,
|
|
349
385
|
language: response.language || defaults.language || "en",
|
|
350
386
|
framework: defaults.framework || "nextjs",
|
|
@@ -605,13 +641,32 @@ var KNOWN_API_URLS = {
|
|
|
605
641
|
production: "https://api.brainerce.com",
|
|
606
642
|
staging: "https://api-staging.brainerce.com"
|
|
607
643
|
};
|
|
608
|
-
var
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
644
|
+
var PROBE_ORIGINS = ["http://localhost:3000", "http://localhost"];
|
|
645
|
+
var StoreInfoProbeError = class extends Error {
|
|
646
|
+
constructor(kind, baseUrl, message, originRetryable = false) {
|
|
647
|
+
super(message);
|
|
648
|
+
this.kind = kind;
|
|
649
|
+
this.baseUrl = baseUrl;
|
|
650
|
+
this.originRetryable = originRetryable;
|
|
651
|
+
this.name = "StoreInfoProbeError";
|
|
612
652
|
}
|
|
613
653
|
};
|
|
614
|
-
|
|
654
|
+
function envLabel(baseUrl) {
|
|
655
|
+
if (baseUrl === KNOWN_API_URLS.production) return "production";
|
|
656
|
+
if (baseUrl === KNOWN_API_URLS.staging) return "staging";
|
|
657
|
+
return baseUrl;
|
|
658
|
+
}
|
|
659
|
+
async function readErrorMessage(res) {
|
|
660
|
+
try {
|
|
661
|
+
const body = await res.json();
|
|
662
|
+
if (typeof body.message === "string" && body.message.trim() !== "") {
|
|
663
|
+
return body.message.trim();
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
|
+
return `HTTP ${res.status}`;
|
|
668
|
+
}
|
|
669
|
+
async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production, origin = PROBE_ORIGINS[0]) {
|
|
615
670
|
const url = `${baseUrl}/api/vc/${connectionId}/info`;
|
|
616
671
|
const controller = new AbortController();
|
|
617
672
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -619,35 +674,46 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
|
|
|
619
674
|
try {
|
|
620
675
|
res = await fetch(url, {
|
|
621
676
|
signal: controller.signal,
|
|
622
|
-
headers: { Origin:
|
|
677
|
+
headers: { Origin: origin }
|
|
623
678
|
});
|
|
624
679
|
} catch (err) {
|
|
625
680
|
if (err.name === "AbortError") {
|
|
626
|
-
throw new
|
|
681
|
+
throw new StoreInfoProbeError("OTHER", baseUrl, "request timed out after 10s");
|
|
627
682
|
}
|
|
628
|
-
throw new
|
|
683
|
+
throw new StoreInfoProbeError(
|
|
684
|
+
"OTHER",
|
|
685
|
+
baseUrl,
|
|
686
|
+
`could not connect (${err.message})`
|
|
687
|
+
);
|
|
629
688
|
} finally {
|
|
630
689
|
clearTimeout(timeout);
|
|
631
690
|
}
|
|
632
691
|
if (res.status === 404) {
|
|
633
|
-
throw new
|
|
692
|
+
throw new StoreInfoProbeError("NOT_FOUND", baseUrl, "404 connection not found");
|
|
693
|
+
}
|
|
694
|
+
if (res.status === 403) {
|
|
695
|
+
const message = await readErrorMessage(res);
|
|
696
|
+
const originRetryable = message === "Origin not allowed for TEST channel";
|
|
697
|
+
throw new StoreInfoProbeError("ORIGIN_REFUSED", baseUrl, `403 ${message}`, originRetryable);
|
|
634
698
|
}
|
|
635
699
|
if (!res.ok) {
|
|
636
|
-
throw new
|
|
700
|
+
throw new StoreInfoProbeError("OTHER", baseUrl, `returned status ${res.status}`);
|
|
637
701
|
}
|
|
638
702
|
let json;
|
|
639
703
|
try {
|
|
640
704
|
json = await res.json();
|
|
641
705
|
} catch {
|
|
642
|
-
throw new
|
|
706
|
+
throw new StoreInfoProbeError("OTHER", baseUrl, "invalid (non-JSON) response");
|
|
643
707
|
}
|
|
644
708
|
const storeName = json.storeName || json.name || "My Store";
|
|
645
709
|
const displayName = json.channelName || storeName;
|
|
646
710
|
const currency = json.currency;
|
|
647
711
|
const language = json.language;
|
|
648
712
|
if (!currency || !language) {
|
|
649
|
-
throw new
|
|
650
|
-
|
|
713
|
+
throw new StoreInfoProbeError(
|
|
714
|
+
"OTHER",
|
|
715
|
+
baseUrl,
|
|
716
|
+
`malformed /info response (missing ${!currency ? "currency" : "language"})`
|
|
651
717
|
);
|
|
652
718
|
}
|
|
653
719
|
return {
|
|
@@ -658,32 +724,63 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
|
|
|
658
724
|
...json.i18n ? { i18n: json.i18n } : {}
|
|
659
725
|
};
|
|
660
726
|
}
|
|
727
|
+
function asProbeError(err, baseUrl) {
|
|
728
|
+
return err instanceof StoreInfoProbeError ? err : new StoreInfoProbeError("OTHER", baseUrl, err?.message || String(err));
|
|
729
|
+
}
|
|
730
|
+
async function probeStoreInfo(connectionId, baseUrl) {
|
|
731
|
+
let lastError;
|
|
732
|
+
for (const origin of PROBE_ORIGINS) {
|
|
733
|
+
try {
|
|
734
|
+
return await fetchStoreInfo(connectionId, baseUrl, origin);
|
|
735
|
+
} catch (err) {
|
|
736
|
+
const probeError = asProbeError(err, baseUrl);
|
|
737
|
+
lastError = probeError;
|
|
738
|
+
if (!probeError.originRetryable) throw probeError;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
throw lastError ?? new StoreInfoProbeError("OTHER", baseUrl, "no origins to try");
|
|
742
|
+
}
|
|
661
743
|
async function resolveStoreInfo(connectionId, candidateUrls) {
|
|
662
744
|
const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
|
|
663
745
|
if (urls.length === 0) {
|
|
664
746
|
throw new Error("No API URLs to try");
|
|
665
747
|
}
|
|
666
|
-
|
|
667
|
-
let allNotFound = true;
|
|
748
|
+
const failures = [];
|
|
668
749
|
for (let i = 0; i < urls.length; i++) {
|
|
669
750
|
const baseUrl = urls[i];
|
|
670
751
|
try {
|
|
671
|
-
const info = await
|
|
752
|
+
const info = await probeStoreInfo(connectionId, baseUrl);
|
|
672
753
|
return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
|
|
673
754
|
} catch (err) {
|
|
674
|
-
|
|
675
|
-
const isNotFound = err.code === "NOT_FOUND";
|
|
676
|
-
if (!isNotFound) {
|
|
677
|
-
allNotFound = false;
|
|
678
|
-
}
|
|
755
|
+
failures.push(asProbeError(err, baseUrl));
|
|
679
756
|
}
|
|
680
757
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
758
|
+
throw new Error(describeFailures(connectionId, failures));
|
|
759
|
+
}
|
|
760
|
+
function describeFailures(connectionId, failures) {
|
|
761
|
+
if (failures.every((f) => f.kind === "NOT_FOUND")) {
|
|
762
|
+
return `Connection "${connectionId}" was not found in any known environment (${failures.map((f) => envLabel(f.baseUrl)).join(", ")}). Check the connection ID in your dashboard.`;
|
|
685
763
|
}
|
|
686
|
-
|
|
764
|
+
const lines = failures.map((f) => {
|
|
765
|
+
const label = envLabel(f.baseUrl);
|
|
766
|
+
const where = label === f.baseUrl ? f.baseUrl : `${label} (${f.baseUrl})`;
|
|
767
|
+
return ` ${where}: ${f.message}`;
|
|
768
|
+
});
|
|
769
|
+
let report = `Connection "${connectionId}" could not be read:
|
|
770
|
+
${lines.join("\n")}`;
|
|
771
|
+
const testRefusal = failures.find((f) => f.kind === "ORIGIN_REFUSED" && f.originRetryable);
|
|
772
|
+
if (testRefusal) {
|
|
773
|
+
return report + `
|
|
774
|
+
|
|
775
|
+
The channel exists on ${envLabel(testRefusal.baseUrl)} but refused this machine's origin. Its Domain / Allowed Origins cover neither ${PROBE_ORIGINS.join(" nor ")}. On a TEST channel you can clear the Domain field, which opens it to any origin, or add the origin your dev server runs on to Allowed Origins.`;
|
|
776
|
+
}
|
|
777
|
+
const liveRefusal = failures.find((f) => f.kind === "ORIGIN_REFUSED");
|
|
778
|
+
if (liveRefusal) {
|
|
779
|
+
report += `
|
|
780
|
+
|
|
781
|
+
${envLabel(liveRefusal.baseUrl)} refused the request outright, which is what a LIVE (domain-locked) channel does: it answers its own registered domain and never localhost. Scaffold against a TEST channel instead.`;
|
|
782
|
+
}
|
|
783
|
+
return report;
|
|
687
784
|
}
|
|
688
785
|
|
|
689
786
|
// src/utils/logger.ts
|
|
@@ -767,6 +864,34 @@ async function checkForUpdate(name, current) {
|
|
|
767
864
|
return null;
|
|
768
865
|
}
|
|
769
866
|
}
|
|
867
|
+
async function resolveStoreInfoOrExit(connectionId, candidateApiUrls) {
|
|
868
|
+
const spinner = createSpinner("Fetching store info...");
|
|
869
|
+
spinner.start();
|
|
870
|
+
try {
|
|
871
|
+
const resolved = await resolveStoreInfo(connectionId, candidateApiUrls);
|
|
872
|
+
const info = resolved.info;
|
|
873
|
+
const i18nStatus = info.i18n?.enabled && info.i18n.supportedLocales.length > 1 ? ` | i18n: ${info.i18n.supportedLocales.join(", ")}` : "";
|
|
874
|
+
const channelLabel = info.name !== info.storeName ? `"${info.name}" (channel in "${info.storeName}")` : `"${info.name}"`;
|
|
875
|
+
spinner.succeed(
|
|
876
|
+
`Channel: ${channelLabel} | ${info.currency} | ${info.language}${i18nStatus} [${envLabel(resolved.apiBaseUrl)}]`
|
|
877
|
+
);
|
|
878
|
+
if (resolved.fellBack) {
|
|
879
|
+
logger.warn(
|
|
880
|
+
`Resolved on ${envLabel(resolved.apiBaseUrl)}, not production. This project will be wired to ${resolved.apiBaseUrl}.`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
return { info, apiBaseUrl: resolved.apiBaseUrl };
|
|
884
|
+
} catch (err) {
|
|
885
|
+
spinner.fail("Could not fetch store info");
|
|
886
|
+
logger.error(
|
|
887
|
+
err instanceof Error ? err.message : "Could not reach the Brainerce API to read this connection."
|
|
888
|
+
);
|
|
889
|
+
logger.info(
|
|
890
|
+
"Re-run after verifying:\n \u2022 the connection ID matches a real sales channel in your dashboard\n \u2022 the machine running create-brainerce-store can reach the Brainerce API\n \u2022 if you are offline / behind a firewall, scaffold from a machine that can reach the API"
|
|
891
|
+
);
|
|
892
|
+
process.exit(1);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
770
895
|
var program = new import_commander.Command();
|
|
771
896
|
program.name("create-brainerce-store").description("Scaffold a production-ready e-commerce storefront connected to Brainerce").version(pkg.version).argument("[project-name]", "Name for the project directory").option("--connection-id <id>", "Brainerce vibe-coded connection ID (vc_*)").option(
|
|
772
897
|
"--api-url <url>",
|
|
@@ -843,33 +968,19 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
843
968
|
logger.error(connError2);
|
|
844
969
|
process.exit(1);
|
|
845
970
|
}
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const resolved = await resolveStoreInfo(connectionId, candidateApiUrls);
|
|
850
|
-
storeInfo = resolved.info;
|
|
851
|
-
resolvedApiUrl = resolved.apiBaseUrl;
|
|
852
|
-
const envLabel = resolved.apiBaseUrl === KNOWN_API_URLS.staging ? " [staging]" : resolved.apiBaseUrl === KNOWN_API_URLS.production ? "" : ` [${resolved.apiBaseUrl}]`;
|
|
853
|
-
const i18nStatus = storeInfo.i18n?.enabled && storeInfo.i18n.supportedLocales.length > 1 ? ` | i18n: ${storeInfo.i18n.supportedLocales.join(", ")}` : "";
|
|
854
|
-
const channelLabel = storeInfo.name !== storeInfo.storeName ? `"${storeInfo.name}" (channel in "${storeInfo.storeName}")` : `"${storeInfo.name}"`;
|
|
855
|
-
prefetchSpinner.succeed(
|
|
856
|
-
`Channel: ${channelLabel} | ${storeInfo.currency} | ${storeInfo.language}${i18nStatus}${envLabel}`
|
|
857
|
-
);
|
|
858
|
-
} catch (err) {
|
|
859
|
-
prefetchSpinner.fail("Could not fetch store info");
|
|
860
|
-
logger.warn(
|
|
861
|
-
err instanceof Error ? err.message : "Using defaults. Make sure the connection ID is correct and the Brainerce API is reachable."
|
|
862
|
-
);
|
|
863
|
-
}
|
|
971
|
+
const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
|
|
972
|
+
storeInfo = resolved.info;
|
|
973
|
+
resolvedApiUrl = resolved.apiBaseUrl;
|
|
864
974
|
}
|
|
865
975
|
const slugify = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9._-]+/g, "-").replace(/^[.-]+|[.-]+$/g, "").slice(0, 50);
|
|
866
976
|
const channelSlug = storeInfo ? slugify(storeInfo.name) : "";
|
|
867
977
|
const storeSlug = storeInfo ? slugify(storeInfo.storeName) : "";
|
|
868
|
-
const
|
|
978
|
+
const projectNameSuggestion = channelSlug || storeSlug || void 0;
|
|
869
979
|
const languageDefault = language || storeInfo?.language;
|
|
870
980
|
if (!projectName || !connectionId || !language) {
|
|
871
981
|
const answers = await runInteractive({
|
|
872
|
-
projectName
|
|
982
|
+
projectName,
|
|
983
|
+
projectNameSuggestion,
|
|
873
984
|
connectionId,
|
|
874
985
|
language: languageDefault,
|
|
875
986
|
framework,
|
|
@@ -895,28 +1006,9 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
895
1006
|
process.exit(1);
|
|
896
1007
|
}
|
|
897
1008
|
if (!storeInfo) {
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const resolved = await resolveStoreInfo(connectionId, candidateApiUrls);
|
|
902
|
-
storeInfo = resolved.info;
|
|
903
|
-
resolvedApiUrl = resolved.apiBaseUrl;
|
|
904
|
-
const envLabel = resolved.apiBaseUrl === KNOWN_API_URLS.staging ? " [staging]" : resolved.apiBaseUrl === KNOWN_API_URLS.production ? "" : ` [${resolved.apiBaseUrl}]`;
|
|
905
|
-
const i18nStatus = storeInfo.i18n?.enabled && storeInfo.i18n.supportedLocales.length > 1 ? ` | i18n: ${storeInfo.i18n.supportedLocales.join(", ")}` : "";
|
|
906
|
-
const channelLabel = storeInfo.name !== storeInfo.storeName ? `"${storeInfo.name}" (channel in "${storeInfo.storeName}")` : `"${storeInfo.name}"`;
|
|
907
|
-
spinner.succeed(
|
|
908
|
-
`Channel: ${channelLabel} | ${storeInfo.currency} | ${storeInfo.language}${i18nStatus}${envLabel}`
|
|
909
|
-
);
|
|
910
|
-
} catch (err) {
|
|
911
|
-
spinner.fail("Could not fetch store info");
|
|
912
|
-
logger.error(
|
|
913
|
-
err instanceof Error ? err.message : "Could not reach the Brainerce API to read this connection."
|
|
914
|
-
);
|
|
915
|
-
logger.info(
|
|
916
|
-
"Re-run after verifying:\n \u2022 the connection ID matches a real sales channel in your dashboard\n \u2022 the machine running create-brainerce-store can reach the Brainerce API\n \u2022 if you are offline / behind a firewall, scaffold from a machine that can reach the API"
|
|
917
|
-
);
|
|
918
|
-
process.exit(1);
|
|
919
|
-
}
|
|
1009
|
+
const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
|
|
1010
|
+
storeInfo = resolved.info;
|
|
1011
|
+
resolvedApiUrl = resolved.apiBaseUrl;
|
|
920
1012
|
}
|
|
921
1013
|
if (!pkgManager) {
|
|
922
1014
|
pkgManager = detectInvokingPackageManager();
|
package/messages/en.json
CHANGED
|
@@ -38,7 +38,28 @@
|
|
|
38
38
|
"allRightsReserved": "All rights reserved.",
|
|
39
39
|
"store": "Store",
|
|
40
40
|
"save": "Save",
|
|
41
|
-
"cancel": "Cancel"
|
|
41
|
+
"cancel": "Cancel",
|
|
42
|
+
"birthday": "Birthday",
|
|
43
|
+
"birthdayMonth": "Month",
|
|
44
|
+
"birthdayDay": "Day",
|
|
45
|
+
"birthdayGiftNote": "Add your birthday and a gift arrives by email on the day.",
|
|
46
|
+
"birthdayIncomplete": "Choose both a month and a day, or leave both empty.",
|
|
47
|
+
"birthdayRequired": "Choose your birthday month and day to continue.",
|
|
48
|
+
"birthdayPlaceholder": "Choose your birthday",
|
|
49
|
+
"birthdayClear": "Clear",
|
|
50
|
+
"birthdayDisplay": "{month} {day}",
|
|
51
|
+
"monthJanuary": "January",
|
|
52
|
+
"monthFebruary": "February",
|
|
53
|
+
"monthMarch": "March",
|
|
54
|
+
"monthApril": "April",
|
|
55
|
+
"monthMay": "May",
|
|
56
|
+
"monthJune": "June",
|
|
57
|
+
"monthJuly": "July",
|
|
58
|
+
"monthAugust": "August",
|
|
59
|
+
"monthSeptember": "September",
|
|
60
|
+
"monthOctober": "October",
|
|
61
|
+
"monthNovember": "November",
|
|
62
|
+
"monthDecember": "December"
|
|
42
63
|
},
|
|
43
64
|
"nav": {
|
|
44
65
|
"products": "Products",
|
|
@@ -130,7 +151,13 @@
|
|
|
130
151
|
"saving": "Saving…",
|
|
131
152
|
"submittingBtn": "Submitting…",
|
|
132
153
|
"saveChanges": "Save changes",
|
|
133
|
-
"submitReview": "Submit review"
|
|
154
|
+
"submitReview": "Submit review",
|
|
155
|
+
"addPhotos": "Add photos",
|
|
156
|
+
"removePhoto": "Remove photo",
|
|
157
|
+
"photoUploading": "Uploading…",
|
|
158
|
+
"photoTooLarge": "That photo is too large. Please pick a smaller one.",
|
|
159
|
+
"photoUploadFailed": "Could not upload that photo. Please try another.",
|
|
160
|
+
"photosNeedApproval": "Photos are checked before they appear, so yours may not show right away."
|
|
134
161
|
},
|
|
135
162
|
"customization": {
|
|
136
163
|
"title": "Personalize this product",
|
|
@@ -161,7 +188,8 @@
|
|
|
161
188
|
"addBundleItem": "Add & Save",
|
|
162
189
|
"addingBundle": "Adding...",
|
|
163
190
|
"selectOptions": "Select options",
|
|
164
|
-
"outOfStock": "Out of stock"
|
|
191
|
+
"outOfStock": "Out of stock",
|
|
192
|
+
"unavailableItemsHint": "Remove the items marked out of stock or unavailable to continue to checkout."
|
|
165
193
|
},
|
|
166
194
|
"checkout": {
|
|
167
195
|
"pageTitle": "Checkout",
|
|
@@ -258,6 +286,8 @@
|
|
|
258
286
|
"customFieldsDateNextMonth": "Next month",
|
|
259
287
|
"customFieldsTimeLabel": "Time",
|
|
260
288
|
"customFieldsTimeClosed": "No delivery times are available on this date.",
|
|
289
|
+
"customFieldsTimeNoneLeft": "Every time on this date has already passed. Please pick a later date.",
|
|
290
|
+
"customFieldsTimeTo": "to",
|
|
261
291
|
"changeOptions": "Change options",
|
|
262
292
|
"surcharges": "Additional charges"
|
|
263
293
|
},
|
|
@@ -390,12 +420,18 @@
|
|
|
390
420
|
"memberSince": "Member since",
|
|
391
421
|
"orderPrefix": "Order",
|
|
392
422
|
"productFallback": "Product",
|
|
423
|
+
"statusDraft": "Draft",
|
|
393
424
|
"statusPending": "Pending",
|
|
394
425
|
"statusProcessing": "Processing",
|
|
426
|
+
"statusOnHold": "On hold",
|
|
427
|
+
"statusPaid": "Paid",
|
|
395
428
|
"statusShipped": "Shipped",
|
|
396
429
|
"statusDelivered": "Delivered",
|
|
430
|
+
"statusCompleted": "Completed",
|
|
431
|
+
"statusFulfilled": "Fulfilled",
|
|
397
432
|
"statusCancelled": "Cancelled",
|
|
398
433
|
"statusRefunded": "Refunded",
|
|
434
|
+
"statusPartiallyRefunded": "Partially refunded",
|
|
399
435
|
"editProfile": "Edit Profile",
|
|
400
436
|
"firstName": "First Name",
|
|
401
437
|
"lastName": "Last Name",
|
|
@@ -472,6 +508,9 @@
|
|
|
472
508
|
},
|
|
473
509
|
"reservation": {
|
|
474
510
|
"expired": "Reservation expired. Items may no longer be available.",
|
|
511
|
+
"expiredHint": "We refreshed your cart. Check it before you continue.",
|
|
512
|
+
"expiredCheckout": "Your reserved stock was released, so payment is on hold. Go back to your cart and review it.",
|
|
513
|
+
"backToCart": "Back to cart",
|
|
475
514
|
"hurry": "Hurry!",
|
|
476
515
|
"reservedFor": "Items reserved for"
|
|
477
516
|
},
|
|
@@ -496,6 +535,27 @@
|
|
|
496
535
|
"genericError": "Something went wrong. Please try again.",
|
|
497
536
|
"fieldRequired": "{label} is required"
|
|
498
537
|
},
|
|
538
|
+
"backInStock": {
|
|
539
|
+
"title": "Email me when it's back",
|
|
540
|
+
"subtitle": "One message about this item, nothing else. You can stop it any time.",
|
|
541
|
+
"placeholder": "your@email.com",
|
|
542
|
+
"submit": "Notify me",
|
|
543
|
+
"submitting": "Sending...",
|
|
544
|
+
"doneTitle": "We'll email you when it's back",
|
|
545
|
+
"doneBody": "Stock is limited, so it may go quickly once it returns.",
|
|
546
|
+
"genericError": "Something went wrong. Please try again."
|
|
547
|
+
},
|
|
548
|
+
"newsletter": {
|
|
549
|
+
"title": "Join our newsletter",
|
|
550
|
+
"subtitle": "Offers and new arrivals, straight to your inbox.",
|
|
551
|
+
"placeholder": "your@email.com",
|
|
552
|
+
"submit": "Subscribe",
|
|
553
|
+
"submitting": "Sending...",
|
|
554
|
+
"checkEmailTitle": "Almost there — check your email",
|
|
555
|
+
"checkEmailBody": "We've sent you a link to confirm. Look in your spam folder too; you won't get another copy for 24 hours.",
|
|
556
|
+
"discountCode": "Your code: {code}",
|
|
557
|
+
"genericError": "Something went wrong. Please try again."
|
|
558
|
+
},
|
|
499
559
|
"content": {
|
|
500
560
|
"faqTitle": "Frequently Asked Questions",
|
|
501
561
|
"faqEmptyTitle": "FAQ coming soon",
|