@vitrine-kit/vitrine 0.4.1 → 0.5.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/README.md +1 -1
  2. package/dist/index.js +114 -44
  3. package/kit/registry/_index.json +70 -9
  4. package/kit/registry/accounts/docs/accounts.md +20 -0
  5. package/kit/registry/accounts/feature.json +55 -0
  6. package/kit/registry/accounts/files/app/(frontend)/account/forgot-password/page.tsx +24 -0
  7. package/kit/registry/accounts/files/app/(frontend)/account/login/page.tsx +29 -0
  8. package/kit/registry/accounts/files/app/(frontend)/account/orders/page.tsx +90 -0
  9. package/kit/registry/accounts/files/app/(frontend)/account/page.tsx +86 -0
  10. package/kit/registry/accounts/files/app/(frontend)/account/register/page.tsx +24 -0
  11. package/kit/registry/accounts/files/app/(frontend)/account/reset-password/page.tsx +25 -0
  12. package/kit/registry/accounts/files/app/api/account/orders/route.ts +39 -0
  13. package/kit/registry/accounts/files/components/accounts/AccountLink.tsx +15 -0
  14. package/kit/registry/accounts/files/components/accounts/ForgotPasswordForm.tsx +90 -0
  15. package/kit/registry/accounts/files/components/accounts/LoginForm.tsx +84 -0
  16. package/kit/registry/accounts/files/components/accounts/LogoutButton.tsx +31 -0
  17. package/kit/registry/accounts/files/components/accounts/RegisterForm.tsx +108 -0
  18. package/kit/registry/accounts/files/components/accounts/ResetPasswordForm.tsx +84 -0
  19. package/kit/registry/accounts/files/lib/accounts/blueprint.ts +47 -0
  20. package/kit/registry/accounts/files/lib/accounts/register.ts +6 -0
  21. package/kit/registry/accounts/files/lib/accounts/session.ts +18 -0
  22. package/kit/registry/cart/feature.json +1 -1
  23. package/kit/registry/cart/files/app/api/cart/route.ts +101 -11
  24. package/kit/registry/cart/files/components/cart/AddToCart.tsx +142 -15
  25. package/kit/registry/cart/files/components/cart/CartIndicator.tsx +27 -2
  26. package/kit/registry/cart/files/components/cart/CartLineItem.tsx +71 -13
  27. package/kit/registry/cart/files/components/cart/CartView.tsx +21 -1
  28. package/kit/registry/catalog/feature.json +1 -1
  29. package/kit/registry/catalog/files/components/catalog/ProductCard.tsx +5 -1
  30. package/kit/registry/catalog/files/lib/catalog/data.ts +73 -5
  31. package/kit/registry/checkout/docs/checkout.md +19 -1
  32. package/kit/registry/checkout/feature.json +1 -1
  33. package/kit/registry/checkout/files/app/(frontend)/order/success/page.tsx +49 -0
  34. package/kit/registry/checkout/files/app/api/checkout/route.ts +25 -4
  35. package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +5 -3
  36. package/kit/registry/checkout/files/lib/checkout/fulfill.ts +22 -0
  37. package/kit/registry/checkout/files/lib/checkout/notify.ts +52 -0
  38. package/kit/registry/checkout-paddle/feature.json +1 -1
  39. package/kit/registry/checkout-paddle/files/app/api/webhooks/paddle/route.ts +9 -2
  40. package/kit/registry/checkout-stripe/feature.json +1 -1
  41. package/kit/registry/checkout-stripe/files/app/api/webhooks/stripe/route.ts +9 -2
  42. package/kit/registry/checkout-yookassa/feature.json +1 -1
  43. package/kit/registry/checkout-yookassa/files/app/api/webhooks/yookassa/route.ts +9 -2
  44. package/kit/registry/email/docs/email.md +19 -0
  45. package/kit/registry/email/feature.json +60 -0
  46. package/kit/registry/email/files/lib/email/adapter.ts +89 -0
  47. package/kit/registry/filters/docs/filters.md +13 -0
  48. package/kit/registry/filters/feature.json +45 -0
  49. package/kit/registry/filters/files/components/filters/CatalogToolbar.tsx +123 -0
  50. package/kit/registry/filters/files/lib/filters/register.ts +6 -0
  51. package/kit/registry/i18n/docs/i18n.md +24 -0
  52. package/kit/registry/i18n/feature.json +47 -0
  53. package/kit/registry/i18n/files/components/i18n/LocaleSwitcher.tsx +64 -0
  54. package/kit/registry/i18n/files/lib/i18n/dictionary.ts +52 -0
  55. package/kit/registry/i18n/files/lib/i18n/locale.ts +11 -0
  56. package/kit/registry/i18n/files/lib/i18n/register.ts +6 -0
  57. package/kit/registry/i18n/files/lib/i18n/t.ts +1 -0
  58. package/kit/registry/i18n/files/lib/i18n/useChromeLabel.ts +28 -0
  59. package/kit/registry/i18n/files/middleware.ts +51 -0
  60. package/kit/registry/product-page/feature.json +1 -1
  61. package/kit/registry/product-page/files/components/product/ProductGallery.tsx +23 -10
  62. package/kit/registry/product-page/files/components/product/ProductView.tsx +8 -4
  63. package/kit/registry/reviews/docs/reviews.md +11 -0
  64. package/kit/registry/reviews/feature.json +55 -0
  65. package/kit/registry/reviews/files/app/api/reviews/route.ts +55 -0
  66. package/kit/registry/reviews/files/components/reviews/ReviewList.tsx +119 -0
  67. package/kit/registry/reviews/files/lib/reviews/blueprint.ts +13 -0
  68. package/kit/registry/reviews/files/lib/reviews/register.ts +6 -0
  69. package/kit/registry/reviews/files/lib/reviews/types.ts +22 -0
  70. package/kit/registry/search/docs/search.md +12 -0
  71. package/kit/registry/search/feature.json +49 -0
  72. package/kit/registry/search/files/app/(frontend)/search/page.tsx +34 -0
  73. package/kit/registry/search/files/components/search/SearchForm.tsx +32 -0
  74. package/kit/registry/search/files/components/search/SearchResults.tsx +87 -0
  75. package/kit/registry/search/files/lib/search/data.ts +8 -0
  76. package/kit/registry/search/files/lib/search/register.ts +8 -0
  77. package/kit/registry/seo/docs/seo.md +4 -1
  78. package/kit/registry/seo/feature.json +5 -1
  79. package/kit/registry/seo/files/app/robots.ts +13 -0
  80. package/kit/registry/seo/files/app/sitemap.ts +27 -0
  81. package/kit/registry/stubs/route-glue.d.ts +71 -0
  82. package/kit/registry/tsconfig.json +7 -2
  83. package/kit/registry/wishlist/docs/wishlist.md +11 -0
  84. package/kit/registry/wishlist/feature.json +55 -0
  85. package/kit/registry/wishlist/files/app/(frontend)/wishlist/page.tsx +16 -0
  86. package/kit/registry/wishlist/files/components/wishlist/WishlistButton.tsx +37 -0
  87. package/kit/registry/wishlist/files/components/wishlist/WishlistIndicator.tsx +30 -0
  88. package/kit/registry/wishlist/files/components/wishlist/WishlistView.tsx +73 -0
  89. package/kit/registry/wishlist/files/lib/wishlist/register.ts +8 -0
  90. package/kit/registry/wishlist/files/lib/wishlist/storage.ts +30 -0
  91. package/kit/templates/backend-payload/files/Dockerfile +3 -0
  92. package/kit/templates/backend-payload/files/docker-compose.yml +8 -0
  93. package/kit/templates/backend-payload/files/lib/adapter/payload-catalog.ts +51 -17
  94. package/kit/templates/backend-payload/files/lib/adapter/payload-commerce.ts +18 -0
  95. package/kit/templates/backend-payload/files/lib/adapter/product-query.ts +126 -0
  96. package/kit/templates/backend-payload/files/lib/seed/admin.ts +16 -6
  97. package/kit/templates/backend-payload/files/lib/seed/demo.ts +74 -28
  98. package/kit/templates/backend-payload/files/lib/seed/guards.ts +17 -4
  99. package/kit/templates/backend-payload/files/lib/seed/locales.ts +53 -0
  100. package/kit/templates/backend-payload/files/lib/seed/run.ts +92 -16
  101. package/kit/templates/backend-payload/files/next.config.mjs +34 -0
  102. package/kit/templates/backend-payload/files/payload.config.ts +33 -3
  103. package/kit/templates/backend-payload/files/seed-assets/placeholder-1b.svg +6 -0
  104. package/kit/templates/backend-payload/files/seed-assets/placeholder-2b.svg +6 -0
  105. package/kit/templates/backend-vendure/files/Dockerfile +1 -1
  106. package/kit/templates/backend-vendure/files/docker-compose.yml +2 -1
  107. package/kit/templates/backend-vendure/files/lib/adapter/product-query.ts +126 -0
  108. package/kit/templates/backend-vendure/files/lib/adapter/vendure-catalog.ts +18 -11
  109. package/kit/templates/backend-vendure/files/vendure-config.ts +38 -1
  110. package/kit/templates/base/files/.claude/commands/update.md +1 -1
  111. package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +57 -5
  112. package/kit/templates/base/files/app/(frontend)/layout.tsx +13 -7
  113. package/kit/templates/base/files/app/(frontend)/not-found.tsx +26 -0
  114. package/kit/templates/base/files/app/(frontend)/page.tsx +84 -6
  115. package/kit/templates/base/files/app/(frontend)/products/[slug]/page.tsx +5 -2
  116. package/kit/templates/base/files/app/globals.css +13 -0
  117. package/kit/templates/base/files/components/SlotsProvider.tsx +19 -0
  118. package/kit/templates/base/files/components/layout/SiteFooter.tsx +6 -1
  119. package/kit/templates/base/files/components/layout/SiteHeader.tsx +8 -2
  120. package/kit/templates/base/files/lib/i18n/dictionary.ts +36 -0
  121. package/kit/templates/base/files/lib/i18n/locale.ts +11 -0
  122. package/kit/templates/base/files/lib/i18n/useChromeLabel.ts +28 -0
  123. package/kit/templates/base/files/lib/site.ts +3 -0
  124. package/kit/templates/base/files/next.config.mjs +34 -0
  125. package/package.json +2 -2
