@vitrine-kit/vitrine 0.4.3 → 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 (117) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +114 -45
  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 +46 -8
  24. package/kit/registry/cart/files/components/cart/AddToCart.tsx +38 -3
  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 +9 -7
  28. package/kit/registry/catalog/feature.json +1 -1
  29. package/kit/registry/catalog/files/lib/catalog/data.ts +73 -5
  30. package/kit/registry/checkout/docs/checkout.md +19 -1
  31. package/kit/registry/checkout/feature.json +1 -1
  32. package/kit/registry/checkout/files/app/(frontend)/order/success/page.tsx +49 -0
  33. package/kit/registry/checkout/files/app/api/checkout/route.ts +14 -3
  34. package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +5 -3
  35. package/kit/registry/checkout/files/lib/checkout/fulfill.ts +22 -0
  36. package/kit/registry/checkout/files/lib/checkout/notify.ts +52 -0
  37. package/kit/registry/checkout-paddle/feature.json +1 -1
  38. package/kit/registry/checkout-stripe/feature.json +1 -1
  39. package/kit/registry/checkout-yookassa/feature.json +1 -1
  40. package/kit/registry/email/docs/email.md +19 -0
  41. package/kit/registry/email/feature.json +60 -0
  42. package/kit/registry/email/files/lib/email/adapter.ts +89 -0
  43. package/kit/registry/filters/docs/filters.md +13 -0
  44. package/kit/registry/filters/feature.json +45 -0
  45. package/kit/registry/filters/files/components/filters/CatalogToolbar.tsx +123 -0
  46. package/kit/registry/filters/files/lib/filters/register.ts +6 -0
  47. package/kit/registry/i18n/docs/i18n.md +24 -0
  48. package/kit/registry/i18n/feature.json +47 -0
  49. package/kit/registry/i18n/files/components/i18n/LocaleSwitcher.tsx +64 -0
  50. package/kit/registry/i18n/files/lib/i18n/dictionary.ts +52 -0
  51. package/kit/registry/i18n/files/lib/i18n/locale.ts +11 -0
  52. package/kit/registry/i18n/files/lib/i18n/register.ts +6 -0
  53. package/kit/registry/i18n/files/lib/i18n/t.ts +1 -0
  54. package/kit/registry/i18n/files/lib/i18n/useChromeLabel.ts +28 -0
  55. package/kit/registry/i18n/files/middleware.ts +51 -0
  56. package/kit/registry/product-page/feature.json +1 -1
  57. package/kit/registry/product-page/files/components/product/ProductGallery.tsx +23 -10
  58. package/kit/registry/product-page/files/components/product/ProductView.tsx +3 -3
  59. package/kit/registry/reviews/docs/reviews.md +11 -0
  60. package/kit/registry/reviews/feature.json +55 -0
  61. package/kit/registry/reviews/files/app/api/reviews/route.ts +55 -0
  62. package/kit/registry/reviews/files/components/reviews/ReviewList.tsx +119 -0
  63. package/kit/registry/reviews/files/lib/reviews/blueprint.ts +13 -0
  64. package/kit/registry/reviews/files/lib/reviews/register.ts +6 -0
  65. package/kit/registry/reviews/files/lib/reviews/types.ts +22 -0
  66. package/kit/registry/search/docs/search.md +12 -0
  67. package/kit/registry/search/feature.json +49 -0
  68. package/kit/registry/search/files/app/(frontend)/search/page.tsx +34 -0
  69. package/kit/registry/search/files/components/search/SearchForm.tsx +32 -0
  70. package/kit/registry/search/files/components/search/SearchResults.tsx +87 -0
  71. package/kit/registry/search/files/lib/search/data.ts +8 -0
  72. package/kit/registry/search/files/lib/search/register.ts +8 -0
  73. package/kit/registry/seo/docs/seo.md +4 -1
  74. package/kit/registry/seo/feature.json +5 -1
  75. package/kit/registry/seo/files/app/robots.ts +13 -0
  76. package/kit/registry/seo/files/app/sitemap.ts +27 -0
  77. package/kit/registry/stubs/route-glue.d.ts +71 -0
  78. package/kit/registry/tsconfig.json +7 -2
  79. package/kit/registry/wishlist/docs/wishlist.md +11 -0
  80. package/kit/registry/wishlist/feature.json +55 -0
  81. package/kit/registry/wishlist/files/app/(frontend)/wishlist/page.tsx +16 -0
  82. package/kit/registry/wishlist/files/components/wishlist/WishlistButton.tsx +37 -0
  83. package/kit/registry/wishlist/files/components/wishlist/WishlistIndicator.tsx +30 -0
  84. package/kit/registry/wishlist/files/components/wishlist/WishlistView.tsx +73 -0
  85. package/kit/registry/wishlist/files/lib/wishlist/register.ts +8 -0
  86. package/kit/registry/wishlist/files/lib/wishlist/storage.ts +30 -0
  87. package/kit/templates/backend-payload/files/Dockerfile +3 -0
  88. package/kit/templates/backend-payload/files/docker-compose.yml +8 -0
  89. package/kit/templates/backend-payload/files/lib/adapter/payload-catalog.ts +51 -17
  90. package/kit/templates/backend-payload/files/lib/adapter/payload-commerce.ts +18 -0
  91. package/kit/templates/backend-payload/files/lib/adapter/product-query.ts +126 -0
  92. package/kit/templates/backend-payload/files/lib/seed/admin.ts +7 -3
  93. package/kit/templates/backend-payload/files/lib/seed/guards.ts +17 -4
  94. package/kit/templates/backend-payload/files/lib/seed/locales.ts +53 -0
  95. package/kit/templates/backend-payload/files/lib/seed/run.ts +57 -6
  96. package/kit/templates/backend-payload/files/next.config.mjs +8 -2
  97. package/kit/templates/backend-payload/files/payload.config.ts +27 -3
  98. package/kit/templates/backend-vendure/files/Dockerfile +1 -1
  99. package/kit/templates/backend-vendure/files/docker-compose.yml +2 -1
  100. package/kit/templates/backend-vendure/files/lib/adapter/product-query.ts +126 -0
  101. package/kit/templates/backend-vendure/files/lib/adapter/vendure-catalog.ts +18 -11
  102. package/kit/templates/base/files/.claude/commands/update.md +1 -1
  103. package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +51 -4
  104. package/kit/templates/base/files/app/(frontend)/layout.tsx +13 -7
  105. package/kit/templates/base/files/app/(frontend)/not-found.tsx +26 -0
  106. package/kit/templates/base/files/app/(frontend)/page.tsx +49 -4
  107. package/kit/templates/base/files/app/(frontend)/products/[slug]/page.tsx +5 -2
  108. package/kit/templates/base/files/app/globals.css +13 -0
  109. package/kit/templates/base/files/components/SlotsProvider.tsx +19 -0
  110. package/kit/templates/base/files/components/layout/SiteFooter.tsx +6 -1
  111. package/kit/templates/base/files/components/layout/SiteHeader.tsx +8 -2
  112. package/kit/templates/base/files/lib/i18n/dictionary.ts +36 -0
  113. package/kit/templates/base/files/lib/i18n/locale.ts +11 -0
  114. package/kit/templates/base/files/lib/i18n/useChromeLabel.ts +28 -0
  115. package/kit/templates/base/files/lib/site.ts +3 -0
  116. package/kit/templates/base/files/next.config.mjs +8 -2
  117. 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.3";
