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.
Files changed (50) hide show
  1. package/README.md +31 -10
  2. package/dist/index.js +197 -105
  3. package/messages/en.json +63 -3
  4. package/messages/he.json +63 -3
  5. package/package.json +1 -1
  6. package/templates/nextjs/base/TRANSLATIONS.md +14 -7
  7. package/templates/nextjs/base/src/app/checkout/page.tsx +1074 -1017
  8. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +21 -2
  9. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +1 -1
  10. package/templates/nextjs/base/src/app/register/page.tsx +67 -64
  11. package/templates/nextjs/base/src/components/account/order-history.tsx +25 -39
  12. package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +30 -11
  13. package/templates/nextjs/base/src/components/account/profile-section.tsx +303 -226
  14. package/templates/nextjs/base/src/components/auth/register-form.tsx +326 -245
  15. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +306 -294
  16. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +13 -1
  17. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +61 -21
  18. package/templates/nextjs/base/src/components/shared/birthday-picker.tsx +258 -0
  19. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +71 -2
  20. package/templates/nextjs/base/src/core/lib/auth.ts +155 -154
  21. package/templates/nextjs/base/src/core/lib/birthday.ts +74 -0
  22. package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +5 -16
  23. package/templates/nextjs/base/src/core/lib/store-info.ts +10 -0
  24. package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +3 -6
  25. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +19 -1
  26. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +42 -6
  27. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +52 -10
  28. package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +143 -0
  29. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +18 -2
  30. package/templates/nextjs/base/src/ui/product/back-in-stock-form.tsx +173 -0
  31. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +484 -455
  32. package/templates/nextjs/base/src/ui/product/review-form.tsx +136 -12
  33. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +139 -108
  34. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +21 -3
  35. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +19 -1
  36. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +44 -7
  37. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +52 -10
  38. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +155 -142
  39. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +500 -477
  40. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +135 -11
  41. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +179 -148
  42. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +15 -1
  43. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +38 -4
  44. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +54 -11
  45. package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +122 -0
  46. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +87 -83
  47. package/templates/nextjs/ui-canvas/product/back-in-stock-form.tsx +151 -0
  48. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +373 -352
  49. package/templates/nextjs/ui-canvas/product/review-form.tsx +129 -11
  50. 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 | 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 |
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.68.0",
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
- brainerce: "^1.58.0",
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
- questions.push({
284
- type: "text",
285
- name: "projectName",
286
- message: "Project name:",
287
- initial: defaults.projectName || "",
288
- validate: (value) => {
289
- const error = validateProjectName(value);
290
- return error || true;
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
- questions.push({
318
- type: "select",
319
- name: "pkgManager",
320
- message: "Package manager:",
321
- choices: [
322
- {
323
- title: `pnpm${detectInvokingPackageManager() === "pnpm" ? " (detected)" : ""}`,
324
- value: "pnpm"
325
- },
326
- {
327
- title: `npm${detectInvokingPackageManager() === "npm" ? " (detected)" : ""}`,
328
- value: "npm"
329
- },
330
- {
331
- title: `yarn${detectInvokingPackageManager() === "yarn" ? " (detected)" : ""}`,
332
- value: "yarn"
333
- },
334
- {
335
- title: `bun${detectInvokingPackageManager() === "bun" ? " (detected)" : ""}`,
336
- value: "bun"
337
- }
338
- ],
339
- initial: ["pnpm", "npm", "yarn", "bun"].indexOf(detectInvokingPackageManager())
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
- projectName: defaults.projectName || response.projectName,
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 ConnectionNotFoundError = class extends Error {
609
- constructor(connectionId, baseUrl) {
610
- super(`Connection "${connectionId}" not found at ${baseUrl}`);
611
- this.code = "NOT_FOUND";
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
- async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production) {
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: "http://localhost" }
677
+ headers: { Origin: origin }
623
678
  });
624
679
  } catch (err) {
625
680
  if (err.name === "AbortError") {
626
- throw new Error(`Request to ${baseUrl} timed out`);
681
+ throw new StoreInfoProbeError("OTHER", baseUrl, "request timed out after 10s");
627
682
  }
628
- throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
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 ConnectionNotFoundError(connectionId, baseUrl);
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 Error(`${baseUrl} returned status ${res.status}`);
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 Error(`Invalid response from ${baseUrl}`);
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 Error(
650
- `Malformed /info response from ${baseUrl} (missing ${!currency ? "currency" : "language"}).`
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
- let lastError;
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 fetchStoreInfo(connectionId, baseUrl);
752
+ const info = await probeStoreInfo(connectionId, baseUrl);
672
753
  return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
673
754
  } catch (err) {
674
- lastError = err;
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
- if (allNotFound) {
682
- throw new Error(
683
- `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}). Check your dashboard.`
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
- throw lastError || new Error("Failed to resolve store info");
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 prefetchSpinner = createSpinner("Fetching store info...");
847
- prefetchSpinner.start();
848
- try {
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 projectNameDefault = projectName || channelSlug || storeSlug || void 0;
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: projectNameDefault,
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 spinner = createSpinner("Fetching store info...");
899
- spinner.start();
900
- try {
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",