@forgecart/cli 2.202610052310.0 → 2.202610060357.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 (125) hide show
  1. package/package.json +1 -1
  2. package/templates/storefront-shadcn/.forgecartignore +2 -0
  3. package/templates/storefront-shadcn/Procfile +1 -0
  4. package/templates/storefront-shadcn/README.md +229 -0
  5. package/templates/storefront-shadcn/SEO-MIGRATION.md +708 -0
  6. package/templates/storefront-shadcn/components.json +21 -0
  7. package/templates/storefront-shadcn/next.config.js +105 -0
  8. package/templates/storefront-shadcn/package.json +39 -0
  9. package/templates/storefront-shadcn/postcss.config.js +5 -0
  10. package/templates/storefront-shadcn/src/app/%5F%5Ffc/identify/route.ts +205 -0
  11. package/templates/storefront-shadcn/src/app/%5F%5Ffc/track/route.ts +189 -0
  12. package/templates/storefront-shadcn/src/app/%5F%5Fforge_beacon/route.ts +87 -0
  13. package/templates/storefront-shadcn/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
  14. package/templates/storefront-shadcn/src/app/cart/page.tsx +53 -0
  15. package/templates/storefront-shadcn/src/app/checkout/page.tsx +53 -0
  16. package/templates/storefront-shadcn/src/app/error.tsx +23 -0
  17. package/templates/storefront-shadcn/src/app/global-error.tsx +23 -0
  18. package/templates/storefront-shadcn/src/app/globals.css +156 -0
  19. package/templates/storefront-shadcn/src/app/layout.tsx +185 -0
  20. package/templates/storefront-shadcn/src/app/page.tsx +217 -0
  21. package/templates/storefront-shadcn/src/app/pages/[slug]/not-found.tsx +24 -0
  22. package/templates/storefront-shadcn/src/app/pages/[slug]/page.tsx +115 -0
  23. package/templates/storefront-shadcn/src/app/ping/route.ts +21 -0
  24. package/templates/storefront-shadcn/src/app/products/[slug]/not-found.tsx +18 -0
  25. package/templates/storefront-shadcn/src/app/products/[slug]/page.tsx +317 -0
  26. package/templates/storefront-shadcn/src/app/products/page.tsx +107 -0
  27. package/templates/storefront-shadcn/src/app/register/page.tsx +54 -0
  28. package/templates/storefront-shadcn/src/app/reset-password/page.tsx +60 -0
  29. package/templates/storefront-shadcn/src/app/robots.ts +69 -0
  30. package/templates/storefront-shadcn/src/app/sitemap.ts +106 -0
  31. package/templates/storefront-shadcn/src/app/verify/page.tsx +157 -0
  32. package/templates/storefront-shadcn/src/components/CartView.tsx +333 -0
  33. package/templates/storefront-shadcn/src/components/ForgeErrorBeacon.tsx +102 -0
  34. package/templates/storefront-shadcn/src/components/ForgeTracker.tsx +479 -0
  35. package/templates/storefront-shadcn/src/components/ForgecartDesigner.tsx +43 -0
  36. package/templates/storefront-shadcn/src/components/Header.tsx +73 -0
  37. package/templates/storefront-shadcn/src/components/LanguageSwitcher.tsx +96 -0
  38. package/templates/storefront-shadcn/src/components/LocaleLink.tsx +49 -0
  39. package/templates/storefront-shadcn/src/components/ProductCard.tsx +59 -0
  40. package/templates/storefront-shadcn/src/components/ProductPurchase.tsx +235 -0
  41. package/templates/storefront-shadcn/src/components/account/AccountMessage.tsx +63 -0
  42. package/templates/storefront-shadcn/src/components/account/RegisterForm.tsx +257 -0
  43. package/templates/storefront-shadcn/src/components/account/RequestPasswordResetForm.tsx +93 -0
  44. package/templates/storefront-shadcn/src/components/account/ResetPasswordForm.tsx +163 -0
  45. package/templates/storefront-shadcn/src/components/checkout/AddressStep.tsx +271 -0
  46. package/templates/storefront-shadcn/src/components/checkout/CheckoutFlow.tsx +551 -0
  47. package/templates/storefront-shadcn/src/components/checkout/CheckoutGate.tsx +55 -0
  48. package/templates/storefront-shadcn/src/components/checkout/PaymentElementForm.tsx +140 -0
  49. package/templates/storefront-shadcn/src/components/checkout/PaymentFormEmbed.tsx +89 -0
  50. package/templates/storefront-shadcn/src/components/checkout/RatesStep.tsx +115 -0
  51. package/templates/storefront-shadcn/src/components/ui/alert.tsx +75 -0
  52. package/templates/storefront-shadcn/src/components/ui/badge.tsx +40 -0
  53. package/templates/storefront-shadcn/src/components/ui/button.tsx +64 -0
  54. package/templates/storefront-shadcn/src/components/ui/card.tsx +28 -0
  55. package/templates/storefront-shadcn/src/components/ui/input.tsx +26 -0
  56. package/templates/storefront-shadcn/src/components/ui/label.tsx +22 -0
  57. package/templates/storefront-shadcn/src/components/ui/native-select.tsx +27 -0
  58. package/templates/storefront-shadcn/src/components/ui/skeleton.tsx +21 -0
  59. package/templates/storefront-shadcn/src/components/ui/utils.ts +16 -0
  60. package/templates/storefront-shadcn/src/instrumentation.ts +109 -0
  61. package/templates/storefront-shadcn/src/lib/account/account-link.ts +76 -0
  62. package/templates/storefront-shadcn/src/lib/account/register-state.ts +133 -0
  63. package/templates/storefront-shadcn/src/lib/account/reset-password-state.ts +111 -0
  64. package/templates/storefront-shadcn/src/lib/account/verify-state.ts +56 -0
  65. package/templates/storefront-shadcn/src/lib/account-actions.ts +76 -0
  66. package/templates/storefront-shadcn/src/lib/account-session.ts +47 -0
  67. package/templates/storefront-shadcn/src/lib/action-result.ts +30 -0
  68. package/templates/storefront-shadcn/src/lib/asset-alt.ts +34 -0
  69. package/templates/storefront-shadcn/src/lib/backend-actions.ts +20 -0
  70. package/templates/storefront-shadcn/src/lib/backend-client.ts +47 -0
  71. package/templates/storefront-shadcn/src/lib/cart-context.tsx +236 -0
  72. package/templates/storefront-shadcn/src/lib/checkout-session.ts +185 -0
  73. package/templates/storefront-shadcn/src/lib/content/page-metadata.ts +113 -0
  74. package/templates/storefront-shadcn/src/lib/content/render-fields.tsx +256 -0
  75. package/templates/storefront-shadcn/src/lib/content/resolve-page.ts +143 -0
  76. package/templates/storefront-shadcn/src/lib/error-messages.ts +24 -0
  77. package/templates/storefront-shadcn/src/lib/experiments.ts +333 -0
  78. package/templates/storefront-shadcn/src/lib/forgecart.ts +464 -0
  79. package/templates/storefront-shadcn/src/lib/format.ts +89 -0
  80. package/templates/storefront-shadcn/src/lib/identify-forward.ts +152 -0
  81. package/templates/storefront-shadcn/src/lib/locale/channel-locales-loader.ts +169 -0
  82. package/templates/storefront-shadcn/src/lib/locale/channel-locales-map.ts +46 -0
  83. package/templates/storefront-shadcn/src/lib/locale/channel-locales.ts +191 -0
  84. package/templates/storefront-shadcn/src/lib/locale/grammar.ts +194 -0
  85. package/templates/storefront-shadcn/src/lib/locale/localized-path.ts +55 -0
  86. package/templates/storefront-shadcn/src/lib/locale/middleware-plan.ts +107 -0
  87. package/templates/storefront-shadcn/src/lib/locale/request-binding.ts +80 -0
  88. package/templates/storefront-shadcn/src/lib/locale/request-locale.ts +66 -0
  89. package/templates/storefront-shadcn/src/lib/marketing-params.ts +213 -0
  90. package/templates/storefront-shadcn/src/lib/money.ts +50 -0
  91. package/templates/storefront-shadcn/src/lib/seo/alternates.ts +123 -0
  92. package/templates/storefront-shadcn/src/lib/seo/json-ld.ts +266 -0
  93. package/templates/storefront-shadcn/src/lib/seo/metadata.ts +419 -0
  94. package/templates/storefront-shadcn/src/lib/seo/noindex.ts +218 -0
  95. package/templates/storefront-shadcn/src/lib/seo/public-origin.ts +166 -0
  96. package/templates/storefront-shadcn/src/lib/seo/redirect-plan.ts +86 -0
  97. package/templates/storefront-shadcn/src/lib/seo/resolve-path.ts +107 -0
  98. package/templates/storefront-shadcn/src/lib/seo/scaffolded-routes.ts +83 -0
  99. package/templates/storefront-shadcn/src/lib/seo/sidecar.ts +75 -0
  100. package/templates/storefront-shadcn/src/lib/seo/site-verification.ts +98 -0
  101. package/templates/storefront-shadcn/src/lib/seo/sitemap-cache.ts +114 -0
  102. package/templates/storefront-shadcn/src/lib/seo/sitemap-entries.ts +321 -0
  103. package/templates/storefront-shadcn/src/lib/session-actions.ts +61 -0
  104. package/templates/storefront-shadcn/src/lib/session-cookies.ts +98 -0
  105. package/templates/storefront-shadcn/src/lib/shop-config.ts +51 -0
  106. package/templates/storefront-shadcn/src/lib/shop-session.ts +151 -0
  107. package/templates/storefront-shadcn/src/lib/track-forward.ts +200 -0
  108. package/templates/storefront-shadcn/src/lib/uuid.ts +19 -0
  109. package/templates/storefront-shadcn/src/middleware.ts +379 -0
  110. package/templates/storefront-shadcn/src/seo/redirects.ts +44 -0
  111. package/templates/storefront-shadcn/src/server/app.module.ts +18 -0
  112. package/templates/storefront-shadcn/src/server/backend-api.ts +26 -0
  113. package/templates/storefront-shadcn/src/server/backend-method.decorator.ts +23 -0
  114. package/templates/storefront-shadcn/src/server/bootstrap.ts +122 -0
  115. package/templates/storefront-shadcn/src/server/customer-extras/customer-extras.module.ts +13 -0
  116. package/templates/storefront-shadcn/src/server/customer-extras/service/customer-extras.service.ts +58 -0
  117. package/templates/storefront-shadcn/src/server/customer-extras/type/customer-extras.types.ts +11 -0
  118. package/templates/storefront-shadcn/src/server/forge/live-revision.ts +158 -0
  119. package/templates/storefront-shadcn/src/server/forgecart/forgecart-client.factory.ts +69 -0
  120. package/templates/storefront-shadcn/src/server/forgecart/forgecart.module.ts +9 -0
  121. package/templates/storefront-shadcn/src/server/runner.ts +90 -0
  122. package/templates/storefront-shadcn/src/server/types.ts +36 -0
  123. package/templates/storefront-shadcn/tsconfig.json +25 -0
  124. package/templates/storefront-shadcn-sdk-floor.json +1174 -0
  125. package/templates/template-set.json +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgecart/cli",