1200
+ var KIT_VERSION = "0.5.0";
1196
1201
  var CONTRACTS_VERSION = "1.0.0";
1197
1202
  var CONTRACTS_RANGE = "^1.0.0";
1198
1203
  var CORE_RANGE = "^0.3.0";
1199
- var BLUEPRINT_RANGE = "^0.2.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,17 +1398,17 @@ 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, size/color variants, gallery images) and creates a",
1389
- "dev admin (login/password printed to the console once). Disable the fallback in",
1390
- "dev too with `VITRINE_DB_STRICT=1`."
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",
1411
+ "to the console once). Disable the fallback in dev too with `VITRINE_DB_STRICT=1`."
1391
1412
  ].join("\n");
1392
1413
  const deploySecret = backend === "vendure" ? "export VENDURE_COOKIE_SECRET=... # Vendure cookie secret" : "export PAYLOAD_SECRET=... # random Payload secret";
1393
1414
  return `# ${name}
@@ -1426,7 +1447,7 @@ vitrine design apply # style the new feature
1426
1447
  ## 5. Updates and checks
1427
1448
 
1428
1449
  \`\`\`bash
1429
- vitrine kit update # update the local registry/templates cache from GitHub
1450
+ vitrine kit update # update the local registry/templates cache from npm
1430
1451
  vitrine diff <feature> # preview a feature update
1431
1452
  vitrine update [feature] # 3-way merge of the new feature version (base = your snapshot)
1432
1453
  vitrine doctor # consistency: vitrine.json \u2194 files \u2194 packages \u2194 env
@@ -1471,6 +1492,7 @@ function scaffoldBase(opts) {
1471
1492
  `import type { SiteConfig } from '@vitrine-kit/contracts';