package/README.md CHANGED
@@ -31,4 +31,4 @@ with flags is in the generated project's `CLAUDE.md`.
31
31
 
32
32
  At its core is the **feature install primitive**, shared by `init` and `add` (guarantee: "added by
33
33
  the wizard" ≡ "added later"). `add`/`update` work offline from the `~/.vitrine` cache; the network
34
- is only needed for `kit update`.
34
+ is only needed for `kit update` (public npm — `@vitrine-kit/vitrine`).
package/dist/index.js CHANGED
@@ -363,9 +363,13 @@ function activePaymentProvider(features) {
363
363
  }
364
364
  function renderIntegrationsRegion(features) {
365
365
  const payments = activePaymentProvider(features);
366
- if (!payments) return " integrations: {},";
366
+ const email = features.some((f) => f.name === "email") ? "smtp" : void 0;
367
+ const lines = [];
368
+ if (payments) lines.push(` payments: ${JSON.stringify(payments)},`);
369
+ if (email) lines.push(` email: ${JSON.stringify(email)},`);
370
+ if (lines.length === 0) return " integrations: {},";
367
371
  return ` integrations: {
368
- payments: ${JSON.stringify(payments)},
372
+ ${lines.join("\n")}
369
373
  },`;
370
374
  }
371
375
  function assertNoPascalCollisions(features) {
@@ -1188,15 +1192,16 @@ function diffFeatureCmd(name, registryRoot, projectRoot) {
1188
1192
  }
1189
1193
 
1190
1194
  // src/init.ts
1195
+ import { randomBytes } from "crypto";
1191
1196
  import { readdirSync as readdirSync4 } from "fs";
1192
1197
  import { join as join13 } from "path";
1193
1198
 
1194
1199
  // src/kit-versions.generated.ts
1195
- var KIT_VERSION = "0.4.1";
1200
+ var KIT_VERSION = "0.5.0";
1196
1201
  var CONTRACTS_VERSION = "1.0.0";
1197
1202
  var CONTRACTS_RANGE = "^1.0.0";
1198
- var CORE_RANGE = "^0.2.0";
1199
- var BLUEPRINT_RANGE = "^0.2.0";
1203
+ var CORE_RANGE = "^0.3.0";
1204
+ var BLUEPRINT_RANGE = "^0.3.0";
1200
1205
 
1201
1206
  // src/kit.ts
1202
1207
  var REACT_RANGE = "^18.3.1";
@@ -1230,7 +1235,7 @@ var PAYMENT_PROVIDER_FEATURES = [
1230
1235
  "checkout-yookassa"
1231
1236
  ];
1232
1237
  function suggestFeatures(tier, registry, backend = defaultBackend(tier)) {
1233
- const core = ["catalog", "product-page", "seo"];
1238
+ const core = ["catalog", "product-page", "seo", "search", "filters"];
1234
1239
  const shop = backend === "vendure" ? ["cart"] : ["cart", "checkout-stripe"];
1235
1240
  const desired = tier === "catalog" ? core : [...core, ...shop];
1236
1241
  return desired.filter((name) => registry.hasFeature(name));
@@ -1350,10 +1355,14 @@ function clientEnvExample(backend) {
1350
1355
  "# Payload secret (required; generate a random one for prod).",
1351
1356
  "PAYLOAD_SECRET=",
1352
1357
  "",
1353
- "# Dev admin (created only in dev when the DB is empty; the password is printed to the console).",
1358
+ "# Dev / bootstrap admin (created when the users collection is empty in dev,",
1359
+ "# or in production when SEED_ON_BOOT=1 \u2014 password printed to the console).",
1354
1360
  "DEV_ADMIN_EMAIL=",
1355
1361
  "DEV_ADMIN_PASSWORD=",
1356
1362
  "",
1363
+ "# Docker compose: seed demo catalog + admin on first boot (default 1 in compose).",
1364
+ "# SEED_ON_BOOT=1",
1365
+ "",
1357
1366
  "# Site base URL (canonical, OG).",
1358
1367
  "NEXT_PUBLIC_SITE_URL=http://localhost:3000",
1359
1368
  "",
@@ -1362,6 +1371,18 @@ function clientEnvExample(backend) {
1362
1371
  ""
1363
1372
  ].join("\n");
1364
1373
  }
1374
+ function clientEnvFile(backend) {
1375
+ const example = clientEnvExample(backend);
1376
+ if (backend === "payload") {
1377
+ const secret = randomBytes(24).toString("base64url");
1378
+ return example.replace(/^PAYLOAD_SECRET=$/m, `PAYLOAD_SECRET=${secret}`);
1379
+ }
1380
+ if (backend === "vendure") {
1381
+ const secret = randomBytes(24).toString("base64url");
1382
+ return example.replace(/^VENDURE_COOKIE_SECRET=$/m, `VENDURE_COOKIE_SECRET=${secret}`);
1383
+ }
1384
+ return example;
1385
+ }
1365
1386
  function clientReadme(name, backend, tier) {
1366
1387
  const run = backend === "vendure" ? [
1367
1388
  "```bash",
@@ -1377,15 +1398,16 @@ function clientReadme(name, backend, tier) {
1377
1398
  ].join("\n") : [
1378
1399
  "```bash",
1379
1400
  "pnpm install",
1380
- "cp .env.example .env",
1381
1401
  "pnpm dev",
1382
1402
  "```",
1383
1403
  "",
1384
1404
  "- Storefront: http://localhost:3000",
1385
1405
  "- Admin: http://localhost:3000/admin",
1386
1406
  "",
1387
- "Without Postgres, dev starts a built-in SQLite (`.vitrine/dev.sqlite`), seeds a",
1388
- "demo catalog (5 products, 2 categories) and creates a dev admin (login/password printed",
1407
+ "`vitrine init` writes a starter `.env` (including `PAYLOAD_SECRET`). Copy from",
1408
+ "`.env.example` only if you need a fresh file. Without Postgres, dev starts a built-in",
1409
+ "SQLite (`.vitrine/dev.sqlite`), seeds a demo catalog (5 products, 2 categories,",
1410
+ "size/color variants, gallery images) and creates a dev admin (login/password printed",
1389
1411
  "to the console once). Disable the fallback in dev too with `VITRINE_DB_STRICT=1`."
1390
1412
  ].join("\n");
1391
1413
  const deploySecret = backend === "vendure" ? "export VENDURE_COOKIE_SECRET=... # Vendure cookie secret" : "export PAYLOAD_SECRET=... # random Payload secret";
@@ -1425,7 +1447,7 @@ vitrine design apply # style the new feature
1425
1447
  ## 5. Updates and checks
1426
1448
 
1427
1449
  \`\`\`bash
1428
- vitrine kit update # update the local registry/templates cache from GitHub
1450
+ vitrine kit update # update the local registry/templates cache from npm
1429
1451
  vitrine diff <feature> # preview a feature update
1430
1452
  vitrine update [feature] # 3-way merge of the new feature version (base = your snapshot)
1431
1453
  vitrine doctor # consistency: vitrine.json \u2194 files \u2194 packages \u2194 env
@@ -1470,6 +1492,7 @@ function scaffoldBase(opts) {
1470
1492
  `import type { SiteConfig } from '@vitrine-kit/contracts';
1471
1493
 
1472
1494
  export const siteConfig: SiteConfig = {
1495
+ name: ${JSON.stringify(name)},
1473
1496
  backend: ${JSON.stringify(backend)},
1474
1497
  tier: ${JSON.stringify(tier)},
1475
1498
  // vitrine:features:start
@@ -1510,7 +1533,7 @@ _No features installed yet._
1510
1533
  | \`vitrine diff <feature>\` | Preview an update (without writing) | before \`update\` | \`--registry\` |
1511
1534
  | \`vitrine doctor\` | Consistency: \`vitrine.json\` \u2194 files \u2194 packages \u2194 env | after edits, when in doubt | \`--registry\` |
1512
1535
  | \`vitrine design apply\` | Apply the design from \`/design\` to tokens (via Claude Code) | after \`add\` or a rebrand | \`--bin\`, \`--dry-run\` |
1513
- | \`vitrine kit update\` | Update the local registry/templates cache from GitHub | before updating features | \`--from\`, \`--version\`, \`--channel\` |
1536
+ | \`vitrine kit update\` | Update the local registry/templates cache from npm | before updating features | \`[version]\`, \`--from\`, \`--channel\` |
1514
1537
  | \`vitrine kit status\` | Cache version vs the CLI's expected one | diagnostics | \u2014 |
1515
1538
  | \`vitrine self-update\` | Update the CLI itself | rarely | \`--dry-run\` |
1516
1539
 
@@ -1558,6 +1581,7 @@ The step is idempotent: re-running converges and doesn't accumulate cruft.
1558
1581
  writeText(join13(root, "lib", "blueprint.ts"), renderBlueprintFile([]));
1559
1582
  writeText(join13(root, "theme", "client.css"), renderNeutralTheme());
1560
1583
  writeText(join13(root, ".env.example"), clientEnvExample(backend));
1584
+ writeText(join13(root, ".env"), clientEnvFile(backend));
1561
1585
  writeText(join13(root, "package.json"), `${JSON.stringify(clientPackageJson(name, backend), null, 2)}
1562
1586
  `);
1563
1587
  }
@@ -1572,43 +1596,89 @@ function initProject(opts) {
1572
1596
 
1573
1597
  // src/kit-update.ts
1574
1598
  import { spawnSync as spawnSync2 } from "child_process";
1575
- import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4, statSync as statSync2 } from "fs";
1599
+ import { existsSync as existsSync9, mkdtempSync, rmSync as rmSync4 } from "fs";
1576
1600
  import { tmpdir } from "os";
1577
- import { join as join14, resolve as resolve6 } from "path";
1578
- var REPO = "vitrine-kit/vitrine";
1579
- function hasBin(bin) {
1580
- const probe = process.platform === "win32" ? "where" : "which";
1581
- return spawnSync2(probe, [bin], { stdio: "ignore" }).status === 0;
1601
+ import { dirname as dirname7, join as join14, resolve as resolve6 } from "path";
1602
+ var NPM_PKG = "@vitrine-kit/vitrine";
1603
+ function resolveNpmCli() {
1604
+ const candidates = [
1605
+ join14(dirname7(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
1606
+ join14(dirname7(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
1607
+ ];
1608
+ return candidates.find((p2) => existsSync9(p2)) ?? null;
1582
1609
  }
1583
- function acquireFromGh(tmp, version) {
1584
- if (!hasBin("gh")) {
1585
- throw new Error("[vitrine] network update needs gh (GitHub CLI), or pass --from <dir>");
1586
- }
1587
- if (!hasBin("tar")) {
1588
- throw new Error("[vitrine] unpacking needs tar, or pass --from <dir>");
1610
+ function hasNpm() {
1611
+ if (resolveNpmCli()) return true;
1612
+ const probe = process.platform === "win32" ? "where" : "which";
1613
+ return spawnSync2(probe, ["npm"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
1614
+ }
1615
+ function runNpm(args, opts = {}) {
1616
+ const cli = resolveNpmCli();
1617
+ if (cli) {
1618
+ return spawnSync2(process.execPath, [cli, ...args], {
1619
+ encoding: opts.encoding,
1620
+ stdio: opts.stdio ?? "inherit"
1621
+ });
1589
1622
  }
1590
- const dl = spawnSync2(
1591
- "gh",
1592
- ["release", "download", ...version ? [version] : [], "--repo", REPO, "--archive=tar.gz", "--dir", tmp, "--clobber"],
1593
- { stdio: "inherit" }
1623
+ return spawnSync2("npm", args, {
1624
+ encoding: opts.encoding,
1625
+ stdio: opts.stdio ?? "inherit",
1626
+ shell: process.platform === "win32"
1627
+ });
1628
+ }
1629
+ function resolveKitNpmSpec(version) {
1630
+ if (!version || version === "latest") return `${NPM_PKG}@latest`;
1631
+ if (version.startsWith(`${NPM_PKG}@`)) return version;
1632
+ if (version.startsWith("@") && version.includes("/")) return version;
1633
+ const semver = version.replace(/^v/, "");
1634
+ return `${NPM_PKG}@${semver}`;
1635
+ }
1636
+ function acquireFromNpm(tmp, version) {
1637
+ if (!hasNpm()) {
1638
+ throw new Error("[vitrine] network update needs npm, or pass --from <dir>");
1639
+ }
1640
+ const spec = resolveKitNpmSpec(version);
1641
+ if (!/^[@A-Za-z0-9_./-]+$/.test(spec)) {
1642
+ throw new Error(`[vitrine] invalid kit version/spec: ${version}`);
1643
+ }
1644
+ const install = runNpm(
1645
+ [
1646
+ "install",
1647
+ spec,
1648
+ "--prefix",
1649
+ tmp,
1650
+ "--no-save",
1651
+ "--ignore-scripts",
1652
+ "--no-package-lock",
1653
+ "--registry",
1654
+ "https://registry.npmjs.org/"
1655
+ ],
1656
+ { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
1594
1657
  );
1595
- if (dl.status !== 0) {
1596
- throw new Error("[vitrine] gh release download failed (check access/auth: gh auth status, and gh auth login if needed)");
1597
- }
1598
- const tarball = readdirSync5(tmp).find((f) => f.endsWith(".tar.gz"));
1599
- if (!tarball) throw new Error("[vitrine] release tarball not found after download");
1600
- if (spawnSync2("tar", ["-xzf", join14(tmp, tarball), "-C", tmp], { stdio: "inherit" }).status !== 0) {
1601
- throw new Error("[vitrine] tarball extraction failed");
1658
+ if (install.status !== 0) {
1659
+ const err = (install.stderr || install.stdout || install.error?.message || "").toString().trim();
1660
+ throw new Error(
1661
+ `[vitrine] npm install ${spec} failed${err ? `: ${err}` : ""} \u2014 check https://www.npmjs.com/package/@vitrine-kit/vitrine`
1662
+ );
1602
1663
  }
1603
- const root = readdirSync5(tmp).map((f) => join14(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync9(join14(p2, "registry", "_index.json")));
1604
- if (!root) throw new Error("[vitrine] the unpacked tarball has no registry/_index.json");
1605
- return root;
1664
+ const kitRoot = join14(tmp, "node_modules", "@vitrine-kit", "vitrine", "kit");
1665
+ if (existsSync9(join14(kitRoot, "registry", "_index.json"))) return kitRoot;
1666
+ throw new Error(
1667
+ "[vitrine] installed package has no kit/registry/_index.json \u2014 reinstall @vitrine-kit/vitrine or pass --from <dir>"
1668
+ );
1606
1669
  }
1607
1670
  function warnOnVersionMismatch(root, requested) {
1608
1671
  if (!requested) return;
1609
1672
  const tagSemver = requested.match(/^@vitrine-kit\/vitrine@(.+)$/)?.[1] ?? requested.match(/^v?(\d+\.\d+\.\d+\S*)$/)?.[1];
1610
- const pkgFile = join14(root, "packages", "cli", "package.json");
1611
- if (!tagSemver || !exists(pkgFile)) return;
1673
+ if (!tagSemver || tagSemver === "latest") return;
1674
+ const pkgCandidates = [
1675
+ join14(root, "..", "package.json"),
1676
+ // …/vitrine/kit → …/vitrine/package.json
1677
+ join14(root, "packages", "cli", "package.json"),
1678
+ join14(root, "package.json")
1679
+ ];
1680
+ const pkgFile = pkgCandidates.find((p2) => exists(p2));
1681
+ if (!pkgFile) return;
1612
1682
  const version = readJson(pkgFile).version;
1613
1683
  if (version && version !== tagSemver) {
1614
1684
  console.warn(`[vitrine] requested "${requested}" but the downloaded kit is @vitrine-kit/vitrine@${version}`);
@@ -1620,7 +1690,7 @@ function kitUpdate(opts = {}) {
1620
1690
  }
1621
1691
  const tmp = mkdtempSync(join14(tmpdir(), "vitrine-kit-"));
1622
1692
  try {
1623
- const source = acquireFromGh(tmp, opts.version);
1693
+ const source = acquireFromNpm(tmp, opts.version);
1624
1694
  warnOnVersionMismatch(source, opts.version);
1625
1695
  return populateCache(source, { home: opts.home, channel: opts.channel ?? "stable" });
1626
1696
  } finally {
@@ -1646,7 +1716,7 @@ function selfUpdate(opts = {}) {
1646
1716
  console.log(`[vitrine] npm ${args.join(" ")}`);
1647
1717
  return 0;
1648
1718
  }
1649
- const res = spawnSync2("npm", args, { stdio: "inherit" });
1719
+ const res = runNpm(args, { stdio: "inherit" });
1650
1720
  if (res.error) throw res.error;
1651
1721
  return res.status ?? 0;
1652
1722
  }
@@ -1690,8 +1760,8 @@ design.command("apply").description("Apply the design from /design to the tokens
1690
1760
  if (code !== 0) process.exit(code);
1691
1761
  });
1692
1762
  var kit = program.command("kit").description("Local tooling (the ~/.vitrine registry cache)");
1693
- kit.command("update").description("Update the local registry/templates cache from GitHub (or --from <dir>)").option("--from <path>", "local kit tree (clone / unpacked tarball) instead of the network").option("--version <tag>", "a specific release").option("--channel <channel>", "stable | main", "stable").action((opts) => {
1694
- const res = kitUpdate({ from: opts.from, version: opts.version, channel: opts.channel });
1763
+ kit.command("update").description("Update the local registry/templates cache from npm (or --from <dir>)").argument("[version]", "npm version or dist-tag (default: latest)").option("--from <path>", "local kit tree (clone / unpacked package) instead of the network").option("--channel <channel>", "stable | main", "stable").action((version, opts) => {
1764
+ const res = kitUpdate({ from: opts.from, version, channel: opts.channel });
1695
1765
  console.log(`Cache updated to kit ${res.kitVersion}.`);
1696
1766
  console.log(formatChangelog(res.changelog));
1697
1767
  });
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "$schema": "../schemas/registry-index.schema.json",
3
- "kitVersion": "0.4.1",
3
+ "kitVersion": "0.5.0",
4
4
  "contracts": "1.0.0",
5
5
  "features": {
6
6
  "catalog": {
7
7
  "title": "Product catalog",
8
- "kitVersion": "0.4.1",
8
+ "kitVersion": "0.5.0",
9
9
  "tier": [
10
10
  "catalog",
11
11
  "simple-store",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "product-page": {
16
16
  "title": "Product page",
17
- "kitVersion": "0.4.1",
17
+ "kitVersion": "0.5.0",
18
18
  "tier": [
19
19
  "catalog",
20
20
  "simple-store",
@@ -23,7 +23,52 @@
23
23
  },
24
24
  "seo": {
25
25
  "title": "SEO — metadata and JSON-LD",
26
- "kitVersion": "0.4.1",
26
+ "kitVersion": "0.5.0",
27
+ "tier": [
28
+ "catalog",
29
+ "simple-store",
30
+ "full-store"
31
+ ]
32
+ },
33
+ "search": {
34
+ "title": "Product search",
35
+ "kitVersion": "0.5.0",
36
+ "tier": [
37
+ "catalog",
38
+ "simple-store",
39
+ "full-store"
40
+ ]
41
+ },
42
+ "filters": {
43
+ "title": "Catalog sort and filters",
44
+ "kitVersion": "0.5.0",
45
+ "tier": [
46
+ "catalog",
47
+ "simple-store",
48
+ "full-store"
49
+ ]
50
+ },
51
+ "wishlist": {
52
+ "title": "Wishlist",
53
+ "kitVersion": "0.5.0",
54
+ "tier": [
55
+ "catalog",
56
+ "simple-store",
57
+ "full-store"
58
+ ]
59
+ },
60
+ "reviews": {
61
+ "title": "Product reviews",
62
+ "kitVersion": "0.5.0",
63
+ "tier": [
64
+ "catalog",
65
+ "simple-store",
66
+ "full-store"
67
+ ]
68
+ },
69
+ "i18n": {
70
+ "title": "Locale switcher and path prefixes",
71
+ "kitVersion": "0.5.0",
27
72
  "tier": [
28
73
  "catalog",
29
74
  "simple-store",
@@ -32,7 +77,7 @@
32
77
  },
33
78
  "cart": {
34
79
  "title": "Cart",
35
- "kitVersion": "0.4.1",
80
+ "kitVersion": "0.5.0",
36
81
  "tier": [
37
82
  "simple-store",
38
83
  "full-store"
@@ -40,7 +85,23 @@
40
85
  },
41
86
  "checkout": {
42
87
  "title": "Checkout",
43
- "kitVersion": "0.4.1",
88
+ "kitVersion": "0.5.0",
89
+ "tier": [
90
+ "simple-store",
91
+ "full-store"
92
+ ]
93
+ },
94
+ "accounts": {
95
+ "title": "Accounts — login and order history",
96
+ "kitVersion": "0.5.0",
97
+ "tier": [
98
+ "simple-store",
99
+ "full-store"
100
+ ]
101
+ },
102
+ "email": {
103
+ "title": "Transactional email",
104
+ "kitVersion": "0.5.0",
44
105
  "tier": [
45
106
  "simple-store",
46
107
  "full-store"
@@ -48,7 +109,7 @@
48
109
  },
49
110
  "checkout-stripe": {
50
111
  "title": "Payment — Stripe",
51
- "kitVersion": "0.4.1",
112
+ "kitVersion": "0.5.0",
52
113
  "tier": [
53
114
  "simple-store",
54
115
  "full-store"
@@ -56,7 +117,7 @@
56
117
  },
57
118
  "checkout-paddle": {
58
119
  "title": "Payment — Paddle",
59
- "kitVersion": "0.4.1",
120
+ "kitVersion": "0.5.0",
60
121
  "tier": [
61
122
  "simple-store",
62
123
  "full-store"
@@ -64,7 +125,7 @@
64
125
  },
65
126
  "checkout-yookassa": {
66
127
  "title": "Payment — YooKassa",
67
- "kitVersion": "0.4.1",
128
+ "kitVersion": "0.5.0",
68
129
  "tier": [
69
130
  "simple-store",
70
131
  "full-store"
@@ -0,0 +1,20 @@
1
+ # Feature: accounts
2
+
3
+ Customer accounts (Payload `customers` auth collection) plus guest order lookup by email.
4
+
5
+ ## Modules
6
+ - `customers` collection — added via blueprint `addCollection` (email/password auth)
7
+ - `orders.customer` — optional relationship field (set on webhook fulfill when email matches)
8
+ - `/account` — signed-in dashboard or sign-in / register / guest lookup
9
+ - `/account/login`, `/account/register`
10
+ - `/account/forgot-password`, `/account/reset-password?token=`
11
+ - `/account/orders` — orders for the session email or `?email=` guest query
12
+ - `AccountLink` — header link
13
+
14
+ ## Auth
15
+ Uses Payload's built-in REST: `POST /api/customers`, `/login`, `/logout`,
16
+ `/forgot-password`, `/reset-password`. Session cookies are managed by Payload.
17
+ Forgot-password emails go through the Payload email adapter (`email` feature / console).
18
+
19
+ ## Guest lookup
20
+ Still available without an account — enter the checkout email on `/account`.
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "../../schemas/feature.schema.json",
3
+ "name": "accounts",
4
+ "title": "Accounts — login and order history",
5
+ "kitVersion": "0.5.0",
6
+ "requiresContracts": ">=1.0.0 <2.0.0",
7
+ "tier": [
8
+ "simple-store",
9
+ "full-store"
10
+ ],
11
+ "registryDependencies": [
12
+ "checkout"
13
+ ],
14
+ "corePackages": {
15
+ "@vitrine-kit/core": ">=0.1.0",
16
+ "@vitrine-kit/payload-blueprint": ">=0.2.0"
17
+ },
18
+ "npm": [],
19
+ "files": [
20
+ {
21
+ "from": "files/components/accounts/",
22
+ "to": "components/accounts/"
23
+ },
24
+ {
25
+ "from": "files/lib/accounts/",
26
+ "to": "lib/accounts/"
27
+ },
28
+ {
29
+ "from": "files/app/",
30
+ "to": "app/"
31
+ }
32
+ ],
33
+ "config": {
34
+ "set": {
35
+ "features.accounts": true
36
+ }
37
+ },
38
+ "slots": [
39
+ {
40
+ "slot": "global.header-actions",
41
+ "component": "AccountLink",
42
+ "order": 25
43
+ }
44
+ ],
45
+ "blueprint": {
46
+ "extend": "order",
47
+ "addFields": [
48
+ "customer"
49
+ ]
50
+ },
51
+ "env": [],
52
+ "claudeDoc": "docs/accounts.md",
53
+ "conflicts": [],
54
+ "removable": true
55
+ }
@@ -0,0 +1,24 @@
1
+ import type { Metadata } from 'next';
2
+ import { ForgotPasswordForm } from '@/components/accounts/ForgotPasswordForm';
3
+
4
+ export const metadata: Metadata = {
5
+ title: 'Forgot password',
6
+ robots: { index: false, follow: false },
7
+ };
8
+
9
+ export default function ForgotPasswordPage() {
10
+ return (
11
+ <div className="flex max-w-md flex-col gap-section">
12
+ <header className="flex flex-col gap-unit">
13
+ <h1 className="font-heading text-fg">Forgot password</h1>
14
+ <p className="text-muted-fg">
15
+ Enter your account email. We send a reset link when the address is registered.
16
+ </p>
17
+ </header>
18
+ <ForgotPasswordForm />
19
+ <a href="/account/login" className="w-fit text-fg underline underline-offset-2">
20
+ Back to sign in
21
+ </a>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,29 @@
1
+ import type { Metadata } from 'next';
2
+ import { LoginForm } from '@/components/accounts/LoginForm';
3
+
4
+ export const metadata: Metadata = {
5
+ title: 'Sign in',
6
+ robots: { index: false, follow: false },
7
+ };
8
+
9
+ export default function AccountLoginPage() {
10
+ return (
11
+ <div className="flex max-w-md flex-col gap-section">
12
+ <header className="flex flex-col gap-unit">
13
+ <h1 className="font-heading text-fg">Sign in</h1>
14
+ <p className="text-muted-fg">
15
+ No account yet?{' '}
16
+ <a href="/account/register" className="underline underline-offset-2">
17
+ Create one
18
+ </a>
19
+ </p>
20
+ </header>
21
+ <LoginForm />
22
+ <p className="text-sm text-muted-fg">
23
+ <a href="/account/forgot-password" className="underline underline-offset-2">
24
+ Forgot password?
25
+ </a>
26
+ </p>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,90 @@
1
+ import type { Metadata } from 'next';
2
+ import { getPayload } from 'payload';
3
+ import config from '@payload-config';
4
+ import { formatMoney } from '@/lib/cart/data';
5
+ import { getCustomer } from '@/lib/accounts/session';
6
+ import { LogoutButton } from '@/components/accounts/LogoutButton';
7
+
8
+ export const metadata: Metadata = {
9
+ title: 'Your orders',
10
+ robots: { index: false, follow: false },
11
+ };
12
+
13
+ interface PageProps {
14
+ searchParams: Promise<{ email?: string }>;
15
+ }
16
+
17
+ export default async function AccountOrdersPage({ searchParams }: PageProps) {
18
+ const customer = await getCustomer();
19
+ const { email: raw } = await searchParams;
20
+ const guestEmail = raw?.trim().toLowerCase() ?? '';
21
+ const email = customer?.email?.trim().toLowerCase() || guestEmail;
22
+
23
+ if (!email?.includes('@')) {
24
+ return (
25
+ <div className="flex flex-col gap-gutter">
26
+ <h1 className="font-heading text-fg">Your orders</h1>
27
+ <p className="text-muted-fg">Sign in or provide a valid email on the account page.</p>
28
+ <a href="/account" className="w-fit text-fg underline underline-offset-2">
29
+ Back
30
+ </a>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ const payload = await getPayload({ config });
36
+ const res = await payload.find({
37
+ collection: 'orders',
38
+ where: { email: { equals: email } },
39
+ sort: '-createdAt',
40
+ limit: 50,
41
+ overrideAccess: true,
42
+ });
43
+
44
+ const orders = res.docs as Array<{
45
+ id: string | number;
46
+ status?: string;
47
+ total?: number;
48
+ currency?: string;
49
+ createdAt?: string;
50
+ paymentRef?: string;
51
+ }>;
52
+
53
+ return (
54
+ <div className="flex flex-col gap-section">
55
+ <header className="flex flex-col gap-unit">
56
+ <h1 className="font-heading text-fg">Your orders</h1>
57
+ <p className="text-muted-fg">
58
+ Showing orders for <span className="text-fg">{email}</span>
59
+ {customer ? ' (signed in)' : ' (guest lookup)'}
60
+ </p>
61
+ </header>
62
+ {orders.length === 0 ? (
63
+ <p className="text-muted-fg">No orders found for that email.</p>
64
+ ) : (
65
+ <ul role="list" className="flex flex-col gap-gutter">
66
+ {orders.map((o) => (
67
+ <li key={String(o.id)} className="flex flex-col gap-unit border-b border-border py-unit">
68
+ <p className="text-fg">
69
+ Order #{String(o.id)} — {o.status ?? 'unknown'}
70
+ </p>
71
+ <p className="text-muted-fg">
72
+ {typeof o.total === 'number' ? formatMoney(o.total, o.currency ?? 'USD') : '—'}
73
+ {o.createdAt ? ` · ${new Date(o.createdAt).toLocaleString()}` : ''}
74
+ </p>
75
+ {o.paymentRef ? (
76
+ <p className="text-sm text-muted-fg">Ref: {o.paymentRef}</p>
77
+ ) : null}
78
+ </li>
79
+ ))}
80
+ </ul>
81
+ )}
82
+ <div className="flex flex-wrap gap-gutter">
83
+ <a href="/account" className="w-fit text-fg underline underline-offset-2">
84
+ Account
85
+ </a>
86
+ {customer ? <LogoutButton /> : null}
87
+ </div>
88
+ </div>
89
+ );
90
+ }