@unbox-plus/cli 0.20.4

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 (358) hide show
  1. package/LICENSE.md +46 -0
  2. package/README.md +175 -0
  3. package/bin/cli.js +439 -0
  4. package/package.json +43 -0
  5. package/src/colors.js +100 -0
  6. package/src/presets.js +241 -0
  7. package/src/theme.js +253 -0
  8. package/template/.claude/agents/assets-marca.md +40 -0
  9. package/template/.claude/agents/avaliador-visual.md +48 -0
  10. package/template/.claude/agents/branding-briefing.md +401 -0
  11. package/template/.claude/agents/conteudo-secao.md +46 -0
  12. package/template/.claude/agents/paginas-legais.md +44 -0
  13. package/template/.claude/settings.json +3 -0
  14. package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
  15. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  16. package/template/.claude/skills/frontend-design/SKILL.md +55 -0
  17. package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
  18. package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
  19. package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
  20. package/template/.env.example +158 -0
  21. package/template/.mcp.json.example +14 -0
  22. package/template/.nvmrc +1 -0
  23. package/template/CLAUDE.md +333 -0
  24. package/template/COVERAGE.md +80 -0
  25. package/template/DEPLOY.md +179 -0
  26. package/template/QA.md +108 -0
  27. package/template/README.md +152 -0
  28. package/template/agents/CONSTRUCAO.md +207 -0
  29. package/template/agents/MANAGER.md +192 -0
  30. package/template/agents/PADROES.md +255 -0
  31. package/template/agents/definitions/00-scaffold.md +56 -0
  32. package/template/agents/definitions/01-layout.md +47 -0
  33. package/template/agents/definitions/02-homepage.md +70 -0
  34. package/template/agents/definitions/03-catalog.md +86 -0
  35. package/template/agents/definitions/04-pdp.md +97 -0
  36. package/template/agents/definitions/05-cart.md +116 -0
  37. package/template/agents/definitions/06-checkout.md +146 -0
  38. package/template/agents/definitions/07-auth.md +130 -0
  39. package/template/agents/definitions/08-customer.md +55 -0
  40. package/template/agents/definitions/09-promotions.md +62 -0
  41. package/template/agents/definitions/10-feedback.md +128 -0
  42. package/template/agents/definitions/11-seo-infra.md +159 -0
  43. package/template/agents/definitions/12-deploy.md +173 -0
  44. package/template/agents/definitions/13-cro.md +275 -0
  45. package/template/agents/definitions/14-seo.md +388 -0
  46. package/template/agents/definitions/15-branding.md +26 -0
  47. package/template/agents/definitions/16-qa-visual.md +138 -0
  48. package/template/agents/definitions/17-aeo.md +257 -0
  49. package/template/app/(loja)/busca/page.tsx +72 -0
  50. package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
  51. package/template/app/(loja)/carrinho/page.tsx +160 -0
  52. package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
  53. package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
  54. package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
  55. package/template/app/(loja)/checkout/page.tsx +80 -0
  56. package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
  57. package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
  58. package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
  59. package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
  60. package/template/app/(loja)/conta/entrar/page.tsx +119 -0
  61. package/template/app/(loja)/conta/page.tsx +45 -0
  62. package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
  63. package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
  64. package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
  65. package/template/app/(loja)/devolucoes/page.tsx +36 -0
  66. package/template/app/(loja)/layout.tsx +22 -0
  67. package/template/app/(loja)/oferta/page.tsx +57 -0
  68. package/template/app/(loja)/page.tsx +86 -0
  69. package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
  70. package/template/app/(loja)/privacidade/page.tsx +113 -0
  71. package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
  72. package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
  73. package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
  74. package/template/app/(loja)/produtos/loading.tsx +16 -0
  75. package/template/app/(loja)/produtos/page.tsx +33 -0
  76. package/template/app/(loja)/termos/page.tsx +104 -0
  77. package/template/app/acesso/page.tsx +129 -0
  78. package/template/app/api/account/addresses/route.ts +39 -0
  79. package/template/app/api/account/exists/route.ts +21 -0
  80. package/template/app/api/account/me/route.ts +25 -0
  81. package/template/app/api/account/otp/route.ts +26 -0
  82. package/template/app/api/account/preferences/route.ts +19 -0
  83. package/template/app/api/account/signin/route.ts +27 -0
  84. package/template/app/api/account/signout/route.ts +9 -0
  85. package/template/app/api/acesso/route.ts +171 -0
  86. package/template/app/api/capi/route.ts +34 -0
  87. package/template/app/api/cart/coupon/route.ts +49 -0
  88. package/template/app/api/cart/items/route.ts +45 -0
  89. package/template/app/api/cart/link/route.ts +69 -0
  90. package/template/app/api/cart/route.ts +113 -0
  91. package/template/app/api/cart/share/route.ts +21 -0
  92. package/template/app/api/cep/[code]/route.ts +18 -0
  93. package/template/app/api/checkout/address/route.ts +24 -0
  94. package/template/app/api/checkout/email/route.ts +41 -0
  95. package/template/app/api/checkout/installments/route.ts +16 -0
  96. package/template/app/api/checkout/route.ts +110 -0
  97. package/template/app/api/checkout/shipping/route.ts +65 -0
  98. package/template/app/api/checkout-destination/route.ts +31 -0
  99. package/template/app/api/order/[ref]/route.ts +17 -0
  100. package/template/app/api/payment-link/route.ts +28 -0
  101. package/template/app/api/revalidate/route.ts +52 -0
  102. package/template/app/api/shipping/quote/route.ts +29 -0
  103. package/template/app/api/subscriptions/[id]/route.ts +52 -0
  104. package/template/app/api/track/route.ts +28 -0
  105. package/template/app/api/unbox/catalogo/route.ts +136 -0
  106. package/template/app/api/unbox/paginas/route.ts +167 -0
  107. package/template/app/api/unbox/vitrine/route.ts +84 -0
  108. package/template/app/api/webhooks/unbox/route.ts +97 -0
  109. package/template/app/apple-icon.svg +5 -0
  110. package/template/app/error.tsx +15 -0
  111. package/template/app/globals.css +457 -0
  112. package/template/app/icon.svg +4 -0
  113. package/template/app/layout.tsx +90 -0
  114. package/template/app/llms.txt/route.ts +43 -0
  115. package/template/app/manifest.ts +21 -0
  116. package/template/app/not-found.tsx +18 -0
  117. package/template/app/opengraph-image.tsx +35 -0
  118. package/template/app/robots.ts +31 -0
  119. package/template/app/sitemap.ts +42 -0
  120. package/template/bootstrap.sh +87 -0
  121. package/template/components/account/account-shell.tsx +47 -0
  122. package/template/components/account/address-book.tsx +147 -0
  123. package/template/components/account/preferences-form.tsx +77 -0
  124. package/template/components/account/reorder-button.tsx +47 -0
  125. package/template/components/account/signout-button.tsx +20 -0
  126. package/template/components/account/subscription-actions.tsx +204 -0
  127. package/template/components/account-nav.tsx +41 -0
  128. package/template/components/address-fields.tsx +102 -0
  129. package/template/components/analytics/data-layer-ready.tsx +16 -0
  130. package/template/components/analytics/purchase-tracker.tsx +32 -0
  131. package/template/components/brand-search.tsx +34 -0
  132. package/template/components/cart/cart-provider.tsx +340 -0
  133. package/template/components/cart/mini-cart.tsx +228 -0
  134. package/template/components/cart-button.tsx +35 -0
  135. package/template/components/catalog/catalog-client.tsx +523 -0
  136. package/template/components/catalog/grid-skeleton.tsx +19 -0
  137. package/template/components/catalog/pager.tsx +66 -0
  138. package/template/components/catalog/product-grid-card.tsx +52 -0
  139. package/template/components/catalog/product-grid.tsx +27 -0
  140. package/template/components/checkout/checkout-client.tsx +1333 -0
  141. package/template/components/chrome/announce-bar.tsx +57 -0
  142. package/template/components/chrome/chrome-recipe.ts +13 -0
  143. package/template/components/chrome/footers/captura-botao.tsx +28 -0
  144. package/template/components/chrome/footers/colunas.tsx +110 -0
  145. package/template/components/chrome/footers/conversao.tsx +90 -0
  146. package/template/components/chrome/footers/editorial.tsx +98 -0
  147. package/template/components/chrome/footers/minimal.tsx +55 -0
  148. package/template/components/chrome/header-bar-mobile.tsx +64 -0
  149. package/template/components/chrome/headers/centralizado.tsx +71 -0
  150. package/template/components/chrome/headers/classico.tsx +64 -0
  151. package/template/components/chrome/headers/compacto.tsx +66 -0
  152. package/template/components/chrome/headers/equilibrado.tsx +73 -0
  153. package/template/components/chrome/headers/imersivo.tsx +62 -0
  154. package/template/components/chrome/registry.ts +70 -0
  155. package/template/components/chrome/solid-on-scroll.tsx +39 -0
  156. package/template/components/empty-state.tsx +22 -0
  157. package/template/components/home/combo-card-compact.tsx +67 -0
  158. package/template/components/home/combos-home.tsx +64 -0
  159. package/template/components/home/combos-section.tsx +177 -0
  160. package/template/components/home/home-recipe.ts +41 -0
  161. package/template/components/home/sections/attributes-marquee.tsx +178 -0
  162. package/template/components/home/sections/benefits.tsx +80 -0
  163. package/template/components/home/sections/bloco-html.tsx +47 -0
  164. package/template/components/home/sections/catalogo.ts +165 -0
  165. package/template/components/home/sections/category-pills.tsx +32 -0
  166. package/template/components/home/sections/combos-carousel.tsx +141 -0
  167. package/template/components/home/sections/comparison.tsx +73 -0
  168. package/template/components/home/sections/founder-story.tsx +40 -0
  169. package/template/components/home/sections/hero.tsx +187 -0
  170. package/template/components/home/sections/kits.tsx +11 -0
  171. package/template/components/home/sections/media-cards.tsx +70 -0
  172. package/template/components/home/sections/newsletter.tsx +62 -0
  173. package/template/components/home/sections/product-showcase.tsx +175 -0
  174. package/template/components/home/sections/purchase-hero.tsx +187 -0
  175. package/template/components/home/sections/quote-banner.tsx +50 -0
  176. package/template/components/home/sections/registry.ts +186 -0
  177. package/template/components/home/sections/reviews-carousel.tsx +66 -0
  178. package/template/components/home/sections/reviews.tsx +68 -0
  179. package/template/components/home/sections/ritual.tsx +96 -0
  180. package/template/components/home/sections/savings.tsx +47 -0
  181. package/template/components/home/sections/social-row.tsx +65 -0
  182. package/template/components/home/sections/spec-table.tsx +117 -0
  183. package/template/components/home/sections/stars.tsx +12 -0
  184. package/template/components/home/sections/stats-grid.tsx +54 -0
  185. package/template/components/home/sections/trust-bar.tsx +56 -0
  186. package/template/components/home/sections/trust-strip.tsx +40 -0
  187. package/template/components/home/sections/video-wall.tsx +59 -0
  188. package/template/components/landing/landing-recipe.ts +20 -0
  189. package/template/components/landing/oferta-sections.tsx +40 -0
  190. package/template/components/landing/product-picker.tsx +257 -0
  191. package/template/components/mobile-nav.tsx +71 -0
  192. package/template/components/order-status.tsx +163 -0
  193. package/template/components/powered-by-unbox.tsx +27 -0
  194. package/template/components/product/pdp/buy-box.tsx +604 -0
  195. package/template/components/product/pdp/catalog-grid.tsx +113 -0
  196. package/template/components/product/pdp/faq-modelo.ts +86 -0
  197. package/template/components/product/pdp/gallery.tsx +87 -0
  198. package/template/components/product/pdp/interactive.tsx +200 -0
  199. package/template/components/product/pdp/newsletter.tsx +64 -0
  200. package/template/components/product/pdp/payment-chips.tsx +22 -0
  201. package/template/components/product/pdp/pdp-view.tsx +227 -0
  202. package/template/components/product/pdp/recommendations.tsx +144 -0
  203. package/template/components/product/pdp/sections.tsx +173 -0
  204. package/template/components/quantity-stepper.tsx +36 -0
  205. package/template/components/search-box.tsx +37 -0
  206. package/template/components/site-footer.tsx +59 -0
  207. package/template/components/site-header.tsx +64 -0
  208. package/template/components/ui/accordion.tsx +72 -0
  209. package/template/components/ui/alert-dialog.tsx +187 -0
  210. package/template/components/ui/alert.tsx +76 -0
  211. package/template/components/ui/aspect-ratio.tsx +22 -0
  212. package/template/components/ui/avatar.tsx +109 -0
  213. package/template/components/ui/badge.tsx +52 -0
  214. package/template/components/ui/breadcrumb.tsx +125 -0
  215. package/template/components/ui/button.tsx +58 -0
  216. package/template/components/ui/card.tsx +103 -0
  217. package/template/components/ui/carousel.tsx +242 -0
  218. package/template/components/ui/command.tsx +196 -0
  219. package/template/components/ui/dialog.tsx +160 -0
  220. package/template/components/ui/dropdown-menu.tsx +268 -0
  221. package/template/components/ui/foto.tsx +54 -0
  222. package/template/components/ui/input-group.tsx +158 -0
  223. package/template/components/ui/input-otp.tsx +87 -0
  224. package/template/components/ui/input.tsx +20 -0
  225. package/template/components/ui/label.tsx +20 -0
  226. package/template/components/ui/navigation-menu.tsx +168 -0
  227. package/template/components/ui/pagination.tsx +132 -0
  228. package/template/components/ui/popover.tsx +90 -0
  229. package/template/components/ui/progress.tsx +83 -0
  230. package/template/components/ui/radio-group.tsx +38 -0
  231. package/template/components/ui/scroll-area.tsx +55 -0
  232. package/template/components/ui/select.tsx +201 -0
  233. package/template/components/ui/separator.tsx +25 -0
  234. package/template/components/ui/sheet.tsx +138 -0
  235. package/template/components/ui/skeleton.tsx +13 -0
  236. package/template/components/ui/sonner.tsx +44 -0
  237. package/template/components/ui/switch.tsx +32 -0
  238. package/template/components/ui/table.tsx +116 -0
  239. package/template/components/ui/tabs.tsx +82 -0
  240. package/template/components/ui/textarea.tsx +18 -0
  241. package/template/components/ui/toggle-group.tsx +89 -0
  242. package/template/components/ui/toggle.tsx +45 -0
  243. package/template/components/ui/tooltip.tsx +66 -0
  244. package/template/components.json +25 -0
  245. package/template/eslint.config.mjs +36 -0
  246. package/template/gitignore +21 -0
  247. package/template/lib/analytics.ts +369 -0
  248. package/template/lib/api.ts +60 -0
  249. package/template/lib/capi.ts +87 -0
  250. package/template/lib/cart-link.ts +51 -0
  251. package/template/lib/cart-normalize.ts +142 -0
  252. package/template/lib/cart-recovery.ts +23 -0
  253. package/template/lib/cart-response.ts +48 -0
  254. package/template/lib/catalog-map.ts +54 -0
  255. package/template/lib/catalog.ts +3 -0
  256. package/template/lib/checkout-lock.ts +21 -0
  257. package/template/lib/checkout-nav.ts +22 -0
  258. package/template/lib/config.ts +51 -0
  259. package/template/lib/crm.ts +40 -0
  260. package/template/lib/customer-session.ts +21 -0
  261. package/template/lib/dataloader.ts +23 -0
  262. package/template/lib/editable/config.ts +11 -0
  263. package/template/lib/editable/document.ts +1762 -0
  264. package/template/lib/editable/index.ts +3 -0
  265. package/template/lib/editable/primitives.tsx +821 -0
  266. package/template/lib/editable/provider.tsx +861 -0
  267. package/template/lib/editable/rastreio-navegacao.tsx +65 -0
  268. package/template/lib/editable/rastreio.tsx +168 -0
  269. package/template/lib/editable/server.ts +189 -0
  270. package/template/lib/editable/tokens.ts +22 -0
  271. package/template/lib/editable/verify.ts +24 -0
  272. package/template/lib/enrichment/combos.ts +184 -0
  273. package/template/lib/enrichment/index.ts +188 -0
  274. package/template/lib/enrichment/products.json +1 -0
  275. package/template/lib/env-check.ts +47 -0
  276. package/template/lib/format.ts +130 -0
  277. package/template/lib/icons.ts +67 -0
  278. package/template/lib/json-ld.ts +9 -0
  279. package/template/lib/llms-txt.ts +75 -0
  280. package/template/lib/mockup.ts +24 -0
  281. package/template/lib/newsletter.ts +12 -0
  282. package/template/lib/orders.ts +160 -0
  283. package/template/lib/queries.ts +66 -0
  284. package/template/lib/ratelimit.ts +47 -0
  285. package/template/lib/rotas-editaveis.ts +192 -0
  286. package/template/lib/sanitize.ts +36 -0
  287. package/template/lib/schemas.ts +78 -0
  288. package/template/lib/session.ts +98 -0
  289. package/template/lib/store-config.ts +67 -0
  290. package/template/lib/unbox/client.ts +829 -0
  291. package/template/lib/unbox/customer.ts +224 -0
  292. package/template/lib/unbox/errors.ts +97 -0
  293. package/template/lib/unbox/index.ts +10 -0
  294. package/template/lib/unbox/store.ts +113 -0
  295. package/template/lib/unbox/types.ts +195 -0
  296. package/template/lib/unbox/webhooks.ts +66 -0
  297. package/template/lib/utils.ts +6 -0
  298. package/template/lib/vitrine.ts +242 -0
  299. package/template/lib/webhook-store.ts +22 -0
  300. package/template/middleware.ts +178 -0
  301. package/template/next.config.ts +63 -0
  302. package/template/package.json +62 -0
  303. package/template/postcss.config.mjs +5 -0
  304. package/template/public/brand/coll/default.png +0 -0
  305. package/template/public/brand/hero-desktop.svg +12 -0
  306. package/template/public/brand/hero-mobile.svg +12 -0
  307. package/template/public/brand/heros/boutique-desktop.svg +11 -0
  308. package/template/public/brand/heros/boutique-mobile.svg +11 -0
  309. package/template/public/brand/heros/editorial-desktop.svg +11 -0
  310. package/template/public/brand/heros/editorial-mobile.svg +11 -0
  311. package/template/public/brand/heros/essencial-desktop.svg +12 -0
  312. package/template/public/brand/heros/essencial-mobile.svg +12 -0
  313. package/template/public/brand/heros/promocional-desktop.svg +12 -0
  314. package/template/public/brand/heros/promocional-mobile.svg +12 -0
  315. package/template/public/brand/logo-chrome.svg +6 -0
  316. package/template/public/brand/logo-white.svg +6 -0
  317. package/template/public/brand/logo.svg +7 -0
  318. package/template/public/brand/pay-amex.webp +0 -0
  319. package/template/public/brand/pay-elo.webp +0 -0
  320. package/template/public/brand/pay-mastercard.webp +0 -0
  321. package/template/public/brand/pay-pix.webp +0 -0
  322. package/template/public/brand/pay-visa.webp +0 -0
  323. package/template/public/brand/ph/avatar-a.svg +5 -0
  324. package/template/public/brand/ph/avatar-b.svg +5 -0
  325. package/template/public/brand/ph/avatar-c.svg +5 -0
  326. package/template/public/brand/ph/avatar-d.svg +5 -0
  327. package/template/public/brand/ph/photo-a.svg +7 -0
  328. package/template/public/brand/ph/photo-b.svg +6 -0
  329. package/template/public/brand/ph/photo-c.svg +6 -0
  330. package/template/public/brand/ph/poster-a.svg +6 -0
  331. package/template/public/brand/ph/poster-b.svg +6 -0
  332. package/template/public/brand/ph/poster-c.svg +6 -0
  333. package/template/public/unbox/powered-by-fundo-preto.png +0 -0
  334. package/template/public/unbox/powered-by-transparente.png +0 -0
  335. package/template/public/unbox/powered-by.png +0 -0
  336. package/template/scripts/abandoned-cart.ts +82 -0
  337. package/template/scripts/check-editable.mjs +467 -0
  338. package/template/scripts/check-honestidade.mjs +111 -0
  339. package/template/scripts/check-placeholder.mjs +200 -0
  340. package/template/scripts/check-recipe.mjs +76 -0
  341. package/template/scripts/check-unbox-brand.mjs +298 -0
  342. package/template/scripts/contraste.js +177 -0
  343. package/template/scripts/dump-catalog.ts +95 -0
  344. package/template/scripts/load-env.ts +13 -0
  345. package/template/scripts/medir-sistema.mjs +56 -0
  346. package/template/scripts/place-order-pix.ts +61 -0
  347. package/template/scripts/qa-screenshots.mjs +150 -0
  348. package/template/scripts/sistema.py +231 -0
  349. package/template/scripts/subscribe-webhook.ts +36 -0
  350. package/template/scripts/test-live.ts +186 -0
  351. package/template/scripts/vocabulario.py +264 -0
  352. package/template/tsconfig.json +27 -0
  353. package/tools/LEIA-ME.md +39 -0
  354. package/tools/check-template-neutro.mjs +111 -0
  355. package/tools/notion-doc.mjs +191 -0
  356. package/tools/tokenize-neutrals.mjs +0 -0
  357. package/tools/tokenize-radius.mjs +52 -0
  358. package/tools/workflow-storefront.legado.js +1207 -0
