create-brainerce-store 1.71.0 → 1.73.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 +179 -107
- package/messages/en.json +14 -1
- package/messages/he.json +14 -1
- package/package.json +1 -1
- package/templates/nextjs/base/TRANSLATIONS.md +207 -200
- package/templates/nextjs/base/src/app/checkout/page.tsx +1074 -1018
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +21 -2
- package/templates/nextjs/base/src/components/account/order-history.tsx +371 -385
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +85 -66
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +127 -58
- package/templates/nextjs/base/src/core/lib/auth.ts +32 -39
- package/templates/nextjs/base/src/core/lib/brainerce.ts.ejs +5 -16
- package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +3 -6
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +164 -146
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +176 -140
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +137 -95
- package/templates/nextjs/base/src/ui/product/review-form.tsx +33 -11
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +177 -163
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +158 -140
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +184 -147
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +131 -89
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +30 -10
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +137 -123
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +140 -106
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +124 -81
- package/templates/nextjs/ui-canvas/product/review-form.tsx +9 -1
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.73.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"
|
|
@@ -226,7 +226,16 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
|
226
226
|
// getMyProductReview() to decide whether to render a picker at all, and the
|
|
227
227
|
// reviews list renders `review.images`. None of those exist on an older SDK,
|
|
228
228
|
// so the scaffold fails type-check outright.
|
|
229
|
-
|
|
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.1",
|
|
230
239
|
"isomorphic-dompurify": "^3.8.0"
|
|
231
240
|
});
|
|
232
241
|
|
|
@@ -300,16 +309,18 @@ function detectInvokingPackageManager() {
|
|
|
300
309
|
// src/cli.ts
|
|
301
310
|
async function runInteractive(defaults) {
|
|
302
311
|
const questions = [];
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
}
|
|
313
324
|
if (!defaults.connectionId) {
|
|
314
325
|
questions.push({
|
|
315
326
|
type: "text",
|
|
@@ -334,37 +345,42 @@ async function runInteractive(defaults) {
|
|
|
334
345
|
initial: 0
|
|
335
346
|
});
|
|
336
347
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
}
|
|
361
374
|
const response = await (0, import_prompts.default)(questions, {
|
|
362
375
|
onCancel: () => {
|
|
363
376
|
throw new Error("PROMPT_CANCELLED");
|
|
364
377
|
}
|
|
365
378
|
});
|
|
366
379
|
return {
|
|
367
|
-
|
|
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 || "",
|
|
368
384
|
connectionId: defaults.connectionId || response.connectionId,
|
|
369
385
|
language: response.language || defaults.language || "en",
|
|
370
386
|
framework: defaults.framework || "nextjs",
|
|
@@ -625,13 +641,32 @@ var KNOWN_API_URLS = {
|
|
|
625
641
|
production: "https://api.brainerce.com",
|
|
626
642
|
staging: "https://api-staging.brainerce.com"
|
|
627
643
|
};
|
|
628
|
-
var
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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";
|
|
632
652
|
}
|
|
633
653
|
};
|
|
634
|
-
|
|
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]) {
|
|
635
670
|
const url = `${baseUrl}/api/vc/${connectionId}/info`;
|
|
636
671
|
const controller = new AbortController();
|
|
637
672
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -639,35 +674,46 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
|
|
|
639
674
|
try {
|
|
640
675
|
res = await fetch(url, {
|
|
641
676
|
signal: controller.signal,
|
|
642
|
-
headers: { Origin:
|
|
677
|
+
headers: { Origin: origin }
|
|
643
678
|
});
|
|
644
679
|
} catch (err) {
|
|
645
680
|
if (err.name === "AbortError") {
|
|
646
|
-
throw new
|
|
681
|
+
throw new StoreInfoProbeError("OTHER", baseUrl, "request timed out after 10s");
|
|
647
682
|
}
|
|
648
|
-
throw new
|
|
683
|
+
throw new StoreInfoProbeError(
|
|
684
|
+
"OTHER",
|
|
685
|
+
baseUrl,
|
|
686
|
+
`could not connect (${err.message})`
|
|
687
|
+
);
|
|
649
688
|
} finally {
|
|
650
689
|
clearTimeout(timeout);
|
|
651
690
|
}
|
|
652
691
|
if (res.status === 404) {
|
|
653
|
-
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);
|
|
654
698
|
}
|
|
655
699
|
if (!res.ok) {
|
|
656
|
-
throw new
|
|
700
|
+
throw new StoreInfoProbeError("OTHER", baseUrl, `returned status ${res.status}`);
|
|
657
701
|
}
|
|
658
702
|
let json;
|
|
659
703
|
try {
|
|
660
704
|
json = await res.json();
|
|
661
705
|
} catch {
|
|
662
|
-
throw new
|
|
706
|
+
throw new StoreInfoProbeError("OTHER", baseUrl, "invalid (non-JSON) response");
|
|
663
707
|
}
|
|
664
708
|
const storeName = json.storeName || json.name || "My Store";
|
|
665
709
|
const displayName = json.channelName || storeName;
|
|
666
710
|
const currency = json.currency;
|
|
667
711
|
const language = json.language;
|
|
668
712
|
if (!currency || !language) {
|
|
669
|
-
throw new
|
|
670
|
-
|
|
713
|
+
throw new StoreInfoProbeError(
|
|
714
|
+
"OTHER",
|
|
715
|
+
baseUrl,
|
|
716
|
+
`malformed /info response (missing ${!currency ? "currency" : "language"})`
|
|
671
717
|
);
|
|
672
718
|
}
|
|
673
719
|
return {
|
|
@@ -678,32 +724,63 @@ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production)
|
|
|
678
724
|
...json.i18n ? { i18n: json.i18n } : {}
|
|
679
725
|
};
|
|
680
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
|
+
}
|
|
681
743
|
async function resolveStoreInfo(connectionId, candidateUrls) {
|
|
682
744
|
const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
|
|
683
745
|
if (urls.length === 0) {
|
|
684
746
|
throw new Error("No API URLs to try");
|
|
685
747
|
}
|
|
686
|
-
|
|
687
|
-
let allNotFound = true;
|
|
748
|
+
const failures = [];
|
|
688
749
|
for (let i = 0; i < urls.length; i++) {
|
|
689
750
|
const baseUrl = urls[i];
|
|
690
751
|
try {
|
|
691
|
-
const info = await
|
|
752
|
+
const info = await probeStoreInfo(connectionId, baseUrl);
|
|
692
753
|
return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
|
|
693
754
|
} catch (err) {
|
|
694
|
-
|
|
695
|
-
const isNotFound = err.code === "NOT_FOUND";
|
|
696
|
-
if (!isNotFound) {
|
|
697
|
-
allNotFound = false;
|
|
698
|
-
}
|
|
755
|
+
failures.push(asProbeError(err, baseUrl));
|
|
699
756
|
}
|
|
700
757
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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.`;
|
|
705
763
|
}
|
|
706
|
-
|
|
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;
|
|
707
784
|
}
|
|
708
785
|
|
|
709
786
|
// src/utils/logger.ts
|
|
@@ -787,6 +864,34 @@ async function checkForUpdate(name, current) {
|
|
|
787
864
|
return null;
|
|
788
865
|
}
|
|
789
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
|
+
}
|
|
790
895
|
var program = new import_commander.Command();
|
|
791
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(
|
|
792
897
|
"--api-url <url>",
|
|
@@ -863,35 +968,21 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
863
968
|
logger.error(connError2);
|
|
864
969
|
process.exit(1);
|
|
865
970
|
}
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const resolved = await resolveStoreInfo(connectionId, candidateApiUrls);
|
|
870
|
-
storeInfo = resolved.info;
|
|
871
|
-
resolvedApiUrl = resolved.apiBaseUrl;
|
|
872
|
-
const envLabel = resolved.apiBaseUrl === KNOWN_API_URLS.staging ? " [staging]" : resolved.apiBaseUrl === KNOWN_API_URLS.production ? "" : ` [${resolved.apiBaseUrl}]`;
|
|
873
|
-
const i18nStatus = storeInfo.i18n?.enabled && storeInfo.i18n.supportedLocales.length > 1 ? ` | i18n: ${storeInfo.i18n.supportedLocales.join(", ")}` : "";
|
|
874
|
-
const channelLabel = storeInfo.name !== storeInfo.storeName ? `"${storeInfo.name}" (channel in "${storeInfo.storeName}")` : `"${storeInfo.name}"`;
|
|
875
|
-
prefetchSpinner.succeed(
|
|
876
|
-
`Channel: ${channelLabel} | ${storeInfo.currency} | ${storeInfo.language}${i18nStatus}${envLabel}`
|
|
877
|
-
);
|
|
878
|
-
} catch (err) {
|
|
879
|
-
prefetchSpinner.fail("Could not fetch store info");
|
|
880
|
-
logger.warn(
|
|
881
|
-
err instanceof Error ? err.message : "Using defaults. Make sure the connection ID is correct and the Brainerce API is reachable."
|
|
882
|
-
);
|
|
883
|
-
}
|
|
971
|
+
const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
|
|
972
|
+
storeInfo = resolved.info;
|
|
973
|
+
resolvedApiUrl = resolved.apiBaseUrl;
|
|
884
974
|
}
|
|
885
975
|
const slugify = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9._-]+/g, "-").replace(/^[.-]+|[.-]+$/g, "").slice(0, 50);
|
|
886
976
|
const channelSlug = storeInfo ? slugify(storeInfo.name) : "";
|
|
887
977
|
const storeSlug = storeInfo ? slugify(storeInfo.storeName) : "";
|
|
888
|
-
const
|
|
889
|
-
|
|
978
|
+
const projectNameSuggestion = channelSlug || storeSlug || void 0;
|
|
979
|
+
language = language || storeInfo?.language;
|
|
890
980
|
if (!projectName || !connectionId || !language) {
|
|
891
981
|
const answers = await runInteractive({
|
|
892
|
-
projectName
|
|
982
|
+
projectName,
|
|
983
|
+
projectNameSuggestion,
|
|
893
984
|
connectionId,
|
|
894
|
-
language
|
|
985
|
+
language,
|
|
895
986
|
framework,
|
|
896
987
|
design,
|
|
897
988
|
pkgManager,
|
|
@@ -915,28 +1006,9 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
915
1006
|
process.exit(1);
|
|
916
1007
|
}
|
|
917
1008
|
if (!storeInfo) {
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const resolved = await resolveStoreInfo(connectionId, candidateApiUrls);
|
|
922
|
-
storeInfo = resolved.info;
|
|
923
|
-
resolvedApiUrl = resolved.apiBaseUrl;
|
|
924
|
-
const envLabel = resolved.apiBaseUrl === KNOWN_API_URLS.staging ? " [staging]" : resolved.apiBaseUrl === KNOWN_API_URLS.production ? "" : ` [${resolved.apiBaseUrl}]`;
|
|
925
|
-
const i18nStatus = storeInfo.i18n?.enabled && storeInfo.i18n.supportedLocales.length > 1 ? ` | i18n: ${storeInfo.i18n.supportedLocales.join(", ")}` : "";
|
|
926
|
-
const channelLabel = storeInfo.name !== storeInfo.storeName ? `"${storeInfo.name}" (channel in "${storeInfo.storeName}")` : `"${storeInfo.name}"`;
|
|
927
|
-
spinner.succeed(
|
|
928
|
-
`Channel: ${channelLabel} | ${storeInfo.currency} | ${storeInfo.language}${i18nStatus}${envLabel}`
|
|
929
|
-
);
|
|
930
|
-
} catch (err) {
|
|
931
|
-
spinner.fail("Could not fetch store info");
|
|
932
|
-
logger.error(
|
|
933
|
-
err instanceof Error ? err.message : "Could not reach the Brainerce API to read this connection."
|
|
934
|
-
);
|
|
935
|
-
logger.info(
|
|
936
|
-
"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"
|
|
937
|
-
);
|
|
938
|
-
process.exit(1);
|
|
939
|
-
}
|
|
1009
|
+
const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
|
|
1010
|
+
storeInfo = resolved.info;
|
|
1011
|
+
resolvedApiUrl = resolved.apiBaseUrl;
|
|
940
1012
|
}
|
|
941
1013
|
if (!pkgManager) {
|
|
942
1014
|
pkgManager = detectInvokingPackageManager();
|
package/messages/en.json
CHANGED
|
@@ -154,6 +154,9 @@
|
|
|
154
154
|
"submitReview": "Submit review",
|
|
155
155
|
"addPhotos": "Add photos",
|
|
156
156
|
"removePhoto": "Remove photo",
|
|
157
|
+
"choosePhotos": "Choose photos",
|
|
158
|
+
"photoFormats": "JPG, PNG, WebP or GIF, up to {mb}MB each.",
|
|
159
|
+
"photoLimitReached": "That is all {max} photos. Remove one to add another.",
|
|
157
160
|
"photoUploading": "Uploading…",
|
|
158
161
|
"photoTooLarge": "That photo is too large. Please pick a smaller one.",
|
|
159
162
|
"photoUploadFailed": "Could not upload that photo. Please try another.",
|
|
@@ -188,7 +191,8 @@
|
|
|
188
191
|
"addBundleItem": "Add & Save",
|
|
189
192
|
"addingBundle": "Adding...",
|
|
190
193
|
"selectOptions": "Select options",
|
|
191
|
-
"outOfStock": "Out of stock"
|
|
194
|
+
"outOfStock": "Out of stock",
|
|
195
|
+
"unavailableItemsHint": "Remove the items marked out of stock or unavailable to continue to checkout."
|
|
192
196
|
},
|
|
193
197
|
"checkout": {
|
|
194
198
|
"pageTitle": "Checkout",
|
|
@@ -419,12 +423,18 @@
|
|
|
419
423
|
"memberSince": "Member since",
|
|
420
424
|
"orderPrefix": "Order",
|
|
421
425
|
"productFallback": "Product",
|
|
426
|
+
"statusDraft": "Draft",
|
|
422
427
|
"statusPending": "Pending",
|
|
423
428
|
"statusProcessing": "Processing",
|
|
429
|
+
"statusOnHold": "On hold",
|
|
430
|
+
"statusPaid": "Paid",
|
|
424
431
|
"statusShipped": "Shipped",
|
|
425
432
|
"statusDelivered": "Delivered",
|
|
433
|
+
"statusCompleted": "Completed",
|
|
434
|
+
"statusFulfilled": "Fulfilled",
|
|
426
435
|
"statusCancelled": "Cancelled",
|
|
427
436
|
"statusRefunded": "Refunded",
|
|
437
|
+
"statusPartiallyRefunded": "Partially refunded",
|
|
428
438
|
"editProfile": "Edit Profile",
|
|
429
439
|
"firstName": "First Name",
|
|
430
440
|
"lastName": "Last Name",
|
|
@@ -501,6 +511,9 @@
|
|
|
501
511
|
},
|
|
502
512
|
"reservation": {
|
|
503
513
|
"expired": "Reservation expired. Items may no longer be available.",
|
|
514
|
+
"expiredHint": "We refreshed your cart. Check it before you continue.",
|
|
515
|
+
"expiredCheckout": "Your reserved stock was released, so payment is on hold. Go back to your cart and review it.",
|
|
516
|
+
"backToCart": "Back to cart",
|
|
504
517
|
"hurry": "Hurry!",
|
|
505
518
|
"reservedFor": "Items reserved for"
|
|
506
519
|
},
|
package/messages/he.json
CHANGED
|
@@ -154,6 +154,9 @@
|
|
|
154
154
|
"submitReview": "שליחת ביקורת",
|
|
155
155
|
"addPhotos": "הוספת תמונות",
|
|
156
156
|
"removePhoto": "הסרת תמונה",
|
|
157
|
+
"choosePhotos": "בחירת תמונות",
|
|
158
|
+
"photoFormats": "JPG, PNG, WebP או GIF, עד {mb}MB לכל תמונה.",
|
|
159
|
+
"photoLimitReached": "אלה כל {max} התמונות. הסר אחת כדי להוסיף אחרת.",
|
|
157
160
|
"photoUploading": "מעלה...",
|
|
158
161
|
"photoTooLarge": "התמונה הזו גדולה מדי. בחרו תמונה קטנה יותר.",
|
|
159
162
|
"photoUploadFailed": "לא הצלחנו להעלות את התמונה. נסו תמונה אחרת.",
|
|
@@ -188,7 +191,8 @@
|
|
|
188
191
|
"addBundleItem": "הוסף וחסוך",
|
|
189
192
|
"addingBundle": "מוסיף...",
|
|
190
193
|
"selectOptions": "בחר אפשרויות",
|
|
191
|
-
"outOfStock": "אזל מהמלאי"
|
|
194
|
+
"outOfStock": "אזל מהמלאי",
|
|
195
|
+
"unavailableItemsHint": "הסירו את המוצרים שמסומנים כאזלו מהמלאי או כלא זמינים כדי להמשיך לתשלום."
|
|
192
196
|
},
|
|
193
197
|
"checkout": {
|
|
194
198
|
"pageTitle": "תשלום",
|
|
@@ -419,12 +423,18 @@
|
|
|
419
423
|
"memberSince": "חבר מאז",
|
|
420
424
|
"orderPrefix": "הזמנה",
|
|
421
425
|
"productFallback": "מוצר",
|
|
426
|
+
"statusDraft": "טיוטה",
|
|
422
427
|
"statusPending": "ממתין",
|
|
423
428
|
"statusProcessing": "בטיפול",
|
|
429
|
+
"statusOnHold": "מושהה",
|
|
430
|
+
"statusPaid": "שולם",
|
|
424
431
|
"statusShipped": "נשלח",
|
|
425
432
|
"statusDelivered": "נמסר",
|
|
433
|
+
"statusCompleted": "הושלם",
|
|
434
|
+
"statusFulfilled": "סופק",
|
|
426
435
|
"statusCancelled": "בוטל",
|
|
427
436
|
"statusRefunded": "הוחזר",
|
|
437
|
+
"statusPartiallyRefunded": "הוחזר חלקית",
|
|
428
438
|
"editProfile": "עריכת פרופיל",
|
|
429
439
|
"firstName": "שם פרטי",
|
|
430
440
|
"lastName": "שם משפחה",
|
|
@@ -501,6 +511,9 @@
|
|
|
501
511
|
},
|
|
502
512
|
"reservation": {
|
|
503
513
|
"expired": "השריון פג. ייתכן שהמוצרים כבר לא זמינים.",
|
|
514
|
+
"expiredHint": "רעננו עבורכם את העגלה. כדאי לבדוק אותה לפני שממשיכים.",
|
|
515
|
+
"expiredCheckout": "השריון על המלאי שוחרר, ולכן התשלום מושהה. חזרו לעגלה ובדקו אותה.",
|
|
516
|
+
"backToCart": "חזרה לעגלה",
|
|
504
517
|
"hurry": "מהרו!",
|
|
505
518
|
"reservedFor": "המוצרים שמורים למשך"
|
|
506
519
|
},
|