3
- "version": "2.202610052310.0",
3
+ "version": "2.202610060357.0",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and operating ForgeCart channel storefronts",
6
6
  "bin": {
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ .next
@@ -0,0 +1 @@
1
+ web: npm run start
@@ -0,0 +1,229 @@
1
+ # Storefront
2
+
3
+ A [Next.js](https://nextjs.org) (App Router) storefront for a ForgeCart
4
+ channel: product browsing, a server-backed cart, and a complete multi-step
5
+ checkout — all speaking the ForgeCart shop GraphQL API through the generated
6
+ `@forgecart/sdk` client, styled with [shadcn/ui](https://ui.shadcn.com)
7
+ components on Tailwind CSS.
8
+
9
+ This directory is the scaffold copied verbatim into your project by
10
+ `forgecart init --ui shadcn`. Nothing here is compiled by the ForgeCart
11
+ monorepo; it ships as-is in the published `@forgecart/cli` package.
12
+
13
+ To migrate a storefront built before the locale and SEO layer, follow [`SEO-MIGRATION.md`](SEO-MIGRATION.md).
14
+
15
+ ## Configuration
16
+
17
+ The storefront reads three environment variables (server-side only):
18
+
19
+ | Variable | Purpose |
20
+ | ------------------------- | -------------------------------------------------------------- |
21
+ | `FORGECART_CHANNEL_TOKEN` | Identifies the channel; sent as the `forgecart-token` header. |
22
+ | `FORGECART_SHOP_API_URL` | Base URL of the channel's shop GraphQL API (`.../shop-api`). |
23
+ | `FORGECART_ADMIN_SECRET` | Channel admin secret for server-side admin operations. |
24
+
25
+ `forgecart init` writes all of them to `.env` — it is the sole author of that
26
+ file, and `.env` is the only env file this project ever has (no `.env.example`,
27
+ no `.env.local`). None of the values ever reaches the
28
+ browser: every module that constructs an SDK client starts with
29
+ `import 'server-only'`, so a client component importing one is a **build
30
+ error**, and the workspace-pod image build additionally asserts no SDK code
31
+ lands in the client chunks.
32
+
33
+ ## The backend gate (`sdk.backend`)
34
+
35
+ The storefront embeds a real NestJS backend under `src/server/` — booted as
36
+ an application context (no HTTP server; Next.js owns HTTP) in
37
+ `instrumentation.ts`, with normal `@Module`/`@Injectable` structure. A
38
+ service method decorated with `@BackendMethod()` (and declared in
39
+ `backend-api.ts`) becomes callable from any client component through the
40
+ typed proxy:
41
+
42
+ ```ts
43
+ import { backend } from '@/lib/backend-client';
44
+
45
+ const result = await backend.assignCustomerHash({});
46
+ if (!result.ok) renderError(result.error);
47
+ ```
48
+
49
+ One Server Action is the transport — no routes, no fetch calls, no
50
+ websockets. Methods receive a per-invocation session `{ admin, shop,
51
+ hasSession }`: `admin` is the channel-admin SDK gate (from
52
+ `FORGECART_ADMIN_SECRET`) and each method owns what that authority may be
53
+ used for; inputs are untrusted. The decorator is the allowlist, boot fails
54
+ loudly if the decorator scan and `backend-api.ts` drift, and in dev
55
+ `/api/__backend/methods` lists the live registry. The worked example
56
+ (`src/server/customer-extras/`) mints a random referral hash for the
57
+ signed-in customer and stores it as an ACF custom-field entry.
58
+
59
+ ## Design system
60
+
61
+ Styling is Tailwind v4 + shadcn/ui. The components are **vendored**, the way
62
+ shadcn/ui intends: they live in [`src/components/ui/`](src/components/ui/)
63
+ (`button`, `card`, `input`, `native-select`, `label`, `alert`, `badge`,
64
+ `skeleton`) as source you own and edit, built on `class-variance-authority`,
65
+ `clsx`, `tailwind-merge` and the Radix primitives they import. Their `cn`
66
+ helper is [`src/components/ui/utils.ts`](src/components/ui/utils.ts), and
67
+ [`components.json`](components.json) points the shadcn/ui CLI at both, so
68
+ `npx shadcn@latest add <component>` lands a new component beside them.
69
+
70
+ Every component uses **semantic** tokens only (`bg-card`, `text-foreground`,
71
+ `border-input`, `bg-primary`, …), never a palette colour. The single source of
72
+ brand truth is the `:root` block in [`src/app/globals.css`](src/app/globals.css),
73
+ exposed to Tailwind through its `@theme inline` block — restyling the whole
74
+ storefront means editing those variables and nothing else. The stylesheet's
75
+ header carries the mapping from the daisyUI theme tokens of the daisyUI
76
+ storefront onto these variables, so a brand written for one row carries over
77
+ to the other.
78
+
79
+ ## Data flow
80
+
81
+ **Transport.** The generated SDK carries every operation over graphql-ws — a
82
+ per-request client is a per-request websocket connection. All SDK calls run on
83
+ the server; nothing SDK-shaped ships to the browser.
84
+
85
+ - [`src/lib/forgecart.ts`](src/lib/forgecart.ts) — the channel-scoped
86
+ singleton client for anonymous reads (products, selling plans) via typed
87
+ helpers (`getProducts`, `getProductBySlug`, `getFeaturedProducts`,
88
+ `getChannelSellingPlanGroups`, …), plus the SDK type re-exports the
89
+ components render. Pure display helpers (`formatPrice`,
90
+ `getStartingPrice`, plan labels) live in
91
+ [`src/lib/format.ts`](src/lib/format.ts), which client components import
92
+ freely — it carries no SDK runtime.
93
+ - [`src/lib/shop-action.ts`](src/lib/shop-action.ts) — the Server-Action
94
+ runner: builds a per-request client around the shopper's
95
+ `forgecart-session` cookie, runs one operation, persists any newly minted
96
+ session token, translates failure into the typed envelope, and
97
+ `dispose()`s the client so no action leaks its socket.
98
+ - [`src/lib/action-result.ts`](src/lib/action-result.ts) — `ActionResult<T>`:
99
+ Server Actions **never throw** across the RSC boundary (Next production
100
+ redacts thrown action errors). Failures arrive in-band as the SDK's
101
+ `ExtractedError` (`code`, server-localized `message`, `variables`), which
102
+ the UI renders inline.
103
+
104
+ **Cart.** [`src/lib/cart-actions.ts`](src/lib/cart-actions.ts) drives the
105
+ shop **order** API — the real cart, never browser storage. The interactive
106
+ state lives in [`src/lib/cart-context.tsx`](src/lib/cart-context.tsx): seeded
107
+ with the server-fetched order, updated from each action's returned order, and
108
+ carrying the typed `error` channel the cart UI renders. Coupons
109
+ (`applyCoupon`/`removeCoupon`) recalculate discounts and totals server-side;
110
+ setting a line's quantity to `0` removes it.
111
+
112
+ **Checkout.** [`src/lib/checkout-actions.ts`](src/lib/checkout-actions.ts) +
113
+ [`src/components/checkout/`](src/components/checkout/) implement the
114
+ reference flow behind a numbered step indicator (an ordered list whose current
115
+ step carries `aria-current="step"` — shadcn/ui ships no stepper):
116
+
117
+ 1. **Contact** — guest email + name (`setCustomerForOrder`).
118
+ 2. **Address** — type-ahead via the shop `addressAutocomplete` API (optional
119
+ capability; manual entry always works), country select populated from
120
+ `availableCountries` (never a hardcoded list), and the **required phone
121
+ number** the API enforces.
122
+ 3. **Shipping** — `refreshShippingRateGroups` quotes carriers explicitly (the
123
+ read query never does), rates render grouped per shipment with per-group
124
+ carrier failures, and the customer picks — the ArrangingPayment
125
+ transition requires a selection in every group.
126
+ 4. **Payment** — eligible providers → `createPaymentSession` →
127
+ `getSessionTemplate` renders the provider's hosted form in
128
+ [`PaymentFormEmbed`](src/components/checkout/PaymentFormEmbed.tsx), which
129
+ listens for the origin-pinned `iframe-ready` / `payment-success` /
130
+ `payment-error` handshake. No provider-specific code exists in the
131
+ template; coupon edits freeze once a session exists (the amount is
132
+ locked at session creation).
133
+ 5. **Confirmation** — the order code and summary.
134
+
135
+ Errors render where they belong: field-level for coded field errors
136
+ (`SHIPPING_ADDRESS_PHONE_REQUIRED`, `SHIPPING_ADDRESS_COUNTRY_*`,
137
+ `COUPON_CODE_*`) — the field marked `aria-invalid`, which is also what paints
138
+ its destructive border — a step-level `Alert` (`role="alert"`) otherwise, with
139
+ [`src/app/error.tsx`](src/app/error.tsx) and `global-error.tsx` as render
140
+ backstops.
141
+
142
+ ## Sessions
143
+
144
+ The anonymous session is minted by the API on the first cart mutation and
145
+ persisted into the session cookie PAIR — the httpOnly `forgecart-session` plus
146
+ its JS-readable `forgecart-session-client` mirror, whose one contract lives in
147
+ [`src/lib/session-cookies.ts`](src/lib/session-cookies.ts) — by the action
148
+ runner. The `/__fc/track` and `/__fc/identify` relays may **establish** that
149
+ pair for pure browsers but never replace an existing cookie, and the tracker
150
+ defers its first flush so a fast add-to-cart wins the first-touch race. The
151
+ mirror is what the browser's shop socket boots its auth from: written without
152
+ it, a relay-established session would never reach the client at all.
153
+
154
+ ## Account links
155
+
156
+ Two routes exist because the PLATFORM links to them from the e-mails it sends
157
+ a shopper, not because anything in this storefront navigates there:
158
+
159
+ | Route | Mail | What it does |
160
+ | ---------------------------- | ------------------- | ------------------------------------------------------------ |
161
+ | `/verify?token=…` | e-mail verification | consumes the token on RENDER and reports the outcome |
162
+ | `/reset-password?token=…` | password reset | renders the new-password form; the SUBMIT consumes the token |
163
+ | `/reset-password` (no token) | — | asks for an address and has a new link sent |
164
+
165
+ **Do not rename or prefix them.** The path and the `?token=` parameter are the
166
+ contract with the mail templates, and a link already sitting in a shopper's
167
+ inbox cannot be re-issued. They are in the once-per-store set
168
+ ([`src/lib/locale/grammar.ts`](src/lib/locale/grammar.ts)), so they never carry
169
+ a locale prefix, and in `NOINDEX_PATHS`
170
+ ([`src/lib/seo/noindex.ts`](src/lib/seo/noindex.ts)), so they are `noindex`,
171
+ absent from the XML sitemap and `Disallow`ed in `robots.txt` — a crawler that
172
+ fetched one would spend a real shopper's one-shot token.
173
+
174
+ The COPY is yours to rewrite. The states behind it are not invented per page:
175
+ [`src/lib/account/`](src/lib/account/) maps the shop API's `ErrorCode`s onto a
176
+ small state set (verified / link-invalid / wrong-store / …) that both pages
177
+ render, and each surface stamps `data-fc-account-mode` + `data-fc-account-state`
178
+ onto the element it renders, so the state is readable from the served HTML.
179
+
180
+ ## Project layout
181
+
182
+ ```
183
+ components.json # the shadcn/ui CLI's config: style, stylesheet, aliases
184
+ src/
185
+ lib/
186
+ forgecart.ts # server-only shop client + typed reads + SDK types
187
+ format.ts # pure display helpers (client-safe)
188
+ shop-action.ts # per-request action runner (envelope + dispose)
189
+ action-result.ts # ActionResult<T> + fallback error
190
+ cart-actions.ts # cart Server Actions (incl. coupons)
191
+ checkout-actions.ts # checkout Server Actions
192
+ cart-context.tsx # client cart state + typed error channel
193
+ account-actions.ts # password-reset Server Actions
194
+ account/ # pure account-link rules (token, states, ErrorCode map)
195
+ components/
196
+ ui/ # the vendored shadcn/ui components + their cn helper
197
+ Header.tsx # top nav with live cart count
198
+ ProductCard.tsx # product tile
199
+ ProductPurchase.tsx # variant picker + purchase options + add-to-cart
200
+ CartView.tsx # cart lines, coupons, totals, checkout CTA
201
+ checkout/
202
+ CheckoutFlow.tsx # the stepped flow orchestrator
203
+ AddressStep.tsx # autocomplete + countries + phone
204
+ RatesStep.tsx # grouped radio-card rate selection
205
+ PaymentFormEmbed.tsx # origin-checked hosted-form host
206
+ account/
207
+ AccountMessage.tsx # the one outcome block both account pages use
208
+ ResetPasswordForm.tsx # set a new password from a reset token
209
+ RequestPasswordResetForm.tsx # ask for a reset link
210
+ app/
211
+ layout.tsx # html shell + header/footer + CartProvider
212
+ globals.css # Tailwind + the :root brand tokens + @theme inline
213
+ error.tsx # route render backstop
214
+ global-error.tsx # root render backstop
215
+ page.tsx # home: featured products
216
+ products/ # grid + [slug] detail
217
+ cart/page.tsx # cart
218
+ checkout/page.tsx # the stepped checkout
219
+ verify/page.tsx # e-mail verification link target
220
+ reset-password/page.tsx # password-reset link target + request form
221
+ ```
222
+
223
+ ## Development
224
+
225
+ ```bash
226
+ npm install
227
+ npm run dev # Turbopack dev server (what workspace pods run)
228
+ npm run build # production build (what deploys run; see Procfile)
229
+ ```