@@ -0,0 +1,1207 @@
1
+ export const meta = {
2
+ name: 'unbox-storefront-builder',
3
+ description: 'Constrói um storefront headless Unbox completo: Next.js 15 + shadcn/ui + Tailwind v4',
4
+ phases: [
5
+ { title: 'Scaffold', detail: 'Cria estrutura Next.js, dependências e fundação compartilhada' },
6
+ { title: 'Implement', detail: 'Implementa as 10 seções do storefront em paralelo' },
7
+ { title: 'Integrate', detail: 'Integra seções, resolve imports e configura providers' },
8
+ { title: 'Review', detail: 'Valida regras de ouro, segurança e qualidade do código' },
9
+ ],
10
+ }
11
+
12
+ // ─── Configuração ─────────────────────────────────────────────────────────────
13
+ const TARGET = args?.projectDir ?? '/tmp/unbox-storefront'
14
+ const SDK_PATH = args?.sdkPath ?? '/Users/brunoapereira/Documents/Claude/unbox-store-kit-v0.3/unbox-sdk'
15
+ const DOCS_PATH = '/Users/brunoapereira/Documents/Claude/unbox-store-kit-v0.3/unbox-agent-store-front v.0.2'
16
+
17
+ // ─── Contexto compartilhado (injetado em todos os agentes) ───────────────────
18
+ const SHARED = `
19
+ ## Projeto
20
+ - Diretório: ${TARGET}
21
+ - Stack: Next.js 15 (App Router) · TypeScript · Tailwind CSS v4 · shadcn/ui sobre Base UI
22
+ - SDK Unbox: ${SDK_PATH}/src/index.ts → import from "@payflows/unbox-sdk"
23
+ - Docs: ${DOCS_PATH}/
24
+
25
+ ## Regras de Ouro (NUNCA viole)
26
+ 1. UNBOX_API_KEY só no servidor — nunca NEXT_PUBLIC_, nunca client component, nunca logs
27
+ 2. Dois tokens: token de loja (catálogo/checkout) ≠ token do cliente (área /conta via OTP)
28
+ 3. placeOrder não é idempotente — lock por cartId no BFF; nunca retentar cegamente
29
+ 4. customerOTPRequest dispara e-mail real — jamais em testes, CI ou loops automáticos
30
+ 5. Preço e estoque vêm do servidor — nunca computar preço no cliente
31
+ 6. Menu de navegação: use tags() — navigation tree dá erro com token de loja
32
+ 7. displayStatus(language) está quebrado — use status + orderStatusLabel() do SDK
33
+ 8. shadcn/ui usa Base UI (@base-ui/react) — prop render, não asChild
34
+ 9. Tailwind v4 — use tokens/utilitários do tema; evite style inline quando há utilitário
35
+ 10. Erros GraphQL chegam no body HTTP 200 — sempre cheque errors[] antes de usar data
36
+ 11. recurringItemsFrequencyId não existe no live — use isRecurring:true + frequência no placeOrder
37
+
38
+ ## Padrão de arquivo server-only
39
+ \`\`\`ts
40
+ // lib/unbox.ts
41
+ import "server-only"
42
+ import { UnboxClient } from "@payflows/unbox-sdk"
43
+ let _client: UnboxClient | null = null
44
+ export function getUnboxClient(): UnboxClient { ... }
45
+ \`\`\`
46
+
47
+ ## Contratos entre seções
48
+ - lib/unbox.ts → UnboxClient singleton (server-only)
49
+ - lib/auth.ts → getCustomerToken(): cookie httpOnly do cliente
50
+ - lib/metadata.ts → buildMetadata(product) → Metadata
51
+ - lib/feedback.ts → re-exporta friendlyError, cartEventLabel do SDK
52
+ - components/cart/ → CartDrawer + useCart hook (client)
53
+ - components/promotions/ → ShopSalesBanner (server component)
54
+ `
55
+
56
+ // ─── Schemas ──────────────────────────────────────────────────────────────────
57
+ const FILE_SCHEMA = {
58
+ type: 'object',
59
+ properties: {
60
+ files: {
61
+ type: 'array',
62
+ items: {
63
+ type: 'object',
64
+ properties: {
65
+ path: { type: 'string', description: 'Caminho relativo ao diretório do projeto' },
66
+ content: { type: 'string', description: 'Conteúdo completo do arquivo' },
67
+ },
68
+ required: ['path', 'content'],
69
+ },
70
+ },
71
+ packages: { type: 'array', items: { type: 'string' }, description: 'npm packages extras além do base' },
72
+ notes: { type: 'string', description: 'Notas de implementação, gotchas, próximos passos' },
73
+ },
74
+ required: ['files'],
75
+ }
76
+
77
+ const REVIEW_SCHEMA = {
78
+ type: 'object',
79
+ properties: {
80
+ violations: {
81
+ type: 'array',
82
+ items: {
83
+ type: 'object',
84
+ properties: {
85
+ rule: { type: 'string' },
86
+ file: { type: 'string' },
87
+ line: { type: 'string' },
88
+ fix: { type: 'string' },
89
+ },
90
+ required: ['rule', 'file', 'fix'],
91
+ },
92
+ },
93
+ approved: { type: 'boolean' },
94
+ summary: { type: 'string' },
95
+ },
96
+ required: ['violations', 'approved', 'summary'],
97
+ }
98
+
99
+ // ─── Helper: escreve arquivos retornados pelo agente ─────────────────────────
100
+ async function writeSection(result, label) {
101
+ if (!result?.files?.length) { log(`⚠️ ${label}: nenhum arquivo retornado`); return }
102
+ const writer = await agent(
103
+ `Escreva os seguintes arquivos no disco. Para cada arquivo, use o caminho exato fornecido
104
+ (se o caminho for relativo, prefixe com "${TARGET}/"). Crie os diretórios necessários.
105
+ Arquivos:
106
+ ${JSON.stringify(result.files, null, 2)}
107
+ Após escrever todos os arquivos, retorne apenas "OK: <N> arquivos escritos".`,
108
+ { label: `write:${label}`, effort: 'low' },
109
+ )
110
+ log(`✓ ${label}: ${writer}`)
111
+ }
112
+
113
+ // ═══════════════════════════════════════════════════════════════════════════════
114
+ // FASE 1 — SCAFFOLD
115
+ // ═══════════════════════════════════════════════════════════════════════════════
116
+ phase('Scaffold')
117
+ log(`Scaffolding projeto em ${TARGET}...`)
118
+
119
+ const scaffold = await agent(
120
+ `${SHARED}
121
+
122
+ ## Tarefa: Scaffold do projeto Next.js
123
+
124
+ Crie a estrutura completa de um projeto Next.js 15 em "${TARGET}".
125
+
126
+ ### O que criar:
127
+
128
+ **1. package.json**
129
+ \`\`\`json
130
+ {
131
+ "name": "unbox-storefront",
132
+ "version": "0.1.0",
133
+ "private": true,
134
+ "scripts": { "dev": "next dev --turbopack", "build": "next build", "start": "next start", "lint": "next lint" },
135
+ "dependencies": {
136
+ "next": "^15.0.0",
137
+ "react": "^19.0.0",
138
+ "react-dom": "^19.0.0",
139
+ "@base-ui-components/react": "^1.0.0",
140
+ "server-only": "^0.0.1",
141
+ "clsx": "^2.0.0",
142
+ "tailwind-merge": "^2.0.0"
143
+ },
144
+ "devDependencies": {
145
+ "typescript": "^5.0.0",
146
+ "@types/node": "^22.0.0",
147
+ "@types/react": "^19.0.0",
148
+ "@types/react-dom": "^19.0.0",
149
+ "tailwindcss": "^4.0.0",
150
+ "@tailwindcss/postcss": "^4.0.0",
151
+ "eslint": "^9.0.0",
152
+ "eslint-config-next": "^15.0.0"
153
+ }
154
+ }
155
+ \`\`\`
156
+
157
+ **2. tsconfig.json** — padrão Next.js 15 com paths: { "@/*": ["./src/*"] } mas SEM src dir
158
+
159
+ **3. next.config.ts** — minimal, com images.domains vazio (será preenchido depois)
160
+
161
+ **4. app/globals.css** — import do Tailwind v4: \`@import "tailwindcss"\`
162
+
163
+ **5. app/layout.tsx** — RootLayout shell com html/body, Geist font, CartProvider placeholder
164
+
165
+ **6. lib/unbox.ts** — UnboxClient singleton server-only:
166
+ \`\`\`ts
167
+ import "server-only"
168
+ import { UnboxClient } from "@payflows/unbox-sdk"
169
+ // configura com env vars UNBOX_API_KEY, UNBOX_SHOP_ID
170
+ // implementa signInIfNeeded() com token cacheado (unstable_cache ou módulo-level)
171
+ \`\`\`
172
+
173
+ **7. lib/auth.ts** — placeholder para getCustomerToken():
174
+ \`\`\`ts
175
+ import { cookies } from "next/headers"
176
+ export async function getCustomerToken(): Promise<string | undefined> {
177
+ return (await cookies()).get("unbox_customer_token")?.value
178
+ }
179
+ \`\`\`
180
+
181
+ **8. lib/feedback.ts** — re-exporta do SDK:
182
+ \`\`\`ts
183
+ export { friendlyError, cartEventLabel } from "@payflows/unbox-sdk"
184
+ \`\`\`
185
+
186
+ **9. lib/metadata.ts** — buildMetadata(product) → Metadata do Next.js
187
+
188
+ **10. Estrutura de rotas (arquivos com "// TODO: implementado pelo agente X"):**
189
+ - app/page.tsx
190
+ - app/produtos/page.tsx
191
+ - app/c/[tagSlug]/page.tsx
192
+ - app/p/[productSlug]/page.tsx
193
+ - app/p/[productSlug]/loading.tsx
194
+ - app/carrinho/page.tsx
195
+ - app/checkout/page.tsx
196
+ - app/conta/entrar/page.tsx
197
+ - app/conta/pedidos/page.tsx
198
+ - app/conta/assinaturas/page.tsx
199
+ - app/pedido/[referenceId]/page.tsx
200
+ - app/busca/page.tsx
201
+ - app/sitemap.ts
202
+ - app/robots.ts
203
+ - app/error.tsx
204
+ - app/not-found.tsx
205
+ - app/loading.tsx
206
+ - app/api/cart/route.ts
207
+ - app/api/checkout/route.ts
208
+ - app/api/auth/otp/route.ts
209
+ - app/api/auth/signin/route.ts
210
+ - app/api/webhooks/unbox/route.ts
211
+
212
+ **11. .env.local.example:**
213
+ \`\`\`
214
+ UNBOX_API_KEY=
215
+ UNBOX_SHOP_ID=
216
+ UNBOX_USERNAME=
217
+ UNBOX_PASSWORD=
218
+ UNBOX_WEBHOOK_SECRET=
219
+ \`\`\`
220
+
221
+ **12. Diretórios (criar com .gitkeep):**
222
+ components/layout/, components/catalog/, components/pdp/,
223
+ components/cart/, components/checkout/, components/customer/,
224
+ components/promotions/, components/ui/
225
+
226
+ Retorne TODOS os arquivos com conteúdo completo (não apenas placeholders vazios para lib/ e app/layout.tsx).`,
227
+ { label: 'scaffold', schema: FILE_SCHEMA, effort: 'high' },
228
+ )
229
+
230
+ await writeSection(scaffold, 'scaffold')
231
+ const extraPackages = scaffold?.packages ?? []
232
+ log(`Scaffold completo. Packages extras: ${extraPackages.join(', ') || 'nenhum'}`)
233
+
234
+ // ═══════════════════════════════════════════════════════════════════════════════
235
+ // FASE 2 — IMPLEMENT (paralelo)
236
+ // ═══════════════════════════════════════════════════════════════════════════════
237
+ phase('Implement')
238
+ log('Iniciando implementação paralela das 10 seções...')
239
+
240
+ const SECTION_AGENTS = [
241
+ {
242
+ id: 'layout',
243
+ label: 'Layout (Header/Footer/Nav)',
244
+ prompt: `${SHARED}
245
+
246
+ ## Tarefa: Agente de Layout
247
+
248
+ Implemente o header, footer e navegação do storefront.
249
+
250
+ ### Arquivos a criar:
251
+ - components/layout/Header.tsx — header com logo, menu de categorias (via tags), carrinho, login
252
+ - components/layout/Footer.tsx — links institucionais, redes sociais, copyright
253
+ - components/layout/Navigation.tsx — menu mobile + desktop (usa tags como categorias)
254
+ - components/layout/CartButton.tsx — ícone de carrinho com badge de quantidade (client component)
255
+
256
+ ### Dados (SDK):
257
+ \`\`\`ts
258
+ // Dentro de um Server Component ou generateStaticParams:
259
+ import { getUnboxClient } from "@/lib/unbox"
260
+ const client = await getUnboxClient()
261
+ const tags = await client.getTags(true) // categorias top-level para o menu
262
+ \`\`\`
263
+
264
+ ### Regras críticas:
265
+ - getTags usa token de loja — correto para header
266
+ - Navigation deve ter loading state e ser Suspense-friendly
267
+ - CartButton é um client component que lê o contexto do carrinho
268
+ - Header fixo (sticky) com z-index adequado para o CartDrawer
269
+ - Links de categoria: href="/c/[tag.slug]"
270
+ - Acessibilidade: role="navigation", aria-label, foco visível
271
+
272
+ ### shadcn/ui (Base UI):
273
+ \`\`\`tsx
274
+ // Exemplo de Dropdown correto com Base UI:
275
+ import * as DropdownMenu from "@base-ui-components/react/menu"
276
+ <DropdownMenu.Root>
277
+ <DropdownMenu.Trigger render={<button>Menu</button>} />
278
+ <DropdownMenu.Positioner>
279
+ <DropdownMenu.Popup>...</DropdownMenu.Popup>
280
+ </DropdownMenu.Positioner>
281
+ </DropdownMenu.Root>
282
+ \`\`\``,
283
+ },
284
+ {
285
+ id: 'homepage',
286
+ label: 'Homepage',
287
+ prompt: `${SHARED}
288
+
289
+ ## Tarefa: Agente de Homepage
290
+
291
+ Implemente a página inicial do storefront (app/page.tsx).
292
+
293
+ ### Arquivos a criar:
294
+ - app/page.tsx — Server Component com Suspense por seção
295
+ - components/home/HeroBanner.tsx — banner principal com CTA
296
+ - components/home/FeaturedProducts.tsx — grade de produtos em destaque
297
+ - components/home/ShopSalesSection.tsx — promoções ativas da loja
298
+ - components/home/CategoryGrid.tsx — grade de categorias (tags)
299
+
300
+ ### Dados (SDK):
301
+ \`\`\`ts
302
+ const client = await getUnboxClient()
303
+ const [shop, featuredItems, tags] = await Promise.all([
304
+ client.getShop(), // shopSales (promoções automáticas)
305
+ client.getCatalog({ first: 8, sortBy: "updatedAt", sortOrder: "desc" }),
306
+ client.getTags(true), // categorias
307
+ ])
308
+ const shopSales = shop.shopSales ?? []
309
+ \`\`\`
310
+
311
+ ### ShopSales (promoções automáticas):
312
+ \`\`\`ts
313
+ interface ShopSale {
314
+ _id: string
315
+ name: string
316
+ description?: string
317
+ discountType: string // "percentage" | "fixed"
318
+ discountAmount: number
319
+ triggerType?: string // "all" | "payment_method" (ex: "pix")
320
+ }
321
+ \`\`\`
322
+
323
+ ### SEO:
324
+ \`\`\`ts
325
+ export const metadata: Metadata = {
326
+ title: "Loja | Unbox Store",
327
+ description: "...",
328
+ openGraph: { ... }
329
+ }
330
+ \`\`\`
331
+
332
+ ### Padrões:
333
+ - generateStaticParams NÃO se aplica à homepage
334
+ - Use unstable_cache ou Next.js fetch cache para dados públicos
335
+ - HeroBanner pode ser um image carrossel simples com Tailwind
336
+ - FeaturedProducts renderiza ProductCard (componente genérico reutilizado pelo Catalog)
337
+ - Cada seção deve ter loading.tsx com skeleton adequado`,
338
+ },
339
+ {
340
+ id: 'catalog',
341
+ label: 'Catalog (Listagem)',
342
+ prompt: `${SHARED}
343
+
344
+ ## Tarefa: Agente de Catalog
345
+
346
+ Implemente as páginas de listagem de produtos, categoria e busca.
347
+
348
+ ### Arquivos a criar:
349
+ - app/produtos/page.tsx — catálogo completo, paginado
350
+ - app/c/[tagSlug]/page.tsx — categoria/tag filtrada
351
+ - app/busca/page.tsx — busca por texto (?q=)
352
+ - app/produtos/loading.tsx — skeleton de grid
353
+ - components/catalog/ProductGrid.tsx — grade responsiva de produtos
354
+ - components/catalog/ProductCard.tsx — card com imagem, título, preço, badge assinatura
355
+ - components/catalog/Pagination.tsx — paginação com offset
356
+ - components/catalog/FilterBar.tsx — filtros de categoria e ordenação
357
+ - components/catalog/SearchBar.tsx — campo de busca (client, redireciona para /busca)
358
+
359
+ ### Dados (SDK):
360
+ \`\`\`ts
361
+ // Listagem com filtros
362
+ const result = await client.getCatalog({
363
+ first: 24,
364
+ offset: page * 24, // page vem de searchParams
365
+ tagIds: tagId ? [tagId] : undefined,
366
+ searchText: q || undefined,
367
+ sortBy: "updatedAt",
368
+ sortOrder: "desc",
369
+ })
370
+ // result.nodes.map(n => n.product)
371
+ // result.totalCount
372
+ // result.pageInfo.hasNextPage
373
+
374
+ // Categoria por slug
375
+ const tags = await client.getTags(true)
376
+ const tag = tags.find(t => t.slug === tagSlug)
377
+ \`\`\`
378
+
379
+ ### ProductCard — campos a exibir:
380
+ - Imagem: product.imageUrls?.[0]
381
+ - Título: product.title
382
+ - Preço: product.pricing?.[0]?.displayPrice
383
+ - Preço de: product.variants?.[0]?.pricing?.[0]?.compareAtPrice?.displayAmount
384
+ - Badge "Assine e Poupe" se product.recurrenceAllowed
385
+ - Badge "Esgotado" se product.isSoldOut
386
+ - Badge "Pré-venda" se product.isBackorder
387
+ - Link: /p/[product.slug]
388
+
389
+ ### Paginação:
390
+ - Use URL searchParams (?page=2) — stateful via Link
391
+ - Não usar state do cliente para paginação (SSR-friendly)
392
+
393
+ ### SEO de categoria:
394
+ \`\`\`ts
395
+ export async function generateMetadata({ params }) {
396
+ const tag = await getTag(params.tagSlug)
397
+ return { title: tag.displayTitle ?? tag.name }
398
+ }
399
+ export async function generateStaticParams() {
400
+ const tags = await getTags()
401
+ return tags.map(t => ({ tagSlug: t.slug }))
402
+ }
403
+ \`\`\``,
404
+ },
405
+ {
406
+ id: 'pdp',
407
+ label: 'PDP (Product Detail)',
408
+ prompt: `${SHARED}
409
+
410
+ ## Tarefa: Agente de PDP — Product Detail Page
411
+
412
+ Implemente a página de detalhe de produto (/p/[productSlug]).
413
+
414
+ ### Arquivos a criar:
415
+ - app/p/[productSlug]/page.tsx — Server Component com SSG + ISR
416
+ - components/pdp/ProductImages.tsx — galeria de imagens (main + thumbs)
417
+ - components/pdp/ProductInfo.tsx — título, preço, badges, descrição
418
+ - components/pdp/VariantSelector.tsx — seleção de variante (client component)
419
+ - components/pdp/AddToCartForm.tsx — quantidade + botão "Adicionar ao Carrinho" (client)
420
+ - components/pdp/SubscriptionToggle.tsx — toggle "Avulso / Assine e Poupe" (client)
421
+ - components/pdp/ProductDescription.tsx — descrição e informações adicionais (accordion)
422
+
423
+ ### Dados (SDK):
424
+ \`\`\`ts
425
+ const client = await getUnboxClient()
426
+ const item = await client.getProductBySlug(params.productSlug)
427
+ // item.product.variants[] — seleção de variante
428
+ // item.product.recurrenceAllowed — mostrar toggle de assinatura
429
+ // item.product.isSoldOut / isBackorder / isLowQuantity — badges
430
+ // item.shortDescription — descrição curta (acima do fold)
431
+ // item.product.description — descrição longa (abaixo)
432
+ // item.product.additionalInformation — informações adicionais (accordion)
433
+ \`\`\`
434
+
435
+ ### ISR:
436
+ \`\`\`ts
437
+ export const revalidate = 3600 // 1h
438
+ export async function generateStaticParams() {
439
+ const catalog = await client.getCatalog({ first: 200 })
440
+ return catalog.nodes.map(n => ({ productSlug: n.product.slug }))
441
+ }
442
+ \`\`\`
443
+
444
+ ### SubscriptionToggle:
445
+ - Só exibir se product.recurrenceAllowed
446
+ - Ao selecionar "Assinar", mostrar frequências disponíveis
447
+ - As frequências vêm de getShop().recurringOrdersPolicy.frequencyOptions
448
+ - No AddToCartForm: enviar isRecurring: true no cartItem
449
+ - A frequência só vai no placeOrder, não no cartItem (regra 11 do AGENTS.md)
450
+
451
+ ### VariantSelector:
452
+ - Produto pode ter N variantes (ex.: tamanhos, sabores)
453
+ - Cada variante tem pricing próprio
454
+ - isSoldOut pode ser no produto OU por variante (checar ambos)
455
+
456
+ ### AddToCartForm (client):
457
+ \`\`\`ts
458
+ // Chama a API Route /api/cart (POST) com:
459
+ // { action: "add", productId, productVariantId, price, quantity, isRecurring }
460
+ // Após sucesso, atualiza contexto do carrinho e mostra CartDrawer
461
+ \`\`\`
462
+
463
+ ### SEO:
464
+ \`\`\`ts
465
+ export async function generateMetadata({ params }) {
466
+ const item = await client.getProductBySlug(params.productSlug)
467
+ return buildMetadata(item.product) // usa lib/metadata.ts
468
+ }
469
+ \`\`\``,
470
+ },
471
+ {
472
+ id: 'cart',
473
+ label: 'Cart (Carrinho)',
474
+ prompt: `${SHARED}
475
+
476
+ ## Tarefa: Agente de Cart — Carrinho
477
+
478
+ Implemente o carrinho completo: contexto, drawer e API routes.
479
+
480
+ ### Arquivos a criar:
481
+ - app/api/cart/route.ts — Route Handler (server-only): create, add, update, remove, get
482
+ - components/cart/CartContext.tsx — Context + Provider (client, lê cookie cartId/cartToken)
483
+ - components/cart/CartDrawer.tsx — drawer lateral (Base UI Dialog)
484
+ - components/cart/CartItem.tsx — item com imagem, título, quantidade, preço, remover
485
+ - components/cart/CartSummary.tsx — total, desconto, frete estimado, botão checkout
486
+ - components/cart/EmptyCart.tsx — estado vazio
487
+ - app/carrinho/page.tsx — página full do carrinho (mesmos componentes, layout expandido)
488
+ - components/cart/DiscountCodeInput.tsx — input + botão aplicar cupom
489
+
490
+ ### Persistência:
491
+ - cartId e cartToken em cookies do browser (httpOnly=false para leitura no cliente)
492
+ - CartContext lê os cookies e sincroniza estado
493
+ - Ao criar novo carrinho, salvar cookies via document.cookie
494
+
495
+ ### API Route (/api/cart):
496
+ \`\`\`ts
497
+ // POST /api/cart
498
+ // body: { action: "create" | "add" | "update" | "remove" | "get" | "discount" | "clear", ...params }
499
+ import { getUnboxClient } from "@/lib/unbox"
500
+ export async function POST(req: Request) {
501
+ const client = await getUnboxClient()
502
+ const { action, cartId, cartToken, ...rest } = await req.json()
503
+ // Rotear por action e chamar o método SDK correto
504
+ // create → client.createCart()
505
+ // add → client.addCartItems(cartId, cartToken, [item])
506
+ // update → client.updateCartItem(cartId, cartToken, itemId, qty)
507
+ // remove → client.removeCartItem(cartId, cartToken, itemId)
508
+ // get → client.getCart(cartId, cartToken)
509
+ // discount → client.applyDiscountCode(cartId, cartToken, code)
510
+ }
511
+ \`\`\`
512
+
513
+ ### CartDrawer (Base UI):
514
+ \`\`\`tsx
515
+ import * as Dialog from "@base-ui-components/react/dialog"
516
+ <Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
517
+ <Dialog.Backdrop />
518
+ <Dialog.Popup render={<aside className="fixed inset-y-0 right-0 w-96" />}>
519
+ ...
520
+ </Dialog.Popup>
521
+ </Dialog.Root>
522
+ \`\`\`
523
+
524
+ ### DiscountCode:
525
+ - SDK: client.applyDiscountCode(cartId, cartToken, code) → CartResult
526
+ - Mostrar erro amigável via friendlyError() do lib/feedback.ts
527
+ - Mostrar desconto aplicado no CartSummary
528
+
529
+ ### CartContext — interface mínima:
530
+ \`\`\`ts
531
+ interface CartContextValue {
532
+ cartId: string | null
533
+ cartToken: string | null
534
+ items: CartItem[]
535
+ totalCount: number
536
+ total: number
537
+ isOpen: boolean
538
+ isLoading: boolean
539
+ openCart(): void
540
+ closeCart(): void
541
+ addItem(item: CartItemInput & { isRecurring?: boolean }): Promise<void>
542
+ updateQuantity(itemId: string, qty: number): Promise<void>
543
+ removeItem(itemId: string): Promise<void>
544
+ applyDiscount(code: string): Promise<void>
545
+ refreshCart(): Promise<void>
546
+ }
547
+ \`\`\``,
548
+ },
549
+ {
550
+ id: 'checkout',
551
+ label: 'Checkout (Pagamento)',
552
+ prompt: `${SHARED}
553
+
554
+ ## Tarefa: Agente de Checkout
555
+
556
+ Implemente o fluxo completo de checkout: endereço → frete → pagamento → pedido.
557
+
558
+ ### Arquivos a criar:
559
+ - app/checkout/page.tsx — página de checkout (multi-step, client-heavy)
560
+ - app/api/checkout/route.ts — Route Handler para todas as etapas
561
+ - app/pedido/[referenceId]/page.tsx — confirmação de pedido
562
+ - components/checkout/AddressForm.tsx — formulário de endereço completo
563
+ - components/checkout/ShippingSelector.tsx — seleção de opção de frete
564
+ - components/checkout/PaymentSelector.tsx — Pix ou Cartão de Crédito
565
+ - components/checkout/PixPayment.tsx — QR code + copia-e-cola + polling de status
566
+ - components/checkout/CreditCardForm.tsx — dados do cartão (número, nome, validade, CVV)
567
+ - components/checkout/OrderSummary.tsx — resumo do pedido (sidebar)
568
+ - components/checkout/CheckoutProgress.tsx — indicador de etapas
569
+
570
+ ### Fluxo (API Unbox):
571
+ \`\`\`
572
+ 1. setShippingAddressOnCart → fulfillmentGroupId
573
+ 2. updateFulfillmentOptionsForGroup → availableFulfillmentOptions[]
574
+ 3. (opcional) applyDiscountCodeToCart
575
+ 4. selectFulfillmentOptionForGroup → total com frete
576
+ 5. placeOrder → order (Pix: QR + copia-e-cola | Cartão: aprovado/recusado)
577
+ \`\`\`
578
+
579
+ ### API Route (/api/checkout):
580
+ \`\`\`ts
581
+ // POST /api/checkout
582
+ // { action: "address" | "quote" | "select-shipping" | "place" | "apply-discount" }
583
+ // IDEMPOTÊNCIA: lock por cartId (use Map em memória ou KV store)
584
+ // Desabilitar botão de submit + token de uso único no form
585
+ \`\`\`
586
+
587
+ ### Idempotência (CRÍTICO):
588
+ \`\`\`ts
589
+ const inFlight = new Map<string, Promise<any>>()
590
+ export async function POST(req: Request) {
591
+ const { cartId, action } = await req.json()
592
+ if (action === "place") {
593
+ if (inFlight.has(cartId)) return Response.json({ error: "pedido_em_andamento" }, { status: 409 })
594
+ const promise = doPlaceOrder(...)
595
+ inFlight.set(cartId, promise)
596
+ try { return Response.json(await promise) }
597
+ finally { inFlight.delete(cartId) }
598
+ }
599
+ }
600
+ \`\`\`
601
+
602
+ ### Pix:
603
+ - placeOrder retorna paymentMethod.data.pixQrCode e pixQrCodeText
604
+ - Exibir QR code (como <img src="data:image/png;base64,..."> ou via qrcode lib)
605
+ - Mostrar copia-e-cola para Pix
606
+ - Polling de status: GET /api/checkout?orderId=... a cada 5s (usa client.getOrder())
607
+ - Redirecionar para /pedido/[referenceId] quando status mudar de PENDING
608
+
609
+ ### Cartão de Crédito (UnboxPay):
610
+ \`\`\`ts
611
+ // No placeOrder, paymentInput:
612
+ {
613
+ paymentMethodId: "unboxpay_credit",
614
+ amount: totalAmount,
615
+ data: {
616
+ cardNumber: "...",
617
+ cardholderName: "...",
618
+ expiryMonth: "12",
619
+ expiryYear: "2027",
620
+ cvv: "..."
621
+ }
622
+ }
623
+ \`\`\`
624
+
625
+ ### AddressInput (campos obrigatórios):
626
+ fullName, phone, postal, address1, city, region, country, taxPayerId, number, neighborhood
627
+
628
+ ### Página de confirmação (/pedido/[referenceId]):
629
+ - Busca pedido por referenceId + token (ou session do cliente)
630
+ - Mostra status, itens, tracking se disponível
631
+ - status: use orderStatusLabel() do SDK (não displayStatus)`,
632
+ },
633
+ {
634
+ id: 'auth',
635
+ label: 'Auth (OTP Login)',
636
+ prompt: `${SHARED}
637
+
638
+ ## Tarefa: Agente de Autenticação
639
+
640
+ Implemente login passwordless (OTP) e gestão de sessão do cliente.
641
+
642
+ ### Arquivos a criar:
643
+ - app/conta/entrar/page.tsx — página de login (2 etapas: email → código)
644
+ - app/api/auth/otp/route.ts — POST: dispara OTP; GET/DELETE: status/logout
645
+ - app/api/auth/signin/route.ts — troca OTP pelo token do cliente
646
+ - lib/auth.ts — getCustomerToken(), setCustomerToken(), clearCustomerToken()
647
+ - components/auth/OTPForm.tsx — formulário de email + campo de código OTP
648
+ - middleware.ts — protege rotas /conta/pedidos e /conta/assinaturas
649
+
650
+ ### lib/auth.ts:
651
+ \`\`\`ts
652
+ import { cookies } from "next/headers"
653
+ const COOKIE = "unbox_customer_token"
654
+ const OPTS = { httpOnly: true, secure: true, sameSite: "lax", path: "/" } as const
655
+
656
+ export async function getCustomerToken() {
657
+ return (await cookies()).get(COOKIE)?.value
658
+ }
659
+ export async function setCustomerToken(token: string, maxAge = 86400 * 7) {
660
+ (await cookies()).set(COOKIE, token, { ...OPTS, maxAge })
661
+ }
662
+ export async function clearCustomerToken() {
663
+ (await cookies()).delete(COOKIE)
664
+ }
665
+ \`\`\`
666
+
667
+ ### /api/auth/otp (POST):
668
+ \`\`\`ts
669
+ // Dispara OTP por email
670
+ // ⚠️ Exige x-captcha-verification no SDK (já implementado internamente)
671
+ // ⚠️ NUNCA chamar em testes — manda e-mail real
672
+ // Rate-limit: máximo 1 requisição por email a cada 60s
673
+ const client = await getUnboxClient()
674
+ await client.gql(\`mutation($i:CustomerOTPRequestInput!){ customerOTPRequest(input:$i){success} }\`,
675
+ { i: { email, shopId: process.env.UNBOX_SHOP_ID } },
676
+ { captcha: true }
677
+ )
678
+ \`\`\`
679
+
680
+ ### /api/auth/signin (POST):
681
+ \`\`\`ts
682
+ // Troca o código OTP pelo token do cliente
683
+ const data = await client.gql(\`
684
+ mutation($i:CustomerPasswordlessSignInInput){
685
+ customerPasswordlessSignIn(input:$i){
686
+ accessToken idToken firstAccess newShopSignIn
687
+ }
688
+ }\`,
689
+ { i: { email, otpCode: code, shopId } },
690
+ { captcha: true }
691
+ )
692
+ // Salvar accessToken em cookie httpOnly via setCustomerToken()
693
+ // Redirecionar para /conta/pedidos
694
+ \`\`\`
695
+
696
+ ### middleware.ts:
697
+ \`\`\`ts
698
+ import { NextResponse } from "next/server"
699
+ import type { NextRequest } from "next/server"
700
+ export function middleware(req: NextRequest) {
701
+ const token = req.cookies.get("unbox_customer_token")?.value
702
+ if (!token) return NextResponse.redirect(new URL("/conta/entrar", req.url))
703
+ return NextResponse.next()
704
+ }
705
+ export const config = { matcher: ["/conta/pedidos/:path*", "/conta/assinaturas/:path*"] }
706
+ \`\`\`
707
+
708
+ ### OTPForm (client component):
709
+ - Step 1: campo de email → POST /api/auth/otp
710
+ - Step 2: campo de 6 dígitos → POST /api/auth/signin
711
+ - Auto-foco no step 2
712
+ - Botão "Reenviar código" com cooldown de 60s
713
+ - Erro amigável via friendlyError()`,
714
+ },
715
+ {
716
+ id: 'customer',
717
+ label: 'Customer Area (Conta)',
718
+ prompt: `${SHARED}
719
+
720
+ ## Tarefa: Agente de Área do Cliente
721
+
722
+ Implemente a área logada: pedidos, rastreio e gerenciamento de assinaturas.
723
+
724
+ ### Arquivos a criar:
725
+ - app/conta/pedidos/page.tsx — lista de pedidos do cliente
726
+ - app/conta/pedidos/[orderId]/page.tsx — detalhe de pedido + rastreio
727
+ - app/conta/assinaturas/page.tsx — lista de assinaturas ativas
728
+ - app/conta/assinaturas/[subscriptionId]/page.tsx — detalhe + ações da assinatura
729
+ - app/conta/layout.tsx — layout compartilhado da área logada (sidebar de navegação)
730
+ - components/customer/OrderCard.tsx — card de pedido na listagem
731
+ - components/customer/OrderDetail.tsx — detalhe completo com itens e status
732
+ - components/customer/SubscriptionCard.tsx — card de assinatura
733
+ - components/customer/SubscriptionActions.tsx — pausar/retomar/pular/cancelar
734
+
735
+ ### Token do cliente (DIFERENTE do token de loja):
736
+ \`\`\`ts
737
+ import { getCustomerToken } from "@/lib/auth"
738
+ import { UnboxCustomerClient } from "@payflows/unbox-sdk"
739
+
740
+ export async function getCustomerClient() {
741
+ const token = await getCustomerToken()
742
+ if (!token) throw new Error("Não autenticado")
743
+ return new UnboxCustomerClient({
744
+ apiKey: process.env.UNBOX_API_KEY!,
745
+ shopId: process.env.UNBOX_SHOP_ID!,
746
+ customerToken: token,
747
+ })
748
+ }
749
+ \`\`\`
750
+
751
+ ### Pedidos:
752
+ \`\`\`ts
753
+ const client = await getCustomerClient()
754
+ const orders = await client.getOrders({ first: 20 }) // paginado
755
+ // order.status → use orderStatusLabel(order.status) para texto PT-BR
756
+ // order.referenceId → ID curto para exibição
757
+ // order.totalPrice?.amount → total pago
758
+ // ⚠️ NÃO use totalItemQuantity (campo quebrado)
759
+ // ⚠️ NÃO use displayStatus(language) (resolver quebrado)
760
+ \`\`\`
761
+
762
+ ### Rastreio:
763
+ \`\`\`ts
764
+ // Em contexto de cliente: use trackingCode, não trackingUrl
765
+ const tracking = order.fulfillmentGroups?.data?.[0]?.tracking
766
+ // tracking.trackingCode — código de rastreio
767
+ // tracking.carrier — transportadora
768
+ \`\`\`
769
+
770
+ ### Assinaturas (RecurringOrders):
771
+ \`\`\`ts
772
+ const subscriptions = await client.getRecurringOrders({ first: 10 })
773
+ // subscription.status: "active" | "paused" | "cancelled"
774
+ // subscription.nextOrderDate — próxima data de cobrança
775
+ // subscription.items[] — produtos
776
+ // subscription.recurrencePolicy.frequencyOptions[] — frequências
777
+
778
+ // Ações disponíveis:
779
+ await client.pauseRecurringOrder(subscriptionId)
780
+ await client.resumeRecurringOrder(subscriptionId)
781
+ await client.skipRecurringOrderCycle(subscriptionId)
782
+ await client.cancelRecurringOrder(subscriptionId)
783
+ // Status: use subscriptionStatusLabel() do SDK
784
+ \`\`\`
785
+
786
+ ### Status labels:
787
+ \`\`\`ts
788
+ import { orderStatusLabel, paymentStatusLabel, subscriptionStatusLabel } from "@payflows/unbox-sdk"
789
+ orderStatusLabel("new") // "Novo"
790
+ subscriptionStatusLabel("active") // "Ativa"
791
+ \`\`\`
792
+
793
+ ### Paginação de pedidos:
794
+ - Implementar paginação com cursor (pageInfo.endCursor)
795
+ - "Carregar mais" (não paginação por página — UX mais suave para histórico)`,
796
+ },
797
+ {
798
+ id: 'promotions',
799
+ label: 'Promotions (Promoções)',
800
+ prompt: `${SHARED}
801
+
802
+ ## Tarefa: Agente de Promoções
803
+
804
+ Implemente os componentes de promoções da loja (shopSales e cupons).
805
+
806
+ ### Arquivos a criar:
807
+ - components/promotions/ShopSalesBanner.tsx — banner de promoções automáticas (topo do site)
808
+ - components/promotions/SalesBadge.tsx — badge numa listagem/card de produto
809
+ - components/promotions/DiscountBanner.tsx — banner de desconto em destaque (homepage)
810
+ - components/promotions/CouponInput.tsx — input de cupom reutilizável (usado no Checkout e Cart)
811
+ - components/promotions/ActiveDiscount.tsx — exibe desconto aplicado com valor e remoção
812
+ - app/api/promotions/route.ts — GET shopSales cached
813
+
814
+ ### shopSales (promoções automáticas — sem código):
815
+ \`\`\`ts
816
+ const shop = await client.getShop()
817
+ interface ShopSale {
818
+ _id: string
819
+ name: string
820
+ description?: string
821
+ discountType: "percentage" | "fixed"
822
+ discountAmount: number
823
+ triggerType?: string // "all" | "payment_method" (ex: "5% no Pix")
824
+ conditions?: object
825
+ }
826
+ \`\`\`
827
+
828
+ ### ShopSalesBanner — lógica:
829
+ - Se há shopSale com triggerType "payment_method" para Pix → banner "5% de desconto no Pix"
830
+ - Se há shopSale geral → banner de desconto global
831
+ - Banner deve ser dismissível (localStorage) e acessível (role="banner")
832
+ - Server Component por default (dados em cache), com parte client só para dismiss
833
+
834
+ ### Cupons (discountCodes):
835
+ \`\`\`ts
836
+ // Listar cupons disponíveis (para exibir na homepage se quiser)
837
+ const codes = await client.listDiscountCodes({ first: 20 })
838
+ // ⚠️ first deve ser ≤ 20 (ConnectionLimitInt, não Int comum)
839
+
840
+ // Aplicar cupom (via CartContext / API Route /api/cart)
841
+ await client.applyDiscountCode(cartId, cartToken, "CODIGO10")
842
+ // Resultado vem no CartResult — exibir via ActiveDiscount
843
+ \`\`\`
844
+
845
+ ### SalesBadge — uso nos ProductCards:
846
+ \`\`\`tsx
847
+ // Recebe shopSales como prop e exibe o desconto aplicável ao produto
848
+ // Ex: "5% OFF no Pix" ou "10% OFF assinatura"
849
+ <SalesBadge sales={shopSales} isRecurring={product.recurrenceAllowed} />
850
+ \`\`\`
851
+
852
+ ### Cache:
853
+ - shopSales: cache de 5min (dados mudam pouco, mas são dinâmicos)
854
+ - discountCodes: não cachear (podem expirar)
855
+ \`\`\`ts
856
+ import { unstable_cache } from "next/cache"
857
+ const getShopSales = unstable_cache(
858
+ async () => { const s = await client.getShop(); return s.shopSales ?? [] },
859
+ ["shop-sales"],
860
+ { revalidate: 300 }
861
+ )
862
+ \`\`\``,
863
+ },
864
+ {
865
+ id: 'feedback',
866
+ label: 'Feedback/UX',
867
+ prompt: `${SHARED}
868
+
869
+ ## Tarefa: Agente de Feedback e UX
870
+
871
+ Implemente os padrões de UX: estados de loading, erros, toasts e páginas especiais.
872
+
873
+ ### Arquivos a criar:
874
+ - components/ui/Toast.tsx — toast notifications (Base UI Toast ou custom)
875
+ - components/ui/ToastProvider.tsx — provider global
876
+ - components/ui/LoadingSpinner.tsx — spinner genérico
877
+ - components/ui/Skeleton.tsx — skeleton genérico (polimórfico com className)
878
+ - components/ui/ErrorBoundary.tsx — error boundary client component
879
+ - components/ui/EmptyState.tsx — estado vazio genérico
880
+ - app/error.tsx — error page global (client component)
881
+ - app/not-found.tsx — 404 page
882
+ - app/loading.tsx — loading page global (Suspense fallback)
883
+ - lib/feedback.ts — re-exporta + mapeia erros Unbox para mensagens PT-BR
884
+
885
+ ### lib/feedback.ts:
886
+ \`\`\`ts
887
+ export { friendlyError, cartEventLabel } from "@payflows/unbox-sdk"
888
+
889
+ // Mapas de UX extras:
890
+ export const CHECKOUT_STEP_LABELS = {
891
+ address: "Endereço",
892
+ shipping: "Frete",
893
+ payment: "Pagamento",
894
+ confirmation: "Confirmação",
895
+ } as const
896
+
897
+ export function getPaymentErrorMessage(error: string): string {
898
+ const map: Record<string, string> = {
899
+ INSUFFICIENT_FUNDS_ERROR: "Cartão sem limite disponível",
900
+ CARD_DECLINED: "Cartão recusado pela operadora",
901
+ INVALID_CARD: "Dados do cartão inválidos",
902
+ EXPIRED_CARD: "Cartão expirado",
903
+ }
904
+ return map[error] ?? "Erro no pagamento. Tente outro cartão."
905
+ }
906
+ \`\`\`
907
+
908
+ ### Toast (Base UI se disponível, senão custom):
909
+ \`\`\`tsx
910
+ // Interface de uso:
911
+ const { toast } = useToast()
912
+ toast({ title: "Produto adicionado!", description: "Veja seu carrinho", type: "success" })
913
+ toast({ title: "Erro ao aplicar cupom", description: friendlyError(error), type: "error" })
914
+ \`\`\`
915
+
916
+ ### Skeleton — polimórfico:
917
+ \`\`\`tsx
918
+ // <Skeleton className="h-4 w-32" /> → linha
919
+ // <Skeleton className="aspect-square w-full" /> → imagem
920
+ // ProductCardSkeleton, CartItemSkeleton, etc.
921
+ \`\`\`
922
+
923
+ ### app/error.tsx (CRÍTICO — client component):
924
+ \`\`\`tsx
925
+ "use client"
926
+ export default function Error({ error, reset }: { error: Error; reset(): void }) {
927
+ // Não exibir detalhes técnicos ao usuário final
928
+ // Log interno: console.error(error.message) apenas
929
+ // Botão "Tentar novamente" chama reset()
930
+ }
931
+ \`\`\`
932
+
933
+ ### Padrões de loading states:
934
+ - Catálogo: grid de 8 ProductCardSkeleton
935
+ - PDP: ImageSkeleton + InfoSkeleton lado a lado
936
+ - Carrinho: lista de CartItemSkeleton
937
+ - Checkout: FormSkeleton por seção
938
+ - Usar Tailwind animate-pulse nos skeletons
939
+
940
+ ### Acessibilidade:
941
+ - aria-live="polite" nos toasts
942
+ - aria-busy="true" em botões durante carregamento
943
+ - Focus trap no CartDrawer e dialogs
944
+ - Não remover foco visível (outline)`,
945
+ },
946
+ {
947
+ id: 'seo',
948
+ label: 'SEO + Infra',
949
+ prompt: `${SHARED}
950
+
951
+ ## Tarefa: Agente de SEO e Infraestrutura
952
+
953
+ Implemente SEO, sitemap, robots, metadados e páginas de suporte.
954
+
955
+ ### Arquivos a criar:
956
+ - app/sitemap.ts — sitemap dinâmico (produtos + categorias)
957
+ - app/robots.ts — robots.txt
958
+ - lib/metadata.ts — buildMetadata(product) e helpers
959
+ - app/pedido/[referenceId]/page.tsx — confirmação de pedido (noindex)
960
+ - app/conta/layout.tsx — layout da área logada (se não criado pelo auth agent)
961
+
962
+ ### lib/metadata.ts:
963
+ \`\`\`ts
964
+ import type { Metadata } from "next"
965
+ import type { CatalogProduct } from "@payflows/unbox-sdk"
966
+
967
+ const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME ?? "Unbox Store"
968
+ const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://seudominio.com.br"
969
+
970
+ export function buildMetadata(product: CatalogProduct, canonical?: string): Metadata {
971
+ const title = product.pageTitle ?? product.title
972
+ const description = product.metaDescription ?? product.description?.slice(0, 160)
973
+ const image = product.imageUrls?.[0]
974
+ const url = canonical ?? \`\${SITE_URL}/p/\${product.slug}\`
975
+ return {
976
+ title: \`\${title} | \${SITE_NAME}\`,
977
+ description,
978
+ alternates: { canonical: url },
979
+ openGraph: {
980
+ title,
981
+ description,
982
+ url,
983
+ images: image ? [{ url: image, alt: title }] : [],
984
+ type: "website",
985
+ },
986
+ twitter: { card: "summary_large_image", title, description, images: image ? [image] : [] },
987
+ }
988
+ }
989
+
990
+ export function buildCategoryMetadata(tag: { name: string; description?: string; slug: string }): Metadata {
991
+ return {
992
+ title: \`\${tag.name} | \${SITE_NAME}\`,
993
+ description: tag.description,
994
+ alternates: { canonical: \`\${SITE_URL}/c/\${tag.slug}\` },
995
+ }
996
+ }
997
+ \`\`\`
998
+
999
+ ### app/sitemap.ts:
1000
+ \`\`\`ts
1001
+ import type { MetadataRoute } from "next"
1002
+ import { getUnboxClient } from "@/lib/unbox"
1003
+
1004
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
1005
+ const client = await getUnboxClient()
1006
+ const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? ""
1007
+
1008
+ // Páginas estáticas
1009
+ const staticRoutes = ["/", "/produtos", "/busca"].map(path => ({
1010
+ url: \`\${SITE_URL}\${path}\`,
1011
+ lastModified: new Date(),
1012
+ changeFrequency: "daily" as const,
1013
+ priority: path === "/" ? 1 : 0.8,
1014
+ }))
1015
+
1016
+ // Categorias
1017
+ const tags = await client.getTags(true)
1018
+ const categoryRoutes = tags.filter(t => t.isVisible).map(tag => ({
1019
+ url: \`\${SITE_URL}/c/\${tag.slug}\`,
1020
+ lastModified: new Date(),
1021
+ changeFrequency: "weekly" as const,
1022
+ priority: 0.7,
1023
+ }))
1024
+
1025
+ // Produtos (paginado para não buscar tudo de uma vez)
1026
+ const productRoutes = []
1027
+ let offset = 0
1028
+ for (;;) {
1029
+ const page = await client.getCatalog({ first: 100, offset })
1030
+ const visible = page.nodes.filter(n => n.product.isVisible)
1031
+ productRoutes.push(...visible.map(n => ({
1032
+ url: \`\${SITE_URL}/p/\${n.product.slug}\`,
1033
+ lastModified: new Date(),
1034
+ changeFrequency: "weekly" as const,
1035
+ priority: 0.6,
1036
+ })))
1037
+ if (!page.pageInfo?.hasNextPage) break
1038
+ offset += 100
1039
+ }
1040
+
1041
+ return [...staticRoutes, ...categoryRoutes, ...productRoutes]
1042
+ }
1043
+ \`\`\`
1044
+
1045
+ ### app/robots.ts:
1046
+ \`\`\`ts
1047
+ import type { MetadataRoute } from "next"
1048
+ export default function robots(): MetadataRoute.Robots {
1049
+ return {
1050
+ rules: [
1051
+ { userAgent: "*", allow: "/", disallow: ["/conta/", "/checkout", "/api/"] }
1052
+ ],
1053
+ sitemap: \`\${process.env.NEXT_PUBLIC_SITE_URL}/sitemap.xml\`,
1054
+ }
1055
+ }
1056
+ \`\`\`
1057
+
1058
+ ### Variáveis de ambiente públicas (NEXT_PUBLIC):
1059
+ Apenas NEXT_PUBLIC_SITE_NAME e NEXT_PUBLIC_SITE_URL são seguros como public —
1060
+ NÃO expor shopId, apiKey ou qualquer credencial Unbox.
1061
+
1062
+ ### Dados estruturados (JSON-LD) — PDP:
1063
+ \`\`\`ts
1064
+ export function buildProductJsonLd(product: CatalogProduct) {
1065
+ return {
1066
+ "@context": "https://schema.org",
1067
+ "@type": "Product",
1068
+ name: product.title,
1069
+ description: product.description,
1070
+ image: product.imageUrls,
1071
+ sku: product.variants?.[0]?.sku,
1072
+ offers: {
1073
+ "@type": "Offer",
1074
+ availability: product.isSoldOut
1075
+ ? "https://schema.org/OutOfStock"
1076
+ : "https://schema.org/InStock",
1077
+ price: product.pricing?.[0]?.price,
1078
+ priceCurrency: "BRL",
1079
+ },
1080
+ }
1081
+ }
1082
+ \`\`\``,
1083
+ },
1084
+ ]
1085
+
1086
+ const sectionResults = await parallel(
1087
+ SECTION_AGENTS.map(section => async () => {
1088
+ const result = await agent(section.prompt, {
1089
+ label: section.label,
1090
+ phase: 'Implement',
1091
+ schema: FILE_SCHEMA,
1092
+ effort: 'high',
1093
+ })
1094
+ await writeSection(result, section.id)
1095
+ return { id: section.id, files: result?.files?.map(f => f.path) ?? [], notes: result?.notes }
1096
+ })
1097
+ )
1098
+
1099
+ const validSections = sectionResults.filter(Boolean)
1100
+ log(`Implement completo: ${validSections.length}/10 seções implementadas`)
1101
+
1102
+ // ═══════════════════════════════════════════════════════════════════════════════
1103
+ // FASE 3 — INTEGRATE
1104
+ // ═══════════════════════════════════════════════════════════════════════════════
1105
+ phase('Integrate')
1106
+ log('Integrando seções...')
1107
+
1108
+ const sectionSummary = validSections.map(s =>
1109
+ `${s.id}: ${s.files?.slice(0, 5).join(', ')}${s.files?.length > 5 ? '...' : ''}`
1110
+ ).join('\n')
1111
+
1112
+ const integration = await agent(
1113
+ `${SHARED}
1114
+
1115
+ ## Tarefa: Agente Integrador
1116
+
1117
+ As seções do storefront foram implementadas em paralelo. Agora integre tudo.
1118
+
1119
+ ### Seções implementadas:
1120
+ ${sectionSummary}
1121
+
1122
+ ### O que fazer:
1123
+
1124
+ 1. **Leia todos os arquivos em ${TARGET}** e identifique:
1125
+ - Imports quebrados ou inconsistentes
1126
+ - Exports que faltam nos arquivos de índice
1127
+ - Tipos compartilhados que precisam ser centralizados
1128
+
1129
+ 2. **Atualize app/layout.tsx** para incluir:
1130
+ - CartProvider wrapping tudo
1131
+ - ToastProvider
1132
+ - Header e Footer reais (não placeholder)
1133
+ - Fonts do Next.js (Geist ou Inter)
1134
+
1135
+ 3. **Atualize next.config.ts** com:
1136
+ - domains de imagem (unbox CDN: images.unbox.com.br ou similar)
1137
+ - redirects se necessário
1138
+
1139
+ 4. **Crie/atualize index de componentes** (barrel exports) se necessário
1140
+
1141
+ 5. **Verifique middleware.ts** — deve proteger /conta/pedidos e /conta/assinaturas
1142
+
1143
+ 6. **Resolva conflitos** entre seções (ex: se dois agentes criaram o mesmo componente)
1144
+
1145
+ 7. **Crie tsconfig paths** corretos para todos os @/ imports
1146
+
1147
+ 8. **Verifique app/globals.css** — deve ter @import "tailwindcss" e variáveis CSS base
1148
+
1149
+ Leia os arquivos existentes antes de editar. Use Edit, não Write, para modificações parciais.`,
1150
+ { label: 'integrate', effort: 'high' },
1151
+ )
1152
+
1153
+ log(`Integração concluída.`)
1154
+
1155
+ // ═══════════════════════════════════════════════════════════════════════════════
1156
+ // FASE 4 — REVIEW
1157
+ // ═══════════════════════════════════════════════════════════════════════════════
1158
+ phase('Review')
1159
+ log('Verificando regras de ouro e qualidade...')
1160
+
1161
+ const review = await agent(
1162
+ `${SHARED}
1163
+
1164
+ ## Tarefa: Agente Revisor
1165
+
1166
+ Faça uma revisão de segurança e qualidade do storefront gerado em ${TARGET}.
1167
+
1168
+ ### Checklist das Regras de Ouro:
1169
+
1170
+ 1. **UNBOX_API_KEY**: grep por "UNBOX_API_KEY" em arquivos com "use client" → VIOLAÇÃO se encontrar
1171
+ 2. **NEXT_PUBLIC_**: grep por "NEXT_PUBLIC_UNBOX" → VIOLAÇÃO
1172
+ 3. **placeOrder idempotência**: verificar se há lock por cartId na API route de checkout
1173
+ 4. **customerOTPRequest**: confirmar que só é chamado em Route Handlers (nunca client)
1174
+ 5. **Token de cliente**: confirmar que UnboxCustomerClient só é instanciado com customerToken
1175
+ 6. **displayStatus**: grep por "displayStatus(" → se encontrar, é VIOLAÇÃO
1176
+ 7. **trackingUrl**: grep por "trackingUrl" em contexto cliente → VIOLAÇÃO (use trackingCode)
1177
+ 8. **asChild**: grep por 'asChild' → VIOLAÇÃO (shadcn/ui usa Base UI, prop render)
1178
+ 9. **totalItemQuantity**: grep → VIOLAÇÃO no contexto customer
1179
+ 10. **recurringItemsFrequencyId**: grep → VIOLAÇÃO
1180
+
1181
+ ### Checklist de qualidade:
1182
+ - Todo Server Component que busca dados tem \`export const revalidate\` ou cache configurado
1183
+ - Páginas de catálogo/PDP têm generateMetadata
1184
+ - Há loading.tsx para rotas lentas
1185
+ - CartContext não vaza para componentes server
1186
+ - Formulários têm validação de campos obrigatórios do AddressInput
1187
+ - Erros são tratados com try/catch e exibidos via friendlyError()
1188
+
1189
+ ### Execute os greps e leia os arquivos críticos, então retorne o resultado estruturado.`,
1190
+ { label: 'review', schema: REVIEW_SCHEMA, effort: 'high' },
1191
+ )
1192
+
1193
+ if (review?.violations?.length) {
1194
+ log(`⚠️ ${review.violations.length} violação(ões) encontrada(s):`)
1195
+ review.violations.forEach(v => log(` ❌ [${v.rule}] ${v.file}: ${v.fix}`))
1196
+ } else {
1197
+ log('✅ Nenhuma violação encontrada')
1198
+ }
1199
+
1200
+ log(review?.summary ?? '')
1201
+
1202
+ return {
1203
+ sections: validSections,
1204
+ approved: review?.approved ?? false,
1205
+ violations: review?.violations ?? [],
1206
+ summary: review?.summary ?? '',
1207
+ }