1472
1493
 
1473
1494
  export const siteConfig: SiteConfig = {
1495
+ name: ${JSON.stringify(name)},
1474
1496
  backend: ${JSON.stringify(backend)},
1475
1497
  tier: ${JSON.stringify(tier)},
1476
1498
  // vitrine:features:start
@@ -1511,7 +1533,7 @@ _No features installed yet._
1511
1533
  | \`vitrine diff <feature>\` | Preview an update (without writing) | before \`update\` | \`--registry\` |
1512
1534
  | \`vitrine doctor\` | Consistency: \`vitrine.json\` \u2194 files \u2194 packages \u2194 env | after edits, when in doubt | \`--registry\` |
1513
1535
  | \`vitrine design apply\` | Apply the design from \`/design\` to tokens (via Claude Code) | after \`add\` or a rebrand | \`--bin\`, \`--dry-run\` |
1514
- | \`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\` |
1515
1537
  | \`vitrine kit status\` | Cache version vs the CLI's expected one | diagnostics | \u2014 |
1516
1538
  | \`vitrine self-update\` | Update the CLI itself | rarely | \`--dry-run\` |
1517
1539
 
@@ -1559,6 +1581,7 @@ The step is idempotent: re-running converges and doesn't accumulate cruft.
1559
1581
  writeText(join13(root, "lib", "blueprint.ts"), renderBlueprintFile([]));
1560
1582
  writeText(join13(root, "theme", "client.css"), renderNeutralTheme());
1561
1583
  writeText(join13(root, ".env.example"), clientEnvExample(backend));
1584
+ writeText(join13(root, ".env"), clientEnvFile(backend));
1562
1585
  writeText(join13(root, "package.json"), `${JSON.stringify(clientPackageJson(name, backend), null, 2)}
1563
1586
  `);
1564
1587
  }
@@ -1573,43 +1596,89 @@ function initProject(opts) {
1573
1596
 
1574
1597
  // src/kit-update.ts
1575
1598
  import { spawnSync as spawnSync2 } from "child_process";
1576
- 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";
1577
1600
  import { tmpdir } from "os";
1578
- import { join as join14, resolve as resolve6 } from "path";
1579
- var REPO = "vitrine-kit/vitrine";
1580
- function hasBin(bin) {
1581
- const probe = process.platform === "win32" ? "where" : "which";
1582
- 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;
1583
1609
  }
1584
- function acquireFromGh(tmp, version) {
1585
- if (!hasBin("gh")) {
1586
- throw new Error("[vitrine] network update needs gh (GitHub CLI), or pass --from <dir>");
1587
- }
1588
- if (!hasBin("tar")) {
1589
- 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
+ });
1590
1622
  }
1591
- const dl = spawnSync2(
1592
- "gh",
1593
- ["release", "download", ...version ? [version] : [], "--repo", REPO, "--archive=tar.gz", "--dir", tmp, "--clobber"],
1594
- { 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"] }
1595
1657
  );
1596
- if (dl.status !== 0) {
1597
- throw new Error("[vitrine] gh release download failed (check access/auth: gh auth status, and gh auth login if needed)");
1598
- }
1599
- const tarball = readdirSync5(tmp).find((f) => f.endsWith(".tar.gz"));
1600
- if (!tarball) throw new Error("[vitrine] release tarball not found after download");
1601
- if (spawnSync2("tar", ["-xzf", join14(tmp, tarball), "-C", tmp], { stdio: "inherit" }).status !== 0) {
1602
- 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
+ );
1603
1663
  }
1604
- const root = readdirSync5(tmp).map((f) => join14(tmp, f)).find((p2) => statSync2(p2).isDirectory() && existsSync9(join14(p2, "registry", "_index.json")));
1605
- if (!root) throw new Error("[vitrine] the unpacked tarball has no registry/_index.json");
1606
- 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
+ );
1607
1669
  }
1608
1670
  function warnOnVersionMismatch(root, requested) {
1609
1671
  if (!requested) return;
1610
1672
  const tagSemver = requested.match(/^@vitrine-kit\/vitrine@(.+)$/)?.[1] ?? requested.match(/^v?(\d+\.\d+\.\d+\S*)$/)?.[1];
1611
- const pkgFile = join14(root, "packages", "cli", "package.json");
1612
- 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;
1613
1682
  const version = readJson(pkgFile).version;
1614
1683
  if (version && version !== tagSemver) {
1615
1684
  console.warn(`[vitrine] requested "${requested}" but the downloaded kit is @vitrine-kit/vitrine@${version}`);
@@ -1621,7 +1690,7 @@ function kitUpdate(opts = {}) {
1621
1690
  }
1622
1691
  const tmp = mkdtempSync(join14(tmpdir(), "vitrine-kit-"));
1623
1692
  try {
1624
- const source = acquireFromGh(tmp, opts.version);
1693
+ const source = acquireFromNpm(tmp, opts.version);
1625
1694
  warnOnVersionMismatch(source, opts.version);
1626
1695
  return populateCache(source, { home: opts.home, channel: opts.channel ?? "stable" });
1627
1696
  } finally {
@@ -1647,7 +1716,7 @@ function selfUpdate(opts = {}) {
1647
1716
  console.log(`[vitrine] npm ${args.join(" ")}`);
1648
1717
  return 0;
1649
1718
  }
1650
- const res = spawnSync2("npm", args, { stdio: "inherit" });
1719
+ const res = runNpm(args, { stdio: "inherit" });
1651
1720
  if (res.error) throw res.error;
1652
1721
  return res.status ?? 0;
1653
1722
  }
@@ -1691,8 +1760,8 @@ design.command("apply").description("Apply the design from /design to the tokens
1691
1760
  if (code !== 0) process.exit(code);
1692
1761
  });
1693
1762
  var kit = program.command("kit").description("Local tooling (the ~/.vitrine registry cache)");
1694
- 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) => {
1695
- 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 });
1696
1765
  console.log(`Cache updated to kit ${res.kitVersion}.`);
1697
1766
  console.log(formatChangelog(res.changelog));
1698
1767
  });
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "$schema": "../schemas/registry-index.schema.json",
3
- "kitVersion": "0.4.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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.3",
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
+ }